Files
Your NamebaishaliHolocron b9ac5ae0b2 first commit
2026-06-15 12:57:03 +05:30

365 lines
15 KiB
TypeScript

import React, { useEffect, useState, useMemo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { v4 } from "uuid";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "ikoncomponents";
import ExpenseDataTable from "./expenseDataTable";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Textarea,
} from "ikoncomponents";
import { CountryDetails } from "@/app/utils/mapping/country";
import { updateProductApi } from "@/app/utils/api/productOfProjectApi";
import { useAppCache } from "@/app/utils/context/AppCacheContext";
// ─── Zod Schema ───────────────────────────────────────────────────────────────
const expenseSchema = z.object({
expenseName: z.string().min(1, "Expense name is required"),
location: z.string().min(1, "Location is required"),
currency: z.string().min(1, "Currency is required"),
cost: z.number().positive("Cost must be greater than 0"),
quantity: z.number().positive("Quantity must be greater than 0"),
description: z.string().optional(),
totalCost: z.number(),
});
type ExpenseData = z.infer<typeof expenseSchema>;
interface ExpenseModalProps {
isOpen: boolean;
onClose: (updatedData?: any) => void;
productIdentifier: string;
productData: any;
setProductData: (data: any) => void;
}
const ExpenseModal: React.FC<ExpenseModalProps> = ({
isOpen,
onClose,
productIdentifier,
productData,
setProductData,
}) => {
const [expenseDetails, setExpenseDetails] = useState<Record<string, ExpenseData>>({});
const [editingExpenseId, setEditingExpenseId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// FX rates come from the shared app-wide cache (year -> currency -> rate).
// Expenses carry no year, so use the current year's rates, falling back to
// the most recent year available.
const { fxRateMap } = useAppCache();
const fxRates = useMemo<Map<string, number>>(() => {
if (!fxRateMap || fxRateMap.size === 0) return new Map();
const years = Array.from(fxRateMap.keys()).sort();
const currentYear = String(new Date().getFullYear());
const yearKey = fxRateMap.has(currentYear)
? currentYear
: years[years.length - 1];
return fxRateMap.get(yearKey) ?? new Map();
}, [fxRateMap]);
const form = useForm<ExpenseData>({
resolver: zodResolver(expenseSchema),
defaultValues: {
expenseName: "",
location: "",
currency: "USD",
cost: 0,
quantity: 0,
description: "",
totalCost: 0,
},
});
// Load existing expense details
useEffect(() => {
if (!productData || !productData.expenseDetails) return;
const transformed: Record<string, ExpenseData> = {};
Object.keys(productData.expenseDetails).forEach((uuid) => {
const e = productData.expenseDetails[uuid];
transformed[uuid] = {
expenseName: e.expenseName || "",
location: e.location || "",
currency: e.currency || "USD",
cost: Number(e.cost) || 0,
quantity: Number(e.quantity) || 0,
totalCost: Number(e.totalCost) || 0,
description: e.remarks || "",
};
});
setExpenseDetails(transformed);
}, [productData, isOpen]);
// Auto-calculate totalCost on watch
useEffect(() => {
const subscription = form.watch((value) => {
const { cost, quantity, currency } = value;
if (cost && quantity && currency) {
const fx = fxRates.get(currency) || 1;
const total = Number(cost) * Number(quantity) * fx;
if (form.getValues("totalCost") !== total) {
form.setValue("totalCost", total, { shouldValidate: false });
}
}
});
return () => subscription.unsubscribe();
}, [form, fxRates]);
// ─── Add / Update (Zod validates here via handleSubmit) ───────────────────
const handleAddOrUpdate = (data: ExpenseData) => {
const fx = fxRates.get(data.currency) || 1;
const totalCost = data.cost * data.quantity * fx;
const expenseId = editingExpenseId || v4();
setExpenseDetails((prev) => ({
...prev,
[expenseId]: { ...data, totalCost },
}));
setEditingExpenseId(null);
form.reset();
};
const handleEdit = (id: string) => {
const item = expenseDetails[id];
if (item) {
form.reset(item);
setEditingExpenseId(id);
}
};
// ─── Done / Save (No Zod validation, bypasses form submit) ───────────────
const handleSave = async () => {
if (!productData) {
console.error("No productData passed to modal");
return;
}
setIsLoading(true);
try {
const transformedExpenseDetails: Record<string, any> = {};
Object.keys(expenseDetails).forEach((uuid) => {
transformedExpenseDetails[uuid] = {
expenseName: expenseDetails[uuid].expenseName,
location: expenseDetails[uuid].location,
currency: expenseDetails[uuid].currency,
cost: Number(expenseDetails[uuid].cost),
quantity: Number(expenseDetails[uuid].quantity),
totalCost: Number(expenseDetails[uuid].totalCost),
remarks: expenseDetails[uuid].description || "",
};
});
const newProductData = {
...productData,
productIdentifier,
expenseDetails: transformedExpenseDetails,
};
// console.log("Final Payload Before PUT:", newProductData);
const response = await updateProductApi(newProductData);
setProductData(newProductData);
console.log("Backend response pf expense ",response)
onClose();
} catch (error) {
console.error("Error saving expenses:", error);
} finally {
setIsLoading(false);
}
};
const expenseArray = Object.keys(expenseDetails).map((uuid) => ({
uuid,
...expenseDetails[uuid],
}));
return (
<Dialog open={isOpen} onOpenChange={(open) => open || onClose()}>
<DialogContent className="sm:max-w-4xl!">
<DialogHeader>
<DialogTitle>Expense</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleAddOrUpdate)}
className="grid grid-cols-1 gap-3"
>
<div className="grid grid-cols-3 gap-3">
<FormField
control={form.control}
name="expenseName"
render={({ field }) => (
<FormItem>
<FormLabel>Expense Name*</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter expense name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Location*</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Choose location" />
</SelectTrigger>
<SelectContent>
{CountryDetails.map((option: any) => (
<SelectItem key={option.code} value={option.name}>
{option.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="currency"
render={({ field }) => (
<FormItem>
<FormLabel>Currency*</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Choose currency" />
</SelectTrigger>
<SelectContent>
{["USD", "GBP", "INR", "QAR", "SAR", "AED"].map((c) => (
<SelectItem key={c} value={c}>{c}</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-3 gap-3">
<FormField
control={form.control}
name="cost"
render={({ field }) => (
<FormItem>
<FormLabel>Cost*</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="quantity"
render={({ field }) => (
<FormItem>
<FormLabel>Quantity*</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="totalCost"
render={({ field }) => (
<FormItem>
<FormLabel>Total Cost*</FormLabel>
<FormControl>
<Input
disabled
type="number"
{...field}
onChange={(e) => field.onChange(e.target.valueAsNumber)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} placeholder="Enter description" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">
{editingExpenseId ? "Update" : "Add"}
</Button>
</div>
</form>
</Form>
<ExpenseDataTable expenseDetails={expenseArray} onEdit={handleEdit} />
<DialogFooter>
<Button type="button" onClick={handleSave} disabled={isLoading}>
{isLoading ? "Saving..." : "Done"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default ExpenseModal;