# auth/signup.py from dash import html, dcc, Input, Output, State, callback, no_update import dash_bootstrap_components as dbc from database import engine, users from sqlalchemy import select, insert from werkzeug.security import generate_password_hash # Opsi untuk Jabatan role_options = [ {'label': 'Petugas Kesehatan Dasar', 'value': 'petugas_kesdas'}, {'label': 'Petugas Rekam Medis Puskesmas', 'value': 'pmik'}, {'label': 'Peneliti', 'value': 'peneliti'}, {'label': 'Lainnya', 'value': 'lainnya'} ] signup_layout = dbc.Container( fluid=True, # Mengambil lebar penuh viewport className="d-flex flex-column justify-content-center align-items-center min-vh-100 p-0 m-0", # Center vertikal & horizontal, tinggi minimal 100% viewport, hapus padding/margin default container style={'background': 'linear-gradient(135deg, #1e2a47, #2c3e50)'}, children=[ dcc.Location(id='signup-url-redirect', refresh=True), dbc.Row( # Baris untuk menengahkan Card dbc.Col( # Kolom untuk Card, batasi lebar dbc.Card( dbc.CardBody([ html.H2("Daftar Akun Baru", className="text-center mb-4", style={'color': '#2c3e50'}), # Pesan error/sukses di atas form html.Div(id='signup-message', className="mb-3 text-center"), dbc.Form([ # Nama Lengkap dbc.Row([ dbc.Label("Nama Lengkap", width=4, className="text-md-end"), # Label di kiri pada layar medium+ dbc.Col( dbc.Input(id='signup-fullname', type='text', placeholder='Masukkan nama lengkap Anda'), width=8 ) ], className="mb-3 align-items-center"), # Username dbc.Row([ dbc.Label("Username", width=4, className="text-md-end"), dbc.Col( dbc.Input(id='signup-username', type='text', placeholder='Pilih username (unik)'), width=8 ) ], className="mb-3 align-items-center"), # Email dbc.Row([ dbc.Label("Email (Opsional)", width=4, className="text-md-end"), dbc.Col( dbc.Input(id='signup-email', type='email', placeholder='Masukkan alamat email'), width=8 ) ], className="mb-3 align-items-center"), # Password dbc.Row([ dbc.Label("Password", width=4, className="text-md-end"), dbc.Col( dbc.Input(id='signup-password', type='password', placeholder='Buat password (minimal 6 karakter)'), width=8 ) ], className="mb-3 align-items-center"), # Konfirmasi Password dbc.Row([ dbc.Label("Konfirmasi Password", width=4, className="text-md-end"), dbc.Col( dbc.Input(id='signup-confirm-password', type='password', placeholder='Ulangi password'), width=8 ) ], className="mb-3 align-items-center"), # Jabatan dbc.Row([ dbc.Label("Jabatan", width=4, className="text-md-end"), dbc.Col( dcc.Dropdown( id='signup-role', options=role_options, placeholder='Pilih jabatan Anda', ), width=8 ) ], className="mb-3 align-items-center"), dbc.Button("Daftar", id='signup-button', color="success", className="w-100 mt-4", n_clicks=0, size="lg"), ]), html.Div( dcc.Link("Sudah punya akun? Login di sini", href="/login", className="d-block mt-3 text-center"), ) ]), className="shadow-lg", # Shadow pada card style={'padding': '2rem'} # Tambahkan padding dalam card ), width=12, # Lebar kolom untuk layar kecil sm=10, # Sedikit lebih sempit di layar small md=8, # Lebih sempit lagi di layar medium lg=6, # Paling ideal di layar large xl=5 # Mungkin terlalu sempit, bisa disesuaikan ), justify="center", # Tengahkan kolom di dalam baris className="w-100" # Pastikan baris mengambil lebar penuh ) ] ) # Callback untuk handle signup (TETAP SAMA SEPERTI SEBELUMNYA) @callback( Output('signup-url-redirect', 'pathname', allow_duplicate=True), Output('signup-message', 'children', allow_duplicate=True), Input('signup-button', 'n_clicks'), State('signup-fullname', 'value'), State('signup-username', 'value'), State('signup-password', 'value'), State('signup-confirm-password', 'value'), State('signup-email', 'value'), State('signup-role', 'value'), prevent_initial_call=True ) def handle_signup(n_clicks_signup, fullname, username, password, confirm_password, email, role): if not fullname or not username or not password or not confirm_password or not role: return no_update, dbc.Alert("Semua field (Nama Lengkap, Username, Password, Konfirmasi, Jabatan) harus diisi.", color="danger", dismissable=True, duration=4000) if len(password) < 6: return no_update, dbc.Alert("Password minimal 6 karakter.", color="danger", dismissable=True, duration=4000) if password != confirm_password: return no_update, dbc.Alert("Password dan konfirmasi password tidak cocok.", color="danger", dismissable=True, duration=4000) username = username.strip() email = email.strip() if email else None with engine.connect() as conn: stmt_check_username = select(users.c.id_user).where(users.c.username == username) if conn.execute(stmt_check_username).fetchone(): return no_update, dbc.Alert(f"Username '{username}' sudah digunakan.", color="warning", dismissable=True, duration=4000) if email: stmt_check_email = select(users.c.id_user).where(users.c.email == email) if conn.execute(stmt_check_email).fetchone(): return no_update, dbc.Alert(f"Email '{email}' sudah terdaftar.", color="warning", dismissable=True, duration=4000) hashed_password_to_store = generate_password_hash(password) # print(f"--- SIGNUP DEBUG: Username: {username}, Password Asli: '{password}', Hashed untuk DB: {hashed_password_to_store[:20]}... ---") try: new_user_values = { 'nama_lengkap': fullname.strip(), 'username': username, 'password': hashed_password_to_store, 'jabatan': role } if email: new_user_values['email'] = email insert_stmt = users.insert().values(**new_user_values) conn.execute(insert_stmt) conn.commit() return '/login', dbc.Alert("Registrasi berhasil! Silakan login.", color="success", duration=5000) except Exception as e: conn.rollback() # print(f"--- SIGNUP ERROR: {e} ---") # import traceback # traceback.print_exc() return no_update, dbc.Alert(f"Terjadi kesalahan saat registrasi.", color="danger", dismissable=True, duration=4000) return no_update, "" layout = signup_layout