181 lines
6.3 KiB
TypeScript
181 lines
6.3 KiB
TypeScript
'use client';
|
|
import { useState } from 'react';
|
|
import { useLocale, useTranslations } from 'next-intl';
|
|
import { useSnackbar } from 'notistack';
|
|
import {
|
|
Box,
|
|
Chip,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
FormControlLabel,
|
|
MenuItem,
|
|
Skeleton,
|
|
Stack,
|
|
Switch,
|
|
TextField,
|
|
} from '@mui/material';
|
|
import { AppButton } from '@/components';
|
|
import { AdminDataTable, AdminEmptyState, AdminErrorState, AdminPageHeader, type AdminTableColumn } from '@/components/admin';
|
|
import { formatShamsiDate } from '@/utils';
|
|
import { useAdminCapabilities } from '@/hooks';
|
|
import type { Holiday, HolidayInput, HolidayType } from '@/services/admin/types';
|
|
import { useHolidays, useUpsertHoliday } from '@/services/admin';
|
|
|
|
const HOLIDAY_TYPES: readonly HolidayType[] = ['official', 'religious', 'national'];
|
|
|
|
/**
|
|
* Iranian-holiday calendar manager (f15). Lists `iranian_holidays`, each with its Shamsi date, name, type,
|
|
* and an `is_bank_closed` flag — the flag that shifts payout scheduling (the copy surfaces that consequence).
|
|
* The client only maintains the calendar the **server** uses for the next-business-day shift; it never
|
|
* computes the shift itself (phase §5).
|
|
*/
|
|
export default function AdminHolidaysPage() {
|
|
const t = useTranslations('admin');
|
|
const locale = useLocale();
|
|
const caps = useAdminCapabilities();
|
|
const holidays = useHolidays({}, 1);
|
|
const [editing, setEditing] = useState<Holiday | 'new' | null>(null);
|
|
|
|
const columns: AdminTableColumn<Holiday>[] = [
|
|
{ key: 'date', header: t('hol_col_date'), render: (h) => formatShamsiDate(h.holidayDate, locale) },
|
|
{ key: 'name', header: t('hol_col_name'), render: (h) => h.nameFa },
|
|
{ key: 'type', header: t('hol_col_type'), render: (h) => <Chip size="small" variant="outlined" label={t(`htype_${h.type}`)} /> },
|
|
{
|
|
key: 'bank',
|
|
header: t('hol_col_bank'),
|
|
render: (h) => (
|
|
<Chip
|
|
size="small"
|
|
label={h.isBankClosed ? t('yes') : t('no')}
|
|
sx={{
|
|
bgcolor: h.isBankClosed ? 'var(--bal-warning)' : 'var(--bal-divider)',
|
|
color: h.isBankClosed ? 'var(--bal-warning-contrast)' : 'var(--bal-text-secondary)',
|
|
fontWeight: 700,
|
|
}}
|
|
/>
|
|
),
|
|
},
|
|
...(caps.canConfig
|
|
? [
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (h: Holiday) => (
|
|
<AppButton variant="text" color="primary" startIcon="edit" onClick={() => setEditing(h)}>
|
|
{t('cfg_edit')}
|
|
</AppButton>
|
|
),
|
|
} as AdminTableColumn<Holiday>,
|
|
]
|
|
: []),
|
|
];
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
<AdminPageHeader
|
|
title={t('hol_title')}
|
|
subtitle={t('hol_subtitle')}
|
|
actions={
|
|
caps.canConfig ? (
|
|
<AppButton variant="contained" color="primary" startIcon="add" onClick={() => setEditing('new')}>
|
|
{t('hol_add')}
|
|
</AppButton>
|
|
) : undefined
|
|
}
|
|
/>
|
|
|
|
{holidays.isLoading ? (
|
|
<Skeleton variant="rounded" height={200} />
|
|
) : holidays.isError ? (
|
|
<AdminErrorState message={t('error_generic')} retryLabel={t('retry')} onRetry={() => holidays.refetch()} />
|
|
) : (holidays.data?.items.length ?? 0) === 0 ? (
|
|
<AdminEmptyState icon="calendar" title={t('hol_empty')} />
|
|
) : (
|
|
<AdminDataTable columns={columns} rows={holidays.data?.items ?? []} getRowKey={(h) => h.id} ariaLabel={t('hol_title')} />
|
|
)}
|
|
|
|
{editing ? (
|
|
<HolidayDialog holiday={editing === 'new' ? null : editing} onClose={() => setEditing(null)} />
|
|
) : null}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const TODAY_ISO = ''; // seeded below via state default so no Date at module load
|
|
|
|
function HolidayDialog({ holiday, onClose }: { holiday: Holiday | null; onClose: () => void }) {
|
|
const t = useTranslations('admin');
|
|
const { enqueueSnackbar } = useSnackbar();
|
|
const upsert = useUpsertHoliday();
|
|
const [form, setForm] = useState<HolidayInput>({
|
|
holidayDate: holiday?.holidayDate?.slice(0, 10) ?? TODAY_ISO,
|
|
nameFa: holiday?.nameFa ?? '',
|
|
type: holiday?.type ?? 'official',
|
|
isBankClosed: holiday?.isBankClosed ?? true,
|
|
});
|
|
|
|
const valid = form.holidayDate.length > 0 && form.nameFa.trim().length > 0;
|
|
|
|
const onSave = () => {
|
|
if (!valid) return;
|
|
upsert.mutate(
|
|
{ ...form, nameFa: form.nameFa.trim() },
|
|
{
|
|
onSuccess: () => {
|
|
enqueueSnackbar(t('hol_saved'), { variant: 'success' });
|
|
onClose();
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Dialog open onClose={upsert.isPending ? undefined : onClose} fullWidth maxWidth="xs">
|
|
<DialogTitle sx={{ fontWeight: 800 }}>{holiday ? t('hol_edit') : t('hol_add')}</DialogTitle>
|
|
<DialogContent>
|
|
<Stack sx={{ gap: 2, mt: 1 }}>
|
|
<TextField
|
|
type="date"
|
|
label={t('hol_col_date')}
|
|
value={form.holidayDate}
|
|
onChange={(e) => setForm((f) => ({ ...f, holidayDate: e.target.value }))}
|
|
disabled={!!holiday}
|
|
slotProps={{ inputLabel: { shrink: true } }}
|
|
/>
|
|
<TextField
|
|
label={t('hol_name_fa')}
|
|
value={form.nameFa}
|
|
onChange={(e) => setForm((f) => ({ ...f, nameFa: e.target.value }))}
|
|
/>
|
|
<TextField
|
|
select
|
|
label={t('hol_col_type')}
|
|
value={form.type}
|
|
onChange={(e) => setForm((f) => ({ ...f, type: e.target.value as HolidayType }))}
|
|
>
|
|
{HOLIDAY_TYPES.map((ty) => (
|
|
<MenuItem key={ty} value={ty}>
|
|
{t(`htype_${ty}`)}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
<FormControlLabel
|
|
control={<Switch checked={form.isBankClosed} onChange={(e) => setForm((f) => ({ ...f, isBankClosed: e.target.checked }))} />}
|
|
label={t('hol_bank_hint')}
|
|
/>
|
|
</Stack>
|
|
</DialogContent>
|
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
|
<AppButton variant="text" color="inherit" onClick={onClose} disabled={upsert.isPending}>
|
|
{t('cancel')}
|
|
</AppButton>
|
|
<AppButton variant="contained" color="primary" onClick={onSave} disabled={!valid || upsert.isPending}>
|
|
{upsert.isPending ? t('saving') : t('save')}
|
|
</AppButton>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|