ui phase 8
This commit is contained in:
@@ -2,30 +2,21 @@
|
||||
import { FunctionComponent, useMemo, useState } from 'react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { AppButton, AppLoading, CategoryTile, ErrorState, PriceDisplay, StepperHeader } from '@/components';
|
||||
import { Box, Chip, MenuItem, Paper, Skeleton, Stack, TextField, Typography } from '@mui/material';
|
||||
import { AccentCard, AppButton, AppIcon, AppLoading, CategoryTile, ErrorState, StepperHeader, VariantCard } from '@/components';
|
||||
import { ApiError } from '@/lib/api/errors';
|
||||
import { digitsOnly, rialToToman, tomanToRial } from '@/utils';
|
||||
import {
|
||||
useCategoryOptionGroups,
|
||||
useCreateVariant,
|
||||
useMyVariants,
|
||||
useServiceCategories,
|
||||
useUpdateVariant,
|
||||
} from '@/services/catalog';
|
||||
import { pickCatalogName } from '@/services/catalog/names';
|
||||
import {
|
||||
PRICE_UNITS,
|
||||
optionSetSignature,
|
||||
type NurseServiceVariant,
|
||||
type PriceUnit,
|
||||
type VariantOptionSelection,
|
||||
@@ -36,6 +27,8 @@ interface VariantBuilderProps {
|
||||
initial: NurseServiceVariant | null;
|
||||
onDone: () => void;
|
||||
onCancel: () => void;
|
||||
/** Jump straight into editing an already-existing listing — the create step's 409-duplicate recovery. */
|
||||
onEditExisting: (variant: NurseServiceVariant) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_UNIT: PriceUnit = 'per_hour';
|
||||
@@ -55,7 +48,7 @@ const MAX_DURATION_DIGITS = 4;
|
||||
* **Edit** locks the category + option-set (changing them would change identity) and edits only
|
||||
* price/unit/duration/display via `update`.
|
||||
*/
|
||||
const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDone, onCancel }) => {
|
||||
const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDone, onCancel, onEditExisting }) => {
|
||||
const t = useTranslations('services');
|
||||
const tCatalog = useTranslations('catalog');
|
||||
const tc = useTranslations('common');
|
||||
@@ -92,6 +85,21 @@ const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDon
|
||||
const selectedCategory = categories.find((category) => category.id === categoryId) ?? null;
|
||||
const missingRequiredGroups = groups.filter((group) => group.isRequired && selectedOptions[group.id] == null);
|
||||
|
||||
// Reuses the already-cached offerings list (MyServicesList holds the same query) to resolve which
|
||||
// existing listing a 409 duplicate collided with, so the recovery can offer "edit that one" directly.
|
||||
const myVariantsQuery = useMyVariants();
|
||||
const existingMatch = useMemo(() => {
|
||||
if (isEdit || categoryId == null) return null;
|
||||
const signature = optionSetSignature(categoryId, Object.values(selectedOptions));
|
||||
return (
|
||||
(myVariantsQuery.data?.items ?? []).find(
|
||||
(variant) =>
|
||||
variant.serviceCategoryId === categoryId &&
|
||||
optionSetSignature(categoryId, variant.options.map((option) => option.optionValueId)) === signature,
|
||||
) ?? null
|
||||
);
|
||||
}, [isEdit, categoryId, selectedOptions, myVariantsQuery.data]);
|
||||
|
||||
// Auto-generated display name preview (create): category + chosen value labels, in the active locale.
|
||||
const autoName = useMemo(() => {
|
||||
if (isEdit) return initial.displayName;
|
||||
@@ -188,6 +196,21 @@ const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDon
|
||||
);
|
||||
};
|
||||
|
||||
// Step 3's live preview — the actual VariantCard, so the nurse sees the listing they're composing,
|
||||
// not just an abstract price readout.
|
||||
const previewVariant: NurseServiceVariant = {
|
||||
id: 0,
|
||||
serviceCategoryId: categoryId ?? 0,
|
||||
categoryNameFa: selectedCategory?.nameFa ?? '',
|
||||
categoryNameEn: selectedCategory?.nameEn ?? '',
|
||||
price: irr ?? '0',
|
||||
priceUnit,
|
||||
sessionCount,
|
||||
displayName: displayValue || t('preview_untitled'),
|
||||
isActive: true,
|
||||
options: [],
|
||||
};
|
||||
|
||||
const priceStep = (
|
||||
<Stack sx={{ gap: 2.5 }}>
|
||||
<TextField
|
||||
@@ -230,14 +253,17 @@ const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDon
|
||||
</Stack>
|
||||
|
||||
{irr ? (
|
||||
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, bgcolor: 'var(--bal-primary-soft)' }}>
|
||||
<PriceDisplay price={irr} priceUnit={priceUnit} sessionCount={sessionCount} showEstimate />
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 700 }}>
|
||||
{t('preview_heading')}
|
||||
</Typography>
|
||||
<VariantCard variant={previewVariant} interactive={false} />
|
||||
{!sessionCount ? (
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', mt: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{t('rate_note')}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Paper>
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
<TextField
|
||||
@@ -249,21 +275,28 @@ const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDon
|
||||
/>
|
||||
|
||||
{duplicate ? (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderInlineStartWidth: 4,
|
||||
borderInlineStartColor: 'var(--bal-warning)',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-warning)', fontWeight: 500 }}>
|
||||
{t('duplicate_warning')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<AccentCard tone="warning" padding="sm">
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'flex-start' }}>
|
||||
<AppIcon icon="warning" size={20} color="var(--bal-warning)" />
|
||||
{/* Warning is reserved for the edge + icon; the body reads as normal text, not amber-on-paper. */}
|
||||
<Typography variant="body2" sx={{ color: 'text.primary', fontWeight: 500 }}>
|
||||
{t('duplicate_warning')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{existingMatch ? (
|
||||
<AppButton
|
||||
variant="text"
|
||||
color="primary"
|
||||
startIcon="edit"
|
||||
onClick={() => onEditExisting(existingMatch)}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
{t('duplicate_edit_existing')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
</Stack>
|
||||
</AccentCard>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
@@ -423,20 +456,25 @@ const VariantBuilder: FunctionComponent<VariantBuilderProps> = ({ initial, onDon
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ToggleButtonGroup
|
||||
exclusive
|
||||
size="small"
|
||||
color="primary"
|
||||
value={selectedOptions[group.id] ?? null}
|
||||
onChange={(_event, valueId: number | null) => changeOption(group.id, valueId)}
|
||||
sx={{ flexWrap: 'wrap' }}
|
||||
>
|
||||
{group.values.map((value) => (
|
||||
<ToggleButton key={value.id} value={value.id}>
|
||||
{pickCatalogName(value, locale)}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
<Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
|
||||
{group.values.map((value) => {
|
||||
const selected = selectedOptions[group.id] === value.id;
|
||||
return (
|
||||
<Chip
|
||||
key={value.id}
|
||||
label={pickCatalogName(value, locale)}
|
||||
onClick={() => changeOption(group.id, selected ? null : value.id)}
|
||||
variant={selected ? 'filled' : 'outlined'}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
backgroundColor: selected ? 'var(--bal-primary)' : 'transparent',
|
||||
color: selected ? 'var(--bal-primary-contrast)' : 'var(--bal-primary)',
|
||||
borderColor: 'var(--bal-primary)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user