feat: implement administrative audit allocation and management dashboard pages with supporting API services
This commit is contained in:
@@ -1,58 +1,33 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemButton, ListItemText, CircularProgress, Box } from '@mui/material';
|
||||
import { accountService } from '../../api/accountService';
|
||||
|
||||
interface Organization {
|
||||
org_name: string;
|
||||
org_id: string;
|
||||
db_name: string;
|
||||
}
|
||||
import React from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, List, ListItem, ListItemButton, ListItemText } from '@mui/material';
|
||||
import type { Organization } from '../../api/accountService';
|
||||
|
||||
interface DbSelectionDialogProps {
|
||||
open: boolean;
|
||||
// The org list is decided by the login flow (scoped to orgs where the user
|
||||
// has work, or the full list for supervisors) - this dialog just renders it.
|
||||
organizations: Organization[];
|
||||
onClose: (selectedOrg?: Organization) => void;
|
||||
}
|
||||
|
||||
export const DbSelectionDialog: React.FC<DbSelectionDialogProps> = ({ open, onClose }) => {
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setLoading(true);
|
||||
accountService.getOrganizations()
|
||||
.then((res: any) => {
|
||||
const sorted = res.sort((a: Organization, b: Organization) =>
|
||||
a.org_name.toLowerCase() > b.org_name.toLowerCase() ? 1 : -1
|
||||
);
|
||||
setOrganizations(sorted);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [open]);
|
||||
export const DbSelectionDialog: React.FC<DbSelectionDialogProps> = ({ open, organizations, onClose }) => {
|
||||
const sorted = [...organizations].sort((a, b) =>
|
||||
a.org_name.toLowerCase() > b.org_name.toLowerCase() ? 1 : -1
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => onClose()} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>Select the organization</DialogTitle>
|
||||
<DialogContent>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<List>
|
||||
{organizations.map((org, index) => (
|
||||
<ListItem disablePadding key={`${org.org_id || org.org_name}-${index}`}>
|
||||
<ListItemButton onClick={() => onClose(org)}>
|
||||
<ListItemText primary={org.org_name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
<List>
|
||||
{sorted.map((org, index) => (
|
||||
<ListItem disablePadding key={`${org.org_id || org.org_name}-${index}`}>
|
||||
<ListItemButton onClick={() => onClose(org)}>
|
||||
<ListItemText primary={org.org_name} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Button, TextField, Container, Alert, Paper } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { accountService } from '../../api/accountService';
|
||||
import type { Organization } from '../../api/accountService';
|
||||
import { DbSelectionDialog } from './DbSelectionDialog';
|
||||
import { useToastStore } from '../../store/toastStore';
|
||||
|
||||
@@ -12,6 +13,7 @@ export const Login: React.FC = () => {
|
||||
const [isInvalid, setIsInvalid] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogOrgs, setDialogOrgs] = useState<Organization[]>([]);
|
||||
const [tempLoginResult, setTempLoginResult] = useState<Awaited<ReturnType<typeof accountService.login>> | null>(null);
|
||||
|
||||
const { login, isAuthenticated } = useAuthStore();
|
||||
@@ -24,6 +26,23 @@ export const Login: React.FC = () => {
|
||||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const commitLogin = (
|
||||
result: NonNullable<typeof tempLoginResult>,
|
||||
org: { org_id: number | string; db_name: string },
|
||||
) => {
|
||||
login({
|
||||
access_token: result.access_token,
|
||||
refresh_token: result.refresh_token,
|
||||
user_id: result.user.user_id,
|
||||
username: result.user.username,
|
||||
email: result.user.email,
|
||||
role: result.user.role_name,
|
||||
org_id: String(org.org_id),
|
||||
db_name: org.db_name,
|
||||
});
|
||||
navigate('/activity-feeds', { replace: true });
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
setIsInvalid(false);
|
||||
setLoading(true);
|
||||
@@ -38,9 +57,27 @@ export const Login: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Every user picks which org/DB to work in after login.
|
||||
setTempLoginResult(result);
|
||||
setDialogOpen(true);
|
||||
// Org entry is driven by where the user actually has work: supervisors
|
||||
// get the full list, everyone else only orgs with records assigned to
|
||||
// (or audited by) them. One org -> straight in; several -> pick;
|
||||
// none -> land in their profile org with an empty feed.
|
||||
let orgs: Organization[] = [];
|
||||
try {
|
||||
orgs = await accountService.getMyOrganizations(result.access_token);
|
||||
} catch {
|
||||
orgs = [];
|
||||
}
|
||||
|
||||
if (orgs.length === 1) {
|
||||
commitLogin(result, orgs[0]);
|
||||
} else if (orgs.length === 0) {
|
||||
useToastStore.getState().showToast('No work assigned to you yet.', 'info');
|
||||
commitLogin(result, { org_id: result.user.org_id, db_name: result.user.db_name });
|
||||
} else {
|
||||
setTempLoginResult(result);
|
||||
setDialogOrgs(orgs);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setIsInvalid(true);
|
||||
useToastStore.getState().showToast('Invalid username or password.', 'error');
|
||||
@@ -49,20 +86,10 @@ export const Login: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDbSelection = (selectedOrg?: { org_id: string; db_name: string; org_name?: string }) => {
|
||||
const handleDbSelection = (selectedOrg?: Organization) => {
|
||||
setDialogOpen(false);
|
||||
if (selectedOrg && tempLoginResult) {
|
||||
login({
|
||||
access_token: tempLoginResult.access_token,
|
||||
refresh_token: tempLoginResult.refresh_token,
|
||||
user_id: tempLoginResult.user.user_id,
|
||||
username: tempLoginResult.user.username,
|
||||
email: tempLoginResult.user.email,
|
||||
role: tempLoginResult.user.role_name,
|
||||
org_id: selectedOrg.org_id,
|
||||
db_name: selectedOrg.db_name,
|
||||
});
|
||||
navigate('/activity-feeds', { replace: true });
|
||||
commitLogin(tempLoginResult, selectedOrg);
|
||||
}
|
||||
setTempLoginResult(null);
|
||||
};
|
||||
@@ -198,7 +225,7 @@ export const Login: React.FC = () => {
|
||||
</Paper>
|
||||
</Container>
|
||||
|
||||
<DbSelectionDialog open={dialogOpen} onClose={handleDbSelection} />
|
||||
<DbSelectionDialog open={dialogOpen} organizations={dialogOrgs} onClose={handleDbSelection} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user