514 lines
18 KiB
TypeScript
514 lines
18 KiB
TypeScript
"use client";
|
|
import React, { useEffect, useState } from "react";
|
|
import {
|
|
PeopleSchema,
|
|
PeopleFormData,
|
|
CreatePeopleSchema,
|
|
UpdatePeopleSchema,
|
|
ViewPeopleSchema,
|
|
fieldDefinitions,
|
|
fieldsByMode,
|
|
} from "./schema";
|
|
import { getTranslation, LanguageKey } from "./language";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useForm } from "react-hook-form";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface FormComponentProps {
|
|
lang: LanguageKey;
|
|
mode: "create" | "update" | "view";
|
|
onCancel: () => void;
|
|
refetch: () => void;
|
|
setMode: React.Dispatch<
|
|
React.SetStateAction<"list" | "create" | "view" | "update">
|
|
>;
|
|
setSelectedItem: React.Dispatch<React.SetStateAction<PeopleFormData | null>>;
|
|
initialData?: Partial<PeopleFormData>;
|
|
}
|
|
|
|
export function FormComponent({
|
|
lang,
|
|
mode,
|
|
onCancel,
|
|
refetch,
|
|
setMode,
|
|
setSelectedItem,
|
|
initialData,
|
|
}: FormComponentProps) {
|
|
// Derive readOnly from mode
|
|
const readOnly = mode === "view";
|
|
const t = getTranslation(lang);
|
|
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
|
|
|
|
// Select the appropriate schema based on the mode
|
|
const getSchemaForMode = () => {
|
|
switch (mode) {
|
|
case "create":
|
|
return CreatePeopleSchema;
|
|
case "update":
|
|
return UpdatePeopleSchema;
|
|
case "view":
|
|
return ViewPeopleSchema;
|
|
default:
|
|
return PeopleSchema;
|
|
}
|
|
};
|
|
|
|
// Get field definitions for the current mode
|
|
const modeFieldDefinitions = fieldDefinitions.getDefinitionsByMode(
|
|
mode
|
|
) as Record<string, any>;
|
|
|
|
// Define FormValues type based on the current mode to fix TypeScript errors
|
|
type FormValues = Record<string, any>;
|
|
|
|
// Get default values directly from the field definitions
|
|
const getDefaultValues = (): FormValues => {
|
|
// For view and update modes, use initialData if available
|
|
if ((mode === "view" || mode === "update") && initialData) {
|
|
return initialData as FormValues;
|
|
}
|
|
|
|
// For create mode or when initialData is not available, use default values from schema
|
|
const defaults: FormValues = {};
|
|
|
|
Object.entries(modeFieldDefinitions).forEach(([key, field]) => {
|
|
if (field && typeof field === "object" && "defaultValue" in field) {
|
|
defaults[key] = field.defaultValue;
|
|
}
|
|
});
|
|
|
|
return defaults;
|
|
};
|
|
|
|
// Define form with react-hook-form and zod validation
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(getSchemaForMode()) as any, // Type assertion to fix TypeScript errors
|
|
defaultValues: getDefaultValues(),
|
|
mode: "onChange",
|
|
});
|
|
|
|
// Update form values when initialData changes
|
|
useEffect(() => {
|
|
if (initialData && (mode === "update" || mode === "view")) {
|
|
// Reset the form with initialData
|
|
form.reset(initialData as FormValues);
|
|
}
|
|
}, [initialData, form, mode]);
|
|
|
|
// Define the submission handler function
|
|
const onSubmitHandler = async (data: FormValues) => {
|
|
setFormSubmitting(true);
|
|
|
|
try {
|
|
// Call different API methods based on the current mode
|
|
if (mode === "create") {
|
|
// Call create API
|
|
console.log("Creating new record:", data);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
// In a real application, you would call your API here
|
|
// Example: await createPerson(data);
|
|
} else if (mode === "update") {
|
|
// Call update API
|
|
console.log("Updating existing record:", data);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
// In a real application, you would call your API here
|
|
// Example: await updatePerson(data);
|
|
}
|
|
|
|
// Show success message or notification here
|
|
|
|
// Return to list view and reset selected item
|
|
handleReturnToList();
|
|
|
|
// Refresh data
|
|
refetch();
|
|
} catch (error) {
|
|
// Handle any errors from the API calls
|
|
console.error("Error saving data:", error);
|
|
// You could set an error state here to display to the user
|
|
} finally {
|
|
setFormSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// Helper function to return to list view
|
|
const handleReturnToList = () => {
|
|
setMode("list");
|
|
setSelectedItem(null);
|
|
};
|
|
|
|
// Handle cancel button click
|
|
const handleCancel = () => {
|
|
onCancel();
|
|
|
|
// Return to list view
|
|
handleReturnToList();
|
|
};
|
|
|
|
// Filter fields based on the current mode
|
|
const activeFields = fieldsByMode[readOnly ? "view" : mode];
|
|
|
|
// Group fields by their section using mode-specific field definitions
|
|
const fieldGroups = activeFields.reduce(
|
|
(groups: Record<string, any[]>, fieldName: string) => {
|
|
const field = modeFieldDefinitions[fieldName];
|
|
if (field && typeof field === "object" && "group" in field) {
|
|
const group = field.group as string;
|
|
if (!groups[group]) {
|
|
groups[group] = [];
|
|
}
|
|
groups[group].push({
|
|
name: fieldName,
|
|
type: field.type as string,
|
|
readOnly: (field.readOnly as boolean) || readOnly, // Combine component readOnly with field readOnly
|
|
required: (field.required as boolean) || false,
|
|
label: (field.label as string) || fieldName,
|
|
});
|
|
}
|
|
return groups;
|
|
},
|
|
{} as Record<
|
|
string,
|
|
{
|
|
name: string;
|
|
type: string;
|
|
readOnly: boolean;
|
|
required: boolean;
|
|
label: string;
|
|
}[]
|
|
>
|
|
);
|
|
|
|
// Create helper variables for field group checks
|
|
const hasIdentificationFields = fieldGroups.identificationInfo?.length > 0;
|
|
const hasPersonalFields = fieldGroups.personalInfo?.length > 0;
|
|
const hasLocationFields = fieldGroups.locationInfo?.length > 0;
|
|
const hasExpiryFields = fieldGroups.expiryInfo?.length > 0;
|
|
const hasStatusFields = fieldGroups.statusInfo?.length > 0;
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
{formSubmitting && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
|
|
<div className="bg-white p-4 rounded-lg shadow-lg">
|
|
<div className="flex items-center space-x-3">
|
|
<svg
|
|
className="animate-spin h-5 w-5 text-blue-500"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
></circle>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
></path>
|
|
</svg>
|
|
<span>{mode === "create" ? t.creating : t.updating}...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<h2 className="text-xl font-bold mb-4">{t.title || "Person Details"}</h2>
|
|
<h2 className="text-lg font-semibold mb-4">
|
|
{readOnly ? t.view : initialData?.uu_id ? t.update : t.createNew}
|
|
</h2>
|
|
|
|
<Form {...form}>
|
|
<div className="space-y-6">
|
|
{/* Identification Information Section */}
|
|
{hasIdentificationFields && (
|
|
<div className="bg-gray-50 p-4 rounded-md">
|
|
<h3 className="text-lg font-medium mb-3">Identification</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{fieldGroups.identificationInfo.map((field) => (
|
|
<FormField
|
|
key={field.name}
|
|
control={form.control}
|
|
name={field.name as any}
|
|
render={({ field: formField }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{field.label}
|
|
{field.required && (
|
|
<span className="text-red-500 ml-1">*</span>
|
|
)}
|
|
</FormLabel>
|
|
<FormControl>
|
|
{field.type === "checkbox" ? (
|
|
<Checkbox
|
|
checked={formField.value as boolean}
|
|
onCheckedChange={formField.onChange}
|
|
disabled={field.readOnly}
|
|
/>
|
|
) : field.type === "date" ? (
|
|
<Input
|
|
type="date"
|
|
{...formField}
|
|
value={formField.value || ""}
|
|
disabled={field.readOnly}
|
|
className={cn(
|
|
"w-full",
|
|
field.readOnly && "bg-gray-100"
|
|
)}
|
|
/>
|
|
) : (
|
|
<Input
|
|
{...formField}
|
|
value={formField.value || ""}
|
|
disabled={field.readOnly}
|
|
className={cn(
|
|
"w-full",
|
|
field.readOnly && "bg-gray-100"
|
|
)}
|
|
/>
|
|
)}
|
|
</FormControl>
|
|
<FormDescription>
|
|
{field.name
|
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
|
""
|
|
: ""}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Personal Information Section */}
|
|
{hasPersonalFields && (
|
|
<div className="bg-blue-50 p-4 rounded-md">
|
|
<h3 className="text-lg font-medium mb-3">Personal Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{fieldGroups.personalInfo.map((field) => (
|
|
<FormField
|
|
key={field.name}
|
|
control={form.control}
|
|
name={field.name as any}
|
|
render={({ field: formField }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{field.label}
|
|
{field.required && (
|
|
<span className="text-red-500 ml-1">*</span>
|
|
)}
|
|
</FormLabel>
|
|
<FormControl>
|
|
{field.type === "checkbox" ? (
|
|
<Checkbox
|
|
checked={formField.value as boolean}
|
|
onCheckedChange={formField.onChange}
|
|
disabled={field.readOnly}
|
|
/>
|
|
) : field.type === "date" ? (
|
|
<Input
|
|
type="date"
|
|
{...formField}
|
|
value={formField.value || ""}
|
|
disabled={field.readOnly}
|
|
className={cn(
|
|
"w-full",
|
|
field.readOnly && "bg-gray-100"
|
|
)}
|
|
/>
|
|
) : (
|
|
<Input
|
|
{...formField}
|
|
value={formField.value || ""}
|
|
disabled={field.readOnly}
|
|
className={cn(
|
|
"w-full",
|
|
field.readOnly && "bg-gray-100"
|
|
)}
|
|
/>
|
|
)}
|
|
</FormControl>
|
|
<FormDescription>
|
|
{field.name
|
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
|
""
|
|
: ""}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Location Information Section */}
|
|
{hasLocationFields && (
|
|
<div className="bg-green-50 p-4 rounded-md">
|
|
<h3 className="text-lg font-medium mb-3">Location Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{fieldGroups.locationInfo.map((field) => (
|
|
<FormField
|
|
key={field.name}
|
|
control={form.control}
|
|
name={field.name as any}
|
|
render={({ field: formField }) => (
|
|
<FormItem>
|
|
<FormLabel>{field.label}</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...formField}
|
|
value={formField.value || ""}
|
|
disabled={true} // System fields are always read-only
|
|
className="w-full bg-gray-100"
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{field.name
|
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
|
""
|
|
: ""}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Expiry Information Section */}
|
|
{hasExpiryFields && (
|
|
<div className="bg-yellow-50 p-4 rounded-md">
|
|
<h3 className="text-lg font-medium mb-3">Expiry Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{fieldGroups.expiryInfo.map((field) => (
|
|
<FormField
|
|
key={field.name}
|
|
control={form.control}
|
|
name={field.name as any}
|
|
render={({ field: formField }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
{field.label}
|
|
{field.required && (
|
|
<span className="text-red-500 ml-1">*</span>
|
|
)}
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="date"
|
|
{...formField}
|
|
value={formField.value || ""}
|
|
disabled={field.readOnly}
|
|
className={cn(
|
|
"w-full",
|
|
field.readOnly && "bg-gray-100"
|
|
)}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{field.name
|
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
|
""
|
|
: ""}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Status Information Section */}
|
|
{hasStatusFields && (
|
|
<div className="bg-purple-50 p-4 rounded-md">
|
|
<h3 className="text-lg font-medium mb-3">Status Information</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{fieldGroups.statusInfo.map((field) => (
|
|
<FormField
|
|
key={field.name}
|
|
control={form.control}
|
|
name={field.name as any}
|
|
render={({ field: formField }) => (
|
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
|
<FormControl>
|
|
<Checkbox
|
|
checked={formField.value as boolean}
|
|
onCheckedChange={formField.onChange}
|
|
disabled={field.readOnly}
|
|
/>
|
|
</FormControl>
|
|
<div className="space-y-1 leading-none">
|
|
<FormLabel className="text-sm font-medium">
|
|
{field.label}
|
|
{field.required && (
|
|
<span className="text-red-500 ml-1">*</span>
|
|
)}
|
|
</FormLabel>
|
|
<FormDescription>
|
|
{field.name
|
|
? (t as any)[`form.descriptions.${field.name}`] ||
|
|
""
|
|
: ""}
|
|
</FormDescription>
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end space-x-3 mt-6">
|
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
{readOnly ? t.back : t.cancel}
|
|
</Button>
|
|
|
|
{!readOnly && (
|
|
<Button
|
|
type="button"
|
|
variant="default"
|
|
disabled={formSubmitting || readOnly}
|
|
onClick={form.handleSubmit(onSubmitHandler)}
|
|
>
|
|
{mode === "update"
|
|
? t.update || "Update"
|
|
: t.createNew || "Create"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
</div>
|
|
);
|
|
}
|