195 lines
5.7 KiB
TypeScript
195 lines
5.7 KiB
TypeScript
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 { EXPENSE_CATEGORIES } from "~/lib/constants";
|
||
import type { Expense } from "@prisma/client";
|
||
|
||
interface ExpenseFormProps {
|
||
expense?: Expense;
|
||
onCancel: () => void;
|
||
errors?: Record<string, string>;
|
||
isLoading: boolean;
|
||
}
|
||
|
||
export function ExpenseForm({
|
||
expense,
|
||
onCancel,
|
||
errors = {},
|
||
isLoading,
|
||
}: ExpenseFormProps) {
|
||
const [formData, setFormData] = useState({
|
||
description: expense?.description || "",
|
||
category: expense?.category || "",
|
||
amount: expense?.amount?.toString() || "",
|
||
expenseDate: expense?.expenseDate
|
||
? new Date(expense.expenseDate).toISOString().split('T')[0]
|
||
: new Date().toISOString().split('T')[0],
|
||
});
|
||
|
||
// Reset form data when expense changes
|
||
useEffect(() => {
|
||
if (expense) {
|
||
setFormData({
|
||
description: expense.description || "",
|
||
category: expense.category || "",
|
||
amount: expense.amount?.toString() || "",
|
||
expenseDate: expense.expenseDate
|
||
? new Date(expense.expenseDate).toISOString().split('T')[0]
|
||
: new Date().toISOString().split('T')[0],
|
||
});
|
||
} else {
|
||
setFormData({
|
||
description: "",
|
||
category: "",
|
||
amount: "",
|
||
expenseDate: new Date().toISOString().split('T')[0],
|
||
});
|
||
}
|
||
}, [expense]);
|
||
|
||
const handleInputChange = (field: string, value: string) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[field]: value,
|
||
}));
|
||
};
|
||
|
||
const isEditing = !!expense;
|
||
|
||
return (
|
||
<Form method="post" className="space-y-6">
|
||
<input
|
||
type="hidden"
|
||
name="_action"
|
||
value={isEditing ? "update" : "create"}
|
||
/>
|
||
{isEditing && (
|
||
<input type="hidden" name="id" value={expense.id} />
|
||
)}
|
||
|
||
{/* Description */}
|
||
<div>
|
||
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
|
||
وصف المصروف *
|
||
</label>
|
||
<Input
|
||
id="description"
|
||
name="description"
|
||
type="text"
|
||
value={formData.description}
|
||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||
placeholder="أدخل وصف المصروف"
|
||
error={errors.description}
|
||
required
|
||
disabled={isLoading}
|
||
/>
|
||
{errors.description && (
|
||
<p className="mt-1 text-sm text-red-600">{errors.description}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Category */}
|
||
<div>
|
||
<label htmlFor="category" className="block text-sm font-medium text-gray-700 mb-2">
|
||
الفئة *
|
||
</label>
|
||
<select
|
||
id="category"
|
||
name="category"
|
||
value={formData.category}
|
||
onChange={(e) => handleInputChange("category", 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.category
|
||
? 'border-red-300 focus:ring-red-500 focus:border-red-500'
|
||
: 'border-gray-300'
|
||
}
|
||
`}
|
||
>
|
||
<option value="">اختر الفئة</option>
|
||
{EXPENSE_CATEGORIES.map((cat) => (
|
||
<option key={cat.value} value={cat.value}>
|
||
{cat.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
{errors.category && (
|
||
<p className="mt-1 text-sm text-red-600">{errors.category}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Amount */}
|
||
<div>
|
||
<label htmlFor="amount" className="block text-sm font-medium text-gray-700 mb-2">
|
||
المبلغ *
|
||
</label>
|
||
<Input
|
||
id="amount"
|
||
name="amount"
|
||
type="number"
|
||
step="0.01"
|
||
min="0.01"
|
||
value={formData.amount}
|
||
onChange={(e) => handleInputChange("amount", e.target.value)}
|
||
placeholder="0.00"
|
||
error={errors.amount}
|
||
required
|
||
disabled={isLoading}
|
||
dir="ltr"
|
||
/>
|
||
{errors.amount && (
|
||
<p className="mt-1 text-sm text-red-600">{errors.amount}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Expense Date */}
|
||
<div>
|
||
<label htmlFor="expenseDate" className="block text-sm font-medium text-gray-700 mb-2">
|
||
تاريخ المصروف
|
||
</label>
|
||
<Input
|
||
id="expenseDate"
|
||
name="expenseDate"
|
||
type="date"
|
||
value={formData.expenseDate}
|
||
onChange={(e) => handleInputChange("expenseDate", e.target.value)}
|
||
error={errors.expenseDate}
|
||
disabled={isLoading}
|
||
/>
|
||
{errors.expenseDate && (
|
||
<p className="mt-1 text-sm text-red-600">{errors.expenseDate}</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.description.trim() || !formData.category || !formData.amount}
|
||
className="bg-blue-600 hover:bg-blue-700"
|
||
>
|
||
{isLoading
|
||
? (isEditing ? "جاري التحديث..." : "جاري الإنشاء...")
|
||
: (isEditing ? "تحديث المصروف" : "إنشاء المصروف")
|
||
}
|
||
</Button>
|
||
</Flex>
|
||
</Form>
|
||
);
|
||
} |