1use dioxus::prelude::*;
4
5pub(crate) use crate::auth::{User, UserSession};
6use crate::ui::{Notification, Theme, UILayout};
7use crate::utils::Time;
8
9#[derive(Debug, Clone, Default)]
10pub struct AppStateContext {
11 pub current_user: Option<User>,
12 pub current_session: Option<UserSession>,
13 pub current_layout: UILayout,
14 pub current_theme: Theme,
15 pub is_loading: bool,
16 pub error_message: Option<String>,
17 pub notifications: Vec<Notification>,
18 pub sidebar_collapsed: bool,
19 pub mobile_menu_open: bool,
20}
21
22#[derive(Debug, Clone)]
23pub enum AppAction {
24 SetUser(Option<User>),
25 SetSession(Option<UserSession>),
26 SetLayout(UILayout),
27 SetTheme(Theme),
28 SetLoading(bool),
29 SetError(Option<String>),
30 AddNotification(Notification),
31 RemoveNotification(uuid::Uuid),
32 MarkNotificationRead(uuid::Uuid),
33 ClearNotifications,
34 ToggleSidebar,
35 SetSidebarCollapsed(bool),
36 ToggleMobileMenu,
37 SetMobileMenuOpen(bool),
38}
39
40pub fn app_state_reducer(state: &AppStateContext, action: AppAction) -> AppStateContext {
41 let mut new_state = state.clone();
42
43 match action {
44 AppAction::SetUser(user) => {
45 new_state.current_user = user;
46 }
47 AppAction::SetSession(session) => {
48 new_state.current_session = session;
49 }
50 AppAction::SetLayout(layout) => {
51 new_state.current_layout = layout;
52 }
53 AppAction::SetTheme(theme) => {
54 new_state.current_theme = theme;
55 }
56 AppAction::SetLoading(loading) => {
57 new_state.is_loading = loading;
58 }
59 AppAction::SetError(error) => {
60 new_state.error_message = error;
61 }
62 AppAction::AddNotification(notification) => {
63 new_state.notifications.push(notification);
64 }
65 AppAction::RemoveNotification(id) => {
66 new_state.notifications.retain(|n| n.id != id);
67 }
68 AppAction::MarkNotificationRead(id) => {
69 if let Some(notification) = new_state.notifications.iter_mut().find(|n| n.id == id) {
70 notification.read = true;
71 }
72 }
73 AppAction::ClearNotifications => {
74 new_state.notifications.clear();
75 }
76 AppAction::ToggleSidebar => {
77 new_state.sidebar_collapsed = !new_state.sidebar_collapsed;
78 }
79 AppAction::SetSidebarCollapsed(collapsed) => {
80 new_state.sidebar_collapsed = collapsed;
81 }
82 AppAction::ToggleMobileMenu => {
83 new_state.mobile_menu_open = !new_state.mobile_menu_open;
84 }
85 AppAction::SetMobileMenuOpen(open) => {
86 new_state.mobile_menu_open = open;
87 }
88 }
89
90 new_state
91}
92
93#[component]
94pub fn AppStateProvider(children: Element) -> Element {
95 let mut app_state = use_signal(AppStateContext::default);
97
98 let dispatch = use_callback(move |action: AppAction| {
100 app_state.with_mut(|state| {
101 *state = app_state_reducer(state, action);
102 });
103 });
104
105 use_context_provider(|| app_state);
107 use_context_provider(|| dispatch);
108
109 use_effect(move || {
111 spawn(async move {
113 let now = Time::now();
115 let two_hours_ago = now - Time::duration_hours(2);
116
117 dispatch(AppAction::AddNotification(Notification {
118 id: uuid::Uuid::new_v4(),
119 title: "Welcome to Qorzen!".to_string(),
120 message: "Your application is ready to use.".to_string(),
121 notification_type: crate::ui::NotificationType::Info,
122 timestamp: now,
123 read: false,
124 actions: vec![],
125 }));
126
127 dispatch(AppAction::AddNotification(Notification {
128 id: uuid::Uuid::new_v4(),
129 title: "System Update".to_string(),
130 message: "A new version is available for download.".to_string(),
131 notification_type: crate::ui::NotificationType::System,
132 timestamp: two_hours_ago,
133 read: false,
134 actions: vec![],
135 }));
136 });
137 });
138
139 rsx! {
140 {children}
141 }
142}
143
144pub fn use_app_state() -> AppStateContext {
146 let state_signal = use_context::<Signal<AppStateContext>>();
147 state_signal()
148}
149
150pub fn use_app_dispatch() -> Callback<AppAction> {
152 use_context::<Callback<AppAction>>()
153}
154
155pub fn use_app_state_with_dispatch() -> (AppStateContext, Callback<AppAction>) {
157 let state = use_app_state();
158 let dispatch = use_app_dispatch();
159 (state, dispatch)
160}
161
162pub mod auth {
163 use super::*;
164 use crate::auth::{Credentials, User, UserSession};
165
166 pub fn use_login() -> Callback<Credentials, ()> {
168 let dispatch = use_app_dispatch();
169
170 use_callback(move |_credentials: Credentials| {
171 let dispatch = dispatch;
172
173 spawn({
174 async move {
175 dispatch(AppAction::SetLoading(true));
176
177 #[cfg(not(target_arch = "wasm32"))]
179 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
180 #[cfg(target_arch = "wasm32")]
181 gloo_timers::future::TimeoutFuture::new(1000).await;
182
183 let now = Time::now();
185 let thirty_days_ago = now - Time::duration_days(30);
186 let eight_hours_from_now = now + Time::duration_hours(8);
187
188 let mock_user = User {
189 id: uuid::Uuid::new_v4(),
190 username: "demo_user".to_string(),
191 email: "demo@qorzen.com".to_string(),
192 roles: vec![],
193 permissions: vec![],
194 preferences: crate::auth::UserPreferences::default(),
195 profile: crate::auth::UserProfile {
196 display_name: "Demo User".to_string(),
197 avatar_url: None,
198 bio: Some("A demonstration user account".to_string()),
199 department: Some("Engineering".to_string()),
200 title: Some("Software Developer".to_string()),
201 contact_info: crate::auth::ContactInfo {
202 phone: None,
203 address: None,
204 emergency_contact: None,
205 },
206 },
207 created_at: thirty_days_ago,
208 last_login: Some(now),
209 is_active: true,
210 };
211
212 let mock_session = UserSession {
213 id: uuid::Uuid::new_v4(),
214 user_id: mock_user.id,
215 created_at: now,
216 expires_at: eight_hours_from_now,
217 last_activity: now,
218 ip_address: Some("127.0.0.1".to_string()),
219 user_agent: Some("Qorzen App".to_string()),
220 is_active: true,
221 };
222
223 dispatch(AppAction::SetUser(Some(mock_user)));
224 dispatch(AppAction::SetSession(Some(mock_session)));
225 dispatch(AppAction::SetLoading(false));
226 dispatch(AppAction::AddNotification(Notification {
227 id: uuid::Uuid::new_v4(),
228 title: "Login Successful".to_string(),
229 message: "Welcome back! You have been successfully logged in.".to_string(),
230 notification_type: crate::ui::NotificationType::Success,
231 timestamp: Time::now(),
232 read: false,
233 actions: vec![],
234 }));
235 }
236 });
237 })
238 }
239
240 pub fn use_logout() -> Callback<(), ()> {
242 let dispatch = use_app_dispatch();
243
244 use_callback(move |_| {
245 let dispatch = dispatch;
246
247 spawn({
248 async move {
249 dispatch(AppAction::SetLoading(true));
250
251 #[cfg(not(target_arch = "wasm32"))]
253 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
254 #[cfg(target_arch = "wasm32")]
255 gloo_timers::future::TimeoutFuture::new(500).await;
256
257 dispatch(AppAction::SetUser(None));
258 dispatch(AppAction::SetSession(None));
259 dispatch(AppAction::ClearNotifications);
260 dispatch(AppAction::SetLoading(false));
261 }
262 });
263 })
264 }
265
266 pub fn use_is_authenticated() -> bool {
268 let state = use_app_state();
269 state.current_user.is_some()
270 }
271
272 pub fn use_current_user() -> Option<User> {
274 let state = use_app_state();
275 state.current_user
276 }
277
278 pub fn use_has_permission() -> impl Fn(&str, &str) -> bool {
280 let state_signal = use_context::<Signal<AppStateContext>>();
281
282 move |resource: &str, action: &str| {
283 let state = state_signal();
284
285 match &state.current_user {
286 Some(user) => {
287 let direct = user.permissions.iter().any(|perm| {
289 (perm.resource == resource || perm.resource == "*")
290 && (perm.action == action || perm.action == "*")
291 });
292
293 let role_based = user.roles.iter().any(|role| {
295 role.permissions.iter().any(|perm| {
296 (perm.resource == resource || perm.resource == "*")
297 && (perm.action == action || perm.action == "*")
298 })
299 });
300
301 direct || role_based
302 }
303 None => false,
304 }
305 }
306 }
307}
308
309pub mod ui {
310 use super::*;
311
312 pub fn use_sidebar() -> (bool, Callback<(), ()>, Callback<bool, ()>) {
314 let state = use_app_state();
315 let dispatch = use_app_dispatch();
316
317 let toggle = use_callback(move |_| dispatch(AppAction::ToggleSidebar));
318
319 let set_collapsed = use_callback({
320 move |collapsed: bool| dispatch(AppAction::SetSidebarCollapsed(collapsed))
321 });
322
323 (state.sidebar_collapsed, toggle, set_collapsed)
324 }
325
326 pub fn use_mobile_menu() -> (bool, Callback<(), ()>, Callback<bool, ()>) {
328 let state = use_app_state();
329 let dispatch = use_app_dispatch();
330
331 let toggle = use_callback(move |_| dispatch(AppAction::ToggleMobileMenu));
332
333 let set_open = use_callback(move |open: bool| dispatch(AppAction::SetMobileMenuOpen(open)));
334
335 (state.mobile_menu_open, toggle, set_open)
336 }
337
338 pub fn use_notifications() -> (
340 Vec<Notification>,
341 Callback<uuid::Uuid, ()>,
342 Callback<uuid::Uuid, ()>,
343 Callback<(), ()>,
344 ) {
345 let state = use_app_state();
346 let dispatch = use_app_dispatch();
347
348 let remove =
349 use_callback(move |id: uuid::Uuid| dispatch(AppAction::RemoveNotification(id)));
350
351 let mark_read =
352 use_callback(move |id: uuid::Uuid| dispatch(AppAction::MarkNotificationRead(id)));
353
354 let clear_all = use_callback(move |_| dispatch(AppAction::ClearNotifications));
355
356 (state.notifications, remove, mark_read, clear_all)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_default_app_state() {
366 let state = AppStateContext::default();
367 assert!(state.current_user.is_none());
368 assert!(state.current_session.is_none());
369 assert!(!state.is_loading);
370 assert!(state.error_message.is_none());
371 assert!(state.notifications.is_empty());
372 assert!(!state.sidebar_collapsed);
373 assert!(!state.mobile_menu_open);
374 }
375
376 #[test]
377 fn test_app_state_reducer() {
378 let initial_state = AppStateContext::default();
379
380 let new_state = app_state_reducer(&initial_state, AppAction::SetLoading(true));
382 assert!(new_state.is_loading);
383
384 let new_state = app_state_reducer(&initial_state, AppAction::ToggleSidebar);
386 assert!(new_state.sidebar_collapsed);
387
388 let error_msg = "Test error".to_string();
390 let new_state =
391 app_state_reducer(&initial_state, AppAction::SetError(Some(error_msg.clone())));
392 assert_eq!(new_state.error_message, Some(error_msg));
393 }
394}