feat: implement grouped table views for purchase items and cutting materials/results with updated type definitions

This commit is contained in:
Yoga Pangestu 2026-06-13 12:55:20 +07:00
parent d8079fb3ff
commit e5f2f526c3
3 changed files with 212 additions and 99 deletions

View File

@ -73,6 +73,63 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
return 'outline';
}
interface GroupedCuttingMaterials {
rawMaterialId: number;
rawMaterialName: string;
unitLabel?: string;
items: any[];
}
interface GroupedCuttingResults {
productId: number;
productName: string;
items: any[];
}
function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
const groups: Record<number, GroupedCuttingMaterials> = {};
materials.forEach((item) => {
const rawMaterial = item.raw_material_price?.raw_material;
const rawMaterialId = rawMaterial?.id ?? 0;
const rawMaterialName = rawMaterial?.name ?? 'Bahan Baku Tidak Diketahui';
const unitLabel = rawMaterial?.unit_label;
if (!groups[rawMaterialId]) {
groups[rawMaterialId] = {
rawMaterialId,
rawMaterialName,
unitLabel,
items: [],
};
}
groups[rawMaterialId].items.push(item);
});
return Object.values(groups);
}
function getGroupedResults(results: any[]): GroupedCuttingResults[] {
const groups: Record<number, GroupedCuttingResults> = {};
results.forEach((item) => {
const product = item.product_variant?.product;
const productId = product?.id ?? 0;
const productName = product?.name ?? 'Produk Tidak Diketahui';
if (!groups[productId]) {
groups[productId] = {
productId,
productName,
items: [],
};
}
groups[productId].items.push(item);
});
return Object.values(groups);
}
</script>
<template>
@ -114,84 +171,93 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
</div>
</div>
<div class="grid gap-0 lg:grid-cols-2">
<Table>
<TableHeader>
<TableRow>
<TableHead colspan="4" class="bg-muted/20">
Bahan Baku
</TableHead>
</TableRow>
<TableRow>
<TableHead>Bahan</TableHead>
<TableHead>Varian</TableHead>
<TableHead>Pemakaian</TableHead>
<TableHead>Sisa</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
<TableCell colspan="4" class="text-muted-foreground">
Belum ada bahan baku
</TableCell>
</TableRow>
<TableRow v-for="material in cutting.materials" :key="material.id">
<TableCell class="font-medium">
{{ material.raw_material_price?.raw_material?.name }}
</TableCell>
<TableCell>
{{ material.raw_material_price?.variant }}
</TableCell>
<TableCell class="tabular-nums">
{{ material.material_usage_formatted }}
</TableCell>
<TableCell class="tabular-nums">
{{ material.remaining_material_formatted }}
</TableCell>
</TableRow>
</TableBody>
</Table>
<div class="space-y-6 p-4">
<div class="space-y-2">
<h4 class="text-sm font-semibold tracking-tight text-foreground">Bahan Baku</h4>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Pemakaian</TableHead>
<TableHead>Sisa</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
<TableCell colspan="3" class="text-muted-foreground">
Belum ada bahan baku
</TableCell>
</TableRow>
<template v-else v-for="group in getGroupedMaterials(cutting.materials)" :key="group.rawMaterialId">
<TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="3" class="font-semibold text-foreground">
{{ group.rawMaterialName }}
<Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
{{ group.unitLabel }}
</Badge>
</TableCell>
</TableRow>
<TableRow v-for="material in group.items" :key="material.id">
<TableCell class="pl-6 font-medium">
{{ material.raw_material_price?.variant }}
</TableCell>
<TableCell class="tabular-nums">
{{ material.material_usage_formatted }}
</TableCell>
<TableCell class="tabular-nums">
{{ material.remaining_material_formatted }}
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead colspan="5" class="bg-muted/20">
Hasil Produk
</TableHead>
</TableRow>
<TableRow>
<TableHead>Produk</TableHead>
<TableHead>Varian</TableHead>
<TableHead>Hasil</TableHead>
<TableHead>Gudang</TableHead>
<TableHead>Reject</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
<TableCell colspan="5" class="text-muted-foreground">
Belum ada hasil produk
</TableCell>
</TableRow>
<TableRow v-for="result in cutting.results" :key="result.id">
<TableCell class="font-medium">
{{ result.product_variant?.product?.name }}
</TableCell>
<TableCell>
{{ result.product_variant?.name }}
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.warehouse_stock }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_reject }} pcs
</TableCell>
</TableRow>
</TableBody>
</Table>
<div class="space-y-2">
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Produk</h4>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Hasil</TableHead>
<TableHead>Gudang</TableHead>
<TableHead>Reject</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
<TableCell colspan="4" class="text-muted-foreground">
Belum ada hasil produk
</TableCell>
</TableRow>
<template v-else v-for="group in getGroupedResults(cutting.results)" :key="group.productId">
<TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="4" class="font-semibold text-foreground">
{{ group.productName }}
</TableCell>
</TableRow>
<TableRow v-for="result in group.items" :key="result.id">
<TableCell class="pl-6 font-medium">
{{ result.product_variant?.name }}
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_result }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.warehouse_stock }} pcs
</TableCell>
<TableCell class="tabular-nums">
{{ result.cutting_reject }} pcs
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</div>
</div>
</div>

View File

@ -4,6 +4,7 @@ import { computed } from 'vue';
import DataTableActions from '@/components/admin/manage/purchases/data-table-actions.vue';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Empty,
@ -57,6 +58,36 @@ const paginationSummary = computed(() => {
function rowNumber(index: number): number {
return (props.firstItem ?? 1) + index;
}
interface GroupedPurchaseItems {
rawMaterialId: number;
rawMaterialName: string;
unitLabel?: string;
items: any[];
}
function getGroupedItems(items: any[]): GroupedPurchaseItems[] {
const groups: Record<number, GroupedPurchaseItems> = {};
items.forEach((item) => {
const rawMaterial = item.raw_material_price?.raw_material;
const rawMaterialId = rawMaterial?.id ?? 0;
const rawMaterialName = rawMaterial?.name ?? 'Bahan Baku Tidak Diketahui';
const unitLabel = rawMaterial?.unit_label;
if (!groups[rawMaterialId]) {
groups[rawMaterialId] = {
rawMaterialId,
rawMaterialName,
unitLabel,
items: [],
};
}
groups[rawMaterialId].items.push(item);
});
return Object.values(groups);
}
</script>
<template>
@ -102,7 +133,6 @@ function rowNumber(index: number): number {
<Table>
<TableHeader>
<TableRow>
<TableHead>Bahan Baku</TableHead>
<TableHead>Varian</TableHead>
<TableHead>Jumlah</TableHead>
<TableHead>Harga Satuan</TableHead>
@ -111,27 +141,34 @@ function rowNumber(index: number): number {
</TableHeader>
<TableBody>
<TableRow v-if="!purchase.items.length" :key="`${purchase.id}-empty`">
<TableCell colspan="5" class="text-muted-foreground">
<TableCell colspan="4" class="text-muted-foreground">
Belum ada item
</TableCell>
</TableRow>
<TableRow v-for="item in purchase.items" :key="item.id">
<TableCell class="font-medium">
{{ item.raw_material_price?.raw_material?.name }}
</TableCell>
<TableCell>
{{ item.raw_material_price?.variant }}
</TableCell>
<TableCell class="tabular-nums">
{{ item.quantity_formatted }}
</TableCell>
<TableCell class="tabular-nums">
{{ item.unit_price_formatted }}
</TableCell>
<TableCell class="tabular-nums font-medium">
{{ item.subtotal_formatted }}
</TableCell>
</TableRow>
<template v-else v-for="group in getGroupedItems(purchase.items)" :key="group.rawMaterialId">
<TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="4" class="font-semibold text-foreground">
{{ group.rawMaterialName }}
<Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
{{ group.unitLabel }}
</Badge>
</TableCell>
</TableRow>
<TableRow v-for="item in group.items" :key="item.id">
<TableCell class="pl-6 font-medium">
{{ item.raw_material_price?.variant }}
</TableCell>
<TableCell class="tabular-nums">
{{ item.quantity_formatted }}
</TableCell>
<TableCell class="tabular-nums">
{{ item.unit_price_formatted }}
</TableCell>
<TableCell class="tabular-nums font-medium">
{{ item.subtotal_formatted }}
</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>

View File

@ -17,15 +17,25 @@ export type PurchaseItemListItem = {
export type PurchaseListItem = {
id: number;
supplier_name: string;
supplier_name?: string;
supplier?: {
id: number;
name: string;
} | null;
subtotal_formatted: string;
discount_formatted: string;
total_formatted: string;
notes: string | null;
created_at_formatted: string;
created_by_name: string;
created_by_name?: string;
created_by?: {
username: string;
profile?: {
full_name: string;
} | null;
} | null;
photos?: MediaItem | null;
items: PurchaseItemListItem[];
items: any[];
};
export type PurchaseCartItem = {