uup
This commit is contained in:
313
app/components/customers/CustomerDetailsView.tsx
Normal file
313
app/components/customers/CustomerDetailsView.tsx
Normal file
@@ -0,0 +1,313 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Flex } from "~/components/layout/Flex";
|
||||
import { useSettings } from "~/contexts/SettingsContext";
|
||||
import { PAYMENT_STATUS_NAMES } from "~/lib/constants";
|
||||
import type { CustomerWithVehicles } from "~/types/database";
|
||||
|
||||
interface CustomerDetailsViewProps {
|
||||
customer: CustomerWithVehicles;
|
||||
onEdit: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CustomerDetailsView({
|
||||
customer,
|
||||
onEdit,
|
||||
onClose,
|
||||
}: CustomerDetailsViewProps) {
|
||||
const { formatDate, formatCurrency } = useSettings();
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Enhanced Basic Information Section */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-xl border border-blue-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-blue-600 ml-2">👤</span>
|
||||
المعلومات الأساسية
|
||||
</h3>
|
||||
<span className="text-sm text-gray-500 bg-white px-3 py-1 rounded-full">
|
||||
العميل #{customer.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-lg font-semibold text-gray-900">{customer.name}</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" dir="ltr">
|
||||
{customer.phone ? (
|
||||
<a
|
||||
href={`tel:${customer.phone}`}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
||||
>
|
||||
📞 {customer.phone}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-400">غير محدد</span>
|
||||
)}
|
||||
</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" dir="ltr">
|
||||
{customer.email ? (
|
||||
<a
|
||||
href={`mailto:${customer.email}`}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
||||
>
|
||||
✉️ {customer.email}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-gray-400">غير محدد</span>
|
||||
)}
|
||||
</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">
|
||||
{formatDate(customer.createdDate)}
|
||||
</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">
|
||||
{formatDate(customer.updateDate)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{customer.address && (
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm md:col-span-2 lg:col-span-1">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-1">العنوان</label>
|
||||
<p className="text-gray-900">{customer.address}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Customer Vehicles Section */}
|
||||
<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" className="flex-wrap gap-4">
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-600 text-xl ml-2">🚗</span>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
مركبات العميل ({customer.vehicles.length})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{customer.vehicles.length > 0 && (
|
||||
<Link
|
||||
to={`/vehicles?customerId=${customer.id}`}
|
||||
className="inline-flex items-center px-4 py-2 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">
|
||||
{customer.vehicles.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="/vehicles"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
إضافة مركبة جديدة
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{customer.vehicles.map((vehicle) => (
|
||||
<Link
|
||||
key={vehicle.id}
|
||||
to={`/vehicles?plateNumber=${encodeURIComponent(vehicle.plateNumber)}`}
|
||||
target="_blank"
|
||||
className="block bg-gray-50 rounded-lg p-4 border border-gray-200 hover:border-blue-300 hover:bg-blue-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">
|
||||
{vehicle.plateNumber}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">#{vehicle.id}</p>
|
||||
</div>
|
||||
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-1 rounded-full">
|
||||
انقر للعرض
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">الصانع:</span>
|
||||
<span className="text-sm font-medium text-gray-900">{vehicle.manufacturer}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">الموديل:</span>
|
||||
<span className="text-sm font-medium text-gray-900">{vehicle.model}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">سنة الصنع:</span>
|
||||
<span className="text-sm font-medium text-gray-900">{vehicle.year}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">آخر زيارة:</span>
|
||||
<span className="text-sm text-gray-900">
|
||||
{vehicle.lastVisitDate
|
||||
? formatDate(vehicle.lastVisitDate)
|
||||
: <span className="text-gray-400">لا توجد زيارات</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Latest Maintenance Visits Section */}
|
||||
<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" className="flex-wrap gap-4">
|
||||
<div className="flex items-center">
|
||||
<span className="text-gray-600 text-xl ml-2">🔧</span>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
آخر زيارات الصيانة ({customer.maintenanceVisits.length > 3 ? '3 من ' + customer.maintenanceVisits.length : customer.maintenanceVisits.length})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{customer.maintenanceVisits.length > 0 && (
|
||||
<Link
|
||||
to={`/maintenance-visits?customerId=${customer.id}`}
|
||||
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>
|
||||
عرض جميع الزيارات
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{customer.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>
|
||||
<p className="text-sm text-gray-400 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">
|
||||
{customer.maintenanceVisits.slice(0, 3).map((visit) => (
|
||||
<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>
|
||||
</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.vehicle?.plateNumber || ''}
|
||||
</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>
|
||||
))}
|
||||
|
||||
{customer.maintenanceVisits.length > 3 && (
|
||||
<div className="text-center py-4 border-t border-gray-200">
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
عرض 3 من أصل {customer.maintenanceVisits.length} زيارة صيانة
|
||||
</p>
|
||||
<Link
|
||||
to={`/maintenance-visits?customerId=${customer.id}`}
|
||||
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>
|
||||
عرض جميع الزيارات ({customer.maintenanceVisits.length})
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
onClick={onEdit}
|
||||
className="bg-blue-600 hover:bg-blue-700 flex-1 sm:flex-none"
|
||||
>
|
||||
<span className="ml-2">✏️</span>
|
||||
تعديل العميل
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
إغلاق
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
app/components/customers/CustomerForm.tsx
Normal file
183
app/components/customers/CustomerForm.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Form } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Input } from "~/components/ui/Input";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Flex } from "~/components/layout/Flex";
|
||||
import type { Customer } from "~/types/database";
|
||||
|
||||
interface CustomerFormProps {
|
||||
customer?: Customer;
|
||||
onCancel: () => void;
|
||||
errors?: Record<string, string>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function CustomerForm({
|
||||
customer,
|
||||
onCancel,
|
||||
errors = {},
|
||||
isLoading,
|
||||
}: CustomerFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: customer?.name || "",
|
||||
phone: customer?.phone || "",
|
||||
email: customer?.email || "",
|
||||
address: customer?.address || "",
|
||||
});
|
||||
|
||||
// Reset form data when customer changes
|
||||
useEffect(() => {
|
||||
if (customer) {
|
||||
setFormData({
|
||||
name: customer.name || "",
|
||||
phone: customer.phone || "",
|
||||
email: customer.email || "",
|
||||
address: customer.address || "",
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
});
|
||||
}
|
||||
}, [customer]);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const isEditing = !!customer;
|
||||
|
||||
return (
|
||||
<Form method="post" className="space-y-6">
|
||||
<input
|
||||
type="hidden"
|
||||
name="_action"
|
||||
value={isEditing ? "update" : "create"}
|
||||
/>
|
||||
{isEditing && (
|
||||
<input type="hidden" name="id" value={customer.id} />
|
||||
)}
|
||||
|
||||
{/* Customer Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
اسم العميل *
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
placeholder="أدخل اسم العميل"
|
||||
error={errors.name}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Phone Number */}
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
رقم الهاتف
|
||||
</label>
|
||||
<Input
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||
placeholder="أدخل رقم الهاتف"
|
||||
error={errors.phone}
|
||||
disabled={isLoading}
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
البريد الإلكتروني
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder="أدخل البريد الإلكتروني"
|
||||
error={errors.email}
|
||||
disabled={isLoading}
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
العنوان
|
||||
</label>
|
||||
<textarea
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={(e) => handleInputChange("address", e.target.value)}
|
||||
placeholder="أدخل عنوان العميل"
|
||||
rows={3}
|
||||
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.address
|
||||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
{errors.address && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.address}</p>
|
||||
)}
|
||||
</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.name.trim()}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isLoading
|
||||
? (isEditing ? "جاري التحديث..." : "جاري الإنشاء...")
|
||||
: (isEditing ? "تحديث العميل" : "إنشاء العميل")
|
||||
}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
282
app/components/customers/CustomerList.tsx
Normal file
282
app/components/customers/CustomerList.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
import { Form } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Flex } from "~/components/layout/Flex";
|
||||
import { useSettings } from "~/contexts/SettingsContext";
|
||||
import type { CustomerWithVehicles } from "~/types/database";
|
||||
|
||||
interface CustomerListProps {
|
||||
customers: CustomerWithVehicles[];
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onViewCustomer: (customer: CustomerWithVehicles) => void;
|
||||
onEditCustomer: (customer: CustomerWithVehicles) => void;
|
||||
isLoading: boolean;
|
||||
actionData?: any;
|
||||
}
|
||||
|
||||
export function CustomerList({
|
||||
customers,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
onViewCustomer,
|
||||
onEditCustomer,
|
||||
isLoading,
|
||||
actionData,
|
||||
}: CustomerListProps) {
|
||||
const { formatDate } = useSettings();
|
||||
const [deletingCustomerId, setDeletingCustomerId] = useState<number | null>(null);
|
||||
|
||||
// Reset deleting state when delete action completes
|
||||
useEffect(() => {
|
||||
if (actionData?.success && actionData.action === "delete") {
|
||||
setDeletingCustomerId(null);
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "name",
|
||||
header: "اسم العميل",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{customer.name}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{/* العميل رقم: {customer.id} */}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "contact",
|
||||
header: "معلومات الاتصال",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<div className="space-y-1">
|
||||
{customer.phone && (
|
||||
<div className="text-sm text-gray-900" dir="ltr">
|
||||
📞 {customer.phone}
|
||||
</div>
|
||||
)}
|
||||
{customer.email && (
|
||||
<div className="text-sm text-gray-600" dir="ltr">
|
||||
✉️ {customer.email}
|
||||
</div>
|
||||
)}
|
||||
{!customer.phone && !customer.email && (
|
||||
<div className="text-sm text-gray-400">
|
||||
لا توجد معلومات اتصال
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "address",
|
||||
header: "العنوان",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<div className="text-sm text-gray-900">
|
||||
{customer.address || (
|
||||
<span className="text-gray-400">غير محدد</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
header: "المركبات",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{customer.vehicles.length} مركبة
|
||||
</div>
|
||||
{customer.vehicles.length > 0 && (
|
||||
<div className="text-sm text-gray-500 mt-1">
|
||||
{customer.vehicles.slice(0, 2).map((vehicle) => (
|
||||
<div key={vehicle.id}>
|
||||
{vehicle.plateNumber} - {vehicle.manufacturer} {vehicle.model}
|
||||
</div>
|
||||
))}
|
||||
{customer.vehicles.length > 2 && (
|
||||
<div className="text-gray-400">
|
||||
و {customer.vehicles.length - 2} مركبة أخرى...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "visits",
|
||||
header: "الزيارات",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{customer.maintenanceVisits.length} زيارة
|
||||
</div>
|
||||
{customer.maintenanceVisits.length > 0 && (
|
||||
<div className="text-sm text-gray-500">
|
||||
آخر زيارة: {formatDate(customer.maintenanceVisits[0].visitDate)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "createdDate",
|
||||
header: "تاريخ الإنشاء",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<div className="text-sm text-gray-600">
|
||||
{formatDate(customer.createdDate)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
header: "الإجراءات",
|
||||
render: (customer: CustomerWithVehicles) => (
|
||||
<Flex className="flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="bg-blue-50 text-blue-600 border-blue-300 hover:bg-blue-100"
|
||||
disabled={isLoading}
|
||||
onClick={() => onViewCustomer(customer)}
|
||||
>
|
||||
عرض
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onEditCustomer(customer)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
تعديل
|
||||
</Button>
|
||||
|
||||
<Form method="post" className="inline">
|
||||
<input type="hidden" name="_action" value="delete" />
|
||||
<input type="hidden" name="id" value={customer.id} />
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-red-600 border-red-300 hover:bg-red-50"
|
||||
disabled={isLoading || deletingCustomerId === customer.id}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (window.confirm("هل أنت متأكد من حذف هذا العميل؟")) {
|
||||
setDeletingCustomerId(customer.id);
|
||||
(e.target as HTMLButtonElement).form?.submit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{deletingCustomerId === customer.id ? "جاري الحذف..." : "حذف"}
|
||||
</Button>
|
||||
</Form>
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
{customers.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>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{customers.map((customer) => (
|
||||
<tr key={customer.id} className="hover:bg-gray-50">
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={`${customer.id}-${column.key}`}
|
||||
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
|
||||
>
|
||||
{column.render ? column.render(customer) : String(customer[column.key as keyof CustomerWithVehicles] || '')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="px-4 py-3 border-t border-gray-200 bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 space-x-reverse">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1 || isLoading}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
السابق
|
||||
</button>
|
||||
|
||||
<div className="flex items-center space-x-1 space-x-reverse">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
const page = i + 1;
|
||||
return (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => onPageChange(page)}
|
||||
disabled={isLoading}
|
||||
className={`px-3 py-2 text-sm font-medium rounded-md ${
|
||||
currentPage === page
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages || isLoading}
|
||||
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
التالي
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">
|
||||
صفحة {currentPage} من {totalPages}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user