car_mms/app/components/vehicles/VehicleForm.tsx
2025-09-11 14:22:27 +03:00

576 lines
19 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Form } from "@remix-run/react";
import { useState, useEffect } from "react";
import { Input } from "~/components/ui/Input";
import { AutocompleteInput } from "~/components/ui/AutocompleteInput";
import { Button } from "~/components/ui/Button";
import { Flex } from "~/components/layout/Flex";
import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, VALIDATION } from "~/lib/constants";
import type { Vehicle } from "~/types/database";
interface VehicleFormProps {
vehicle?: Vehicle;
customers: { id: number; name: string; phone?: string | null }[];
onCancel: () => void;
errors?: Record<string, string>;
isLoading: boolean;
}
export function VehicleForm({
vehicle,
customers,
onCancel,
errors = {},
isLoading,
}: VehicleFormProps) {
const [formData, setFormData] = useState({
plateNumber: vehicle?.plateNumber || "",
bodyType: vehicle?.bodyType || "",
manufacturer: vehicle?.manufacturer || "",
model: vehicle?.model || "",
trim: vehicle?.trim || "",
year: vehicle?.year?.toString() || "",
transmission: vehicle?.transmission || "",
fuel: vehicle?.fuel || "",
cylinders: vehicle?.cylinders?.toString() || "",
engineDisplacement: vehicle?.engineDisplacement?.toString() || "",
useType: vehicle?.useType || "",
ownerId: vehicle?.ownerId?.toString() || "",
});
// Car dataset state
const [manufacturers, setManufacturers] = useState<string[]>([]);
const [models, setModels] = useState<{model: string; bodyType: string}[]>([]);
const [isLoadingManufacturers, setIsLoadingManufacturers] = useState(false);
const [isLoadingModels, setIsLoadingModels] = useState(false);
// Autocomplete state
const [manufacturerSearchValue, setManufacturerSearchValue] = useState(vehicle?.manufacturer || "");
const [modelSearchValue, setModelSearchValue] = useState(vehicle?.model || "");
const [ownerSearchValue, setOwnerSearchValue] = useState(() => {
if (vehicle?.ownerId) {
const owner = customers.find(c => c.id === vehicle.ownerId);
return owner ? owner.name : "";
}
return "";
});
// Load manufacturers on component mount
useEffect(() => {
const loadManufacturers = async () => {
setIsLoadingManufacturers(true);
try {
const response = await fetch('/api/car-dataset?action=manufacturers');
const result = await response.json();
if (result.success) {
setManufacturers(result.data);
}
} catch (error) {
console.error('Error loading manufacturers:', error);
} finally {
setIsLoadingManufacturers(false);
}
};
loadManufacturers();
}, []);
// Load models when manufacturer changes
useEffect(() => {
if (formData.manufacturer) {
const loadModels = async () => {
setIsLoadingModels(true);
try {
const response = await fetch(`/api/car-dataset?action=models&manufacturer=${encodeURIComponent(formData.manufacturer)}`);
const result = await response.json();
if (result.success) {
setModels(result.data);
}
} catch (error) {
console.error('Error loading models:', error);
} finally {
setIsLoadingModels(false);
}
};
loadModels();
} else {
setModels([]);
}
}, [formData.manufacturer]);
// Create autocomplete options
const manufacturerOptions = manufacturers.map(manufacturer => ({
value: manufacturer,
label: manufacturer,
data: manufacturer
}));
const modelOptions = models.map(item => ({
value: item.model,
label: item.model,
data: item
}));
const ownerOptions = customers.map(customer => ({
value: customer.name,
label: `${customer.name}${customer.phone ? ` - ${customer.phone}` : ''}`,
data: customer
}));
// Handle manufacturer selection
const handleManufacturerSelect = (option: any) => {
const manufacturer = option.data;
setManufacturerSearchValue(manufacturer);
setFormData(prev => ({
...prev,
manufacturer,
model: "", // Reset model when manufacturer changes
bodyType: "" // Reset body type when manufacturer changes
}));
setModelSearchValue(""); // Reset model search
};
// Handle model selection
const handleModelSelect = (option: any) => {
const modelData = option.data;
setModelSearchValue(modelData.model);
setFormData(prev => ({
...prev,
model: modelData.model,
bodyType: modelData.bodyType // Auto-set body type from dataset
}));
};
// Handle owner selection from autocomplete
const handleOwnerSelect = (option: any) => {
const customer = option.data;
setOwnerSearchValue(customer.name);
setFormData(prev => ({
...prev,
ownerId: customer.id.toString()
}));
};
// Reset form data when vehicle changes
useEffect(() => {
if (vehicle) {
const owner = customers.find(c => c.id === vehicle.ownerId);
setFormData({
plateNumber: vehicle.plateNumber || "",
bodyType: vehicle.bodyType || "",
manufacturer: vehicle.manufacturer || "",
model: vehicle.model || "",
trim: vehicle.trim || "",
year: vehicle.year?.toString() || "",
transmission: vehicle.transmission || "",
fuel: vehicle.fuel || "",
cylinders: vehicle.cylinders?.toString() || "",
engineDisplacement: vehicle.engineDisplacement?.toString() || "",
useType: vehicle.useType || "",
ownerId: vehicle.ownerId?.toString() || "",
});
setManufacturerSearchValue(vehicle.manufacturer || "");
setModelSearchValue(vehicle.model || "");
setOwnerSearchValue(owner ? owner.name : "");
} else {
setFormData({
plateNumber: "",
bodyType: "",
manufacturer: "",
model: "",
trim: "",
year: "",
transmission: "",
fuel: "",
cylinders: "",
engineDisplacement: "",
useType: "",
ownerId: "",
});
setManufacturerSearchValue("");
setModelSearchValue("");
setOwnerSearchValue("");
}
}, [vehicle, customers]);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
};
const isEditing = !!vehicle;
const currentYear = new Date().getFullYear();
return (
<Form method="post" className="space-y-6">
<input
type="hidden"
name="_action"
value={isEditing ? "update" : "create"}
/>
{isEditing && (
<input type="hidden" name="id" value={vehicle.id} />
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Plate Number */}
<div>
<label htmlFor="plateNumber" className="block text-sm font-medium text-gray-700 mb-2">
رقم اللوحة *
</label>
<Input
id="plateNumber"
name="plateNumber"
type="text"
value={formData.plateNumber}
onChange={(e) => handleInputChange("plateNumber", e.target.value)}
placeholder="أدخل رقم اللوحة"
error={errors.plateNumber}
required
disabled={isLoading}
dir="ltr"
/>
{errors.plateNumber && (
<p className="mt-1 text-sm text-red-600">{errors.plateNumber}</p>
)}
</div>
{/* Manufacturer with Autocomplete */}
<div>
<AutocompleteInput
label="الشركة المصنعة *"
placeholder={isLoadingManufacturers ? "جاري التحميل..." : "ابدأ بكتابة اسم الشركة المصنعة..."}
value={manufacturerSearchValue}
onChange={setManufacturerSearchValue}
onSelect={handleManufacturerSelect}
options={manufacturerOptions}
error={errors.manufacturer}
required
disabled={isLoading || isLoadingManufacturers}
/>
{/* Hidden input for form submission */}
<input
type="hidden"
name="manufacturer"
value={formData.manufacturer}
/>
{formData.manufacturer && manufacturerSearchValue && (
<p className="mt-1 text-sm text-green-600">
تم اختيار الشركة المصنعة: {manufacturerSearchValue}
</p>
)}
</div>
{/* Model with Autocomplete */}
<div>
<AutocompleteInput
label="الموديل *"
placeholder={
!formData.manufacturer
? "اختر الشركة المصنعة أولاً"
: isLoadingModels
? "جاري التحميل..."
: "ابدأ بكتابة اسم الموديل..."
}
value={modelSearchValue}
onChange={setModelSearchValue}
onSelect={handleModelSelect}
options={modelOptions}
error={errors.model}
required
disabled={isLoading || isLoadingModels || !formData.manufacturer}
/>
{/* Hidden input for form submission */}
<input
type="hidden"
name="model"
value={formData.model}
/>
{formData.model && modelSearchValue && (
<p className="mt-1 text-sm text-green-600">
تم اختيار الموديل: {modelSearchValue}
</p>
)}
{!formData.manufacturer && (
<p className="mt-1 text-sm text-gray-500">
يرجى اختيار الشركة المصنعة أولاً
</p>
)}
</div>
{/* Body Type (Auto-filled, Read-only) */}
<div>
<label htmlFor="bodyType" className="block text-sm font-medium text-gray-700 mb-2">
نوع الهيكل *
</label>
<Input
id="bodyType"
name="bodyType"
type="text"
value={formData.bodyType}
placeholder={formData.model ? "سيتم تعبئته تلقائياً" : "اختر الموديل أولاً"}
error={errors.bodyType}
required
readOnly={true}
className="bg-gray-50"
/>
{formData.bodyType && (
<p className="mt-1 text-sm text-blue-600">
تم تعبئة نوع الهيكل تلقائياً من قاعدة البيانات
</p>
)}
{errors.bodyType && (
<p className="mt-1 text-sm text-red-600">{errors.bodyType}</p>
)}
</div>
{/* Trim */}
<div>
<label htmlFor="trim" className="block text-sm font-medium text-gray-700 mb-2">
الفئة
</label>
<Input
id="trim"
name="trim"
type="text"
value={formData.trim}
onChange={(e) => handleInputChange("trim", e.target.value)}
placeholder="أدخل الفئة (اختياري)"
error={errors.trim}
disabled={isLoading}
/>
{errors.trim && (
<p className="mt-1 text-sm text-red-600">{errors.trim}</p>
)}
</div>
{/* Year */}
<div>
<label htmlFor="year" className="block text-sm font-medium text-gray-700 mb-2">
سنة الصنع *
</label>
<Input
id="year"
name="year"
type="number"
min={VALIDATION.MIN_YEAR}
max={VALIDATION.MAX_YEAR}
value={formData.year}
onChange={(e) => handleInputChange("year", e.target.value)}
placeholder={`${VALIDATION.MIN_YEAR} - ${currentYear}`}
error={errors.year}
required
disabled={isLoading}
/>
{errors.year && (
<p className="mt-1 text-sm text-red-600">{errors.year}</p>
)}
</div>
{/* Transmission */}
<div>
<label htmlFor="transmission" className="block text-sm font-medium text-gray-700 mb-2">
ناقل الحركة *
</label>
<select
id="transmission"
name="transmission"
value={formData.transmission}
onChange={(e) => handleInputChange("transmission", e.target.value)}
required
disabled={isLoading}
className={`
w-full px-3 py-2 border rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
disabled:bg-gray-50 disabled:text-gray-500
${errors.transmission
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300'
}
`}
>
<option value="">اختر ناقل الحركة</option>
{TRANSMISSION_TYPES.map((transmission) => (
<option key={transmission.value} value={transmission.value}>
{transmission.label}
</option>
))}
</select>
{errors.transmission && (
<p className="mt-1 text-sm text-red-600">{errors.transmission}</p>
)}
</div>
{/* Fuel */}
<div>
<label htmlFor="fuel" className="block text-sm font-medium text-gray-700 mb-2">
نوع الوقود *
</label>
<select
id="fuel"
name="fuel"
value={formData.fuel}
onChange={(e) => handleInputChange("fuel", e.target.value)}
required
disabled={isLoading}
className={`
w-full px-3 py-2 border rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
disabled:bg-gray-50 disabled:text-gray-500
${errors.fuel
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300'
}
`}
>
<option value="">اختر نوع الوقود</option>
{FUEL_TYPES.map((fuel) => (
<option key={fuel.value} value={fuel.value}>
{fuel.label}
</option>
))}
</select>
{errors.fuel && (
<p className="mt-1 text-sm text-red-600">{errors.fuel}</p>
)}
</div>
{/* Cylinders */}
<div>
<label htmlFor="cylinders" className="block text-sm font-medium text-gray-700 mb-2">
عدد الأسطوانات
</label>
<Input
id="cylinders"
name="cylinders"
type="number"
min="1"
max={VALIDATION.MAX_CYLINDERS}
value={formData.cylinders}
onChange={(e) => handleInputChange("cylinders", e.target.value)}
placeholder="عدد الأسطوانات (اختياري)"
error={errors.cylinders}
disabled={isLoading}
/>
{errors.cylinders && (
<p className="mt-1 text-sm text-red-600">{errors.cylinders}</p>
)}
</div>
{/* Engine Displacement */}
<div>
<label htmlFor="engineDisplacement" className="block text-sm font-medium text-gray-700 mb-2">
سعة المحرك (لتر)
</label>
<Input
id="engineDisplacement"
name="engineDisplacement"
type="number"
step="0.1"
min="0.1"
max={VALIDATION.MAX_ENGINE_DISPLACEMENT}
value={formData.engineDisplacement}
onChange={(e) => handleInputChange("engineDisplacement", e.target.value)}
placeholder="سعة المحرك (اختياري)"
error={errors.engineDisplacement}
disabled={isLoading}
/>
{errors.engineDisplacement && (
<p className="mt-1 text-sm text-red-600">{errors.engineDisplacement}</p>
)}
</div>
{/* Use Type */}
<div>
<label htmlFor="useType" className="block text-sm font-medium text-gray-700 mb-2">
نوع الاستخدام *
</label>
<select
id="useType"
name="useType"
value={formData.useType}
onChange={(e) => handleInputChange("useType", e.target.value)}
required
disabled={isLoading}
className={`
w-full px-3 py-2 border rounded-lg shadow-sm
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
disabled:bg-gray-50 disabled:text-gray-500
${errors.useType
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300'
}
`}
>
<option value="">اختر نوع الاستخدام</option>
{USE_TYPES.map((useType) => (
<option key={useType.value} value={useType.value}>
{useType.label}
</option>
))}
</select>
{errors.useType && (
<p className="mt-1 text-sm text-red-600">{errors.useType}</p>
)}
</div>
{/* Owner with Autocomplete */}
<div>
<AutocompleteInput
label="المالك *"
placeholder="ابدأ بكتابة اسم المالك..."
value={ownerSearchValue}
onChange={setOwnerSearchValue}
onSelect={handleOwnerSelect}
options={ownerOptions}
error={errors.ownerId}
required
disabled={isLoading}
/>
{/* Hidden input for form submission */}
<input
type="hidden"
name="ownerId"
value={formData.ownerId}
/>
{formData.ownerId && ownerSearchValue && (
<p className="mt-1 text-sm text-green-600">
تم اختيار المالك: {ownerSearchValue}
</p>
)}
{!formData.ownerId && ownerSearchValue && (
<p className="mt-1 text-sm text-amber-600">
يرجى اختيار المالك من القائمة المنسدلة
</p>
)}
</div>
</div>
{/* Form Actions */}
<Flex justify="end" className="pt-4 gap-2 border-t">
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isLoading}
className="w-20"
>
إلغاء
</Button>
<Button
type="submit"
disabled={isLoading || !formData.plateNumber.trim() || !formData.ownerId}
className="bg-blue-600 hover:bg-blue-700"
>
{isLoading
? (isEditing ? "جاري التحديث..." : "جاري الإنشاء...")
: (isEditing ? "تحديث المركبة" : "إنشاء المركبة")
}
</Button>
</Flex>
</Form>
);
}