car_mms/app/components/maintenance-visits/MaintenanceVisitForm.tsx
2025-09-11 14:22:27 +03:00

461 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 { useState, useEffect } from "react";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { Button, Input, Select, Text, Card, CardHeader, CardBody, MultiSelect } from "~/components/ui";
import { AutocompleteInput } from "~/components/ui/AutocompleteInput";
import type { MaintenanceVisitWithRelations, Customer, Vehicle, MaintenanceType, MaintenanceJob } from "~/types/database";
import { PAYMENT_STATUS_NAMES, VISIT_DELAY_OPTIONS } from "~/lib/constants";
interface MaintenanceVisitFormProps {
visit?: MaintenanceVisitWithRelations;
customers: Customer[];
vehicles: Vehicle[];
maintenanceTypes: { id: number; name: string; }[];
onCancel?: () => void;
}
export function MaintenanceVisitForm({
visit,
customers,
vehicles,
maintenanceTypes,
onCancel
}: MaintenanceVisitFormProps) {
const actionData = useActionData<any>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
// Form state
const [plateNumberInput, setPlateNumberInput] = useState<string>(
visit?.vehicle?.plateNumber || ""
);
const [selectedCustomerId, setSelectedCustomerId] = useState<string>(
visit?.customerId?.toString() || ""
);
const [selectedVehicleId, setSelectedVehicleId] = useState<string>(
visit?.vehicleId?.toString() || ""
);
const [filteredVehicles, setFilteredVehicles] = useState<Vehicle[]>(vehicles);
// Selected maintenance types state
const [selectedMaintenanceTypes, setSelectedMaintenanceTypes] = useState<number[]>(() => {
if (visit?.maintenanceJobs) {
try {
const jobs = JSON.parse(visit.maintenanceJobs);
return jobs.map((job: MaintenanceJob) => job.typeId).filter((id: number) => id > 0);
} catch {
return [];
}
}
return [];
});
// Create autocomplete options for plate numbers
const plateNumberOptions = vehicles.map(vehicle => {
const customer = customers.find(c => c.id === vehicle.ownerId);
return {
value: vehicle.plateNumber,
label: `${vehicle.plateNumber} - ${vehicle.manufacturer} ${vehicle.model} (${customer?.name || 'غير محدد'})`,
data: {
vehicle,
customer
}
};
});
// Handle plate number selection
const handlePlateNumberSelect = (option: any) => {
const { vehicle, customer } = option.data;
setPlateNumberInput(vehicle.plateNumber);
setSelectedCustomerId(customer?.id?.toString() || "");
setSelectedVehicleId(vehicle.id.toString());
};
// Reset form state when visit prop changes (switching between create/edit modes)
useEffect(() => {
if (visit) {
// Editing mode - populate with visit data
setPlateNumberInput(visit.vehicle?.plateNumber || "");
setSelectedCustomerId(visit.customerId?.toString() || "");
setSelectedVehicleId(visit.vehicleId?.toString() || "");
// Parse maintenance jobs from JSON
try {
const jobs = JSON.parse(visit.maintenanceJobs);
const typeIds = jobs.map((job: MaintenanceJob) => job.typeId).filter((id: number) => id > 0);
setSelectedMaintenanceTypes(typeIds);
} catch {
setSelectedMaintenanceTypes([]);
}
} else {
// Create mode - reset to empty state
setPlateNumberInput("");
setSelectedCustomerId("");
setSelectedVehicleId("");
setSelectedMaintenanceTypes([]);
}
}, [visit]);
// Filter vehicles based on selected customer
useEffect(() => {
if (selectedCustomerId) {
const customerId = parseInt(selectedCustomerId);
const customerVehicles = vehicles.filter(v => v.ownerId === customerId);
setFilteredVehicles(customerVehicles);
// Reset vehicle selection if current vehicle doesn't belong to selected customer
if (selectedVehicleId) {
const vehicleId = parseInt(selectedVehicleId);
const vehicleBelongsToCustomer = customerVehicles.some(v => v.id === vehicleId);
if (!vehicleBelongsToCustomer) {
setSelectedVehicleId("");
}
}
} else {
setFilteredVehicles(vehicles);
}
}, [selectedCustomerId, vehicles, selectedVehicleId]);
// Format date for input
const formatDateForInput = (date: Date | string | null) => {
if (!date) return "";
const d = new Date(date);
return d.toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM format
};
// Convert selected maintenance types to jobs format for submission
const getMaintenanceJobsForSubmission = () => {
return selectedMaintenanceTypes.map(typeId => {
const type = maintenanceTypes.find(t => t.id === typeId);
return {
typeId,
job: type?.name || '',
notes: ''
};
});
};
return (
<Card>
<CardHeader>
<Text weight="medium" size="lg">
{visit ? "تعديل زيارة الصيانة" : "إضافة زيارة صيانة جديدة"}
</Text>
</CardHeader>
<CardBody>
<Form method="post" className="space-y-6">
{visit && (
<input type="hidden" name="id" value={visit.id} />
)}
{/* Plate Number Autocomplete - Only show for new visits */}
{!visit && (
<div>
<AutocompleteInput
label="رقم اللوحة"
placeholder="ابدأ بكتابة رقم اللوحة..."
value={plateNumberInput}
onChange={setPlateNumberInput}
onSelect={handlePlateNumberSelect}
options={plateNumberOptions}
required
/>
<Text size="sm" color="secondary" className="mt-1">
ابدأ بكتابة رقم اللوحة لاختيار المركبة والعميل تلقائياً
</Text>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Customer Selection */}
<div>
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-2">
العميل
<span className="text-red-500 mr-1">*</span>
</label>
<select
name="customerId"
value={selectedCustomerId}
onChange={(e) => setSelectedCustomerId(e.target.value)}
required
disabled={false}
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 appearance-none bg-white ${actionData?.errors?.customerId
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300'
}`}
>
<option value="">اختر العميل</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id.toString()}>
{customer.name}{customer.phone ? ` - ${customer.phone}` : ''}
</option>
))}
</select>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none mt-7">
<svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</div>
{actionData?.errors?.customerId && (
<Text size="sm" color="error" className="mt-1">
{actionData.errors.customerId}
</Text>
)}
{!visit && plateNumberInput && selectedCustomerId && (
<Text size="sm" color="success" className="mt-1">
تم اختيار العميل تلقائياً من رقم اللوحة
</Text>
)}
</div>
{/* Vehicle Selection */}
<div>
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-2">
المركبة
<span className="text-red-500 mr-1">*</span>
</label>
<select
name="vehicleId"
value={selectedVehicleId}
onChange={(e) => setSelectedVehicleId(e.target.value)}
required
disabled={false}
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 appearance-none bg-white ${actionData?.errors?.vehicleId
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-300'
}`}
>
<option value="">اختر المركبة</option>
{filteredVehicles.map((vehicle) => (
<option key={vehicle.id} value={vehicle.id.toString()}>
{vehicle.plateNumber} - {vehicle.manufacturer} {vehicle.model} ({vehicle.year})
</option>
))}
</select>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none mt-7">
<svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</div>
{actionData?.errors?.vehicleId && (
<Text size="sm" color="error" className="mt-1">
{actionData.errors.vehicleId}
</Text>
)}
{!selectedCustomerId && !plateNumberInput && (
<Text size="sm" color="secondary" className="mt-1">
يرجى اختيار العميل أولاً أو البحث برقم اللوحة
</Text>
)}
{!visit && plateNumberInput && selectedVehicleId && (
<Text size="sm" color="success" className="mt-1">
تم اختيار المركبة تلقائياً من رقم اللوحة
</Text>
)}
</div>
</div>
{/* Maintenance Types Selection */}
<div>
<MultiSelect
name="maintenanceJobs"
label="أنواع الصيانة"
options={maintenanceTypes.map(type => ({
value: type.id,
label: type.name
}))}
value={selectedMaintenanceTypes}
onChange={setSelectedMaintenanceTypes}
placeholder="اختر أنواع الصيانة المطلوبة..."
error={actionData?.errors?.maintenanceJobs}
required
/>
<Text size="sm" color="secondary" className="mt-1">
يمكنك اختيار أكثر من نوع صيانة واحد
</Text>
{/* Hidden input to pass maintenance jobs data in the expected format */}
<input
type="hidden"
name="maintenanceJobsData"
value={JSON.stringify(getMaintenanceJobsForSubmission())}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
{/* Payment Status */}
<div>
<Select
name="paymentStatus"
label="حالة الدفع"
defaultValue={visit?.paymentStatus || "pending"}
error={actionData?.errors?.paymentStatus}
required
options={Object.entries(PAYMENT_STATUS_NAMES).map(([value, label]) => ({
value: value,
label: label
}))}
/>
</div>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
وصف الصيانة
<span className="text-red-500 mr-1">*</span>
</label>
<textarea
name="description"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
defaultValue={visit?.description || ""}
placeholder="اكتب وصف تفصيلي للأعمال المنجزة..."
required
/>
{actionData?.errors?.description && (
<Text size="sm" color="error" className="mt-1">
{actionData.errors.description}
</Text>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Cost */}
<div>
<Input
type="number"
name="cost"
label="التكلفة (ريال)"
defaultValue={visit?.cost?.toString() || ""}
error={actionData?.errors?.cost}
step="0.01"
min="0"
max="999999.99"
required
/>
</div>
{/* Kilometers */}
<div>
<Input
type="number"
name="kilometers"
label="عدد الكيلومترات"
defaultValue={visit?.kilometers?.toString() || ""}
error={actionData?.errors?.kilometers}
min="0"
required
/>
</div>
{/* Next Visit Delay */}
<div>
<Select
name="nextVisitDelay"
label="الزيارة التالية بعد"
defaultValue={visit?.nextVisitDelay?.toString() || "3"}
error={actionData?.errors?.nextVisitDelay}
required
options={VISIT_DELAY_OPTIONS.map((option) => ({
value: option.value,
label: option.label
}))}
/>
</div>
</div>
{/* Visit Date */}
<div>
<Input
type="datetime-local"
name="visitDate"
label="تاريخ ووقت الزيارة"
defaultValue={formatDateForInput(visit?.visitDate || new Date())}
error={actionData?.errors?.visitDate}
required
/>
</div>
{/* Action Buttons */}
{/* Debug Info */}
{!visit && (
<div className="bg-gray-50 p-3 rounded text-xs">
<strong>Debug Info:</strong><br />
Customer ID: {selectedCustomerId || "Not selected"}<br />
Vehicle ID: {selectedVehicleId || "Not selected"}<br />
Plate Number: {plateNumberInput || "Not entered"}<br />
Selected Maintenance Types: {selectedMaintenanceTypes.length} types<br />
Types: {selectedMaintenanceTypes.join(', ') || "None selected"}
</div>
)}
<div className="flex justify-end gap-3 pt-4">
{onCancel && (
<Button
type="button"
variant="secondary"
onClick={onCancel}
disabled={isSubmitting}
>
إلغاء
</Button>
)}
<Button
type="submit"
name="intent"
value={visit ? "update" : "create"}
disabled={isSubmitting}
onClick={(e) => {
// Client-side validation before submission
if (!visit) {
const form = e.currentTarget.form;
if (!form) return;
const formData = new FormData(form);
const customerId = formData.get("customerId") as string;
const vehicleId = formData.get("vehicleId") as string;
const description = formData.get("description") as string;
const cost = formData.get("cost") as string;
const kilometers = formData.get("kilometers") as string;
const hasValidCustomer = customerId && customerId !== "";
const hasValidVehicle = vehicleId && vehicleId !== "";
const hasValidJobs = selectedMaintenanceTypes.length > 0;
const hasValidDescription = description && description.trim() !== "";
const hasValidCost = cost && cost.trim() !== "" && parseFloat(cost) > 0;
const hasValidKilometers = kilometers && kilometers.trim() !== "" && parseInt(kilometers) >= 0;
const missingFields = [];
if (!hasValidCustomer) missingFields.push("العميل");
if (!hasValidVehicle) missingFields.push("المركبة");
if (!hasValidJobs) missingFields.push("نوع صيانة واحد على الأقل");
if (!hasValidDescription) missingFields.push("وصف الصيانة");
if (!hasValidCost) missingFields.push("التكلفة");
if (!hasValidKilometers) missingFields.push("عدد الكيلومترات");
if (missingFields.length > 0) {
e.preventDefault();
alert(`يرجى ملء الحقول المطلوبة التالية:\n- ${missingFields.join('\n- ')}`);
return;
}
}
}}
>
{isSubmitting
? visit
? "جاري التحديث..."
: "جاري الحفظ..."
: visit
? "تحديث الزيارة"
: "حفظ الزيارة"}
</Button>
</div>
</Form>
</CardBody>
</Card>
);
}