Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState } 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, | |
| FormDescription, | |
| } from '@/components/ui/form'; | |
| import { CustomDropdown } from './shared/CustomDropdown'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| import { Loader2, Save, X, Monitor, Search } from 'lucide-react'; | |
| import { | |
| SystemDetails, | |
| SystemDetailsCreateRequest, | |
| SystemDetailsUpdateRequest, | |
| SystemDetailsCreateSchema, | |
| SystemDetailsUpdateSchema, | |
| Asset | |
| } from '@/types/asset-management'; | |
| import { useAssets } from '@/hooks/asset-management/useAssets'; | |
| interface SystemDetailsFormProps { | |
| systemDetails?: SystemDetails; | |
| preselectedAssetId?: number; | |
| onSubmit: (data: SystemDetailsCreateRequest | SystemDetailsUpdateRequest) => Promise<void>; | |
| onCancel: () => void; | |
| isSubmitting?: boolean; | |
| title?: string; | |
| } | |
| type SystemDetailsFormValues = { | |
| assetID: number; | |
| systemModel: string; | |
| processor: string; | |
| installedRAM: string; | |
| graphics: string; | |
| screenResolution: string; | |
| diskModel: string; | |
| diskSize: string; | |
| manufacturer: string; | |
| }; | |
| const SystemDetailsForm: React.FC<SystemDetailsFormProps> = ({ | |
| systemDetails, | |
| preselectedAssetId, | |
| onSubmit, | |
| onCancel, | |
| isSubmitting = false, | |
| title | |
| }) => { | |
| const isEditing = !!systemDetails; | |
| const formTitle = title || (isEditing ? 'Edit System Details' : 'Create System Details'); | |
| const { data: assets = [], isLoading: isLoadingAssets } = useAssets(); | |
| const [assetSearch, setAssetSearch] = useState(''); | |
| const form = useForm<SystemDetailsFormValues>({ | |
| resolver: zodResolver(isEditing ? SystemDetailsUpdateSchema.omit({ systemDetailsID: true }) : SystemDetailsCreateSchema), | |
| defaultValues: { | |
| assetID: systemDetails?.assetID || preselectedAssetId || 0, | |
| systemModel: systemDetails?.systemModel || '', | |
| processor: systemDetails?.processor || '', | |
| installedRAM: systemDetails?.installedRAM || '', | |
| graphics: systemDetails?.graphics || '', | |
| screenResolution: systemDetails?.screenResolution || '', | |
| diskModel: systemDetails?.diskModel || '', | |
| diskSize: systemDetails?.diskSize || '', | |
| manufacturer: systemDetails?.manufacturer || '', | |
| }, | |
| }); | |
| // Filter assets based on search term | |
| const filteredAssets = assets.filter(asset => | |
| asset.assetType.toLowerCase().includes(assetSearch.toLowerCase()) || | |
| asset.brandModel.toLowerCase().includes(assetSearch.toLowerCase()) || | |
| asset.serialNumber.toLowerCase().includes(assetSearch.toLowerCase()) || | |
| asset.assetCode.toLowerCase().includes(assetSearch) | |
| ); | |
| const handleSubmit = async (values: SystemDetailsFormValues) => { | |
| try { | |
| if (isEditing && systemDetails) { | |
| const updateData: SystemDetailsUpdateRequest = { | |
| systemDetailsID: systemDetails.systemDetailsID, | |
| ...values, | |
| }; | |
| await onSubmit(updateData); | |
| } else { | |
| const createData: SystemDetailsCreateRequest = 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('Asset with ID') && error.message.includes('does not exist')) { | |
| form.setError('assetID', { | |
| type: 'manual', | |
| message: 'The selected asset does not exist. Please select a valid asset.' | |
| }); | |
| } else if (error.message.includes('System details already exist')) { | |
| form.setError('assetID', { | |
| type: 'manual', | |
| message: 'System details already exist for this asset. Please select a different asset.' | |
| }); | |
| } else if (error.message.includes('System model')) { | |
| form.setError('systemModel', { | |
| type: 'manual', | |
| message: 'Invalid system model format.' | |
| }); | |
| } else if (error.message.includes('Processor')) { | |
| form.setError('processor', { | |
| type: 'manual', | |
| message: 'Invalid processor information.' | |
| }); | |
| } else { | |
| // Set a general form error | |
| form.setError('root', { | |
| type: 'manual', | |
| message: error.message | |
| }); | |
| } | |
| } | |
| } | |
| }; | |
| // Common RAM options | |
| const ramOptions = [ | |
| '4GB', '8GB', '16GB', '32GB', '64GB', '128GB' | |
| ]; | |
| // Common screen resolutions | |
| const resolutionOptions = [ | |
| '1366x768', '1920x1080', '2560x1440', '3840x2160', '2560x1600', '1440x900' | |
| ]; | |
| // Common disk sizes | |
| const diskSizeOptions = [ | |
| '128GB', '256GB', '512GB', '1TB', '2TB', '4TB' | |
| ]; | |
| return ( | |
| <Card className="w-full max-w-4xl mx-auto"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Monitor className="h-5 w-5" /> | |
| {formTitle} | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <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"> | |
| <p className="text-sm text-destructive font-medium"> | |
| {form.formState.errors.root.message} | |
| </p> | |
| </div> | |
| )} | |
| {/* Asset Selection */} | |
| <FormField | |
| control={form.control} | |
| name="assetID" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Asset *</FormLabel> | |
| <FormDescription> | |
| Select the asset for which you want to add system details. | |
| </FormDescription> | |
| <div className="space-y-2"> | |
| <div className="relative"> | |
| <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" /> | |
| <Input | |
| placeholder="Search assets..." | |
| value={assetSearch} | |
| onChange={(e) => setAssetSearch(e.target.value)} | |
| className="pl-10" | |
| /> | |
| </div> | |
| <FormControl> | |
| <CustomDropdown | |
| options={filteredAssets.map(asset => ({ | |
| value: asset.assetID.toString(), | |
| label: `${asset.assetCode} - ${asset.assetType} - ${asset.brandModel}` | |
| }))} | |
| value={field.value?.toString() || ''} | |
| onChange={(value) => field.onChange(parseInt(value))} | |
| placeholder="Select an asset" | |
| disabled={isLoadingAssets || (isEditing && !!systemDetails)} | |
| error={!!form.formState.errors.assetID} | |
| aria-label="Asset selection" | |
| aria-describedby={form.formState.errors.assetID ? 'assetID-error' : undefined} | |
| /> | |
| </FormControl> | |
| </div> | |
| <FormMessage id="assetID-error" /> | |
| </FormItem> | |
| )} | |
| /> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* System Model */} | |
| <FormField | |
| control={form.control} | |
| name="systemModel" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>System Model *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., Dell Latitude 7420, MacBook Pro 16" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Manufacturer */} | |
| <FormField | |
| control={form.control} | |
| name="manufacturer" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Manufacturer *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., Dell, HP, Apple, Lenovo" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Processor */} | |
| <FormField | |
| control={form.control} | |
| name="processor" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Processor *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., Intel Core i7-11800H, AMD Ryzen 7 5800H" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Installed RAM */} | |
| <FormField | |
| control={form.control} | |
| name="installedRAM" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Installed RAM *</FormLabel> | |
| <FormControl> | |
| <CustomDropdown | |
| options={[ | |
| ...ramOptions.map(ram => ({ value: ram, label: ram })), | |
| { value: 'custom', label: 'Custom Amount' } | |
| ]} | |
| value={field.value} | |
| onChange={field.onChange} | |
| placeholder="Select RAM amount" | |
| error={!!form.formState.errors.installedRAM} | |
| aria-label="RAM amount selection" | |
| aria-describedby={form.formState.errors.installedRAM ? 'installedRAM-error' : undefined} | |
| /> | |
| </FormControl> | |
| {field.value === 'custom' && ( | |
| <Input | |
| placeholder="Enter custom RAM amount" | |
| onChange={(e) => field.onChange(e.target.value)} | |
| className="mt-2" | |
| /> | |
| )} | |
| <FormMessage id="installedRAM-error" /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Graphics */} | |
| <FormField | |
| control={form.control} | |
| name="graphics" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Graphics *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., NVIDIA RTX 3060, Intel Iris Xe" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Screen Resolution */} | |
| <FormField | |
| control={form.control} | |
| name="screenResolution" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Screen Resolution *</FormLabel> | |
| <FormControl> | |
| <CustomDropdown | |
| options={[ | |
| ...resolutionOptions.map(resolution => ({ value: resolution, label: resolution })), | |
| { value: 'custom', label: 'Custom Resolution' } | |
| ]} | |
| value={field.value} | |
| onChange={field.onChange} | |
| placeholder="Select resolution" | |
| error={!!form.formState.errors.screenResolution} | |
| aria-label="Screen resolution selection" | |
| aria-describedby={form.formState.errors.screenResolution ? 'screenResolution-error' : undefined} | |
| /> | |
| </FormControl> | |
| {field.value === 'custom' && ( | |
| <Input | |
| placeholder="Enter custom resolution (e.g., 2880x1800)" | |
| onChange={(e) => field.onChange(e.target.value)} | |
| className="mt-2" | |
| /> | |
| )} | |
| <FormMessage id="screenResolution-error" /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Disk Model */} | |
| <FormField | |
| control={form.control} | |
| name="diskModel" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Disk Model *</FormLabel> | |
| <FormControl> | |
| <Input | |
| placeholder="e.g., Samsung SSD 980 PRO, WD Blue SN570" | |
| {...field} | |
| /> | |
| </FormControl> | |
| <FormMessage /> | |
| </FormItem> | |
| )} | |
| /> | |
| {/* Disk Size */} | |
| <FormField | |
| control={form.control} | |
| name="diskSize" | |
| render={({ field }) => ( | |
| <FormItem> | |
| <FormLabel>Disk Size *</FormLabel> | |
| <FormControl> | |
| <CustomDropdown | |
| options={[ | |
| ...diskSizeOptions.map(size => ({ value: size, label: size })), | |
| { value: 'custom', label: 'Custom Size' } | |
| ]} | |
| value={field.value} | |
| onChange={field.onChange} | |
| placeholder="Select disk size" | |
| error={!!form.formState.errors.diskSize} | |
| aria-label="Disk size selection" | |
| aria-describedby={form.formState.errors.diskSize ? 'diskSize-error' : undefined} | |
| /> | |
| </FormControl> | |
| {field.value === 'custom' && ( | |
| <Input | |
| placeholder="Enter custom disk size" | |
| onChange={(e) => field.onChange(e.target.value)} | |
| className="mt-2" | |
| /> | |
| )} | |
| <FormMessage id="diskSize-error" /> | |
| </FormItem> | |
| )} | |
| /> | |
| </div> | |
| {/* Form Actions */} | |
| <div className="flex justify-end gap-3 pt-4"> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={onCancel} | |
| disabled={isSubmitting} | |
| className="flex items-center gap-2" | |
| > | |
| <X className="h-4 w-4" /> | |
| Cancel | |
| </Button> | |
| <Button | |
| type="submit" | |
| disabled={isSubmitting} | |
| className="flex items-center gap-2" | |
| > | |
| {isSubmitting ? ( | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| ) : ( | |
| <Save className="h-4 w-4" /> | |
| )} | |
| {isSubmitting | |
| ? (isEditing ? 'Updating...' : 'Creating...') | |
| : (isEditing ? 'Update System Details' : 'Create System Details') | |
| } | |
| </Button> | |
| </div> | |
| </form> | |
| </Form> | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |
| export default SystemDetailsForm; |