576 lines
19 KiB
TypeScript
576 lines
19 KiB
TypeScript
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>
|
||
);
|
||
} |