From 540145d80a17a9c8db25027ee2ad9e0b16013e3a Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Wed, 20 Aug 2025 17:22:51 +0300 Subject: [PATCH] feat(service-card): add price and description fields to service data card - Introduced NumberField for price input and TextareaField for service description in the ServiceDataCard component. - Updated ServiceCard component to display the new description and price fields, enhancing service details visibility. - Added formatMoney utility for consistent currency formatting across the application. - Updated GraphQL fragments and types to include price and description fields for services. --- .../profile/services/service-data-card.tsx | 15 +++- .../components/shared/data-fields/index.ts | 2 + .../shared/data-fields/number-field.tsx | 57 ++++++++++++ .../shared/data-fields/textarea-field.tsx | 51 +++++++++++ apps/web/components/shared/service-card.tsx | 87 +++++++++++++++++-- packages/graphql/operations/services.graphql | 2 + .../graphql/types/operations.generated.ts | 52 ++++++----- packages/utils/package.json | 3 +- packages/utils/src/money.ts | 4 + 9 files changed, 242 insertions(+), 31 deletions(-) create mode 100644 apps/web/components/shared/data-fields/number-field.tsx create mode 100644 apps/web/components/shared/data-fields/textarea-field.tsx create mode 100644 packages/utils/src/money.ts diff --git a/apps/web/components/profile/services/service-data-card.tsx b/apps/web/components/profile/services/service-data-card.tsx index 32e42f6..c430232 100644 --- a/apps/web/components/profile/services/service-data-card.tsx +++ b/apps/web/components/profile/services/service-data-card.tsx @@ -1,6 +1,6 @@ 'use client'; -import { TextField, TimeField } from '@/components/shared/data-fields'; +import { NumberField, TextareaField, TextField, TimeField } from '@/components/shared/data-fields'; import { useServiceMutation, useServiceQuery } from '@/hooks/api/services'; import { Card } from '@repo/ui/components/ui/card'; import { convertTimeString } from '@repo/utils/datetime-format'; @@ -36,6 +36,19 @@ export function ServiceDataCard({ serviceId }: Readonly) { } value={service?.duration ?? ''} /> + mutate({ data: { price } })} + value={service?.price ?? null} + /> + mutate({ data: { description } })} + rows={4} + value={service?.description ?? ''} + /> ); diff --git a/apps/web/components/shared/data-fields/index.ts b/apps/web/components/shared/data-fields/index.ts index b650309..5a7afd0 100644 --- a/apps/web/components/shared/data-fields/index.ts +++ b/apps/web/components/shared/data-fields/index.ts @@ -1,3 +1,5 @@ export * from './checkbox-field'; +export * from './number-field'; export * from './text-field'; +export * from './textarea-field'; export * from './time-field'; diff --git a/apps/web/components/shared/data-fields/number-field.tsx b/apps/web/components/shared/data-fields/number-field.tsx new file mode 100644 index 0000000..fe1f5a1 --- /dev/null +++ b/apps/web/components/shared/data-fields/number-field.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useDebouncedOnChangeCallback, useFocus } from './hooks'; +import { Input } from '@repo/ui/components/ui/input'; +import { Label } from '@repo/ui/components/ui/label'; +import { type ChangeEvent, useState } from 'react'; + +type FieldProps = { + readonly disabled?: boolean; + readonly id: string; + readonly label: string; + readonly onChange?: (value: null | number) => Promise | void; + readonly readOnly?: boolean; + readonly value: null | number; +}; + +export function NumberField({ + disabled = false, + id, + label, + onChange, + readOnly, + value: initialValue, +}: FieldProps) { + const [value, setValue] = useState(initialValue?.toString() ?? ''); + const { debouncedCallback, isPending } = useDebouncedOnChangeCallback(onChange); + const inputRef = useFocus(isPending); + + const handleChange = (event: ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + + // Преобразуем строку в число или null + const numericValue = newValue === '' ? null : Number(newValue); + + // Проверяем, что это валидное число + if (numericValue === null || (!Number.isNaN(numericValue) && numericValue >= 0)) { + debouncedCallback(numericValue); + } + }; + + return ( +
+ + +
+ ); +} diff --git a/apps/web/components/shared/data-fields/textarea-field.tsx b/apps/web/components/shared/data-fields/textarea-field.tsx new file mode 100644 index 0000000..586f7d6 --- /dev/null +++ b/apps/web/components/shared/data-fields/textarea-field.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useDebouncedOnChangeCallback, useFocus } from './hooks'; +import { Label } from '@repo/ui/components/ui/label'; +import { type ChangeEvent, useState } from 'react'; + +type FieldProps = { + readonly disabled?: boolean; + readonly id: string; + readonly label: string; + readonly onChange?: (value: string) => Promise | void; + readonly readOnly?: boolean; + readonly rows?: number; + readonly value: string; +}; + +export function TextareaField({ + disabled = false, + id, + label, + onChange, + readOnly, + rows = 3, + value: initialValue, +}: FieldProps) { + const [value, setValue] = useState(initialValue); + const { debouncedCallback, isPending } = useDebouncedOnChangeCallback(onChange); + const textareaRef = useFocus(isPending); + + const handleChange = (event: ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + debouncedCallback(newValue); + }; + + return ( +
+ +