Spaces:
Sleeping
Sleeping
| import React from 'react'; | |
| import { useForm } from 'react-hook-form'; | |
| import { zodResolver } from '@hookform/resolvers/zod'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { | |
| Form, | |
| FormControl, | |
| FormField, | |
| FormItem, | |
| FormLabel, | |
| FormMessage, | |
| } from '@/components/ui/form'; | |
| import { CustomDropdown } from './shared/CustomDropdown'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| import { Loader2, Save, X } from 'lucide-react'; | |
| import { | |
| Asset, | |
| AssetCreateRequest, | |
| AssetUpdateRequest, | |
| AssetCreateSchema, | |
| AssetUpdateSchema, | |
| ASSET_TYPES, | |
| ASSET_CONDITIONS | |
| } from '@/types/asset-management'; | |
| interface AssetFormProps { | |
| asset?: Asset; | |
| onSubmit: (data: AssetCreateRequest | AssetUpdateRequest) => Promise<void>; | |
| onCancel: () => void; | |
| isSubmitting?: boolean; | |
| title?: string; | |
| } | |
| type AssetFormValues = { | |
| assetCode: string; | |
| assetType: string; | |
| brandModel: string; | |
| serialNumber: string; | |
| assetCondition: string; | |
| }; | |
| const AssetForm: React.FC<AssetFormProps> = ({ | |
| asset, | |
| onSubmit, | |
| onCancel, | |
| isSubmitting = false, | |
| title | |
| }) => { | |
| const isEditing = !!asset; | |
| const formTitle = title || (isEditing ? 'Edit Asset' : 'Create New Asset'); | |
| const form = useForm<AssetFormValues>({ | |
| resolver: zodResolver(isEditing ? AssetUpdateSchema.omit({ assetID: true }) : AssetCreateSchema), | |
| defaultValues: { | |
| assetCode: asset?.assetCode || '', | |
| assetType: asset?.assetType || '', | |
| brandModel: asset?.brandModel || '', | |
| serialNumber: asset?.serialNumber || '', | |
| assetCondition: asset?.assetCondition || '', | |
| }, | |
| }); | |
| const handleSubmit = async (values: AssetFormValues) => { | |
| try { | |
| if (isEditing && asset) { | |
| const updateData: AssetUpdateRequest = { | |
| assetID: asset.assetID, | |
| ...values, | |
| }; | |
| await onSubmit(updateData); | |
| } else { | |
| const createData: AssetCreateRequest = values; | |
| await onSubmit(createData); | |
| } | |
| } catch (error) { | |
| console.error('Form submission error:', error); | |
| // Set form errors if validation failed | |
| if (error instanceof Error) { | |
| if (error.message.includes('Serial number')) { | |
| form.setError('serialNumber', { | |
| type: 'manual', | |
| message: 'This serial number is already in use. Please enter a unique serial number.' | |
| }); | |
| } else if (error.message.includes('Brand model')) { | |
| form.setError('brandModel', { | |
| type: 'manual', | |
| message: 'Invalid brand model format.' | |
| }); | |
| } else if (error.message.includes('Asset type')) { | |
| form.setError('assetType', { | |
| type: 'manual', | |
| message: 'Please select a valid asset type.' | |
| }); | |
| } else { | |
| // Set a general form error | |
| form.setError('root', { | |
| type: 'manual', | |
| message: error.message | |
| }); | |
| } | |
| } | |
| } | |
| }; | |
| return ( | |
| <Card className="w-full max-w-2xl mx-auto"> | |
| <CardHeader className="pb-4"> | |
| <CardTitle className="flex items-center gap-2 text-lg sm:text-xl"> | |
| {formTitle} | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="px-4 sm:px-6"> | |
| <Form {...form}> | |
| <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6"> | |
| {/* Display form-level errors */} | |
| {form.formState.errors.root && ( | |
| <div | |
| className="bg-destructive/15 border border-destructive/20 rounded-md p-3" | |
| role="alert" | |
| aria-live="polite" | |
| > | |
| <p className="text-sm text-destructive font-medium"> | |
| {form.formState.errors.root.message} | |
| </p> | |
| </div> | |
| )} | |
| {/* Asset Code */} | |
| <FormField | |
| control={form.control} | |
| name="assetCode" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Asset Code *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., LAP001, DES002, MON003" | |
| className="font-mono" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Asset Type */} | |
| <FormField | |
| control={form.control} | |
| name="assetType" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Asset Type *</FormLabel> | |
| <FormControl> | |
| <CustomDropdown | |
| options={ASSET_TYPES.map(type => ({ value: type, label: type }))} | |
| value={field.value} | |
| onChange={field.onChange} | |
| placeholder="Select asset type" | |
| error={!!form.formState.errors.assetType} | |
| aria-label="Asset type selection" | |
| aria-describedby={form.formState.errors.assetType ? 'assetType-error' : undefined} | |
| /> | |
| </FormControl> | |
| <FormMessage id="assetType-error" /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Brand/Model */} | |
| <FormField | |
| control={form.control} | |
| name="brandModel" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Brand/Model *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., Dell Latitude 7420, HP EliteBook 840" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Serial Number */} | |
| <FormField | |
| control={form.control} | |
| name="serialNumber" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Serial Number *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="Enter unique serial number" | |
| className="font-mono" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Asset Condition */} | |
| <FormField | |
| control={form.control} | |
| name="assetCondition" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Asset Condition *</FormLabel> | |
| <FormControl> | |
| <CustomDropdown | |
| options={ASSET_CONDITIONS.map(condition => ({ value: condition, label: condition }))} | |
| value={field.value} | |
| onChange={field.onChange} | |
| placeholder="Select asset condition" | |
| error={!!form.formState.errors.assetCondition} | |
| aria-label="Asset condition selection" | |
| aria-describedby={form.formState.errors.assetCondition ? 'assetCondition-error' : undefined} | |
| /> | |
| </FormControl> | |
| <FormMessage id="assetCondition-error" /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Form Actions */} | |
| <div className="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-4"> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={onCancel} | |
| disabled={isSubmitting} | |
| className="flex items-center justify-center gap-2 w-full sm:w-auto min-h-10" | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| onCancel(); | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| aria-label="Cancel form and return to previous page" | |
| > | |
| <X className="h-4 w-4" aria-hidden="true" /> | |
| Cancel | |
| </Button> | |
| <Button | |
| type="submit" | |
| disabled={isSubmitting} | |
| className="flex items-center justify-center gap-2 w-full sm:w-auto min-h-10" | |
| aria-describedby={isSubmitting ? 'submit-status' : undefined} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| if (!isSubmitting) { | |
| // Trigger form submission | |
| const form = e.currentTarget.closest('form'); | |
| if (form) { | |
| form.requestSubmit(); | |
| } | |
| } | |
| } | |
| }} | |
| tabIndex={0} | |
| role="button" | |
| aria-label={isEditing ? 'Update asset information' : 'Create new asset'} | |
| > | |
| {isSubmitting ? ( | |
| <> | |
| <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" /> | |
| <span id="submit-status" className="sr-only"> | |
| {isEditing ? 'Updating asset...' : 'Creating asset...'} | |
| </span> | |
| </> | |
| ) : ( | |
| <Save className="h-4 w-4" aria-hidden="true" /> | |
| )} | |
| {isSubmitting | |
| ? (isEditing ? 'Updating...' : 'Creating...') | |
| : (isEditing ? 'Update Asset' : 'Create Asset') | |
| } | |
| </Button> | |
| </div> | |
| </form> | |
| </Form> | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |
| export default AssetForm; |