File size: 24,199 Bytes
d97b8f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
import React, { useState, useRef, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { format } from 'date-fns';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Calendar } from '@/components/ui/calendar';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
  FormDescription,
} from '@/components/ui/form';
import { SearchableDropdown } from './shared/SearchableDropdown';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Loader2, Save, X, Calendar as CalendarIcon, User } from 'lucide-react';

import {
  AssetAssignment,
  AssetAssignmentCreateRequest,
  AssetAssignmentUpdateRequest,
  AssetAssignmentFormSchema,
} from '@/types/asset-management';
import { Employee } from '@/types';
import { useAssets } from '@/hooks/asset-management/useAssets';

// Custom DatePicker component that works in modals
interface CustomDatePickerProps {
  value: Date | null;
  onChange: (date: Date | null) => void;
  placeholder?: string;
  disabled?: (date: Date) => boolean;
}

const CustomDatePicker: React.FC<CustomDatePickerProps> = ({

  value,

  onChange,

  placeholder = "Pick a date",

  disabled

}) => {
  const [isOpen, setIsOpen] = useState(false);
  const datePickerRef = useRef<HTMLDivElement>(null);

  // Close date picker when clicking outside
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (datePickerRef.current && !datePickerRef.current.contains(event.target as Node)) {
        setIsOpen(false);
      }
    };

    if (isOpen) {
      document.addEventListener("mousedown", handleClickOutside);
      return () => {
        document.removeEventListener("mousedown", handleClickOutside);
      };
    }
  }, [isOpen]);

  return (
    <div className="relative w-full" ref={datePickerRef}>

      <button

        type="button"

        onClick={() => setIsOpen(!isOpen)}

        className="flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-h-[44px] md:min-h-[40px] touch-manipulation"

        aria-label={value ? `Selected date: ${format(value, "PPP")}` : placeholder}

      >

        <span className={value ? "" : "text-muted-foreground"}>

          {value ? format(value, "PPP") : placeholder}

        </span>

        <CalendarIcon className="h-4 w-4 opacity-50" />

      </button>



      {isOpen && (

        <div className="fixed inset-0 z-[99999] flex justify-center items-center bg-black/50 overflow-auto">

          <div className="bg-background border rounded-lg p-4 shadow-lg max-w-sm w-full mx-4">

            <div className="flex justify-between items-center mb-2">

              <h3 className="text-base font-medium">Select Date</h3>

              <Button

                variant="ghost"

                size="sm"

                className="h-8 w-8 p-0"

                onClick={() => setIsOpen(false)}

              >

                <X className="h-4 w-4" />

              </Button>

            </div>

            <Calendar

              mode="single"

              selected={value || undefined}

              onSelect={(date) => {

                onChange(date || null);

                setIsOpen(false);

              }}

              disabled={disabled}

              initialFocus

              className="rounded border"

            />

            <div className="mt-2 flex justify-end gap-2">

              <Button

                size="sm"

                variant="outline"

                onClick={() => {

                  onChange(null);

                  setIsOpen(false);

                }}

                className="min-h-[44px] md:min-h-0 touch-manipulation"

                aria-label="Clear date selection"

              >

                Clear

              </Button>

              <Button

                size="sm"

                variant="outline"

                onClick={() => setIsOpen(false)}

                className="min-h-[44px] md:min-h-0 touch-manipulation"

                aria-label="Close date picker"

              >

                Close

              </Button>

            </div>

          </div>

        </div>

      )}

    </div>
  );
};

interface AssignmentFormProps {
  assignment?: AssetAssignment;
  preselectedAssetId?: number;
  preselectedEmployeeId?: number;
  employees?: Employee[];
  onSubmit: (data: AssetAssignmentCreateRequest | AssetAssignmentUpdateRequest) => Promise<void>;
  onCancel: () => void;
  isSubmitting?: boolean;
  title?: string;
}

type AssignmentFormValues = {
  assetID: number;
  employeeID: number;
  employeeName: string;
  assetCode: string;
  assignedDate: Date;
  returnedDate: Date | null;
  remarks: string;
};

const AssignmentForm: React.FC<AssignmentFormProps> = ({

  assignment,

  preselectedAssetId,

  preselectedEmployeeId,

  employees = [],

  onSubmit,

  onCancel,

  isSubmitting = false,

  title

}) => {
  const isEditing = !!assignment;
  const formTitle = title || (isEditing ? 'Edit Assignment' : 'Create New Assignment');

  const { data: assets = [], isLoading: isLoadingAssets } = useAssets();
  const employeeFieldRef = useRef<HTMLButtonElement>(null);

  const form = useForm<AssignmentFormValues>({
    resolver: zodResolver(AssetAssignmentFormSchema),
    defaultValues: {
      assetID: assignment?.assetID || preselectedAssetId || 0,
      employeeID: assignment?.employeeID || preselectedEmployeeId || 0,
      employeeName: assignment?.employeeName || '',
      assetCode: assignment?.assetCode || '',
      assignedDate: assignment ? new Date(assignment.assignedDate) : new Date(),
      returnedDate: assignment?.returnedDate ? new Date(assignment.returnedDate) : null,
      remarks: assignment?.remarks || '',
    },
  });

  // Get preselected asset details
  const preselectedAsset = preselectedAssetId 
    ? assets.find(a => a.assetID === preselectedAssetId)
    : null;

  // Auto-populate asset field when preselectedAssetId is provided
  useEffect(() => {
    if (preselectedAssetId && assets.length > 0 && !assignment) {
      const asset = assets.find(a => a.assetID === preselectedAssetId);
      if (asset) {
        form.setValue('assetID', asset.assetID);
        form.setValue('assetCode', asset.assetCode);
      }
    }
  }, [preselectedAssetId, assets, assignment, form]);

  // Auto-focus employee selection field when asset is pre-selected
  useEffect(() => {
    if (preselectedAssetId && !assignment && employeeFieldRef.current) {
      // Small delay to ensure the form is fully rendered
      const timer = setTimeout(() => {
        employeeFieldRef.current?.focus();
      }, 100);
      return () => clearTimeout(timer);
    }
  }, [preselectedAssetId, assignment]);

  const handleSubmit = async (values: AssignmentFormValues) => {
    try {
      // Validate dates before submission
      if (values.returnedDate && values.returnedDate <= values.assignedDate) {
        form.setError('returnedDate', {
          type: 'manual',
          message: 'Returned date must be after the assigned date.'
        });
        return;
      }

      // Convert dates to ISO strings
      const assignedDateISO = values.assignedDate.toISOString();
      const returnedDateISO = values.returnedDate ? values.returnedDate.toISOString() : null;

      if (isEditing && assignment) {
        const updateData: AssetAssignmentUpdateRequest = {
          assignmentID: assignment.assignmentID,
          assetID: values.assetID,
          employeeID: values.employeeID,
          employeeName: values.employeeName,
          assetCode: values.assetCode,
          assignedDate: assignedDateISO,
          returnedDate: returnedDateISO,
          remarks: values.remarks,
        };
        await onSubmit(updateData);
      } else {
        const createData: AssetAssignmentCreateRequest = {
          assetID: values.assetID,
          employeeID: values.employeeID,
          employeeName: values.employeeName,
          assetCode: values.assetCode,
          assignedDate: assignedDateISO,
          returnedDate: returnedDateISO,
          remarks: values.remarks,
        };
        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('Employee with ID') && error.message.includes('does not exist')) {
          form.setError('employeeID', {
            type: 'manual',
            message: 'The selected employee does not exist. Please select a valid employee.'
          });
        } else if (error.message.includes('already assigned')) {
          form.setError('assetID', {
            type: 'manual',
            message: 'This asset is already assigned to another employee. Please select a different asset or return the current assignment first.'
          });
        } else if (error.message.includes('date')) {
          form.setError('assignedDate', {
            type: 'manual',
            message: 'Invalid date format. Please select a valid date.'
          });
        } else {
          // Set a general form error
          form.setError('root', {
            type: 'manual',
            message: error.message
          });
        }
      }
    }
  };

  const getEmployeeDisplayName = (employee: Employee) => {
    return `${employee.firstName} ${employee.lastName}`;
  };

  return (
    <Card className="w-full mx-auto">

      <CardHeader>

        <CardTitle className="flex items-center gap-2">

          <User 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>

            )}



            {/* Display pre-selected asset information prominently */}

            {preselectedAsset && !assignment && (

              <div className="bg-primary/5 border border-primary/20 rounded-lg p-4">

                <div className="flex items-start gap-3">

                  <div className="flex-shrink-0 w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">

                    <User className="h-5 w-5 text-primary" />

                  </div>

                  <div className="flex-1 min-w-0">

                    <h4 className="text-sm font-semibold text-foreground mb-1">

                      Assigning Asset

                    </h4>

                    <div className="space-y-1">

                      <p className="text-sm text-muted-foreground">

                        <span className="font-medium text-foreground">Code:</span> {preselectedAsset.assetCode}

                      </p>

                      <p className="text-sm text-muted-foreground">

                        <span className="font-medium text-foreground">Type:</span> {preselectedAsset.assetType}

                      </p>

                      <p className="text-sm text-muted-foreground">

                        <span className="font-medium text-foreground">Model:</span> {preselectedAsset.brandModel}

                      </p>

                    </div>

                  </div>

                </div>

              </div>

            )}



            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">

              {/* Asset Selection */}

              <FormField

                control={form.control}

                name="assetID"

                render={({ field }) => (

                  <FormItem>

                    <FormLabel>Asset *</FormLabel>

                    <FormDescription>

                      {preselectedAssetId && !assignment 

                        ? 'Asset is pre-selected for this assignment.'

                        : 'Select the asset to assign to the employee.'}

                    </FormDescription>

                    <FormControl>

                      <SearchableDropdown

                        options={assets.map(asset => ({

                          value: asset.assetID.toString(),

                          label: `${asset.assetCode} - ${asset.assetType} - ${asset.brandModel}`

                        }))}

                        value={field.value?.toString() || ''}

                        onChange={(value) => {

                          const assetId = parseInt(value);

                          field.onChange(assetId);

                          // Auto-populate asset code when asset is selected

                          const selectedAsset = assets.find(asset => asset.assetID === assetId);

                          if (selectedAsset) {

                            form.setValue('assetCode', selectedAsset.assetCode);

                          }

                        }}

                        placeholder="Select an asset"

                        searchPlaceholder="Search assets by code, type, or model..."

                        disabled={isLoadingAssets || (!!preselectedAssetId && !assignment)}

                        error={!!form.formState.errors.assetID}

                        aria-label="Asset selection"

                        aria-describedby={form.formState.errors.assetID ? 'assetID-error' : undefined}

                        emptyMessage="No assets found matching your search"

                      />

                    </FormControl>

                    <FormMessage id="assetID-error" />

                  </FormItem>

                )}

              />



              {/* Employee Selection */}

              <FormField

                control={form.control}

                name="employeeID"

                render={({ field }) => (

                  <FormItem>

                    <FormLabel>Employee *</FormLabel>

                    <FormDescription>

                      Select the employee to assign the asset to.

                    </FormDescription>

                    <FormControl>

                      <div ref={(el) => {

                        if (el && preselectedAssetId && !assignment) {

                          const trigger = el.querySelector('[role="combobox"]') as HTMLElement;

                          if (trigger) {

                            (employeeFieldRef as any).current = trigger;

                          }

                        }

                      }}>

                        <SearchableDropdown

                          options={employees.map(employee => ({

                            value: employee.id.toString(),

                            label: `#${employee.empCode} - ${getEmployeeDisplayName(employee)} - ${employee.email}`

                          }))}

                          value={field.value?.toString() || ''}

                          onChange={(value) => {

                            const employeeId = parseInt(value);

                            field.onChange(employeeId);

                            // Auto-populate employee name when employee is selected

                            const selectedEmployee = employees.find(employee => employee.id === employeeId);

                            if (selectedEmployee) {

                              form.setValue('employeeName', getEmployeeDisplayName(selectedEmployee));

                            }

                          }}

                          placeholder="Select an employee"

                          searchPlaceholder="Search employees by name, email, or ID..."

                          error={!!form.formState.errors.employeeID}

                          aria-label="Employee selection"

                          aria-describedby={form.formState.errors.employeeID ? 'employeeID-error' : undefined}

                          emptyMessage="No employees found matching your search"

                        />

                      </div>

                    </FormControl>

                    <FormMessage id="employeeID-error" />

                  </FormItem>

                )}

              />



              {/* Employee Name (Auto-populated) */}

              <FormField

                control={form.control}

                name="employeeName"

                render={({ field }) => (

                  <FormItem>

                    <FormLabel>Employee Name *</FormLabel>

                    <FormControl>

                      <input

                        {...field}

                        type="text"

                        placeholder="Employee name will be auto-populated"

                        className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"

                        readOnly

                      />

                    </FormControl>

                    <FormDescription>

                      This field is automatically populated when you select an employee.

                    </FormDescription>

                    <FormMessage />

                  </FormItem>

                )}

              />



              {/* Asset Code (Auto-populated) */}

              <FormField

                control={form.control}

                name="assetCode"

                render={({ field }) => (

                  <FormItem>

                    <FormLabel>Asset Code *</FormLabel>

                    <FormControl>

                      <input

                        {...field}

                        type="text"

                        placeholder="Asset code will be auto-populated"

                        className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"

                        readOnly

                      />

                    </FormControl>

                    <FormDescription>

                      This field is automatically populated when you select an asset.

                    </FormDescription>

                    <FormMessage />

                  </FormItem>

                )}

              />



              {/* Assigned Date */}

              <FormField

                control={form.control}

                name="assignedDate"

                render={({ field }) => (

                  <FormItem className="flex flex-col">

                    <FormLabel>Assigned Date *</FormLabel>

                    <FormControl>

                      <CustomDatePicker

                        value={field.value}

                        onChange={field.onChange}

                        placeholder="Pick a date"

                        disabled={(date) => date < new Date("1900-01-01")}

                      />

                    </FormControl>

                    <FormDescription>

                      The date when the asset was assigned to the employee.

                    </FormDescription>

                    <FormMessage />

                  </FormItem>

                )}

              />



              {/* Returned Date */}

              <FormField

                control={form.control}

                name="returnedDate"

                render={({ field }) => (

                  <FormItem className="flex flex-col">

                    <FormLabel>Returned Date</FormLabel>

                    <FormControl>

                      <CustomDatePicker

                        value={field.value}

                        onChange={field.onChange}

                        placeholder="Not returned yet"

                        disabled={(date) => {

                          const assignedDate = form.getValues('assignedDate');

                          return (

                            date < new Date("1900-01-01") ||

                            (assignedDate && date < assignedDate)

                          );

                        }}

                      />

                    </FormControl>

                    <FormDescription>

                      Leave empty if the asset is still assigned. The returned date must be after the assigned date.

                    </FormDescription>

                    <FormMessage />

                  </FormItem>

                )}

              />

            </div>



            {/* Remarks */}

            <FormField

              control={form.control}

              name="remarks"

              render={({ field }) => (

                <FormItem>

                  <FormLabel>Remarks *</FormLabel>

                  <FormControl>

                    <Textarea

                      placeholder="Enter any additional notes about this assignment..."

                      className="min-h-20"

                      {...field}

                    />

                  </FormControl>

                  <FormDescription>

                    Provide any relevant information about this assignment (purpose, conditions, etc.).

                  </FormDescription>

                  <FormMessage />

                </FormItem>

              )}

            />



            {/* Form Actions */}

            <div className="flex flex-col 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 min-h-[44px] md:min-h-0 touch-manipulation w-full sm:w-auto"

                aria-label="Cancel assignment form"

              >

                <X className="h-4 w-4" />

                <span>Cancel</span>

              </Button>

              <Button

                type="submit"

                disabled={isSubmitting}

                className="flex items-center justify-center gap-2 min-h-[44px] md:min-h-0 touch-manipulation w-full sm:w-auto"

                aria-label={isEditing ? 'Update assignment' : 'Create assignment'}

              >

                {isSubmitting ? (

                  <Loader2 className="h-4 w-4 animate-spin" />

                ) : (

                  <Save className="h-4 w-4" />

                )}

                <span>

                  {isSubmitting

                    ? (isEditing ? 'Updating...' : 'Creating...')

                    : (isEditing ? 'Update Assignment' : 'Create Assignment')

                  }

                </span>

              </Button>

            </div>

          </form>

        </Form>

      </CardContent>

    </Card>
  );
};

export default AssignmentForm;