# app.py (Versi Revisi Siap Deploy) import dash import dash_bootstrap_components as dbc from dash import Dash, dcc, html, Output, Input, State, no_update # Impor dari proyek Anda from pages import ( beranda, analisis_tren_penyakit, distribusi_kasus_demografi, input_data, laporan, pengaturan ) from auth import login, signup from components import sidebar # ----------------------------------------------------------------------------- # BAGIAN 1: INISIALISASI APLIKASI (PERUBAHAN UTAMA DI SINI) # ----------------------------------------------------------------------------- # Tidak perlu mengimpor Flask atau menginisialisasi server Flask secara manual lagi. # Dash akan melakukannya secara otomatis. # Konfigurasi Tema THEME_LIGHT = dbc.themes.FLATLY THEME_DARK = dbc.themes.DARKLY # Inisialisasi aplikasi Dash app = Dash( __name__, suppress_callback_exceptions=True, external_stylesheets=[THEME_LIGHT, '/assets/style.css'], meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}] ) # Baris ini SANGAT PENTING untuk deployment. # Gunicorn akan mencari variabel bernama 'server'. server = app.server # ----------------------------------------------------------------------------- # BAGIAN 2: LAYOUT UTAMA APLIKASI (TIDAK ADA PERUBAHAN) # ----------------------------------------------------------------------------- app.layout = html.Div([ dcc.Location(id='url', refresh=False), dcc.Store(id='login-status', storage_type='session'), dcc.Store(id='user-theme-preference-store', storage_type='local'), dcc.Store(id='previous-url-store', storage_type='session'), html.Div(id='app-wrapper'), dbc.Modal( [ dbc.ModalHeader(dbc.ModalTitle("Konfirmasi Logout")), dbc.ModalBody("Apakah Anda yakin ingin keluar dari sesi ini?"), dbc.ModalFooter([ dbc.Button("Tidak", id="logout-confirm-no", color="secondary", className="ms-auto", n_clicks=0), dbc.Button("Ya, Logout", id="logout-confirm-yes", color="danger", className="ms-2", n_clicks=0), ]), ], id="logout-confirm-modal", is_open=False, centered=True, ), dcc.Location(id='logout-redirect-location', refresh=True) ]) # ----------------------------------------------------------------------------- # BAGIAN 3: CALLBACKS (TIDAK ADA PERUBAHAN) # ----------------------------------------------------------------------------- # --- Callbacks untuk Tema (JavaScript Inline) --- app.clientside_callback( """ function(pathname, storedThemePreference) { let themeToApply = 'LIGHT'; if (storedThemePreference && typeof storedThemePreference.theme === 'string') { themeToApply = storedThemePreference.theme; } const lightThemeUrl = "https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/flatly/bootstrap.min.css"; const darkThemeUrl = "https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/darkly/bootstrap.min.css"; let newThemeUrl = lightThemeUrl; document.body.classList.remove('theme-dark', 'theme-light'); if (themeToApply === 'DARK') { newThemeUrl = darkThemeUrl; document.body.classList.add('theme-dark'); } else { newThemeUrl = lightThemeUrl; document.body.classList.add('theme-light'); } let themeLink = document.getElementById('bootstrap-theme'); if (!themeLink) { themeLink = document.createElement('link'); themeLink.id = 'bootstrap-theme'; themeLink.rel = 'stylesheet'; themeLink.type = 'text/css'; document.getElementsByTagName('head')[0].appendChild(themeLink); } if (themeLink.href !== newThemeUrl) { themeLink.href = newThemeUrl; } return null; } """, Output('app-wrapper', 'className'), Input('url', 'pathname'), State('user-theme-preference-store', 'data') ) app.clientside_callback( """ function(themePreferenceFromStore) { let themeToApply = 'LIGHT'; if (themePreferenceFromStore && typeof themePreferenceFromStore.theme === 'string') { themeToApply = themePreferenceFromStore.theme; } const lightThemeUrl = "https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/flatly/bootstrap.min.css"; const darkThemeUrl = "https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/darkly/bootstrap.min.css"; let newThemeUrl = lightThemeUrl; document.body.classList.remove('theme-dark', 'theme-light'); if (themeToApply === 'DARK') { newThemeUrl = darkThemeUrl; document.body.classList.add('theme-dark'); } else { newThemeUrl = lightThemeUrl; document.body.classList.add('theme-light'); } let themeLink = document.getElementById('bootstrap-theme'); if (!themeLink) { themeLink = document.createElement('link'); themeLink.id = 'bootstrap-theme'; themeLink.rel = 'stylesheet'; themeLink.type = 'text/css'; document.getElementsByTagName('head')[0].appendChild(themeLink); } if (themeLink.href !== newThemeUrl) { themeLink.href = newThemeUrl; } return null; } """, Output('app-wrapper', 'className', allow_duplicate=True), Input('user-theme-preference-store', 'data'), prevent_initial_call=True ) # Callback untuk Navigasi Halaman, Kontrol Sidebar, dan Modal Logout @app.callback( Output('app-wrapper', 'children'), Output('logout-confirm-modal', 'is_open'), Input('url', 'pathname'), State('login-status', 'data') ) def display_page_logic(pathname, login_data): is_logged_in = login_data and login_data.get('logged_in', False) no_sidebar_pages = ['/login', '/signup'] open_logout_modal = False if pathname == '/logout' and is_logged_in: open_logout_modal = True # KASUS 1: Pengguna belum login if not is_logged_in: if pathname in no_sidebar_pages or pathname == '/' or pathname is None: if pathname == '/signup': return signup.layout, False else: return login.layout, False else: return dcc.Location(pathname="/login", id="redirect-to-login-unauth"), False # KASUS 2: Pengguna sudah login else: if pathname == '/logout': page_layout_content = beranda.layout elif pathname in no_sidebar_pages or pathname == '/' or pathname is None: return dcc.Location(pathname="/beranda", id="redirect-to-home-auth"), False elif pathname == '/beranda': page_layout_content = beranda.layout elif pathname == '/analisis_tren_penyakit': page_layout_content = analisis_tren_penyakit.layout elif pathname == '/distribusi_kasus_demografi': page_layout_content = distribusi_kasus_demografi.layout elif pathname == '/input_data': page_layout_content = input_data.layout elif pathname == '/laporan': page_layout_content = laporan.layout elif pathname == '/pengaturan': page_layout_content = pengaturan.layout else: page_layout_content = html.H1("404 - Halaman Tidak Ditemukan", className="text-center mt-5") return html.Div([ sidebar.sidebar_layout, html.Div(page_layout_content, id="page-content") ]), open_logout_modal # Callback untuk menyimpan URL sebelum logout @app.callback( Output('previous-url-store', 'data'), Input('url', 'pathname'), State('previous-url-store', 'data') ) def store_previous_url(current_pathname, last_stored_url): excluded_paths = ['/logout', '/login', '/signup'] if current_pathname not in excluded_paths and current_pathname != last_stored_url: return current_pathname return no_update # Callback untuk Update Profil di Sidebar @app.callback( Output('sidebar-profile-name', 'children'), Output('sidebar-profile-section', 'style'), Input('login-status', 'data'), Input('url', 'pathname') ) def update_sidebar_profile(login_data, pathname): no_sidebar_pages_or_logout = ['/login', '/signup', '/logout'] if login_data and login_data.get('logged_in'): nama_pengguna = login_data.get('nama_lengkap', login_data.get('username', 'Pengguna')) if pathname not in no_sidebar_pages_or_logout: return nama_pengguna, {'display': 'block'} else: return no_update, {'display': 'none'} return "Nama Pengguna", {'display': 'none'} # Callback untuk Aksi Modal Logout @app.callback( Output('logout-redirect-location', 'pathname'), Output('login-status', 'clear_data', allow_duplicate=True), Output('logout-confirm-modal', 'is_open', allow_duplicate=True), Input('logout-confirm-yes', 'n_clicks'), Input('logout-confirm-no', 'n_clicks'), State('previous-url-store', 'data'), prevent_initial_call=True ) def handle_logout_confirmation_app(n_yes, n_no, previous_url): triggered_id = dash.callback_context.triggered_id if dash.callback_context.triggered_id else None if triggered_id == 'logout-confirm-yes': return '/login', True, False elif triggered_id == 'logout-confirm-no': url_to_return = previous_url if previous_url else '/beranda' return url_to_return, False, False return no_update, no_update, False # ----------------------------------------------------------------------------- # BAGIAN 4: MENJALANKAN APLIKASI (TIDAK ADA PERUBAHAN) # ----------------------------------------------------------------------------- # Blok ini memastikan server development hanya berjalan saat file ini dieksekusi langsung, # dan tidak akan berjalan saat diimpor oleh Gunicorn di server produksi. if __name__ == '__main__': app.run(debug=True, port=8050)