File size: 10,031 Bytes
4253e50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# 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)