pmtool / src /components /asset-request /AssetRequestForm.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
13 kB
import React, { useState } from 'react';
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from '@/components/ui/form';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2, Plus, X, Send, Trash2 } from 'lucide-react';
import { CustomDropdown } from '@/components/asset-management/shared/CustomDropdown';
import {
AssetRequestCreateRequest,
AssetRequestCreateSchema,
RequestedAssetItem,
ASSET_REQUEST_CONFIG,
} from '@/types/asset-request';
import { ASSET_TYPES } from '@/types/asset-management';
interface AssetRequestFormProps {
employeeID: number;
onSubmit: (data: AssetRequestCreateRequest) => Promise<void>;
isSubmitting?: boolean;
}
type AssetRequestFormValues = {
requestedAssets: RequestedAssetItem[];
justification: string;
};
const AssetRequestForm: React.FC<AssetRequestFormProps> = ({
employeeID,
onSubmit,
isSubmitting = false,
}) => {
const [justificationLength, setJustificationLength] = useState(0);
const form = useForm<AssetRequestFormValues>({
resolver: zodResolver(AssetRequestCreateSchema.omit({ employeeID: true })),
defaultValues: {
requestedAssets: [
{
assetType: '',
quantity: 1,
specifications: '',
},
],
justification: '',
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'requestedAssets',
});
const handleSubmit = async (values: AssetRequestFormValues) => {
try {
const requestData: AssetRequestCreateRequest = {
employeeID,
requestedAssets: values.requestedAssets,
justification: values.justification,
};
await onSubmit(requestData);
// Reset form on successful submission
form.reset({
requestedAssets: [
{
assetType: '',
quantity: 1,
specifications: '',
},
],
justification: '',
});
setJustificationLength(0);
} catch (error) {
console.error('Form submission error:', error);
if (error instanceof Error) {
form.setError('root', {
type: 'manual',
message: error.message,
});
}
}
};
const handleAddItem = () => {
if (fields.length < ASSET_REQUEST_CONFIG.MAX_ITEMS_PER_REQUEST) {
append({
assetType: '',
quantity: 1,
specifications: '',
});
}
};
const handleRemoveItem = (index: number) => {
if (fields.length > 1) {
remove(index);
}
};
return (
<Card className="w-full">
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2 text-lg sm:text-xl">
Request Assets
</CardTitle>
<p className="text-sm text-muted-foreground">
Submit a request for assets you need. You can request multiple items at once.
</p>
</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>
)}
{/* Requested Assets Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold">Requested Assets</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddItem}
disabled={fields.length >= ASSET_REQUEST_CONFIG.MAX_ITEMS_PER_REQUEST}
className="flex items-center gap-2"
aria-label="Add another asset to request"
>
<Plus className="h-4 w-4" aria-hidden="true" />
Add Item
</Button>
</div>
{fields.map((field, index) => (
<Card key={field.id} className="border-2">
<CardContent className="p-4 space-y-4">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium">Item {index + 1}</h4>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveItem(index)}
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
aria-label={`Remove item ${index + 1}`}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Asset Type */}
<FormField
control={form.control}
name={`requestedAssets.${index}.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.requestedAssets?.[index]?.assetType
}
aria-label={`Asset type for item ${index + 1}`}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Quantity */}
<FormField
control={form.control}
name={`requestedAssets.${index}.quantity`}
render={({ field }) => (
<FormItem>
<FormLabel>Quantity *</FormLabel>
<FormControl>
<Input
type="number"
min="1"
max={ASSET_REQUEST_CONFIG.MAX_QUANTITY_PER_ITEM}
placeholder="1"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
aria-label={`Quantity for item ${index + 1}`}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Specifications */}
<FormField
control={form.control}
name={`requestedAssets.${index}.specifications`}
render={({ field }) => (
<FormItem>
<FormLabel>Specifications (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder="e.g., 16GB RAM, i7 processor, specific brand preference..."
className="min-h-20 resize-none"
maxLength={500}
{...field}
aria-label={`Specifications for item ${index + 1}`}
/>
</FormControl>
<FormDescription className="text-xs">
{field.value?.length || 0}/500 characters
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
))}
{fields.length >= ASSET_REQUEST_CONFIG.MAX_ITEMS_PER_REQUEST && (
<p className="text-sm text-muted-foreground">
Maximum of {ASSET_REQUEST_CONFIG.MAX_ITEMS_PER_REQUEST} items per request
reached.
</p>
)}
</div>
{/* Justification */}
<FormField
control={form.control}
name="justification"
render={({ field }) => (
<FormItem>
<FormLabel>Justification *</FormLabel>
<FormControl>
<Textarea
placeholder="Please explain why you need these assets and how they will help you perform your job..."
className="min-h-32 resize-none"
maxLength={ASSET_REQUEST_CONFIG.MAX_JUSTIFICATION_LENGTH}
{...field}
onChange={(e) => {
field.onChange(e);
setJustificationLength(e.target.value.length);
}}
aria-label="Justification for asset request"
/>
</FormControl>
<FormDescription className="flex justify-between text-xs">
<span>
Minimum {ASSET_REQUEST_CONFIG.MIN_JUSTIFICATION_LENGTH} characters
required
</span>
<span
className={
justificationLength < ASSET_REQUEST_CONFIG.MIN_JUSTIFICATION_LENGTH
? 'text-destructive'
: ''
}
>
{justificationLength}/{ASSET_REQUEST_CONFIG.MAX_JUSTIFICATION_LENGTH}
</span>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Form Actions */}
<div className="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => {
form.reset();
setJustificationLength(0);
}}
disabled={isSubmitting}
className="flex items-center justify-center gap-2 w-full sm:w-auto min-h-10"
aria-label="Clear form"
>
<X className="h-4 w-4" aria-hidden="true" />
Clear
</Button>
<Button
type="submit"
disabled={isSubmitting}
className="flex items-center justify-center gap-2 w-full sm:w-auto min-h-10"
aria-label="Submit asset request"
>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
<span>Submitting...</span>
</>
) : (
<>
<Send className="h-4 w-4" aria-hidden="true" />
<span>Submit Request</span>
</>
)}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
};
export default AssetRequestForm;