revisi_skripsi / app.py
aslasacacc's picture
The real final commit, clean start
4253e50
# 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/[email protected]/dist/flatly/bootstrap.min.css";
const darkThemeUrl = "https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/dist/flatly/bootstrap.min.css";
const darkThemeUrl = "https://cdn.jsdelivr.net/npm/[email protected]/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)