import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { useLoaderData, useActionData, useNavigation, useSearchParams } from "@remix-run/react"; import { useState, useEffect } from "react"; import { requireAuth } from "~/lib/auth-middleware.server"; import { useDebounce } from "~/hooks/useDebounce"; import { getExpenses, createExpense, updateExpense, deleteExpense, getExpenseById, getExpenseCategories } from "~/lib/expense-management.server"; import { validateExpense } from "~/lib/validation"; import { DashboardLayout } from "~/components/layout/DashboardLayout"; import { ExpenseForm } from "~/components/expenses/ExpenseForm"; import { Button } from "~/components/ui/Button"; import { Input } from "~/components/ui/Input"; import { Modal } from "~/components/ui/Modal"; import { DataTable } from "~/components/ui/DataTable"; import { Flex } from "~/components/layout/Flex"; import { useSettings } from "~/contexts/SettingsContext"; import { EXPENSE_CATEGORIES, PAGINATION } from "~/lib/constants"; import type { Expense } from "@prisma/client"; export async function loader({ request }: LoaderFunctionArgs) { const user = await requireAuth(request, 2); // Admin level required const url = new URL(request.url); const searchQuery = url.searchParams.get("search") || ""; const page = parseInt(url.searchParams.get("page") || "1"); const category = url.searchParams.get("category") || ""; const dateFrom = url.searchParams.get("dateFrom") ? new Date(url.searchParams.get("dateFrom")!) : undefined; const dateTo = url.searchParams.get("dateTo") ? new Date(url.searchParams.get("dateTo")!) : undefined; const { expenses, total, totalPages } = await getExpenses( searchQuery, page, PAGINATION.DEFAULT_PAGE_SIZE, category, dateFrom, dateTo ); const categories = await getExpenseCategories(); return json({ user, expenses, total, totalPages, currentPage: page, searchQuery, category, dateFrom: dateFrom?.toISOString().split('T')[0] || "", dateTo: dateTo?.toISOString().split('T')[0] || "", categories, }); } export async function action({ request }: ActionFunctionArgs) { await requireAuth(request, 2); // Admin level required const formData = await request.formData(); const action = formData.get("_action") as string; switch (action) { case "create": { const expenseData = { description: formData.get("description") as string, category: formData.get("category") as string, amount: parseFloat(formData.get("amount") as string), expenseDate: formData.get("expenseDate") as string, }; const validation = validateExpense({ description: expenseData.description, category: expenseData.category, amount: expenseData.amount, }); if (!validation.isValid) { return json({ success: false, errors: validation.errors, action: "create" }, { status: 400 }); } try { const expense = await createExpense({ description: expenseData.description, category: expenseData.category, amount: expenseData.amount, expenseDate: expenseData.expenseDate ? new Date(expenseData.expenseDate) : undefined, }); return json({ success: true, expense, action: "create", message: "تم إنشاء المصروف بنجاح" }); } catch (error) { return json({ success: false, error: "حدث خطأ أثناء إضافة المصروف", action: "create" }, { status: 500 }); } } case "update": { const id = parseInt(formData.get("id") as string); const expenseData = { description: formData.get("description") as string, category: formData.get("category") as string, amount: parseFloat(formData.get("amount") as string), expenseDate: formData.get("expenseDate") as string, }; const validation = validateExpense({ description: expenseData.description, category: expenseData.category, amount: expenseData.amount, }); if (!validation.isValid) { return json({ success: false, errors: validation.errors, action: "update" }, { status: 400 }); } try { const expense = await updateExpense(id, { description: expenseData.description, category: expenseData.category, amount: expenseData.amount, expenseDate: expenseData.expenseDate ? new Date(expenseData.expenseDate) : undefined, }); return json({ success: true, expense, action: "update", message: "تم تحديث المصروف بنجاح" }); } catch (error) { return json({ success: false, error: "حدث خطأ أثناء تحديث المصروف", action: "update" }, { status: 500 }); } } case "delete": { const id = parseInt(formData.get("id") as string); try { await deleteExpense(id); return json({ success: true, action: "delete", message: "تم حذف المصروف بنجاح" }); } catch (error) { return json({ success: false, error: "حدث خطأ أثناء حذف المصروف", action: "delete" }, { status: 500 }); } } case "get": { const id = parseInt(formData.get("id") as string); const expense = await getExpenseById(id); if (expense) { return json({ success: true, expense, 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 ExpensesPage() { const { formatCurrency, formatDate } = useSettings(); const { user, expenses, total, totalPages, currentPage, searchQuery, category, dateFrom, dateTo, categories } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const [searchParams, setSearchParams] = useSearchParams(); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [selectedExpense, setSelectedExpense] = useState(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(""); }; const handleFilter = (filterType: string, value: string) => { const newParams = new URLSearchParams(searchParams); if (value) { newParams.set(filterType, value); } else { newParams.delete(filterType); } newParams.set("page", "1"); setSearchParams(newParams); }; // Handle pagination const handlePageChange = (page: number) => { const newParams = new URLSearchParams(searchParams); newParams.set("page", page.toString()); setSearchParams(newParams); }; // Handle create expense const handleCreateExpense = () => { setSelectedExpense(null); setShowCreateModal(true); }; // Handle edit expense const handleEditExpense = (expense: Expense) => { setSelectedExpense(expense); 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]); const columns = [ { key: "description", header: "الوصف", render: (expense: Expense) => expense.description, }, { key: "category", header: "الفئة", render: (expense: Expense) => { const categoryLabel = EXPENSE_CATEGORIES.find(c => c.value === expense.category)?.label; return categoryLabel || expense.category; }, }, { key: "amount", header: "المبلغ", render: (expense: Expense) => formatCurrency(expense.amount), }, { key: "expenseDate", header: "تاريخ المصروف", render: (expense: Expense) => formatDate(expense.expenseDate), }, { key: "createdDate", header: "تاريخ الإضافة", render: (expense: Expense) => formatDate(expense.createdDate), }, { key: "actions", header: "الإجراءات", render: (expense: Expense) => (
), }, ]; return (
{/* Header */}

إدارة المصروفات

إجمالي المصروفات: {total}

{/* Search */}
setSearchValue(e.target.value)} startIcon={ } endIcon={ searchValue && (
) } />
{(searchQuery || debouncedSearchValue !== searchQuery) && (
{debouncedSearchValue !== searchQuery && ( جاري البحث... )}
)}
{/* Filters */}
handleFilter("dateFrom", e.target.value)} /> handleFilter("dateTo", e.target.value)} />
{/* Action Messages */} {actionData?.success && actionData.message && (
{actionData.message}
)} {actionData?.error && (
{actionData.error}
)} {/* Expenses Table */} {/* Create Expense Modal */} setShowCreateModal(false)} title="إضافة مصروف جديد" > setShowCreateModal(false)} errors={actionData?.action === "create" ? actionData.errors : undefined} isLoading={isLoading} /> {/* Edit Expense Modal */} setShowEditModal(false)} title="تعديل المصروف" > {selectedExpense && ( setShowEditModal(false)} errors={actionData?.action === "update" ? actionData.errors : undefined} isLoading={isLoading} /> )}
); }