uup
This commit is contained in:
388
app/components/vehicles/VehicleDetailsView.tsx
Normal file
388
app/components/vehicles/VehicleDetailsView.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Flex } from "~/components/layout/Flex";
|
||||
import { useSettings } from "~/contexts/SettingsContext";
|
||||
import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, BODY_TYPES } from "~/lib/constants";
|
||||
import type { VehicleWithOwner, VehicleWithRelations } from "~/types/database";
|
||||
|
||||
interface VehicleDetailsViewProps {
|
||||
vehicle: VehicleWithOwner | VehicleWithRelations;
|
||||
onEdit?: () => void;
|
||||
onClose?: () => void;
|
||||
isLoadingVisits?: boolean;
|
||||
}
|
||||
|
||||
export function VehicleDetailsView({ vehicle, onEdit, onClose, isLoadingVisits }: VehicleDetailsViewProps) {
|
||||
const { formatDate, formatCurrency, formatNumber } = useSettings();
|
||||
// Helper functions to get display labels
|
||||
const getTransmissionLabel = (value: string) => {
|
||||
return TRANSMISSION_TYPES.find(t => t.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getFuelLabel = (value: string) => {
|
||||
return FUEL_TYPES.find(f => f.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getUseTypeLabel = (value: string) => {
|
||||
return USE_TYPES.find(u => u.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getBodyTypeLabel = (value: string) => {
|
||||
return BODY_TYPES.find(b => b.value === value)?.label || value;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Enhanced Vehicle Information Section */}
|
||||
<div className="bg-gradient-to-r from-green-50 to-emerald-50 p-6 rounded-xl border border-green-100">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-900 flex items-center">
|
||||
<span className="text-green-600 ml-2">🚗</span>
|
||||
معلومات المركبة
|
||||
</h3>
|
||||
<span className="text-sm text-gray-500 bg-white px-3 py-1 rounded-full">
|
||||
المركبة #{vehicle.id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">رقم اللوحة</label>
|
||||
<p className="text-xl font-bold text-gray-900 font-mono tracking-wider">
|
||||
{vehicle.plateNumber}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">الشركة المصنعة</label>
|
||||
<p className="text-lg font-semibold text-gray-900">{vehicle.manufacturer}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">الموديل</label>
|
||||
<p className="text-lg font-semibold text-gray-900">{vehicle.model}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">سنة الصنع</label>
|
||||
<p className="text-lg font-semibold text-gray-900">{vehicle.year}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">نوع الهيكل</label>
|
||||
<p className="text-gray-900">{getBodyTypeLabel(vehicle.bodyType)}</p>
|
||||
</div>
|
||||
|
||||
{vehicle.trim && (
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">الفئة</label>
|
||||
<p className="text-gray-900">{vehicle.trim}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technical Specifications */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<span className="text-gray-600 text-xl ml-2">⚙️</span>
|
||||
المواصفات التقنية
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">ناقل الحركة</label>
|
||||
<p className="text-gray-900 font-medium">{getTransmissionLabel(vehicle.transmission)}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">نوع الوقود</label>
|
||||
<p className="text-gray-900 font-medium">{getFuelLabel(vehicle.fuel)}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">نوع الاستخدام</label>
|
||||
<p className="text-gray-900 font-medium">{getUseTypeLabel(vehicle.useType)}</p>
|
||||
</div>
|
||||
|
||||
{vehicle.cylinders && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">عدد الأسطوانات</label>
|
||||
<p className="text-gray-900 font-medium">{vehicle.cylinders}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{vehicle.engineDisplacement && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">سعة المحرك</label>
|
||||
<p className="text-gray-900 font-medium">{vehicle.engineDisplacement} لتر</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owner Information */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<Flex justify="between" align="center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<span className="text-gray-600 text-xl ml-2">👤</span>
|
||||
معلومات المالك
|
||||
</h3>
|
||||
<Link
|
||||
to={`/customers?search=${encodeURIComponent(vehicle.owner.name)}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center px-3 py-1 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<span className="ml-2">👁️</span>
|
||||
عرض تفاصيل المالك
|
||||
</Link>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">اسم المالك</label>
|
||||
<p className="text-lg font-semibold text-gray-900">{vehicle.owner.name}</p>
|
||||
</div>
|
||||
|
||||
{vehicle.owner.phone && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">رقم الهاتف</label>
|
||||
<a
|
||||
href={`tel:${vehicle.owner.phone}`}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
||||
dir="ltr"
|
||||
>
|
||||
📞 {vehicle.owner.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{vehicle.owner.email && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">البريد الإلكتروني</label>
|
||||
<a
|
||||
href={`mailto:${vehicle.owner.email}`}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
||||
dir="ltr"
|
||||
>
|
||||
✉️ {vehicle.owner.email}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{vehicle.owner.address && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg md:col-span-2 lg:col-span-3">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">العنوان</label>
|
||||
<p className="text-gray-900">{vehicle.owner.address}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Maintenance Status */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<Flex justify="between" align="center">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<span className="text-gray-600 text-xl ml-2">🔧</span>
|
||||
حالة الصيانة
|
||||
</h3>
|
||||
<Link
|
||||
to={`/maintenance-visits?vehicleId=${vehicle.id}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center px-3 py-1 text-sm font-medium text-green-600 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<span className="ml-2">📋</span>
|
||||
عرض جميع زيارات الصيانة
|
||||
</Link>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">آخر زيارة صيانة</label>
|
||||
<p className="text-gray-900 font-medium">
|
||||
{vehicle.lastVisitDate
|
||||
? formatDate(vehicle.lastVisitDate)
|
||||
: <span className="text-gray-400">لا توجد زيارات</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{vehicle.suggestedNextVisitDate && (
|
||||
<div className="bg-orange-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">الزيارة المقترحة التالية</label>
|
||||
<p className="text-orange-600 font-bold">
|
||||
{formatDate(vehicle.suggestedNextVisitDate)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">تاريخ التسجيل</label>
|
||||
<p className="text-gray-900">
|
||||
{formatDate(vehicle.createdDate)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">آخر تحديث</label>
|
||||
<p className="text-gray-900">
|
||||
{formatDate(vehicle.updateDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Maintenance Visits
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<span className="text-gray-600 text-xl ml-2">🔧</span>
|
||||
آخر زيارات الصيانة
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{isLoadingVisits ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-4xl mb-4">🔧</div>
|
||||
<h4 className="text-lg font-medium text-gray-900 mb-2">جاري تحميل زيارات الصيانة...</h4>
|
||||
<div className="flex items-center justify-center">
|
||||
<svg className="animate-spin h-6 w-6 text-gray-400" 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>
|
||||
</div>
|
||||
</div>
|
||||
) : !('maintenanceVisits' in vehicle) || !vehicle.maintenanceVisits || vehicle.maintenanceVisits.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-4xl mb-4">🔧</div>
|
||||
<h4 className="text-lg font-medium text-gray-900 mb-2">لا توجد زيارات صيانة</h4>
|
||||
<p className="text-gray-500 mb-4">لم يتم تسجيل أي زيارات صيانة لهذه المركبة بعد</p>
|
||||
<Link
|
||||
to="/maintenance-visits"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
تسجيل زيارة صيانة جديدة
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).slice(0, 3).map((visit: any) => (
|
||||
<div
|
||||
key={visit.id}
|
||||
className="bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-green-300 hover:bg-green-50 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900 text-lg">
|
||||
{(() => {
|
||||
try {
|
||||
const jobs = JSON.parse(visit.maintenanceJobs);
|
||||
return jobs.length > 1
|
||||
? `${jobs.length} أعمال صيانة`
|
||||
: jobs[0]?.job || 'نوع صيانة غير محدد';
|
||||
} catch {
|
||||
return 'نوع صيانة غير محدد';
|
||||
}
|
||||
})()}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">زيارة #{visit.id}</p>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-lg font-bold text-green-600">
|
||||
{formatCurrency(visit.cost)}
|
||||
</div>
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${visit.paymentStatus === "paid"
|
||||
? 'bg-green-100 text-green-800'
|
||||
: visit.paymentStatus === 'pending'
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{visit.paymentStatus === 'paid' ? 'مدفوع' :
|
||||
visit.paymentStatus === 'pending' ? 'معلق' : 'غير مدفوع'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">تاريخ الزيارة:</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{formatDate(visit.visitDate)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">عداد الكيلومترات:</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{visit.kilometers ? formatNumber(visit.kilometers) : 'غير محدد'} كم
|
||||
</span>
|
||||
</div>
|
||||
{visit.description && (
|
||||
<div className="md:col-span-2">
|
||||
<span className="text-gray-600">الوصف:</span>
|
||||
<p className="text-gray-900 mt-1">{visit.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).length > 3 && (
|
||||
<div className="text-center py-4 border-t border-gray-200">
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
عرض 3 من أصل {('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).length} زيارة صيانة
|
||||
</p>
|
||||
<Link
|
||||
to={`/maintenance-visits?vehicleId=${vehicle.id}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-green-600 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<span className="ml-2">📋</span>
|
||||
عرض جميع الزيارات ({('maintenanceVisits' in vehicle ? vehicle.maintenanceVisits : []).length})
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Action Buttons */}
|
||||
{(onEdit || onClose) && (
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-6 border-t border-gray-200">
|
||||
{onEdit && (
|
||||
<Button
|
||||
onClick={onEdit}
|
||||
className="bg-blue-600 hover:bg-blue-700 flex-1 sm:flex-none"
|
||||
>
|
||||
<span className="ml-2">✏️</span>
|
||||
تعديل المركبة
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
إغلاق
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
576
app/components/vehicles/VehicleForm.tsx
Normal file
576
app/components/vehicles/VehicleForm.tsx
Normal file
@@ -0,0 +1,576 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
272
app/components/vehicles/VehicleList.tsx
Normal file
272
app/components/vehicles/VehicleList.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { Form, Link } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { DataTable, Pagination } from "~/components/ui/DataTable";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Flex } from "~/components/layout/Flex";
|
||||
import { useSettings } from "~/contexts/SettingsContext";
|
||||
import { TRANSMISSION_TYPES, FUEL_TYPES, USE_TYPES, BODY_TYPES } from "~/lib/constants";
|
||||
import type { VehicleWithOwner } from "~/types/database";
|
||||
|
||||
interface VehicleListProps {
|
||||
vehicles: VehicleWithOwner[];
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onEditVehicle: (vehicle: VehicleWithOwner) => void;
|
||||
onViewVehicle?: (vehicle: VehicleWithOwner) => void;
|
||||
isLoading: boolean;
|
||||
actionData?: any;
|
||||
}
|
||||
|
||||
export function VehicleList({
|
||||
vehicles,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
onEditVehicle,
|
||||
onViewVehicle,
|
||||
isLoading,
|
||||
actionData,
|
||||
}: VehicleListProps) {
|
||||
const { formatDate } = useSettings();
|
||||
const [deletingVehicleId, setDeletingVehicleId] = useState<number | null>(null);
|
||||
|
||||
// Reset deleting state when delete action completes
|
||||
useEffect(() => {
|
||||
if (actionData?.success && actionData.action === "delete") {
|
||||
setDeletingVehicleId(null);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
// Helper functions to get display labels
|
||||
const getTransmissionLabel = (value: string) => {
|
||||
return TRANSMISSION_TYPES.find(t => t.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getFuelLabel = (value: string) => {
|
||||
return FUEL_TYPES.find(f => f.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getUseTypeLabel = (value: string) => {
|
||||
return USE_TYPES.find(u => u.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const getBodyTypeLabel = (value: string) => {
|
||||
return BODY_TYPES.find(b => b.value === value)?.label || value;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "plateNumber",
|
||||
header: "رقم اللوحة",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div>
|
||||
<Link
|
||||
to={`/vehicles/${vehicle.id}`}
|
||||
className="font-mono text-lg font-medium text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{vehicle.plateNumber}
|
||||
</Link>
|
||||
<div className="text-sm text-gray-500">
|
||||
المركبة رقم: {vehicle.id}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "vehicle",
|
||||
header: "تفاصيل المركبة",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{vehicle.manufacturer} {vehicle.model}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{vehicle.year} • {getBodyTypeLabel(vehicle.bodyType)}
|
||||
</div>
|
||||
{vehicle.trim && (
|
||||
<div className="text-sm text-gray-500">
|
||||
فئة: {vehicle.trim}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "specifications",
|
||||
header: "المواصفات",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-gray-900">
|
||||
{getTransmissionLabel(vehicle.transmission)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{getFuelLabel(vehicle.fuel)}
|
||||
</div>
|
||||
{vehicle.cylinders && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{vehicle.cylinders} أسطوانة
|
||||
</div>
|
||||
)}
|
||||
{vehicle.engineDisplacement && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{vehicle.engineDisplacement}L
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "owner",
|
||||
header: "المالك",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div>
|
||||
<Link
|
||||
to={`/customers/${vehicle.owner.id}`}
|
||||
className="font-medium text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
{vehicle.owner.name}
|
||||
</Link>
|
||||
{vehicle.owner.phone && (
|
||||
<div className="text-sm text-gray-500" dir="ltr">
|
||||
{vehicle.owner.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "useType",
|
||||
header: "نوع الاستخدام",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div className="text-sm text-gray-900">
|
||||
{getUseTypeLabel(vehicle.useType)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "maintenance",
|
||||
header: "الصيانة",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div className="space-y-1">
|
||||
{vehicle.lastVisitDate ? (
|
||||
<div className="text-sm text-gray-900">
|
||||
آخر زيارة: {formatDate(vehicle.lastVisitDate)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">
|
||||
لا توجد زيارات
|
||||
</div>
|
||||
)}
|
||||
{vehicle.suggestedNextVisitDate && (
|
||||
<div className="text-sm text-orange-600">
|
||||
الزيارة التالية: {formatDate(vehicle.suggestedNextVisitDate)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "createdDate",
|
||||
header: "تاريخ التسجيل",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<div className="text-sm text-gray-600">
|
||||
{formatDate(vehicle.createdDate)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
header: "الإجراءات",
|
||||
render: (vehicle: VehicleWithOwner) => (
|
||||
<Flex className="flex-wrap gap-2">
|
||||
{onViewVehicle ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onViewVehicle(vehicle)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
عرض
|
||||
</Button>
|
||||
) : (
|
||||
<Link to={`/vehicles/${vehicle.id}`}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
>
|
||||
عرض
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onEditVehicle(vehicle)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
تعديل
|
||||
</Button>
|
||||
|
||||
<Form method="post" className="inline">
|
||||
<input type="hidden" name="_action" value="delete" />
|
||||
<input type="hidden" name="id" value={vehicle.id} />
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-300 hover:bg-red-50"
|
||||
disabled={isLoading || deletingVehicleId === vehicle.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (window.confirm("هل أنت متأكد من حذف هذه المركبة؟")) {
|
||||
setDeletingVehicleId(vehicle.id);
|
||||
(e.target as HTMLButtonElement).form?.submit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{deletingVehicleId === vehicle.id ? "جاري الحذف..." : "حذف"}
|
||||
</Button>
|
||||
</Form>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
{vehicles.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-lg mb-2">🚗</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
لا توجد مركبات
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
لم يتم العثور على أي مركبات. قم بإضافة مركبة جديدة للبدء.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
data={vehicles}
|
||||
columns={columns}
|
||||
loading={isLoading}
|
||||
emptyMessage="لم يتم العثور على أي مركبات"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center">
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user