feat: Implement Staff Detail View
This commit is contained in:
@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|||||||
import AppRoutes from './routes';
|
import AppRoutes from './routes';
|
||||||
import { store } from './store/store';
|
import { store } from './store/store';
|
||||||
import { initializeAuthPersistence } from './services/authService';
|
import { initializeAuthPersistence } from './services/authService';
|
||||||
|
import AuthInitializer from './features/auth/AuthInitializer';
|
||||||
|
|
||||||
// Initialize the QueryClient
|
// Initialize the QueryClient
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -13,13 +14,16 @@ initializeAuthPersistence();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Root Application Component.
|
* Root Application Component.
|
||||||
* Wraps the app with Redux Provider and React Query Provider.
|
* Wraps the app with Redux Provider, React Query Provider, and AuthInitializer.
|
||||||
|
* AuthInitializer ensures auth state is restored from persistence before routes are rendered.
|
||||||
*/
|
*/
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppRoutes />
|
<AuthInitializer>
|
||||||
|
<AppRoutes />
|
||||||
|
</AuthInitializer>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
44
apps/web/src/features/auth/AuthInitializer.tsx
Normal file
44
apps/web/src/features/auth/AuthInitializer.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { checkAuthStatus } from './authSlice';
|
||||||
|
import type { RootState, AppDispatch } from '../../store/store';
|
||||||
|
|
||||||
|
interface AuthInitializerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthInitializer Component
|
||||||
|
* Initializes authentication state from Firebase persistence on app load
|
||||||
|
* Shows a loading screen until the initial auth check is complete
|
||||||
|
* This prevents premature redirect to login before the persisted session is restored
|
||||||
|
*/
|
||||||
|
const AuthInitializer: React.FC<AuthInitializerProps> = ({ children }) => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const { isInitialized } = useSelector((state: RootState) => state.auth);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Perform initial auth check when component mounts
|
||||||
|
// This restores the persisted session from Firebase
|
||||||
|
dispatch(checkAuthStatus());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// Show loading state while initializing auth
|
||||||
|
if (!isInitialized) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen bg-slate-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block">
|
||||||
|
<div className="w-8 h-8 border-4 border-slate-200 border-t-primary rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-slate-600">Loading application...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthInitializer;
|
||||||
@@ -17,6 +17,7 @@ interface AuthState {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
status: "idle" | "loading" | "succeeded" | "failed";
|
status: "idle" | "loading" | "succeeded" | "failed";
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
isInitialized: boolean; // Track whether initial auth check has completed
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: AuthState = {
|
const initialState: AuthState = {
|
||||||
@@ -24,6 +25,7 @@ const initialState: AuthState = {
|
|||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
status: "idle",
|
status: "idle",
|
||||||
error: null,
|
error: null,
|
||||||
|
isInitialized: false, // Start as false until initial auth check completes
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,6 +156,9 @@ const authSlice = createSlice({
|
|||||||
|
|
||||||
// Check auth status thunk
|
// Check auth status thunk
|
||||||
builder
|
builder
|
||||||
|
.addCase(checkAuthStatus.pending, (state) => {
|
||||||
|
state.status = "loading";
|
||||||
|
})
|
||||||
.addCase(checkAuthStatus.fulfilled, (state, action) => {
|
.addCase(checkAuthStatus.fulfilled, (state, action) => {
|
||||||
if (action.payload) {
|
if (action.payload) {
|
||||||
state.user = action.payload;
|
state.user = action.payload;
|
||||||
@@ -163,6 +168,11 @@ const authSlice = createSlice({
|
|||||||
state.isAuthenticated = false;
|
state.isAuthenticated = false;
|
||||||
}
|
}
|
||||||
state.status = "idle";
|
state.status = "idle";
|
||||||
|
state.isInitialized = true; // Mark initialization as complete
|
||||||
|
})
|
||||||
|
.addCase(checkAuthStatus.rejected, (state) => {
|
||||||
|
state.isInitialized = true; // Mark initialization as complete even on error
|
||||||
|
state.isAuthenticated = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function EditStaff() {
|
|||||||
const [staff, setStaff] = useState<Staff | null>(null);
|
const [staff, setStaff] = useState<Staff | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStaff = async () => {
|
const fetchStaff = async () => {
|
||||||
@@ -41,7 +42,8 @@ export default function EditStaff() {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await workforceService.entities.Staff.update(id, staffData);
|
await workforceService.entities.Staff.update(id, staffData);
|
||||||
navigate("/staff");
|
setStaff(staffData);
|
||||||
|
setIsEditing(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update staff", error);
|
console.error("Failed to update staff", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -49,6 +51,10 @@ export default function EditStaff() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
||||||
@@ -60,7 +66,7 @@ export default function EditStaff() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
title={`Edit: ${staff?.employee_name || 'Staff Member'}`}
|
title={isEditing ? `Edit: ${staff?.employee_name || 'Staff Member'}` : staff?.employee_name || 'Staff Member'}
|
||||||
subtitle={`Management of ${staff?.employee_name}'s professional records`}
|
subtitle={`Management of ${staff?.employee_name}'s professional records`}
|
||||||
backAction={
|
backAction={
|
||||||
<Button
|
<Button
|
||||||
@@ -74,11 +80,42 @@ export default function EditStaff() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{staff && (
|
{staff && (
|
||||||
<StaffForm
|
<div>
|
||||||
staff={staff}
|
{!isEditing && (
|
||||||
onSubmit={handleSubmit}
|
<div className="mb-6 flex justify-end">
|
||||||
isSubmitting={isSubmitting}
|
<Button onClick={() => setIsEditing(true)} variant="secondary">
|
||||||
/>
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isEditing && (
|
||||||
|
<div className="mb-6 flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleCancel}
|
||||||
|
variant="outline"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
// Trigger form submission by dispatching event or calling form submit
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
if (form) form.requestSubmit();
|
||||||
|
}}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Saving...' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<StaffForm
|
||||||
|
staff={staff}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
disabled={!isEditing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user