qorzen_oxide/ui/
state.rs

1// src/ui/state.rs - Fixed state management with cross-platform time support
2
3use 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    // Use a single signal for the entire state
96    let mut app_state = use_signal(AppStateContext::default);
97
98    // Create dispatch function that updates the state
99    let dispatch = use_callback(move |action: AppAction| {
100        app_state.with_mut(|state| {
101            *state = app_state_reducer(state, action);
102        });
103    });
104
105    // Provide the state and dispatch functions
106    use_context_provider(|| app_state);
107    use_context_provider(|| dispatch);
108
109    // Initialize mock data - separate from state reading to avoid infinite loop
110    use_effect(move || {
111        // Only run once by not reading any signals inside
112        spawn(async move {
113            // Add mock notifications using cross-platform time
114            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
144/// Hook to get the current app state (read-only)
145pub fn use_app_state() -> AppStateContext {
146    let state_signal = use_context::<Signal<AppStateContext>>();
147    state_signal()
148}
149
150/// Hook to get the dispatch function (write-only)
151pub fn use_app_dispatch() -> Callback<AppAction> {
152    use_context::<Callback<AppAction>>()
153}
154
155/// Hook to get both state and dispatch - use sparingly to avoid infinite loops
156pub 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    /// Hook for login functionality
167    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                    // Simulate API call delay
178                    #[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                    // Mock successful login - using the Time utility correctly
184                    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    /// Hook for logout functionality
241    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                    // Simulate API call delay
252                    #[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    /// Hook to check if user is authenticated
267    pub fn use_is_authenticated() -> bool {
268        let state = use_app_state();
269        state.current_user.is_some()
270    }
271
272    /// Hook to get current user
273    pub fn use_current_user() -> Option<User> {
274        let state = use_app_state();
275        state.current_user
276    }
277
278    /// Hook to check permissions
279    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                    // Check direct permissions
288                    let direct = user.permissions.iter().any(|perm| {
289                        (perm.resource == resource || perm.resource == "*")
290                            && (perm.action == action || perm.action == "*")
291                    });
292
293                    // Check role permissions
294                    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    /// Hook for sidebar state management
313    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    /// Hook for mobile menu state management
327    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    /// Hook for notifications management
339    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        // Test setting loading state
381        let new_state = app_state_reducer(&initial_state, AppAction::SetLoading(true));
382        assert!(new_state.is_loading);
383
384        // Test toggling sidebar
385        let new_state = app_state_reducer(&initial_state, AppAction::ToggleSidebar);
386        assert!(new_state.sidebar_collapsed);
387
388        // Test setting error
389        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}