diff --git a/apps/web/components/profile/data-card/index.tsx b/apps/web/components/profile/data-card/index.tsx index 147cc9f..6b76717 100644 --- a/apps/web/components/profile/data-card/index.tsx +++ b/apps/web/components/profile/data-card/index.tsx @@ -7,6 +7,7 @@ import { useCustomerMutation, useCustomerQuery } from '@/hooks/api/customers'; import { Button } from '@repo/ui/components/ui/button'; import { Card } from '@repo/ui/components/ui/card'; import Link from 'next/link'; +import { useState } from 'react'; export function ContactDataCard({ telegramId }: Readonly) { const { data: { customer } = {} } = useCustomerQuery({ telegramId }); @@ -31,7 +32,8 @@ export function ContactDataCard({ telegramId }: Readonly) { export function ProfileDataCard() { const { data: { customer } = {} } = useCustomerQuery(); - const { mutate: updateCustomer } = useCustomerMutation(); + const { cancelChanges, hasChanges, isPending, resetTrigger, saveChanges, updateField } = + useProfileEdit(); if (!customer) return null; @@ -41,8 +43,9 @@ export function ProfileDataCard() { updateCustomer({ data: { name: value } })} + onChange={(value) => updateField('name', value)} value={customer?.name ?? ''} /> @@ -50,13 +53,54 @@ export function ProfileDataCard() { checked={customer.role !== 'client'} description="Разрешить другим пользователям записываться к вам" onChange={(checked) => - updateCustomer({ - data: { role: checked ? Role.Master : Role.Client }, - }) + updateField('role', checked ? Role.Master : Role.Client) } text="Быть мастером" /> */} + {hasChanges && ( +
+ + +
+ )} ); } + +function useProfileEdit() { + const { isPending, mutate } = useCustomerMutation(); + const [pendingChanges, setPendingChanges] = useState>({}); + const [resetTrigger, setResetTrigger] = useState(0); + + const updateField = (field: string, value: unknown) => { + setPendingChanges((previous) => ({ ...previous, [field]: value })); + }; + + const saveChanges = () => { + if (Object.keys(pendingChanges).length === 0) return; + + mutate({ data: pendingChanges }); + setPendingChanges({}); + }; + + const cancelChanges = () => { + setPendingChanges({}); + setResetTrigger((previous) => previous + 1); + }; + + const hasChanges = Object.keys(pendingChanges).length > 0; + + return { + cancelChanges, + hasChanges, + isPending, + resetTrigger, + saveChanges, + updateField, + }; +} diff --git a/apps/web/components/profile/services/service-data-card.tsx b/apps/web/components/profile/services/service-data-card.tsx index 0768d94..0f4dcc2 100644 --- a/apps/web/components/profile/services/service-data-card.tsx +++ b/apps/web/components/profile/services/service-data-card.tsx @@ -2,8 +2,10 @@ import { NumberField, TextareaField, TextField, TimeField } from '@/components/shared/data-fields'; import { useServiceMutation, useServiceQuery } from '@/hooks/api/services'; +import { Button } from '@repo/ui/components/ui/button'; import { Card } from '@repo/ui/components/ui/card'; import { convertTimeString } from '@repo/utils/datetime-format'; +import { useState } from 'react'; type Props = { serviceId: string; @@ -11,7 +13,8 @@ type Props = { export function ServiceDataCard({ serviceId }: Readonly) { const { data: { service } = {} } = useServiceQuery({ documentId: serviceId }); - const { mutate } = useServiceMutation({ documentId: serviceId }); + const { cancelChanges, hasChanges, isPending, resetTrigger, saveChanges, updateField } = + useServiceEdit(serviceId); if (!service) return null; @@ -20,37 +23,78 @@ export function ServiceDataCard({ serviceId }: Readonly) {
mutate({ data: { name } })} + onChange={(name) => updateField('name', name)} value={service?.name ?? ''} /> - mutate({ - data: { - duration: convertTimeString(time).db(), - }, - }) - } + onChange={(time) => updateField('duration', convertTimeString(time).db())} value={service?.duration ?? ''} /> mutate({ data: { price } })} + onChange={(price) => updateField('price', price)} value={service?.price ?? null} /> mutate({ data: { description } })} + onChange={(description) => updateField('description', description)} rows={4} value={service?.description ?? ''} /> + {hasChanges && ( +
+ + +
+ )}
); } + +function useServiceEdit(serviceId: string) { + const { isPending, mutate } = useServiceMutation({ documentId: serviceId }); + const [pendingChanges, setPendingChanges] = useState>({}); + const [resetTrigger, setResetTrigger] = useState(0); + + const updateField = (field: string, value: unknown) => { + setPendingChanges((previous) => ({ ...previous, [field]: value })); + }; + + const saveChanges = () => { + if (Object.keys(pendingChanges).length === 0) return; + + mutate({ data: pendingChanges }); + setPendingChanges({}); + }; + + const cancelChanges = () => { + setPendingChanges({}); + setResetTrigger((previous) => previous + 1); + }; + + const hasChanges = Object.keys(pendingChanges).length > 0; + + return { + cancelChanges, + hasChanges, + isPending, + resetTrigger, + saveChanges, + updateField, + }; +} diff --git a/apps/web/components/shared/action-panel.tsx b/apps/web/components/shared/action-panel.tsx index cb5f413..5056a2f 100644 --- a/apps/web/components/shared/action-panel.tsx +++ b/apps/web/components/shared/action-panel.tsx @@ -2,7 +2,7 @@ import { Button } from '@repo/ui/components/ui/button'; import { Card } from '@repo/ui/components/ui/card'; -import { Ban, Check, Lock, RotateCcw, Trash2, Undo, Unlock } from 'lucide-react'; +import { Ban, Check, Lock, RotateCcw, Save, Trash2, Undo, Unlock } from 'lucide-react'; type FloatingActionPanelProps = { readonly isLoading?: boolean; @@ -13,6 +13,7 @@ type FloatingActionPanelProps = { readonly onDelete?: () => void; readonly onRepeat?: () => void; readonly onReturn?: () => void; + readonly onSave?: () => void; readonly onToggle?: () => void; }; @@ -25,10 +26,20 @@ export default function FloatingActionPanel({ onDelete, onRepeat, onReturn, + onSave, onToggle, }: FloatingActionPanelProps) { // Если не переданы обработчики, скрываем панель - if (!onCancel && !onConfirm && !onDelete && !onComplete && !onRepeat && !onToggle && !onReturn) + if ( + !onCancel && + !onConfirm && + !onDelete && + !onComplete && + !onRepeat && + !onToggle && + !onReturn && + !onSave + ) return null; return ( @@ -140,6 +151,19 @@ export default function FloatingActionPanel({ Вернуть )} + + {/* Кнопка сохранить */} + {onSave && ( + + )} ); diff --git a/apps/web/components/shared/data-fields/number-field.tsx b/apps/web/components/shared/data-fields/number-field.tsx index 3abede5..a6a750e 100644 --- a/apps/web/components/shared/data-fields/number-field.tsx +++ b/apps/web/components/shared/data-fields/number-field.tsx @@ -10,7 +10,7 @@ type FieldProps = { readonly id: string; readonly label: string; readonly min?: number; - readonly onChange?: (value: null | number) => Promise | void; + readonly onChange?: (value: null | number) => void; readonly readOnly?: boolean; readonly value: null | number; }; diff --git a/apps/web/components/shared/data-fields/text-field.tsx b/apps/web/components/shared/data-fields/text-field.tsx index 54b8442..d508e37 100644 --- a/apps/web/components/shared/data-fields/text-field.tsx +++ b/apps/web/components/shared/data-fields/text-field.tsx @@ -9,7 +9,7 @@ type FieldProps = { readonly disabled?: boolean; readonly id: string; readonly label: string; - readonly onChange?: (value: string) => Promise | void; + readonly onChange?: (value: string) => void; readonly readOnly?: boolean; readonly value: string; }; diff --git a/apps/web/components/shared/data-fields/textarea-field.tsx b/apps/web/components/shared/data-fields/textarea-field.tsx index 4a566aa..ee27851 100644 --- a/apps/web/components/shared/data-fields/textarea-field.tsx +++ b/apps/web/components/shared/data-fields/textarea-field.tsx @@ -8,7 +8,7 @@ type FieldProps = { readonly disabled?: boolean; readonly id: string; readonly label: string; - readonly onChange?: (value: string) => Promise | void; + readonly onChange?: (value: string) => void; readonly readOnly?: boolean; readonly rows?: number; readonly value: string; diff --git a/apps/web/components/shared/data-fields/time-field.tsx b/apps/web/components/shared/data-fields/time-field.tsx index 523a9ca..5d1a106 100644 --- a/apps/web/components/shared/data-fields/time-field.tsx +++ b/apps/web/components/shared/data-fields/time-field.tsx @@ -9,7 +9,7 @@ type FieldProps = { readonly disabled?: boolean; readonly id: string; readonly label: string; - readonly onChange?: (value: string) => Promise | void; + readonly onChange?: (value: string) => void; readonly readOnly?: boolean; readonly value: string; };