diff --git a/src/app/(dashboard)/finance/product-master/[productSysId]/detail-client.tsx b/src/app/(dashboard)/finance/product-master/[productSysId]/detail-client.tsx
index e56fd67..41314e7 100644
--- a/src/app/(dashboard)/finance/product-master/[productSysId]/detail-client.tsx
+++ b/src/app/(dashboard)/finance/product-master/[productSysId]/detail-client.tsx
@@ -1,7 +1,10 @@
"use client"
import Link from "next/link"
-import { ArrowLeft, Package } from "lucide-react"
+import { useState } from "react"
+import { ArrowLeft, Download, Loader2, Package } from "lucide-react"
+import { useRouter } from "next/navigation"
+import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -15,6 +18,7 @@ import { ProductRoutingTab } from "@/components/finance/cost-product-master/rout
import { ProductAuditTab } from "@/components/finance/cost-product-master/audit-tab"
import { CostHistoryTab } from "@/components/finance/cost-results/cost-history-tab"
import { ProductTypeName } from "@/components/common/product-type-name"
+import { exportBulkProductRouting } from "@/services/finance/cost-import-api"
interface Props {
productSysId: number
@@ -22,6 +26,26 @@ interface Props {
export default function ProductMasterDetailClient({ productSysId }: Props) {
const { data: product, isLoading } = useCostProductMaster(productSysId)
+ const router = useRouter()
+ const [exporting, setExporting] = useState(false)
+
+ async function handleExport() {
+ setExporting(true)
+ try {
+ const result = await exportBulkProductRouting({ productSysIds: [productSysId] })
+ toast.success(`Export dijadwalkan — Job #${result.jobId}`, {
+ description: "Termasuk semua intermediate product yang berkaitan.",
+ action: {
+ label: "Lihat job",
+ onClick: () => router.push("/finance/import-jobs"),
+ },
+ })
+ } catch {
+ toast.error("Export gagal, coba lagi.")
+ } finally {
+ setExporting(false)
+ }
+ }
return (
@@ -47,7 +71,19 @@ export default function ProductMasterDetailClient({ productSysId }: Props) {
: undefined
}
/>
- {product &&
}
+
+ {product && }
+ {product && (
+
+ )}
+
{product && (
diff --git a/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx b/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx
index 31f011b..0eebcc2 100644
--- a/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx
+++ b/src/app/(dashboard)/finance/product-master/product-master-page-client.tsx
@@ -84,7 +84,10 @@ export default function ProductMasterPageClient() {
async function handleBulkExport() {
setBulkExportLoading(true)
try {
- const result = await exportBulkProductRouting()
+ const isFiltered = !!filters.search || !!filters.productTypeId
+ const visibleItems = data?.items ?? []
+ const productSysIds = isFiltered ? visibleItems.map((p) => p.productSysId) : undefined
+ const result = await exportBulkProductRouting({ productSysIds })
toast.success(`Export dijadwalkan — Job #${result.jobId}`, {
description: "File akan tersedia di halaman Import Jobs setelah selesai diproses.",
action: {
diff --git a/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts b/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts
index 588ee69..a104b89 100644
--- a/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts
+++ b/src/app/api/v1/finance/costing/export/bulk_product_routing/route.ts
@@ -18,6 +18,9 @@ export async function POST(request: NextRequest) {
const productTypeCodes: string[] = Array.isArray(body.productTypeCodes)
? (body.productTypeCodes as string[])
: []
+ const productSysIds: number[] = Array.isArray(body.productSysIds)
+ ? (body.productSysIds as unknown[]).map(Number)
+ : []
const includeRouting: boolean =
typeof body.includeRouting === "boolean" ? body.includeRouting : true
@@ -25,7 +28,7 @@ export async function POST(request: NextRequest) {
typeof body.activeOnly === "boolean" ? body.activeOnly : false
const res = await getCostDataImportClient().exportBulkProductRouting(
- { productTypeCodes, includeRouting, activeOnly },
+ { productTypeCodes, productSysIds, includeRouting, activeOnly },
metadata,
)
diff --git a/src/services/finance/cost-import-api.ts b/src/services/finance/cost-import-api.ts
index 7472e96..ef59a92 100644
--- a/src/services/finance/cost-import-api.ts
+++ b/src/services/finance/cost-import-api.ts
@@ -227,16 +227,22 @@ export async function validateBulkProductRoutingFile(
}
/**
- * Queue an async export of all product master + routing data to MinIO.
+ * Queue an async export of product master + routing data to MinIO.
+ * When productSysIds is provided, exports only those products and their full
+ * transitive dependency closure (intermediates reachable via PRODUCT-type RMs).
* Returns a job ID that can be polled or viewed on the import-jobs page.
*/
export async function exportBulkProductRouting(options?: {
productTypeCodes?: string[]
+ productSysIds?: number[]
}): Promise<{ jobId: number; status: string }> {
const res = await fetch(`${BASE}/export/bulk_product_routing`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ productTypeCodes: options?.productTypeCodes ?? [] }),
+ body: JSON.stringify({
+ productTypeCodes: options?.productTypeCodes ?? [],
+ productSysIds: options?.productSysIds ?? [],
+ }),
})
if (!res.ok) throw new Error(`Bulk export failed: ${res.status}`)
const json = await res.json()
diff --git a/src/types/generated/finance/v1/cost_import.ts b/src/types/generated/finance/v1/cost_import.ts
index 53c571b..b1c965f 100644
--- a/src/types/generated/finance/v1/cost_import.ts
+++ b/src/types/generated/finance/v1/cost_import.ts
@@ -148,6 +148,13 @@ export interface ExportBulkProductRoutingRequest {
productTypeCodes: string[];
includeRouting: boolean;
activeOnly: boolean;
+ /**
+ * product_sys_ids, when non-empty, restricts the export to the transitive
+ * closure of the given products (the listed products plus every intermediate
+ * product reachable via PRODUCT-type route RMs). Mutually exclusive with
+ * product_type_codes; product_sys_ids takes precedence when both are set.
+ */
+ productSysIds: number[];
}
export interface ExportBulkProductRoutingResponse {
@@ -2419,7 +2426,7 @@ export const ValidateBulkProductRoutingFileResponse: MessageFns = {
@@ -2433,6 +2440,11 @@ export const ExportBulkProductRoutingRequest: MessageFns globalThis.Number(e))
+ : globalThis.Array.isArray(object?.product_sys_ids)
+ ? object.product_sys_ids.map((e: any) => globalThis.Number(e))
+ : [],
};
},
@@ -2507,6 +2542,9 @@ export const ExportBulkProductRoutingRequest: MessageFns Math.round(e));
+ }
return obj;
},
@@ -2518,6 +2556,7 @@ export const ExportBulkProductRoutingRequest: MessageFns e) || [];
message.includeRouting = object.includeRouting ?? false;
message.activeOnly = object.activeOnly ?? false;
+ message.productSysIds = object.productSysIds?.map((e) => e) || [];
return message;
},
};