frontend phase 3: geography — addresses, map-pin picker & nurse coverage areas
Three domain services (mirroring the patients/nurse template): services/geography (cached province→city→district lookups; Infinity staleTime + shared geographyKeys), services/addresses (address book CRUD + set-primary; single-primary invariant), and services/serviceAreas (coverage add/remove; areaExists dup-guard, districtId=null = whole city). Four tested composites in src/components/geography: CascadingRegionSelect (drives the cascade queries), AddressMapPicker (map-pin stand-in emitting real lat/lng), AddressForm, AddressCard. Screens: customer address book (/addresses, reached from the profile hub) and nurse coverage editor (/nurse/coverage, new sidebar tab, inline duplicate block + 409). Adds geo/address/coverage i18n namespaces (both locales), location/delete/coverage icons, ADDRESSES/NURSE_COVERAGE routes. Consumes the b4 geography-addresses contract; filed REQ-008 (accept the map pin on create/update) and REQ-009 (provinceId on CustomerAddressDto) for gaps. Gate: npm run check + npm run test:ci (129, +17) + npm run build all green. A 5-dimension adversarial review fixed 3 findings (map-marker RTL transform, page_size→pageSize pagination casing, coverage districts-scope dead-end on district-less cities). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
'use client';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { Box, Divider, MenuItem, Stack, TextField, Typography } from '@mui/material';
|
||||
import { AppButton, AppLoading, PhoneNumberField } from '@/components';
|
||||
import { Box, Divider, MenuItem, Paper, Stack, TextField, Typography } from '@mui/material';
|
||||
import { AppButton, AppIcon, AppLoading, PhoneNumberField } from '@/components';
|
||||
import { isIranianMobile } from '@/components/PhoneNumberField';
|
||||
import { ROUTES } from '@/constants';
|
||||
import { digitsOnly } from '@/utils';
|
||||
import { useCustomerProfile, useUpsertCustomerProfile } from '@/services/profiles';
|
||||
import type { CustomerProfile } from '@/services/profiles/types';
|
||||
@@ -18,7 +19,9 @@ export default function CustomerProfilePage() {
|
||||
|
||||
const CustomerProfileForm: FunctionComponent<{ initial: CustomerProfile | null }> = ({ initial }) => {
|
||||
const t = useTranslations('profile');
|
||||
const ta = useTranslations('address');
|
||||
const tc = useTranslations('common');
|
||||
const locale = useLocale();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const upsert = useUpsertCustomerProfile();
|
||||
|
||||
@@ -123,6 +126,33 @@ const CustomerProfileForm: FunctionComponent<{ initial: CustomerProfile | null }
|
||||
>
|
||||
{upsert.isPending ? tc('saving') : t('save')}
|
||||
</AppButton>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Address book lives alongside the profile in the customer area; a booking (f7) needs a
|
||||
chosen address, so the entry point is surfaced here on the settings hub. */}
|
||||
<Paper elevation={0} sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 2, display: 'flex', gap: 2 }}>
|
||||
<AppIcon icon="location" size={28} color="var(--bal-primary)" />
|
||||
<Stack sx={{ gap: 1, flexGrow: 1 }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
{ta('manage_title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{ta('manage_body')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<AppButton
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
startIcon="location"
|
||||
to={`/${locale}${ROUTES.ADDRESSES}`}
|
||||
sx={{ m: 0, alignSelf: 'flex-start' }}
|
||||
>
|
||||
{ta('manage_cta')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user