diff --git a/app/admin/add-barometer/add-manufacturer.tsx b/app/admin/add-barometer/add-manufacturer.tsx index 319ef198..bde73448 100644 --- a/app/admin/add-barometer/add-manufacturer.tsx +++ b/app/admin/add-barometer/add-manufacturer.tsx @@ -16,15 +16,6 @@ interface AddManufacturerProps { onAddManufacturer: (newId: string) => void } -interface ManufacturerFormData { - firstName: string - name: string - city: string - countries: number[] - description: string - icon: string | null -} - // Yup validation schema const manufacturerSchema = yup.object().shape({ firstName: yup.string().max(100, 'First name should be shorter than 100 characters').default(''), @@ -39,6 +30,8 @@ const manufacturerSchema = yup.object().shape({ icon: yup.string().nullable().default(null), }) +type ManufacturerFormData = yup.InferType + export function AddManufacturer({ onAddManufacturer }: AddManufacturerProps) { const { countries } = useBarometers() const [open, setOpen] = useState(false) diff --git a/app/admin/add-barometer/add-materials.tsx b/app/admin/add-barometer/add-materials.tsx new file mode 100644 index 00000000..a3d653ca --- /dev/null +++ b/app/admin/add-barometer/add-materials.tsx @@ -0,0 +1,90 @@ +'use client' + +import { Check, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command' + +interface MaterialsMultiSelectProps { + value: number[] + onChange: (value: number[]) => void + materials: Array<{ id: number; name: string }> +} + +export function MaterialsMultiSelect({ value, onChange, materials }: MaterialsMultiSelectProps) { + const selectedMaterials = materials.filter(material => value.includes(material.id)) + + const handleSelect = (materialId: number) => { + if (value.includes(materialId)) { + onChange(value.filter(id => id !== materialId)) + } else { + onChange([...value, materialId]) + } + } + + const handleRemove = (materialId: number) => { + onChange(value.filter(id => id !== materialId)) + } + + return ( +
+ {selectedMaterials.length > 0 && ( +
+ {selectedMaterials.map(material => ( + + {material.name} + + + ))} +
+ )} + + + + + + + + + No materials found. + + {materials.map(material => ( + handleSelect(material.id)} + className="flex items-center space-x-2" + > +
+ {value.includes(material.id) && } +
+ {material.name} +
+ ))} +
+
+
+
+
+
+ ) +} diff --git a/app/admin/add-barometer/file-upload.tsx b/app/admin/add-barometer/file-upload.tsx index efb04a7e..84912426 100644 --- a/app/admin/add-barometer/file-upload.tsx +++ b/app/admin/add-barometer/file-upload.tsx @@ -19,7 +19,7 @@ interface FileUploadProps { export function FileUpload({ name }: FileUploadProps) { const [isUploading, setIsUploading] = useState(false) const fileInputRef = useRef(null) - const { control, watch, setValue } = useFormContext() + const { control, watch, setValue, clearErrors } = useFormContext() const fileNames: string[] = watch(name) || [] @@ -47,6 +47,7 @@ export function FileUpload({ name }: FileUploadProps) { const newFileNames = [...fileNames, ...urlsDto.urls.map(urlObj => urlObj.public)] setValue(name, newFileNames) + clearErrors(name) // Clear any validation errors after successful upload } catch (error) { toast.error(error instanceof Error ? error.message : 'Error uploading files') } finally { @@ -62,6 +63,10 @@ export function FileUpload({ name }: FileUploadProps) { await deleteImage(fileName) const updatedFileNames = fileNames.filter((_, i) => i !== index) setValue(name, updatedFileNames) + // Clear errors if we still have images after deletion + if (updatedFileNames.length > 0) { + clearErrors(name) + } } catch (error) { toast.error(error instanceof Error ? error.message : 'Error deleting file') } diff --git a/app/admin/add-barometer/page.tsx b/app/admin/add-barometer/page.tsx index a4e4e897..43661b06 100644 --- a/app/admin/add-barometer/page.tsx +++ b/app/admin/add-barometer/page.tsx @@ -5,6 +5,7 @@ import { useForm, FormProvider } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import * as yup from 'yup' import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' import { useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Button } from '@/components/ui/button' @@ -29,24 +30,12 @@ import { useBarometers } from '@/hooks/useBarometers' import { FileUpload } from './file-upload' import { AddManufacturer } from './add-manufacturer' import { Dimensions } from './dimensions' - +import { MaterialsMultiSelect } from './add-materials' import { createBarometer } from '@/services/fetch' import { getThumbnailBase64, slug } from '@/utils' import { imageStorage } from '@/constants/globals' -// Form data interface -interface BarometerFormData { - collectionId: string - name: string - categoryId: string - date: string - dateDescription: string - manufacturerId: string - conditionId: string - description: string - dimensions: Array<{ dim: string; value: string }> - images: string[] -} +dayjs.extend(utc) // Yup validation schema const barometerSchema = yup.object().shape({ @@ -81,10 +70,35 @@ const barometerSchema = yup.object().shape({ .of(yup.string().required()) .min(1, 'At least one image is required') .default([]), + purchasedAt: yup + .string() + .test('valid-date', 'Must be a valid date', value => { + if (!value) return true // Allow empty string + return dayjs(value).isValid() + }) + .test('not-future', 'Purchase date cannot be in the future', value => { + if (!value) return true + return dayjs(value).isBefore(dayjs(), 'day') || dayjs(value).isSame(dayjs(), 'day') + }) + .default(''), + serial: yup.string().max(100, 'Serial number must be less than 100 characters').default(''), + estimatedPrice: yup + .string() + .test('is-positive-number', 'Must be a positive number', value => { + if (!value) return true // Allow empty string + const num = parseFloat(value) + return !isNaN(num) && num > 0 + }) + .default(''), + subCategoryId: yup.string().default(''), + materials: yup.array().of(yup.number().required()).default([]), }) +// Auto-generated TypeScript type from Yup schema +type BarometerFormData = yup.InferType + export default function AddCard() { - const { condition, categories, manufacturers } = useBarometers() + const { condition, categories, subcategories, manufacturers, materials } = useBarometers() const methods = useForm({ resolver: yupResolver(barometerSchema), @@ -92,17 +106,22 @@ export default function AddCard() { collectionId: '', name: '', categoryId: '', - date: '', + date: '1900', dateDescription: '', manufacturerId: '', conditionId: '', description: '', dimensions: [], images: [], + purchasedAt: '', + serial: '', + estimatedPrice: '', + subCategoryId: '', + materials: [], }, }) - const { handleSubmit, setValue, watch, reset } = methods + const { handleSubmit, setValue, reset } = methods const queryClient = useQueryClient() const { mutate, isPending } = useMutation({ @@ -110,6 +129,9 @@ export default function AddCard() { const barometerWithImages = { ...values, date: dayjs(`${values.date}-01-01`).toISOString(), + purchasedAt: values.purchasedAt ? dayjs.utc(values.purchasedAt).toISOString() : null, + estimatedPrice: values.estimatedPrice ? parseFloat(values.estimatedPrice) : null, + subCategoryId: values.subCategoryId ? parseInt(values.subCategoryId) : null, images: await Promise.all( (values.images || []).map(async (url, i) => ({ url, @@ -148,10 +170,16 @@ export default function AddCard() { }, [condition.data, setValue]) useEffect(() => { - if (manufacturers.data.length > 0 && !watch('manufacturerId')) { + if (manufacturers.data.length > 0) { setValue('manufacturerId', String(manufacturers.data[0].id)) } - }, [manufacturers.data, setValue, watch]) + }, [manufacturers.data, setValue]) + + useEffect(() => { + if (subcategories.data.length > 0) { + setValue('subCategoryId', String(subcategories.data[0].id)) + } + }, [subcategories.data, setValue]) const handleAddManufacturer = (id: string) => { setValue('manufacturerId', id) @@ -163,11 +191,11 @@ export default function AddCard() { return (
-

Add new barometer

+

Add new barometer

- + Catalogue No. * - + + + + + )} + /> + + ( + + Serial Number + + @@ -189,7 +231,7 @@ export default function AddCard() { Title * - + @@ -205,7 +247,7 @@ export default function AddCard() { { const year = e.target.value.replace(/\D/g, '').slice(0, 4) @@ -225,7 +267,76 @@ export default function AddCard() { Date description * - + + + + + )} + /> + + ( + + Purchase Date + +
+ + +
+
+ +
+ )} + /> + + ( + + Estimated Price, € + + + + + + )} + /> + + ( + + Materials + + @@ -238,7 +349,13 @@ export default function AddCard() { render={({ field }) => ( Category * - 0 ? String(categories.data[0].id) : '') + } + > @@ -257,6 +374,37 @@ export default function AddCard() { )} /> + ( + + Movement Type + + + + )} + /> + Manufacturer *
- 0 ? String(manufacturers.data[0].id) : '') + } + > @@ -291,7 +445,13 @@ export default function AddCard() { render={({ field }) => ( Condition * - 0 ? String(condition.data.at(-1)?.id) : '') + } + > @@ -321,7 +481,12 @@ export default function AddCard() { Description -