prod-wag-backend-automate-s.../WebServices/client-frontend/src/components/Pages/people/superusers/FormComponent.tsx

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>
);
}