updates on the astryx desing
This commit is contained in:
@@ -1,55 +1,48 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { MdWarningAmber, MdAdd, MdCheckCircleOutline } from 'react-icons/md';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
import { Grid } from '@astryxdesign/core/Grid';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { IconButton } from '@astryxdesign/core/IconButton';
|
||||
import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog';
|
||||
import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { TextArea } from '@astryxdesign/core/TextArea';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
import { Badge } from '@astryxdesign/core/Badge';
|
||||
import { EmptyState } from '@astryxdesign/core/EmptyState';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table';
|
||||
|
||||
import Loader from 'components/Loader';
|
||||
import TableLoader from 'components/nearle_components/TableLoader';
|
||||
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
|
||||
import PageHeader from 'components/nearle_components/PageHeader';
|
||||
import StatCard from 'components/nearle_components/StatCard';
|
||||
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
import { getExceptions, createException, updateExceptionStatus, getHubs } from 'pages/api/doormileApi';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { DT, soft, tableScrollSx, tableHeadSx, tableRowSx } from 'themes/dt/tokens';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
import { TableScroll, AccentAvatar } from 'themes/dt/primitives';
|
||||
|
||||
const BRAND = '#000000';
|
||||
const BRAND = DT.brand;
|
||||
|
||||
const EXCEPTION_TYPES = ['Lost', 'Damaged', 'Misrouted', 'Receiver_Refused', 'Missing_Contents', 'Undeliverable'];
|
||||
const EXCEPTION_TYPE_OPTIONS = EXCEPTION_TYPES.map((t) => ({ value: t, label: t.replace(/_/g, ' ') }));
|
||||
const SEVERITIES = ['Low', 'Medium', 'High', 'Critical'];
|
||||
const SEVERITY_COLOR = { Low: '#94a3b8', Medium: '#f59e0b', High: '#ef4444', Critical: '#7f1d1d' };
|
||||
const SEVERITY_OPTIONS = SEVERITIES.map((s) => ({ value: s, label: s }));
|
||||
const SEVERITY_BADGE = { Low: 'neutral', Medium: 'yellow', High: 'orange', Critical: 'error' };
|
||||
const RESOLVE_STATUS_OPTIONS = [
|
||||
{ value: 'Resolved', label: 'Resolved' },
|
||||
{ value: 'Closed', label: 'Closed' }
|
||||
];
|
||||
|
||||
const emptyForm = { consignmentid: '', tripsheetid: '', hub: null, exceptiontype: 'Damaged', severity: 'Medium', description: '' };
|
||||
const emptyForm = { consignmentid: '', tripsheetid: '', hub: '', exceptiontype: 'Damaged', severity: 'Medium', description: '' };
|
||||
|
||||
const Exceptions = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const queryClient = useQueryClient();
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
@@ -62,6 +55,7 @@ const Exceptions = () => {
|
||||
const { data: exceptions = [], isLoading } = useQuery({ queryKey: ['admin-exceptions'], queryFn: getExceptions });
|
||||
const { data: hubs = [] } = useQuery({ queryKey: ['admin-hubs'], queryFn: getHubs });
|
||||
const hubMap = useMemo(() => new Map((hubs || []).map((h) => [h.hubid, h])), [hubs]);
|
||||
const hubOptions = useMemo(() => (hubs || []).map((h) => ({ value: String(h.hubid), label: h.hubname || `Hub #${h.hubid}` })), [hubs]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
if (!debouncedSearch) return exceptions;
|
||||
@@ -75,7 +69,8 @@ const Exceptions = () => {
|
||||
const total = exceptions.length;
|
||||
const open = exceptions.filter((e) => !['Resolved', 'Closed'].includes(e.status)).length;
|
||||
const critical = exceptions.filter((e) => e.severity === 'Critical').length;
|
||||
return { total, open, critical };
|
||||
const resolved = exceptions.filter((e) => ['Resolved', 'Closed'].includes(e.status)).length;
|
||||
return { total, open, critical, resolved };
|
||||
}, [exceptions]);
|
||||
|
||||
const opentoast = (message, variant = 'error') =>
|
||||
@@ -119,13 +114,15 @@ const Exceptions = () => {
|
||||
createMutation.mutate({
|
||||
consignmentid: form.consignmentid,
|
||||
tripsheetid: form.tripsheetid || undefined,
|
||||
hubid: form.hub?.hubid,
|
||||
hubid: form.hub ? Number(form.hub) : undefined,
|
||||
exceptiontype: form.exceptiontype,
|
||||
severity: form.severity,
|
||||
description: form.description
|
||||
});
|
||||
};
|
||||
|
||||
const set = (key) => (value) => setForm((f) => ({ ...f, [key]: value }));
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoading || createMutation.isPending || resolveMutation.isPending) && <Loader />}
|
||||
@@ -134,211 +131,169 @@ const Exceptions = () => {
|
||||
title="Exceptions"
|
||||
subtitle="Live · Lost / Damaged / Misrouted Parcels"
|
||||
live
|
||||
action={
|
||||
<Button variant="contained" startIcon={<MdAdd size={16} />} onClick={() => setDialogOpen(true)} sx={{ bgcolor: BRAND, whiteSpace: 'nowrap' }}>
|
||||
Log Exception
|
||||
</Button>
|
||||
}
|
||||
action={<Button label="Log Exception" variant="primary" icon={<MdAdd size={16} />} onClick={() => setDialogOpen(true)} />}
|
||||
/>
|
||||
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
<Grid item xs={4}>
|
||||
<StatCard title="Total" value={stats.total} icon={<MdWarningAmber size={20} />} color={BRAND} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<StatCard title="Open" value={stats.open} icon={<MdWarningAmber size={20} />} color="#f59e0b" />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<StatCard title="Critical" value={stats.critical} icon={<MdWarningAmber size={20} />} color="#ef4444" />
|
||||
</Grid>
|
||||
<Grid columns={{ minWidth: 180, max: 4, repeat: 'fit' }} gap={3}>
|
||||
<StatCard title="Total" value={stats.total} icon={<MdWarningAmber size={20} />} color={BRAND} />
|
||||
<StatCard title="Open" value={stats.open} icon={<MdWarningAmber size={20} />} color="#f59e0b" />
|
||||
<StatCard title="Critical" value={stats.critical} icon={<MdWarningAmber size={20} />} color="#ef4444" />
|
||||
<StatCard title="Resolved" value={stats.resolved} icon={<MdCheckCircleOutline size={20} />} color="#10b981" />
|
||||
</Grid>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{ mt: { xs: 1.5, md: 2 }, p: { xs: 1, md: 1.5 }, borderTopLeftRadius: DT.radiusCard / 8, borderTopRightRadius: DT.radiusCard / 8, border: '1px solid', borderColor: DT.borderSubtle, borderBottom: 0, background: '#fff' }}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems={{ xs: 'stretch', sm: 'center' }} justifyContent="space-between" spacing={1.25}>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{exceptions.length} total · {rows.length} shown
|
||||
</Typography>
|
||||
<Box sx={{ width: { xs: '100%', sm: 280 } }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search exceptions (ctrl+k)"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<VStack gap={0} padding={0} style={{ marginTop: 16 }}>
|
||||
<Card padding={3} elevation="none" style={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderBottom: 'none' }}>
|
||||
<HStack justify="between" vAlign="center" gap={3} wrap="wrap">
|
||||
<Text weight="bold">
|
||||
{exceptions.length} total · {rows.length} shown
|
||||
</Text>
|
||||
<div style={{ width: 280, maxWidth: '100%' }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search exceptions (ctrl+k)"
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</Card>
|
||||
|
||||
<Paper elevation={0} sx={{ borderBottomLeftRadius: DT.radiusCard / 8, borderBottomRightRadius: DT.radiusCard / 8, border: '1px solid', borderColor: DT.borderSubtle, overflow: 'hidden', background: '#fff' }}>
|
||||
<TableContainer sx={{ maxHeight: { xs: 'calc(100vh - 260px)', md: 'calc(100vh - 190px)' }, ...tableScrollSx }}>
|
||||
<Table stickyHeader sx={{ minWidth: isMobile ? 960 : 1020 }}>
|
||||
<TableHead>
|
||||
<TableRow sx={tableHeadSx}>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Consignment</TableCell>
|
||||
<TableCell>Hub</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Severity</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading && <OrdersTableSkeleton col={6} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<Card padding={0} elevation="none" style={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, overflow: 'hidden' }}>
|
||||
<TableScroll minWidth={1020}>
|
||||
<Table density="balanced" dividers="rows" hasHover>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdWarningAmber size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
No exceptions logged
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableHeaderCell>#</TableHeaderCell>
|
||||
<TableHeaderCell>Consignment</TableHeaderCell>
|
||||
<TableHeaderCell>Hub</TableHeaderCell>
|
||||
<TableHeaderCell>Type</TableHeaderCell>
|
||||
<TableHeaderCell>Severity</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<TableRow key={row.exceptionid || index} sx={tableRowSx}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{row.consignmentid}</TableCell>
|
||||
<TableCell>{hubMap.get(row.hubid)?.hubname || (row.hubid ? `Hub #${row.hubid}` : '—')}</TableCell>
|
||||
<TableCell>{row.exceptiontype}</TableCell>
|
||||
<TableCell>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
px: 1,
|
||||
py: 0.375,
|
||||
borderRadius: 999,
|
||||
bgcolor: soft(SEVERITY_COLOR[row.severity] || '#94a3b8'),
|
||||
color: SEVERITY_COLOR[row.severity] || '#94a3b8',
|
||||
fontSize: 11,
|
||||
fontWeight: 800
|
||||
}}
|
||||
>
|
||||
{row.severity}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 220 }}>
|
||||
<Typography variant="body2" noWrap title={row.description}>
|
||||
{row.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{row.status || 'Open'}</TableCell>
|
||||
<TableCell align="right">
|
||||
{!['Resolved', 'Closed'].includes(row.status) && (
|
||||
<Tooltip title="Resolve / Close">
|
||||
<IconButton size="small" onClick={() => setResolveRow(row)} sx={{ color: '#10b981' }}>
|
||||
<MdCheckCircleOutline size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading && <TableLoader rows={5} columns={8} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8}>
|
||||
<EmptyState
|
||||
title="No exceptions logged"
|
||||
description="Log lost, damaged or misrouted parcels here to track resolution."
|
||||
icon={<MdWarningAmber size={28} />}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<TableRow key={row.exceptionid || index}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{row.consignmentid}</TableCell>
|
||||
<TableCell>{hubMap.get(row.hubid)?.hubname || (row.hubid ? `Hub #${row.hubid}` : '—')}</TableCell>
|
||||
<TableCell>{row.exceptiontype}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={SEVERITY_BADGE[row.severity] || 'neutral'} label={row.severity} />
|
||||
</TableCell>
|
||||
<TableCell style={{ maxWidth: 220 }}>
|
||||
<Text style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={row.description}>
|
||||
{row.description}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>{row.status || 'Open'}</TableCell>
|
||||
<TableCell>
|
||||
{!['Resolved', 'Closed'].includes(row.status) && (
|
||||
<HStack justify="end">
|
||||
<IconButton
|
||||
label="Resolve / Close"
|
||||
tooltip="Resolve / Close"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdCheckCircleOutline size={16} />}
|
||||
onClick={() => setResolveRow(row)}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
</Card>
|
||||
</VStack>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>Log Exception</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Consignment ID</InputLabel>
|
||||
<TextField value={form.consignmentid} onChange={(e) => setForm((f) => ({ ...f, consignmentid: e.target.value }))} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Tripsheet ID (optional)</InputLabel>
|
||||
<TextField value={form.tripsheetid} onChange={(e) => setForm((f) => ({ ...f, tripsheetid: e.target.value }))} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Hub (optional)</InputLabel>
|
||||
<Autocomplete
|
||||
options={hubs || []}
|
||||
getOptionLabel={(option) => option.hubname || ''}
|
||||
value={form.hub}
|
||||
onChange={(e, value) => setForm((f) => ({ ...f, hub: value }))}
|
||||
renderInput={(params) => <TextField {...params} placeholder="Choose hub" />}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Type</InputLabel>
|
||||
<Select fullWidth value={form.exceptiontype} onChange={(e) => setForm((f) => ({ ...f, exceptiontype: e.target.value }))}>
|
||||
{EXCEPTION_TYPES.map((t) => (
|
||||
<MenuItem key={t} value={t}>
|
||||
{t.replace(/_/g, ' ')}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Severity</InputLabel>
|
||||
<Select fullWidth value={form.severity} onChange={(e) => setForm((f) => ({ ...f, severity: e.target.value }))}>
|
||||
{SEVERITIES.map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{s}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Description</InputLabel>
|
||||
<TextField multiline minRows={2} value={form.description} onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button variant="contained" onClick={handleCreate} sx={{ bgcolor: BRAND }}>
|
||||
Log Exception
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<Dialog isOpen={dialogOpen} onOpenChange={(next) => !next && setDialogOpen(false)} width={640} purpose="form">
|
||||
<Layout
|
||||
header={
|
||||
<DialogHeader
|
||||
title="Log Exception"
|
||||
subtitle="Record a lost, damaged or misrouted parcel"
|
||||
onOpenChange={(next) => !next && setDialogOpen(false)}
|
||||
startContent={
|
||||
<AccentAvatar color={BRAND} selected size={36}>
|
||||
<MdWarningAmber size={18} />
|
||||
</AccentAvatar>
|
||||
}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<LayoutContent padding={4}>
|
||||
<VStack gap={3} padding={0}>
|
||||
<Grid columns={{ minWidth: 220 }} gap={3}>
|
||||
<TextInput label="Consignment ID" value={form.consignmentid} onChange={set('consignmentid')} isRequired />
|
||||
<TextInput label="Tripsheet ID" value={form.tripsheetid} onChange={set('tripsheetid')} isOptional />
|
||||
<Selector label="Hub" options={hubOptions} value={form.hub} onChange={set('hub')} placeholder="Choose hub" hasSearch isOptional />
|
||||
<Selector label="Type" options={EXCEPTION_TYPE_OPTIONS} value={form.exceptiontype} onChange={set('exceptiontype')} />
|
||||
<Selector label="Severity" options={SEVERITY_OPTIONS} value={form.severity} onChange={set('severity')} />
|
||||
</Grid>
|
||||
<TextArea label="Description" value={form.description} onChange={set('description')} isRequired />
|
||||
</VStack>
|
||||
</LayoutContent>
|
||||
}
|
||||
footer={
|
||||
<LayoutFooter hasDivider>
|
||||
<HStack gap={2} justify="end">
|
||||
<Button label="Cancel" variant="secondary" onClick={() => setDialogOpen(false)} isDisabled={createMutation.isPending} />
|
||||
<Button label="Log Exception" variant="primary" onClick={handleCreate} isLoading={createMutation.isPending} />
|
||||
</HStack>
|
||||
</LayoutFooter>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={!!resolveRow} onClose={() => setResolveRow(null)} maxWidth="xs" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>Resolve Exception</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select fullWidth value={resolveStatus} onChange={(e) => setResolveStatus(e.target.value)}>
|
||||
<MenuItem value="Resolved">Resolved</MenuItem>
|
||||
<MenuItem value="Closed">Closed</MenuItem>
|
||||
</Select>
|
||||
</Stack>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Resolution Notes</InputLabel>
|
||||
<TextField multiline minRows={2} value={resolution} onChange={(e) => setResolution(e.target.value)} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setResolveRow(null)}>Cancel</Button>
|
||||
<Button variant="contained" onClick={() => resolveMutation.mutate()} sx={{ bgcolor: BRAND }}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<Dialog isOpen={!!resolveRow} onOpenChange={(next) => !next && setResolveRow(null)} width={480} purpose="form">
|
||||
<Layout
|
||||
header={
|
||||
<DialogHeader
|
||||
title="Resolve Exception"
|
||||
subtitle={resolveRow ? `Consignment ${resolveRow.consignmentid}` : ''}
|
||||
onOpenChange={(next) => !next && setResolveRow(null)}
|
||||
startContent={
|
||||
<AccentAvatar color="#10b981" selected size={36}>
|
||||
<MdCheckCircleOutline size={18} />
|
||||
</AccentAvatar>
|
||||
}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<LayoutContent padding={4}>
|
||||
<VStack gap={3} padding={0}>
|
||||
<Selector label="Status" options={RESOLVE_STATUS_OPTIONS} value={resolveStatus} onChange={setResolveStatus} />
|
||||
<TextArea label="Resolution Notes" value={resolution} onChange={setResolution} isOptional />
|
||||
</VStack>
|
||||
</LayoutContent>
|
||||
}
|
||||
footer={
|
||||
<LayoutFooter hasDivider>
|
||||
<HStack gap={2} justify="end">
|
||||
<Button label="Cancel" variant="secondary" onClick={() => setResolveRow(null)} isDisabled={resolveMutation.isPending} />
|
||||
<Button label="Save" variant="primary" onClick={() => resolveMutation.mutate()} isLoading={resolveMutation.isPending} />
|
||||
</HStack>
|
||||
</LayoutFooter>
|
||||
}
|
||||
/>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user