562 lines
22 KiB
TypeScript
562 lines
22 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
|
Paper, Checkbox, FormControl, InputLabel, Select, MenuItem, ToggleButtonGroup,
|
|
ToggleButton, Button, CircularProgress, TablePagination, Typography, Divider,
|
|
Dialog, DialogTitle, DialogContent, DialogActions, DialogContentText, TextField,
|
|
ListItemText, Alert,
|
|
} from '@mui/material';
|
|
import { adminService } from '../../api/adminService';
|
|
import type { AllocationRecord, AdminUser } from '../../api/adminService';
|
|
import { accountService } from '../../api/accountService';
|
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
|
import { useFeedStore } from '../../store/feedStore';
|
|
import { useToastStore } from '../../store/toastStore';
|
|
|
|
interface Organization {
|
|
org_id: string;
|
|
org_name: string;
|
|
db_name: string;
|
|
}
|
|
|
|
interface SiteOption {
|
|
site_id: string;
|
|
site_name: string;
|
|
}
|
|
|
|
// Mirrors the site-parsing logic in ClientsOverview.tsx/MainLayout.tsx —
|
|
// /Master/site's response shape varies (responseData[0].records, nested
|
|
// sub_sites, or a plain array).
|
|
const parseSites = (sitesRes: any): SiteOption[] => {
|
|
let parsedSites: any[] = [];
|
|
if (sitesRes?.responseData?.[0]?.records) {
|
|
parsedSites = sitesRes.responseData[0].records;
|
|
if (parsedSites.length === 1 && Array.isArray(parsedSites[0])) {
|
|
parsedSites = parsedSites[0];
|
|
} else if (parsedSites.length > 0 && Array.isArray(parsedSites[0])) {
|
|
parsedSites = parsedSites.flat();
|
|
}
|
|
} else if (Array.isArray(sitesRes)) {
|
|
parsedSites = sitesRes;
|
|
} else if (sitesRes?.data && Array.isArray(sitesRes.data)) {
|
|
parsedSites = sitesRes.data;
|
|
}
|
|
|
|
return parsedSites
|
|
.map((site) => ({
|
|
site_id: site.site_id || (site.sub_sites || []).map((s: any) => s.site_id).filter(Boolean).join(','),
|
|
site_name: site.site_name || site.name || site.siteName || site.site_id,
|
|
}))
|
|
.filter((s) => s.site_id);
|
|
};
|
|
|
|
const PAGE_SIZE = 25;
|
|
const BULK_PRESETS = [1000, 2000, 5000, 10000];
|
|
|
|
// count split equally, remainder to the first users: 100/3 -> 34/33/33
|
|
const computeSplit = (count: number, n: number): number[] => {
|
|
const base = Math.floor(count / n);
|
|
const rem = count % n;
|
|
return Array.from({ length: n }, (_, i) => base + (i < rem ? 1 : 0));
|
|
};
|
|
|
|
type ConfirmAction =
|
|
| { kind: 'bulk'; count: number }
|
|
| { kind: 'unassignAll' }
|
|
| { kind: 'moveAll'; toUserId: number }
|
|
| { kind: 'unassignSelected' };
|
|
|
|
export const Allocation: React.FC = () => {
|
|
const { assets } = useFeedStore();
|
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
const [dbName, setDbName] = useState('');
|
|
const [type, setType] = useState<'audits' | 'rectification'>('audits');
|
|
const [sort, setSort] = useState<'oldest' | 'newest'>('oldest');
|
|
|
|
const [dateFrom, setDateFrom] = useState('');
|
|
const [dateTo, setDateTo] = useState('');
|
|
const [assetId, setAssetId] = useState('');
|
|
const [siteId, setSiteId] = useState('');
|
|
const [sites, setSites] = useState<SiteOption[]>([]);
|
|
|
|
const [users, setUsers] = useState<AdminUser[]>([]);
|
|
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
|
// '' = all, 'null' = unassigned, otherwise a user_id string
|
|
const [assignedFilter, setAssignedFilter] = useState('');
|
|
const [customCount, setCustomCount] = useState('');
|
|
const [moveTargetId, setMoveTargetId] = useState('');
|
|
|
|
const [records, setRecords] = useState<AllocationRecord[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(0);
|
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
|
const [loading, setLoading] = useState(false);
|
|
const [assigning, setAssigning] = useState(false);
|
|
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
const [orgsRes, usersRes] = await Promise.all([
|
|
accountService.getOrganizations(),
|
|
adminService.getUsers(),
|
|
]);
|
|
const orgs = (orgsRes as any) || [];
|
|
setOrganizations(orgs);
|
|
setUsers(usersRes.filter((u) => !u.disabled));
|
|
if (orgs.length > 0) setDbName(orgs[0].db_name);
|
|
} catch (err) {
|
|
useToastStore.getState().showToast('Failed to load organizations/users.', 'error');
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
// Sites are org-scoped, and the org being managed here may differ from the
|
|
// admin's own logged-in org, so this refetches whenever the selection changes
|
|
// rather than reusing the global feedStore (which is scoped to the admin's own org).
|
|
useEffect(() => {
|
|
const org = organizations.find((o) => o.db_name === dbName);
|
|
if (!org) return;
|
|
(async () => {
|
|
try {
|
|
const sitesRes = await activityFeedsService.getOrganizationSites(org.org_id);
|
|
setSites(parseSites(sitesRes));
|
|
} catch (err) {
|
|
useToastStore.getState().showToast('Failed to load sites for this organization.', 'error');
|
|
}
|
|
})();
|
|
setSiteId('');
|
|
}, [dbName, organizations]);
|
|
|
|
// dateTo is a bare date (e.g. "2026-07-09"); video_date is a full timestamp,
|
|
// so comparing "<= 2026-07-09" would exclude same-day records after
|
|
// midnight - extend it to end-of-day. Also normalize the order: both
|
|
// fields are independent and unset by default, so it's easy to end up with
|
|
// From after To while filling them in, which would silently match zero
|
|
// rows instead of erroring.
|
|
const [effectiveDateFrom, effectiveDateTo] = (() => {
|
|
if (!dateFrom || !dateTo) return [dateFrom || undefined, dateTo ? `${dateTo}T23:59:59` : undefined];
|
|
return dateFrom <= dateTo
|
|
? [dateFrom, `${dateTo}T23:59:59`]
|
|
: [dateTo, `${dateFrom}T23:59:59`];
|
|
})();
|
|
|
|
const load = useCallback(async () => {
|
|
if (!dbName) return;
|
|
setLoading(true);
|
|
try {
|
|
const result = await adminService.getAllocationRecords({
|
|
dbName, type, sort, limit: PAGE_SIZE, offset: page * PAGE_SIZE,
|
|
dateFrom: effectiveDateFrom,
|
|
dateTo: effectiveDateTo,
|
|
assetId: assetId || undefined,
|
|
siteId: siteId || undefined,
|
|
assignedUserId: assignedFilter === '' ? undefined : assignedFilter === 'null' ? 'null' : Number(assignedFilter),
|
|
});
|
|
setRecords(result.records);
|
|
setTotal(result.total);
|
|
} catch (err) {
|
|
useToastStore.getState().showToast('Failed to load records.', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [dbName, type, sort, page, effectiveDateFrom, effectiveDateTo, assetId, siteId, assignedFilter]);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
useEffect(() => {
|
|
setPage(0);
|
|
setSelectedIds(new Set());
|
|
}, [dbName, type, sort, dateFrom, dateTo, assetId, siteId, assignedFilter]);
|
|
|
|
const toggleRow = (id: number) => {
|
|
setSelectedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const toggleAll = () => {
|
|
if (selectedIds.size === records.length) {
|
|
setSelectedIds(new Set());
|
|
} else {
|
|
setSelectedIds(new Set(records.map((r) => r.id)));
|
|
}
|
|
};
|
|
|
|
const selectedUsers = users.filter((u) => selectedUserIds.includes(u.user_id));
|
|
const selectedOrg = organizations.find((o) => o.db_name === dbName);
|
|
const filterUser = users.find((u) => String(u.user_id) === assignedFilter);
|
|
|
|
const perUserToast = (perUser: { user_id: number; assigned: number }[]) =>
|
|
perUser
|
|
.map((p) => `${users.find((u) => u.user_id === p.user_id)?.username || `#${p.user_id}`}: ${p.assigned}`)
|
|
.join(', ');
|
|
|
|
const handleAllocateSelected = async () => {
|
|
if (selectedIds.size === 0 || selectedUserIds.length === 0) return;
|
|
setAssigning(true);
|
|
try {
|
|
const result = await adminService.assignAllocation({
|
|
dbName, type, ids: Array.from(selectedIds), user_ids: selectedUserIds,
|
|
});
|
|
useToastStore.getState().showToast(
|
|
`Allocated ${result.assigned} record(s) — ${perUserToast(result.perUser)}`, 'success');
|
|
setSelectedIds(new Set());
|
|
load();
|
|
} catch (err: any) {
|
|
useToastStore.getState().showToast(err?.response?.data?.message || 'Failed to allocate records.', 'error');
|
|
} finally {
|
|
setAssigning(false);
|
|
}
|
|
};
|
|
|
|
const handleBulkAllocate = async (count: number) => {
|
|
if (selectedUserIds.length === 0) return;
|
|
setConfirmAction(null);
|
|
setAssigning(true);
|
|
try {
|
|
const result = await adminService.assignAllocation({
|
|
dbName, type, count, sort, user_ids: selectedUserIds,
|
|
dateFrom: effectiveDateFrom,
|
|
dateTo: effectiveDateTo,
|
|
assetId: assetId || undefined,
|
|
siteId: siteId || undefined,
|
|
});
|
|
useToastStore.getState().showToast(
|
|
`Allocated ${result.assigned} record(s) — ${perUserToast(result.perUser)}`, 'success');
|
|
setSelectedIds(new Set());
|
|
setPage(0);
|
|
load();
|
|
} catch (err: any) {
|
|
useToastStore.getState().showToast(err?.response?.data?.message || 'Failed to bulk-allocate records.', 'error');
|
|
} finally {
|
|
setAssigning(false);
|
|
}
|
|
};
|
|
|
|
const handleReassign = async (params: { fromUserId?: number; toUserId: number | null; ids?: number[] }) => {
|
|
setConfirmAction(null);
|
|
setAssigning(true);
|
|
try {
|
|
const result = await adminService.reassignAllocation({ dbName, type, ...params });
|
|
const verb = params.toUserId === null ? 'Unassigned' : 'Moved';
|
|
useToastStore.getState().showToast(`${verb} ${result.moved} pending record(s).`, 'success');
|
|
setSelectedIds(new Set());
|
|
setMoveTargetId('');
|
|
load();
|
|
} catch (err: any) {
|
|
useToastStore.getState().showToast(err?.response?.data?.message || 'Failed to update allocation.', 'error');
|
|
} finally {
|
|
setAssigning(false);
|
|
}
|
|
};
|
|
|
|
const customCountNum = parseInt(customCount, 10);
|
|
const customCountValid = Number.isInteger(customCountNum) && customCountNum > 0 && customCountNum <= 10000;
|
|
|
|
const splitPreview = (count: number) => {
|
|
if (selectedUsers.length === 0) return '';
|
|
const split = computeSplit(count, selectedUsers.length);
|
|
return selectedUsers.map((u, i) => `${u.username}: ${split[i]}`).join(' · ');
|
|
};
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
<FormControl size="small" sx={{ minWidth: 180 }}>
|
|
<InputLabel>Organization</InputLabel>
|
|
<Select label="Organization" value={dbName} onChange={(e) => setDbName(e.target.value)}>
|
|
{organizations.map((o) => <MenuItem key={o.org_id} value={o.db_name}>{o.org_name}</MenuItem>)}
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<ToggleButtonGroup
|
|
value={type}
|
|
exclusive
|
|
onChange={(_, value) => value && setType(value)}
|
|
size="small"
|
|
>
|
|
<ToggleButton value="audits">Audits</ToggleButton>
|
|
<ToggleButton value="rectification">Rectification</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
|
|
<ToggleButtonGroup
|
|
value={sort}
|
|
exclusive
|
|
onChange={(_, value) => value && setSort(value)}
|
|
size="small"
|
|
>
|
|
<ToggleButton value="oldest">Oldest first</ToggleButton>
|
|
<ToggleButton value="newest">Newest first</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Box>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, mb: 3, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
<TextField
|
|
label="Video date from"
|
|
type="date"
|
|
size="small"
|
|
value={dateFrom}
|
|
onChange={(e) => setDateFrom(e.target.value)}
|
|
slotProps={{ inputLabel: { shrink: true } }}
|
|
/>
|
|
<TextField
|
|
label="Video date to"
|
|
type="date"
|
|
size="small"
|
|
value={dateTo}
|
|
onChange={(e) => setDateTo(e.target.value)}
|
|
slotProps={{ inputLabel: { shrink: true } }}
|
|
/>
|
|
<FormControl size="small" sx={{ minWidth: 200 }}>
|
|
<InputLabel>Asset</InputLabel>
|
|
<Select label="Asset" value={assetId} onChange={(e) => setAssetId(e.target.value)}>
|
|
<MenuItem value="">All assets</MenuItem>
|
|
{assets.map((a: any) => <MenuItem key={a.id} value={a.id}>{a.name}</MenuItem>)}
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl size="small" sx={{ minWidth: 200 }}>
|
|
<InputLabel>Site</InputLabel>
|
|
<Select label="Site" value={siteId} onChange={(e) => setSiteId(e.target.value)}>
|
|
<MenuItem value="">All sites</MenuItem>
|
|
{sites.map((s) => <MenuItem key={s.site_id} value={s.site_id}>{s.site_name}</MenuItem>)}
|
|
</Select>
|
|
</FormControl>
|
|
<FormControl size="small" sx={{ minWidth: 200 }}>
|
|
<InputLabel>Assigned to</InputLabel>
|
|
<Select label="Assigned to" value={assignedFilter} onChange={(e) => setAssignedFilter(e.target.value)}>
|
|
<MenuItem value="">All records</MenuItem>
|
|
<MenuItem value="null">Unassigned only</MenuItem>
|
|
{users.map((u) => <MenuItem key={u.user_id} value={String(u.user_id)}>{u.username}</MenuItem>)}
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
|
|
{/* Absent-user actions: visible when viewing one user's queue */}
|
|
{filterUser && (
|
|
<Alert severity="info" sx={{ mb: 2, alignItems: 'center' }}
|
|
action={
|
|
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center' }}>
|
|
<Button
|
|
size="small"
|
|
color="warning"
|
|
variant="outlined"
|
|
disabled={assigning || total === 0}
|
|
onClick={() => setConfirmAction({ kind: 'unassignAll' })}
|
|
>
|
|
Unassign all pending
|
|
</Button>
|
|
<FormControl size="small" sx={{ minWidth: 170 }}>
|
|
<InputLabel>Move all pending to</InputLabel>
|
|
<Select
|
|
label="Move all pending to"
|
|
value={moveTargetId}
|
|
disabled={assigning || total === 0}
|
|
onChange={(e) => {
|
|
const target = Number(e.target.value);
|
|
setMoveTargetId(e.target.value);
|
|
setConfirmAction({ kind: 'moveAll', toUserId: target });
|
|
}}
|
|
>
|
|
{users.filter((u) => String(u.user_id) !== assignedFilter).map((u) => (
|
|
<MenuItem key={u.user_id} value={String(u.user_id)}>{u.username}</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
}
|
|
>
|
|
{total.toLocaleString()} pending record(s) assigned to <strong>{filterUser.username}</strong>
|
|
{' '}(matching current filters)
|
|
</Alert>
|
|
)}
|
|
|
|
<FormControl size="small" sx={{ minWidth: 280, mb: 2 }}>
|
|
<InputLabel>Allocate to (select one or more)</InputLabel>
|
|
<Select
|
|
multiple
|
|
label="Allocate to (select one or more)"
|
|
value={selectedUserIds}
|
|
onChange={(e) => setSelectedUserIds(typeof e.target.value === 'string' ? [] : (e.target.value as number[]))}
|
|
renderValue={(ids) =>
|
|
users.filter((u) => (ids as number[]).includes(u.user_id)).map((u) => u.username).join(', ')
|
|
}
|
|
>
|
|
{users.map((u) => (
|
|
<MenuItem key={u.user_id} value={u.user_id}>
|
|
<Checkbox checked={selectedUserIds.includes(u.user_id)} size="small" />
|
|
<ListItemText primary={`${u.username} (${u.role_name})`} />
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, mb: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
<Typography variant="body2">{selectedIds.size} selected on this page</Typography>
|
|
<Button
|
|
variant="contained"
|
|
disabled={selectedIds.size === 0 || selectedUserIds.length === 0 || assigning}
|
|
onClick={handleAllocateSelected}
|
|
>
|
|
{selectedUserIds.length > 1 ? 'Split selected equally' : 'Allocate selected'}
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
color="warning"
|
|
disabled={selectedIds.size === 0 || assigning}
|
|
onClick={() => setConfirmAction({ kind: 'unassignSelected' })}
|
|
>
|
|
Unassign selected
|
|
</Button>
|
|
</Box>
|
|
|
|
<Box sx={{ display: 'flex', gap: 1.5, mb: 3, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
Bulk assign unassigned records (total, split equally · matches current filters):
|
|
</Typography>
|
|
{BULK_PRESETS.map((count) => (
|
|
<Button
|
|
key={count}
|
|
variant="outlined"
|
|
size="small"
|
|
disabled={selectedUserIds.length === 0 || assigning}
|
|
onClick={() => setConfirmAction({ kind: 'bulk', count })}
|
|
>
|
|
{count.toLocaleString()}
|
|
</Button>
|
|
))}
|
|
<TextField
|
|
label="Custom count"
|
|
size="small"
|
|
type="number"
|
|
value={customCount}
|
|
onChange={(e) => setCustomCount(e.target.value)}
|
|
sx={{ width: 130 }}
|
|
slotProps={{ htmlInput: { min: 1, max: 10000 } }}
|
|
/>
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
disabled={selectedUserIds.length === 0 || !customCountValid || assigning}
|
|
onClick={() => setConfirmAction({ kind: 'bulk', count: customCountNum })}
|
|
>
|
|
Assign
|
|
</Button>
|
|
</Box>
|
|
|
|
<Divider sx={{ mb: 2 }} />
|
|
|
|
{loading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>
|
|
) : (
|
|
<TableContainer component={Paper}>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell padding="checkbox">
|
|
<Checkbox
|
|
checked={records.length > 0 && selectedIds.size === records.length}
|
|
indeterminate={selectedIds.size > 0 && selectedIds.size < records.length}
|
|
onChange={toggleAll}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>Anomaly ID</TableCell>
|
|
<TableCell>Asset</TableCell>
|
|
<TableCell>Plaza</TableCell>
|
|
<TableCell>Chainage</TableCell>
|
|
<TableCell>Video Date</TableCell>
|
|
<TableCell>Assigned To</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{records.map((r) => (
|
|
<TableRow key={r.id} selected={selectedIds.has(r.id)}>
|
|
<TableCell padding="checkbox">
|
|
<Checkbox checked={selectedIds.has(r.id)} onChange={() => toggleRow(r.id)} />
|
|
</TableCell>
|
|
<TableCell>{r.id}</TableCell>
|
|
<TableCell>{r.Asset || '—'}</TableCell>
|
|
<TableCell>{r.Plaza || '—'}</TableCell>
|
|
<TableCell>{r.Chainage || '—'}</TableCell>
|
|
<TableCell>{r.video_date ? new Date(r.video_date).toLocaleDateString() : '—'}</TableCell>
|
|
<TableCell>{r.assigned_username || '—'}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
<TablePagination
|
|
component="div"
|
|
count={total}
|
|
page={page}
|
|
onPageChange={(_, newPage) => setPage(newPage)}
|
|
rowsPerPage={PAGE_SIZE}
|
|
rowsPerPageOptions={[PAGE_SIZE]}
|
|
/>
|
|
</TableContainer>
|
|
)}
|
|
|
|
<Dialog open={confirmAction !== null} onClose={() => { setConfirmAction(null); setMoveTargetId(''); }}>
|
|
<DialogTitle>
|
|
{confirmAction?.kind === 'bulk' && 'Confirm bulk allocation'}
|
|
{confirmAction?.kind === 'unassignAll' && 'Unassign all pending'}
|
|
{confirmAction?.kind === 'moveAll' && 'Move all pending'}
|
|
{confirmAction?.kind === 'unassignSelected' && 'Unassign selected'}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
{confirmAction?.kind === 'bulk' && (
|
|
<DialogContentText component="div">
|
|
Split the first {confirmAction.count.toLocaleString()} {sort} <strong>unassigned</strong> {type} record(s) in{' '}
|
|
{selectedOrg?.org_name || dbName} (matching the current date/asset/site filters) equally:
|
|
<Box sx={{ mt: 1.5, fontFamily: 'monospace', fontSize: '0.9rem' }}>
|
|
{splitPreview(confirmAction.count)}
|
|
</Box>
|
|
<Box sx={{ mt: 1.5 }}>
|
|
If fewer records are available, users earlier in the list are filled first.
|
|
</Box>
|
|
</DialogContentText>
|
|
)}
|
|
{confirmAction?.kind === 'unassignAll' && (
|
|
<DialogContentText>
|
|
Remove the assignment from all {total.toLocaleString()} pending record(s) currently allocated to{' '}
|
|
<strong>{filterUser?.username}</strong> in {selectedOrg?.org_name || dbName}? Records they already
|
|
audited keep their name for history/efficiency stats.
|
|
</DialogContentText>
|
|
)}
|
|
{confirmAction?.kind === 'moveAll' && (
|
|
<DialogContentText>
|
|
Move all {total.toLocaleString()} pending record(s) from <strong>{filterUser?.username}</strong> to{' '}
|
|
<strong>{users.find((u) => u.user_id === confirmAction.toUserId)?.username}</strong>? Records already
|
|
audited by {filterUser?.username} keep their name for history/efficiency stats.
|
|
</DialogContentText>
|
|
)}
|
|
{confirmAction?.kind === 'unassignSelected' && (
|
|
<DialogContentText>
|
|
Remove the assignment from the {selectedIds.size} selected pending record(s)?
|
|
</DialogContentText>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => { setConfirmAction(null); setMoveTargetId(''); }}>Cancel</Button>
|
|
<Button
|
|
variant="contained"
|
|
onClick={() => {
|
|
if (!confirmAction) return;
|
|
if (confirmAction.kind === 'bulk') handleBulkAllocate(confirmAction.count);
|
|
else if (confirmAction.kind === 'unassignAll') handleReassign({ fromUserId: Number(assignedFilter), toUserId: null });
|
|
else if (confirmAction.kind === 'moveAll') handleReassign({ fromUserId: Number(assignedFilter), toUserId: confirmAction.toUserId });
|
|
else if (confirmAction.kind === 'unassignSelected') handleReassign({ toUserId: null, ids: Array.from(selectedIds) });
|
|
}}
|
|
>
|
|
Confirm
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Box>
|
|
);
|
|
};
|