diff --git a/src/app/(dashboard)/finance/master/formula/formula-page-client.tsx b/src/app/(dashboard)/finance/master/formula/formula-page-client.tsx new file mode 100644 index 0000000..724f5ff --- /dev/null +++ b/src/app/(dashboard)/finance/master/formula/formula-page-client.tsx @@ -0,0 +1,226 @@ +"use client" + +import { useState, Suspense } from "react" +import { Plus, Download, Upload, Loader2 } from "lucide-react" + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { PageHeader } from "@/components/common/page-header" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { + FormulaFormDialog, + FormulaDeleteDialog, + FormulaImportDialog, + FormulaFilters, + FormulaTable, + FormulaPagination, +} from "@/components/finance/formula" + +import { useFormulas, useExportFormulas } from "@/hooks/finance/use-formula" +import { useUrlState } from "@/lib/hooks" +import { + type Formula, + type ListFormulasParams, + FormulaType, +} from "@/types/finance/formula" +import { ActiveFilter } from "@/types/finance/uom" + +const defaultFilters: ListFormulasParams = { + page: 1, + pageSize: 10, + search: "", + formulaType: FormulaType.FORMULA_TYPE_UNSPECIFIED, + activeFilter: ActiveFilter.ACTIVE_FILTER_UNSPECIFIED, + sortBy: "code", + sortOrder: "asc", +} + +function FormulaPageContent() { + const [filters, setFilters] = useUrlState({ + defaultValues: defaultFilters, + }) + + const [isFormOpen, setIsFormOpen] = useState(false) + const [isDeleteOpen, setIsDeleteOpen] = useState(false) + const [isImportOpen, setIsImportOpen] = useState(false) + const [selectedFormula, setSelectedFormula] = useState(null) + + const { data, isLoading, isError, error } = useFormulas(filters) + const exportMutation = useExportFormulas() + + const handleAddNew = () => { + setSelectedFormula(null) + setIsFormOpen(true) + } + + const handleEdit = (formula: Formula) => { + setSelectedFormula(formula) + setIsFormOpen(true) + } + + const handleDelete = (formula: Formula) => { + setSelectedFormula(formula) + setIsDeleteOpen(true) + } + + const handleExport = async () => { + await exportMutation.mutateAsync({ + formulaType: filters.formulaType, + activeFilter: filters.activeFilter, + }) + } + + const handlePageChange = (page: number) => { + setFilters((prev) => ({ ...prev, page })) + } + + const handlePageSizeChange = (pageSize: number) => { + setFilters((prev) => ({ ...prev, pageSize, page: 1 })) + } + + const totalItems = data?.pagination?.totalItems ?? 0 + + return ( +
+ +
+ + + + + + + + Export to Excel + + setIsImportOpen(true)}> + + Import from Excel + + + + + +
+
+ + + + Formula List + + {isLoading + ? "Loading..." + : `${totalItems} total formulas`} + + + + + + {isError && ( +
+ {error instanceof Error + ? error.message + : "Failed to load formulas"} +
+ )} + + + + +
+
+ + + + + + +
+ ) +} + +function FormulaPageSkeleton() { + return ( +
+ +
+ + +
+
+ + + Formula List + Loading... + + +
+ +
+
+
+
+ ) +} + +export default function FormulaPageClient() { + return ( + }> + + + ) +} diff --git a/src/app/(dashboard)/finance/master/formula/loading.tsx b/src/app/(dashboard)/finance/master/formula/loading.tsx new file mode 100644 index 0000000..d82d9ae --- /dev/null +++ b/src/app/(dashboard)/finance/master/formula/loading.tsx @@ -0,0 +1,14 @@ +import { TableSkeleton } from "@/components/loading" + +export default function FormulaLoading() { + return ( +
+
+
+
+
+
+ +
+ ) +} diff --git a/src/app/(dashboard)/finance/master/formula/page.tsx b/src/app/(dashboard)/finance/master/formula/page.tsx new file mode 100644 index 0000000..5dc243c --- /dev/null +++ b/src/app/(dashboard)/finance/master/formula/page.tsx @@ -0,0 +1,8 @@ +import { generateMetadata as genMeta } from "@/config/site" +import FormulaPageClient from "./formula-page-client" + +export const metadata = genMeta("Formulas") + +export default function FormulaPage() { + return +} diff --git a/src/app/api/v1/finance/formulas/[formulaId]/route.ts b/src/app/api/v1/finance/formulas/[formulaId]/route.ts new file mode 100644 index 0000000..2c767d1 --- /dev/null +++ b/src/app/api/v1/finance/formulas/[formulaId]/route.ts @@ -0,0 +1,93 @@ +// Finance Formula routes - Get, Update, Delete by ID + +import { NextRequest, NextResponse } from "next/server" +import { getFormulaClient, createMetadataFromRequest, isGrpcError, handleGrpcError } from "@/lib/grpc" + +type RouteContext = { params: Promise<{ formulaId: string }> } + +// GET /api/v1/finance/formulas/[formulaId] +export async function GET(request: NextRequest, context: RouteContext) { + try { + const { formulaId } = await context.params + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + const response = await client.getFormula({ formulaId }, metadata) + + return NextResponse.json({ + base: response.base, + data: response.data, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error fetching formula:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to fetch formula", + validationErrors: [], + }, + }, + { status: 500 } + ) + } +} + +// PUT /api/v1/finance/formulas/[formulaId] +export async function PUT(request: NextRequest, context: RouteContext) { + try { + const { formulaId } = await context.params + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + const response = await client.updateFormula({ ...body, formulaId }, metadata) + + return NextResponse.json({ + base: response.base, + data: response.data, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error updating formula:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to update formula", + validationErrors: [], + }, + }, + { status: 500 } + ) + } +} + +// DELETE /api/v1/finance/formulas/[formulaId] +export async function DELETE(request: NextRequest, context: RouteContext) { + try { + const { formulaId } = await context.params + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + const response = await client.deleteFormula({ formulaId }, metadata) + + return NextResponse.json({ + base: response.base, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error deleting formula:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to delete formula", + validationErrors: [], + }, + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/v1/finance/formulas/export/route.ts b/src/app/api/v1/finance/formulas/export/route.ts new file mode 100644 index 0000000..78d8f71 --- /dev/null +++ b/src/app/api/v1/finance/formulas/export/route.ts @@ -0,0 +1,43 @@ +// GET /api/v1/finance/formulas/export - Export Formulas to Excel + +import { NextRequest, NextResponse } from "next/server" +import { getFormulaClient, createMetadataFromRequest, isGrpcError, handleGrpcError } from "@/lib/grpc" + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + + const response = await client.exportFormulas( + { + formulaType: Number(searchParams.get("formulaType") || searchParams.get("formula_type")) || 0, + activeFilter: Number(searchParams.get("activeFilter") || searchParams.get("active_filter")) || 0, + }, + metadata + ) + + // Convert Uint8Array to base64 string for JSON serialization + const fileContentBase64 = Buffer.from(response.fileContent).toString('base64') + + return NextResponse.json({ + base: response.base, + fileContent: fileContentBase64, + fileName: response.fileName, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error exporting formulas:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to export formulas", + validationErrors: [], + }, + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/v1/finance/formulas/import/route.ts b/src/app/api/v1/finance/formulas/import/route.ts new file mode 100644 index 0000000..bdaaba9 --- /dev/null +++ b/src/app/api/v1/finance/formulas/import/route.ts @@ -0,0 +1,49 @@ +// POST /api/v1/finance/formulas/import - Import Formulas from Excel + +import { NextRequest, NextResponse } from "next/server" +import { getFormulaClient, createMetadataFromRequest, isGrpcError, handleGrpcError } from "@/lib/grpc" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + + // Convert fileContent array to Uint8Array for gRPC + const fileContentBytes = new Uint8Array(body.fileContent) + + const response = await client.importFormulas({ + fileContent: fileContentBytes, + fileName: body.fileName, + duplicateAction: body.duplicateAction, + }, metadata) + + return NextResponse.json({ + base: response.base, + successCount: response.successCount, + skippedCount: response.skippedCount, + updatedCount: response.updatedCount, + failedCount: response.failedCount, + errors: response.errors, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error importing formulas:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to import formulas", + validationErrors: [], + }, + successCount: 0, + skippedCount: 0, + updatedCount: 0, + failedCount: 0, + errors: [], + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/v1/finance/formulas/route.ts b/src/app/api/v1/finance/formulas/route.ts new file mode 100644 index 0000000..4becd47 --- /dev/null +++ b/src/app/api/v1/finance/formulas/route.ts @@ -0,0 +1,78 @@ +// Finance Formula routes - List and Create + +import { NextRequest, NextResponse } from "next/server" +import { getFormulaClient, createMetadataFromRequest, isGrpcError, handleGrpcError } from "@/lib/grpc" + +// GET /api/v1/finance/formulas - List Formulas with filters +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + + const response = await client.listFormulas( + { + page: Number(searchParams.get("page")) || 1, + pageSize: Number(searchParams.get("pageSize") || searchParams.get("page_size")) || 10, + search: searchParams.get("search") || "", + formulaType: Number(searchParams.get("formulaType") || searchParams.get("formula_type")) || 0, + activeFilter: Number(searchParams.get("activeFilter") || searchParams.get("active_filter")) || 0, + sortBy: searchParams.get("sortBy") || searchParams.get("sort_by") || "", + sortOrder: searchParams.get("sortOrder") || searchParams.get("sort_order") || "", + }, + metadata + ) + + return NextResponse.json({ + base: response.base, + data: response.data, + pagination: response.pagination, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error fetching formulas:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to fetch formulas", + validationErrors: [], + }, + data: [], + pagination: { currentPage: 1, pageSize: 10, totalItems: 0, totalPages: 0 }, + }, + { status: 500 } + ) + } +} + +// POST /api/v1/finance/formulas - Create Formula +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + + const response = await client.createFormula(body, metadata) + + return NextResponse.json({ + base: response.base, + data: response.data, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error creating formula:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to create formula", + validationErrors: [], + }, + }, + { status: 500 } + ) + } +} diff --git a/src/app/api/v1/finance/formulas/template/route.ts b/src/app/api/v1/finance/formulas/template/route.ts new file mode 100644 index 0000000..7cbbcf1 --- /dev/null +++ b/src/app/api/v1/finance/formulas/template/route.ts @@ -0,0 +1,35 @@ +// GET /api/v1/finance/formulas/template - Download import template + +import { NextRequest, NextResponse } from "next/server" +import { getFormulaClient, createMetadataFromRequest, isGrpcError, handleGrpcError } from "@/lib/grpc" + +export async function GET(request: NextRequest) { + try { + const metadata = createMetadataFromRequest(request) + const client = getFormulaClient() + const response = await client.downloadFormulaTemplate({}, metadata) + + // Convert Uint8Array to base64 string for JSON serialization + const fileContentBase64 = Buffer.from(response.fileContent).toString('base64') + + return NextResponse.json({ + base: response.base, + fileContent: fileContentBase64, + fileName: response.fileName, + }) + } catch (error) { + if (isGrpcError(error)) return handleGrpcError(error) + console.error("Error downloading formula template:", error) + return NextResponse.json( + { + base: { + isSuccess: false, + statusCode: "500", + message: "Failed to download template", + validationErrors: [], + }, + }, + { status: 500 } + ) + } +} diff --git a/src/components/finance/formula/formula-delete-dialog.tsx b/src/components/finance/formula/formula-delete-dialog.tsx new file mode 100644 index 0000000..e2e345d --- /dev/null +++ b/src/components/finance/formula/formula-delete-dialog.tsx @@ -0,0 +1,46 @@ +"use client" + +import { ConfirmDialog } from "@/components/shared" +import type { Formula } from "@/types/finance/formula" +import { useDeleteFormula } from "@/hooks/finance/use-formula" + +interface FormulaDeleteDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + formula: Formula | null + onSuccess?: () => void +} + +export function FormulaDeleteDialog({ + open, + onOpenChange, + formula, + onSuccess, +}: FormulaDeleteDialogProps) { + const deleteMutation = useDeleteFormula() + + const handleDelete = async () => { + if (!formula) return + + try { + await deleteMutation.mutateAsync(formula.formulaId) + onOpenChange(false) + onSuccess?.() + } catch (error) { + console.error("Failed to delete formula:", error) + } + } + + return ( + + ) +} diff --git a/src/components/finance/formula/formula-filters.tsx b/src/components/finance/formula/formula-filters.tsx new file mode 100644 index 0000000..db5ba5c --- /dev/null +++ b/src/components/finance/formula/formula-filters.tsx @@ -0,0 +1,151 @@ +"use client" + +import { useCallback } from "react" +import { X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { DebouncedSearchInput } from "@/components/common" + +import { + FormulaType, + FORMULA_TYPE_OPTIONS, + FORMULA_ACTIVE_FILTER_OPTIONS, + type ListFormulasParams, +} from "@/types/finance/formula" +import { ActiveFilter } from "@/types/finance/uom" + +interface FormulaFiltersProps { + filters: ListFormulasParams + onFiltersChange: (filters: ListFormulasParams) => void +} + +export function FormulaFilters({ filters, onFiltersChange }: FormulaFiltersProps) { + const handleSearchChange = useCallback( + (value: string) => { + onFiltersChange({ ...filters, search: value, page: 1 }) + }, + [filters, onFiltersChange] + ) + + const handleTypeChange = (value: string) => { + onFiltersChange({ + ...filters, + formulaType: Number(value) as FormulaType, + page: 1, + }) + } + + const handleActiveFilterChange = (value: string) => { + onFiltersChange({ + ...filters, + activeFilter: Number(value) as ActiveFilter, + page: 1, + }) + } + + const handleSortChange = (value: string) => { + const [sortBy, sortOrder] = value.split("-") + onFiltersChange({ ...filters, sortBy, sortOrder }) + } + + const handleClearFilters = () => { + onFiltersChange({ + page: 1, + pageSize: filters.pageSize, + search: "", + formulaType: FormulaType.FORMULA_TYPE_UNSPECIFIED, + activeFilter: ActiveFilter.ACTIVE_FILTER_UNSPECIFIED, + sortBy: "code", + sortOrder: "asc", + }) + } + + const hasActiveFilters = + filters.search || + (filters.formulaType !== undefined && filters.formulaType !== FormulaType.FORMULA_TYPE_UNSPECIFIED) || + (filters.activeFilter !== undefined && filters.activeFilter !== ActiveFilter.ACTIVE_FILTER_UNSPECIFIED) + + const currentSort = `${filters.sortBy || "code"}-${filters.sortOrder || "asc"}` + + return ( +
+ + +
+ {/* Type Filter */} + + + {/* Status Filter */} + + + {/* Sort */} + + + {hasActiveFilters && ( + + )} +
+
+ ) +} diff --git a/src/components/finance/formula/formula-form-dialog.tsx b/src/components/finance/formula/formula-form-dialog.tsx new file mode 100644 index 0000000..77d2118 --- /dev/null +++ b/src/components/finance/formula/formula-form-dialog.tsx @@ -0,0 +1,562 @@ +"use client" + +import { useEffect, useState, useMemo } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { Loader2, Search, X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Switch } from "@/components/ui/switch" +import { Badge } from "@/components/ui/badge" +import { Checkbox } from "@/components/ui/checkbox" +import { ScrollArea } from "@/components/ui/scroll-area" + +import { + type Formula, + FormulaType, + FORMULA_TYPE_FORM_OPTIONS, +} from "@/types/finance/formula" +import { ActiveFilter } from "@/types/finance/uom" +import { ParamCategory } from "@/types/finance/parameter" +import { useCreateFormula, useUpdateFormula, useFormula } from "@/hooks/finance/use-formula" +import { useParameters } from "@/hooks/finance/use-parameter" + +interface FormulaFormValues { + formulaCode: string + formulaName: string + formulaType: number + expression: string + resultParamId: string + inputParamIds: string[] + description: string + isActive: boolean +} + +const validTypeValues = [ + FormulaType.FORMULA_TYPE_CALCULATION, + FormulaType.FORMULA_TYPE_SQL_QUERY, + FormulaType.FORMULA_TYPE_CONSTANT, +] + +const formulaFormSchema = z.object({ + formulaCode: z + .string() + .min(1, "Code is required") + .max(50, "Code must be at most 50 characters") + .regex( + /^[A-Z][A-Z0-9_]*$/, + "Code must start with uppercase letter and contain only uppercase letters, numbers, and underscores" + ), + formulaName: z + .string() + .min(1, "Name is required") + .max(200, "Name must be at most 200 characters"), + formulaType: z + .number() + .refine( + (val) => validTypeValues.includes(val), + "Please select a valid formula type" + ), + expression: z + .string() + .min(1, "Expression is required") + .max(5000, "Expression must be at most 5000 characters"), + resultParamId: z + .string() + .min(1, "Result parameter is required"), + inputParamIds: z.array(z.string()), + description: z.string().max(1000, "Description must be at most 1000 characters"), + isActive: z.boolean(), +}).superRefine((data, ctx) => { + if ( + data.formulaType === FormulaType.FORMULA_TYPE_CALCULATION && + data.inputParamIds.length === 0 + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "At least one input parameter is required for CALCULATION type", + path: ["inputParamIds"], + }) + } +}) + +interface FormulaFormDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + formula?: Formula | null + onSuccess?: () => void +} + +export function FormulaFormDialog({ + open, + onOpenChange, + formula, + onSuccess, +}: FormulaFormDialogProps) { + const isEditing = !!formula + const createMutation = useCreateFormula() + const updateMutation = useUpdateFormula() + const [inputParamSearch, setInputParamSearch] = useState("") + + // Fetch full formula data when editing (list may not include inputParams) + const { data: fullFormulaResult } = useFormula( + isEditing && open ? formula.formulaId : "" + ) + const fullFormula = fullFormulaResult?.data ?? null + + // Fetch CALCULATED params for result parameter dropdown + const { data: calculatedParamData } = useParameters({ + page: 1, + pageSize: 200, + activeFilter: ActiveFilter.ACTIVE_FILTER_ACTIVE, + paramCategory: ParamCategory.PARAM_CATEGORY_CALCULATED, + sortBy: "code", + sortOrder: "asc", + }) + + // Fetch INPUT + RATE params for input parameter selection + const { data: inputParamData } = useParameters({ + page: 1, + pageSize: 200, + activeFilter: ActiveFilter.ACTIVE_FILTER_ACTIVE, + paramCategory: ParamCategory.PARAM_CATEGORY_INPUT, + sortBy: "code", + sortOrder: "asc", + }) + + const { data: rateParamData } = useParameters({ + page: 1, + pageSize: 200, + activeFilter: ActiveFilter.ACTIVE_FILTER_ACTIVE, + paramCategory: ParamCategory.PARAM_CATEGORY_RATE, + sortBy: "code", + sortOrder: "asc", + }) + + const calculatedParams = useMemo(() => calculatedParamData?.data || [], [calculatedParamData]) + const inputAndRateParams = useMemo(() => [ + ...(inputParamData?.data || []), + ...(rateParamData?.data || []), + ].sort((a, b) => (a.paramCode || "").localeCompare(b.paramCode || "")), [inputParamData, rateParamData]) + + // Filter parameters for input param search + const filteredInputParams = useMemo(() => { + if (!inputParamSearch) return inputAndRateParams + const search = inputParamSearch.toLowerCase() + return inputAndRateParams.filter( + (p) => + p.paramCode?.toLowerCase().includes(search) || + p.paramName?.toLowerCase().includes(search) + ) + }, [inputAndRateParams, inputParamSearch]) + + const form = useForm({ + resolver: zodResolver(formulaFormSchema) as never, + defaultValues: { + formulaCode: "", + formulaName: "", + formulaType: FormulaType.FORMULA_TYPE_CALCULATION, + expression: "", + resultParamId: "", + inputParamIds: [], + description: "", + isActive: true, + }, + }) + + // Populate form when dialog opens or fullFormula loads + useEffect(() => { + if (!open) return + + if (isEditing) { + // Use fullFormula (fetched by ID) if available, fallback to list formula + const src = fullFormula || formula + if (!src) return + const inputIds = fullFormula?.inputParams?.map((p) => p.paramId).filter(Boolean) + || formula?.inputParams?.map((p) => p.paramId).filter(Boolean) + || [] + form.reset({ + formulaCode: src.formulaCode || "", + formulaName: src.formulaName || "", + formulaType: src.formulaType, + expression: src.expression || "", + resultParamId: src.resultParamId || "", + inputParamIds: inputIds, + description: src.description || "", + isActive: src.isActive ?? true, + }) + } else { + form.reset({ + formulaCode: "", + formulaName: "", + formulaType: FormulaType.FORMULA_TYPE_CALCULATION, + expression: "", + resultParamId: "", + inputParamIds: [], + description: "", + isActive: true, + }) + } + setInputParamSearch("") + }, [open, formula, fullFormula, form, isEditing]) + + const onSubmit = async (values: FormulaFormValues) => { + try { + if (isEditing && formula) { + await updateMutation.mutateAsync({ + id: formula.formulaId, + data: { + formulaId: formula.formulaId, + formulaName: values.formulaName, + formulaType: values.formulaType, + expression: values.expression, + resultParamId: values.resultParamId, + inputParamIds: values.inputParamIds, + description: values.description || "", + isActive: values.isActive, + }, + }) + } else { + await createMutation.mutateAsync({ + formulaCode: values.formulaCode, + formulaName: values.formulaName, + formulaType: values.formulaType, + expression: values.expression, + resultParamId: values.resultParamId, + inputParamIds: values.inputParamIds, + description: values.description || "", + }) + } + onOpenChange(false) + onSuccess?.() + } catch (error) { + console.error("Failed to save formula:", error) + } + } + + const isPending = createMutation.isPending || updateMutation.isPending + const selectedInputIds = form.watch("inputParamIds") + + const toggleInputParam = (paramId: string) => { + const current = form.getValues("inputParamIds") + if (current.includes(paramId)) { + form.setValue("inputParamIds", current.filter((id) => id !== paramId), { shouldValidate: true }) + } else { + form.setValue("inputParamIds", [...current, paramId], { shouldValidate: true }) + } + } + + return ( + + + + {isEditing ? "Edit Formula" : "Add New Formula"} + + {isEditing + ? "Update the formula details. Code cannot be changed." + : "Create a new formula for costing calculations."} + + + +
+ +
+
+ ( + + Code + + + field.onChange(e.target.value.toUpperCase()) + } + /> + + + + )} + /> + + ( + + Type + + + + )} + /> +
+ + ( + + Name + + + + + + )} + /> + + ( + + Expression + +