Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/app/(dashboard)/finance/master/formula/formula-page-client.tsx
Original file line number Diff line number Diff line change
@@ -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<ListFormulasParams>({
defaultValues: defaultFilters,
})

const [isFormOpen, setIsFormOpen] = useState(false)
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
const [isImportOpen, setIsImportOpen] = useState(false)
const [selectedFormula, setSelectedFormula] = useState<Formula | null>(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 (
<div>
<PageHeader
title="Formulas"
subtitle="Manage formulas for costing calculations"
>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
{exportMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Download className="mr-2 h-4 w-4" />
)}
Export/Import
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={handleExport}
disabled={exportMutation.isPending}
>
<Download className="mr-2 h-4 w-4" />
Export to Excel
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsImportOpen(true)}>
<Upload className="mr-2 h-4 w-4" />
Import from Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<Button onClick={handleAddNew}>
<Plus className="mr-2 h-4 w-4" />
Add Formula
</Button>
</div>
</PageHeader>

<Card>
<CardHeader>
<CardTitle>Formula List</CardTitle>
<CardDescription>
{isLoading
? "Loading..."
: `${totalItems} total formulas`}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormulaFilters filters={filters} onFiltersChange={setFilters} />

{isError && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-center text-destructive">
{error instanceof Error
? error.message
: "Failed to load formulas"}
</div>
)}

<FormulaTable
data={data?.data || []}
isLoading={isLoading}
onEdit={handleEdit}
onDelete={handleDelete}
/>

<FormulaPagination
pagination={data?.pagination}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
</CardContent>
</Card>

<FormulaFormDialog
open={isFormOpen}
onOpenChange={setIsFormOpen}
formula={selectedFormula}
/>

<FormulaDeleteDialog
open={isDeleteOpen}
onOpenChange={setIsDeleteOpen}
formula={selectedFormula}
/>

<FormulaImportDialog open={isImportOpen} onOpenChange={setIsImportOpen} />
</div>
)
}

function FormulaPageSkeleton() {
return (
<div>
<PageHeader
title="Formulas"
subtitle="Manage formulas for costing calculations"
>
<div className="flex items-center gap-2">
<Button variant="outline" disabled>
<Download className="mr-2 h-4 w-4" />
Export/Import
</Button>
<Button disabled>
<Plus className="mr-2 h-4 w-4" />
Add Formula
</Button>
</div>
</PageHeader>
<Card>
<CardHeader>
<CardTitle>Formula List</CardTitle>
<CardDescription>Loading...</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</CardContent>
</Card>
</div>
)
}

export default function FormulaPageClient() {
return (
<Suspense fallback={<FormulaPageSkeleton />}>
<FormulaPageContent />
</Suspense>
)
}
14 changes: 14 additions & 0 deletions src/app/(dashboard)/finance/master/formula/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TableSkeleton } from "@/components/loading"

export default function FormulaLoading() {
return (
<div className="space-y-6">
<div className="space-y-2">
<div className="h-4 w-48 bg-muted animate-pulse rounded" />
<div className="h-8 w-64 bg-muted animate-pulse rounded" />
<div className="h-4 w-96 bg-muted animate-pulse rounded" />
</div>
<TableSkeleton rows={8} />
</div>
)
}
8 changes: 8 additions & 0 deletions src/app/(dashboard)/finance/master/formula/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <FormulaPageClient />
}
93 changes: 93 additions & 0 deletions src/app/api/v1/finance/formulas/[formulaId]/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
}
43 changes: 43 additions & 0 deletions src/app/api/v1/finance/formulas/export/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
)
}
}
Loading
Loading