car_mms/app/routes/customers.tsx
2025-09-11 14:22:27 +03:00

387 lines
13 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 type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, useActionData, useNavigation, useSearchParams } from "@remix-run/react";
import { useState, useEffect } from "react";
import { requireUser } from "~/lib/auth.server";
import { useDebounce } from "~/hooks/useDebounce";
import {
getCustomers,
createCustomer,
updateCustomer,
deleteCustomer,
getCustomerById
} from "~/lib/customer-management.server";
import { validateCustomer } from "~/lib/validation";
import { DashboardLayout } from "~/components/layout/DashboardLayout";
import { CustomerList } from "~/components/customers/CustomerList";
import { CustomerForm } from "~/components/customers/CustomerForm";
import { CustomerDetailsView } from "~/components/customers/CustomerDetailsView";
import { Modal } from "~/components/ui/Modal";
import { Button } from "~/components/ui/Button";
import { Input } from "~/components/ui/Input";
import { Flex } from "~/components/layout/Flex";
import type { CustomerWithVehicles } from "~/types/database";
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUser(request);
const url = new URL(request.url);
const searchQuery = url.searchParams.get("search") || "";
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const { customers, total, totalPages } = await getCustomers(searchQuery, page, limit);
return json({
customers,
total,
totalPages,
currentPage: page,
searchQuery,
user,
});
}
export async function action({ request }: ActionFunctionArgs) {
const user = await requireUser(request);
const formData = await request.formData();
const action = formData.get("_action") as string;
switch (action) {
case "create": {
const customerData = {
name: formData.get("name") as string,
phone: formData.get("phone") as string || undefined,
email: formData.get("email") as string || undefined,
address: formData.get("address") as string || undefined,
};
// Validate customer data
const validation = validateCustomer(customerData);
if (!validation.isValid) {
return json({
success: false,
errors: validation.errors,
action: "create"
}, { status: 400 });
}
const result = await createCustomer(customerData);
if (result.success) {
return json({
success: true,
customer: result.customer,
action: "create",
message: "تم إنشاء العميل بنجاح"
});
} else {
return json({
success: false,
error: result.error,
action: "create"
}, { status: 400 });
}
}
case "update": {
const id = parseInt(formData.get("id") as string);
const customerData = {
name: formData.get("name") as string,
phone: formData.get("phone") as string || undefined,
email: formData.get("email") as string || undefined,
address: formData.get("address") as string || undefined,
};
// Validate customer data
const validation = validateCustomer(customerData);
if (!validation.isValid) {
return json({
success: false,
errors: validation.errors,
action: "update"
}, { status: 400 });
}
const result = await updateCustomer(id, customerData);
if (result.success) {
return json({
success: true,
customer: result.customer,
action: "update",
message: "تم تحديث العميل بنجاح"
});
} else {
return json({
success: false,
error: result.error,
action: "update"
}, { status: 400 });
}
}
case "delete": {
const id = parseInt(formData.get("id") as string);
const result = await deleteCustomer(id);
if (result.success) {
return json({
success: true,
action: "delete",
message: "تم حذف العميل بنجاح"
});
} else {
return json({
success: false,
error: result.error,
action: "delete"
}, { status: 400 });
}
}
case "get": {
const id = parseInt(formData.get("id") as string);
const customer = await getCustomerById(id);
if (customer) {
return json({
success: true,
customer,
action: "get"
});
} else {
return json({
success: false,
error: "العميل غير موجود",
action: "get"
}, { status: 404 });
}
}
default:
return json({
success: false,
error: "إجراء غير صحيح",
action: "unknown"
}, { status: 400 });
}
}
export default function CustomersPage() {
const { customers, total, totalPages, currentPage, searchQuery, user } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const [searchParams, setSearchParams] = useSearchParams();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showViewModal, setShowViewModal] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState<CustomerWithVehicles | null>(null);
const [searchValue, setSearchValue] = useState(searchQuery);
const isLoading = navigation.state !== "idle";
// Debounce search value to avoid too many requests
const debouncedSearchValue = useDebounce(searchValue, 300);
// Handle search automatically when debounced value changes
useEffect(() => {
if (debouncedSearchValue !== searchQuery) {
const newSearchParams = new URLSearchParams(searchParams);
if (debouncedSearchValue) {
newSearchParams.set("search", debouncedSearchValue);
} else {
newSearchParams.delete("search");
}
newSearchParams.set("page", "1"); // Reset to first page
setSearchParams(newSearchParams);
}
}, [debouncedSearchValue, searchQuery, searchParams, setSearchParams]);
// Clear search function
const clearSearch = () => {
setSearchValue("");
};
// Handle pagination
const handlePageChange = (page: number) => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set("page", page.toString());
setSearchParams(newSearchParams);
};
// Handle create customer
const handleCreateCustomer = () => {
setSelectedCustomer(null);
setShowCreateModal(true);
};
// Handle view customer
const handleViewCustomer = (customer: CustomerWithVehicles) => {
setSelectedCustomer(customer);
setShowViewModal(true);
};
// Handle edit customer
const handleEditCustomer = (customer: CustomerWithVehicles) => {
setSelectedCustomer(customer);
setShowEditModal(true);
};
// Close modals on successful action
useEffect(() => {
if (actionData?.success && actionData.action === "create") {
setShowCreateModal(false);
}
if (actionData?.success && actionData.action === "update") {
setShowEditModal(false);
}
}, [actionData]);
return (
<DashboardLayout user={user}>
<div className="space-y-6">
{/* Header */}
<Flex justify="between" align="center" className="flex-wrap gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">إدارة العملاء</h1>
<p className="text-gray-600 mt-1">
إجمالي العملاء: {total}
</p>
</div>
<Button
onClick={handleCreateCustomer}
disabled={isLoading}
className="bg-blue-600 hover:bg-blue-700"
>
إضافة عميل جديد
</Button>
</Flex>
{/* Search */}
<div className="bg-white p-4 rounded-lg shadow-sm border">
<Flex gap="md" align="center" className="flex-wrap">
<div className="flex-1 min-w-64">
<Input
type="text"
placeholder="البحث في العملاء... (البحث تلقائي)"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
startIcon={
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
}
endIcon={
searchValue && (
<div className="pointer-events-auto">
<button
onClick={clearSearch}
className="text-gray-400 hover:text-gray-600"
type="button"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)
}
/>
</div>
{(searchQuery || debouncedSearchValue !== searchQuery) && (
<div className="flex items-center text-sm text-gray-500">
{debouncedSearchValue !== searchQuery && (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" 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>
جاري البحث...
</span>
)}
</div>
)}
</Flex>
</div>
{/* Action Messages */}
{actionData?.success && actionData.message && (
<div className="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg">
{actionData.message}
</div>
)}
{actionData?.error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg">
{actionData.error}
</div>
)}
{/* Customer List */}
<CustomerList
customers={customers}
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
onViewCustomer={handleViewCustomer}
onEditCustomer={handleEditCustomer}
isLoading={isLoading}
actionData={actionData}
/>
{/* Create Customer Modal */}
<Modal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
title="إضافة عميل جديد"
>
<CustomerForm
onCancel={() => setShowCreateModal(false)}
errors={actionData?.action === "create" ? actionData.errors : undefined}
isLoading={isLoading}
/>
</Modal>
{/* View Customer Modal */}
<Modal
isOpen={showViewModal}
onClose={() => setShowViewModal(false)}
title={selectedCustomer ? `تفاصيل العميل - ${selectedCustomer.name}` : "تفاصيل العميل"}
size="xl"
>
{selectedCustomer && (
<CustomerDetailsView
customer={selectedCustomer}
onEdit={() => {
setShowViewModal(false);
handleEditCustomer(selectedCustomer);
}}
onClose={() => setShowViewModal(false)}
/>
)}
</Modal>
{/* Edit Customer Modal */}
<Modal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
title="تعديل العميل"
>
{selectedCustomer && (
<CustomerForm
customer={selectedCustomer}
onCancel={() => setShowEditModal(false)}
errors={actionData?.action === "update" ? actionData.errors : undefined}
isLoading={isLoading}
/>
)}
</Modal>
</div>
</DashboardLayout>
);
}