'use client'; import { useState } from 'react'; import { useLocale, useTranslations } from 'next-intl'; import { useSnackbar } from 'notistack'; import { Box, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Paper, Skeleton, Stack, Typography } from '@mui/material'; import { AppButton, AppIcon } from '@/components'; import { CascadingRegionSelect, type CascadingRegionValue } from '@/components/geography'; import { ApiError } from '@/lib/api/errors'; import { useServiceAreas, useAddServiceArea, useRemoveServiceArea } from '@/services/serviceAreas'; import { areaExists, type NurseServiceArea } from '@/services/serviceAreas/types'; const EMPTY_REGION: CascadingRegionValue = { provinceId: null, cityId: null, districtId: null }; /** * The nurse coverage-area editor — the cities/districts a nurse will travel to, so search (f6) * can fan them out geographically. Areas render as chips (whole-city shown explicitly). * * ui-phase-8: **one control owns the whole-city choice** — `CascadingRegionSelect`'s own district * level, whose "کل شهر" empty option *is* the choice (`districtId = null`, matching the serviceAreas * contract both ways). The separate scope toggle this page used to render is gone — it let a nurse * pick "specific districts" and then still land on the district select's own whole-city option, * tripping a "district required" error the UI itself had offered. City is the only required field; * leaving the district unset is a complete, valid whole-city submission, never an error state. */ export default function NurseCoveragePage() { const t = useTranslations('coverage'); const tc = useTranslations('common'); const locale = useLocale(); const { enqueueSnackbar } = useSnackbar(); const { data, isLoading } = useServiceAreas(); const addArea = useAddServiceArea(); const removeArea = useRemoveServiceArea(); const [region, setRegion] = useState(EMPTY_REGION); const [cityError, setCityError] = useState(false); const [duplicate, setDuplicate] = useState(false); const [removeTarget, setRemoveTarget] = useState(null); const areas = data?.items ?? []; const chipLabel = (area: NurseServiceArea) => { const city = locale === 'en' ? area.cityNameEn : area.cityNameFa; if (area.isWholeCity) return `${city} · ${t('whole_city_chip')}`; const district = locale === 'en' ? area.districtNameEn : area.districtNameFa; return `${city} · ${district}`; }; const resetForm = () => { setRegion(EMPTY_REGION); setCityError(false); setDuplicate(false); }; const handleAdd = () => { const cityInvalid = region.cityId == null; setCityError(cityInvalid); setDuplicate(false); if (cityInvalid) return; const cityId = region.cityId as number; // Whatever the district select currently holds is the complete choice — `null` = whole city. const districtId = region.districtId; // Fast path: block a duplicate before firing (null district treated as a real value). if (areaExists(areas, cityId, districtId)) { setDuplicate(true); return; } addArea.mutate( { cityId, districtId }, { onSuccess: () => { resetForm(); enqueueSnackbar(t('added'), { variant: 'success' }); }, onError: (error) => { // Belt-and-braces: the server's UNIQUE 409 maps to the same inline duplicate message. if (error instanceof ApiError && error.status === 409) setDuplicate(true); else enqueueSnackbar(t('add_error'), { variant: 'error' }); }, }, ); }; const confirmRemove = () => { if (!removeTarget) return; const id = removeTarget.id; setRemoveTarget(null); removeArea.mutate(id, { onSuccess: () => enqueueSnackbar(t('removed'), { variant: 'success' }), onError: () => enqueueSnackbar(t('remove_error'), { variant: 'error' }), }); }; return ( {t('title')} {t('subtitle')} {isLoading ? ( {[0, 1].map((key) => ( ))} ) : areas.length > 0 ? ( {t('areas_heading')} {areas.map((area) => ( setRemoveTarget(area)} sx={{ bgcolor: 'var(--bal-primary-soft)', fontWeight: 500 }} /> ))} ) : ( {t('empty_title')} {t('empty_warning')} )} {t('add_title')} { setRegion(next); if (cityError && next.cityId != null) setCityError(false); setDuplicate(false); }} cityError={cityError} cityErrorText={t('city_required')} /> {duplicate ? ( {t('duplicate')} ) : null} {addArea.isPending ? t('adding') : t('add')} setRemoveTarget(null)}> {t('remove_title')} {t('remove_body')} setRemoveTarget(null)}> {tc('cancel')} {t('remove_confirm')} ); }