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'; 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> </script>
<template> <template>
@ -114,84 +171,93 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
</div> </div>
</div> </div>
<div class="grid gap-0 lg:grid-cols-2"> <div class="space-y-6 p-4">
<Table> <div class="space-y-2">
<TableHeader> <h4 class="text-sm font-semibold tracking-tight text-foreground">Bahan Baku</h4>
<TableRow> <div class="rounded-md border">
<TableHead colspan="4" class="bg-muted/20"> <Table>
Bahan Baku <TableHeader>
</TableHead> <TableRow>
</TableRow> <TableHead>Varian</TableHead>
<TableRow> <TableHead>Pemakaian</TableHead>
<TableHead>Bahan</TableHead> <TableHead>Sisa</TableHead>
<TableHead>Varian</TableHead> </TableRow>
<TableHead>Pemakaian</TableHead> </TableHeader>
<TableHead>Sisa</TableHead> <TableBody>
</TableRow> <TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
</TableHeader> <TableCell colspan="3" class="text-muted-foreground">
<TableBody> Belum ada bahan baku
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`"> </TableCell>
<TableCell colspan="4" class="text-muted-foreground"> </TableRow>
Belum ada bahan baku <template v-else v-for="group in getGroupedMaterials(cutting.materials)" :key="group.rawMaterialId">
</TableCell> <TableRow class="bg-muted/20 hover:bg-muted/20">
</TableRow> <TableCell colspan="3" class="font-semibold text-foreground">
<TableRow v-for="material in cutting.materials" :key="material.id"> {{ group.rawMaterialName }}
<TableCell class="font-medium"> <Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
{{ material.raw_material_price?.raw_material?.name }} {{ group.unitLabel }}
</TableCell> </Badge>
<TableCell> </TableCell>
{{ material.raw_material_price?.variant }} </TableRow>
</TableCell> <TableRow v-for="material in group.items" :key="material.id">
<TableCell class="tabular-nums"> <TableCell class="pl-6 font-medium">
{{ material.material_usage_formatted }} {{ material.raw_material_price?.variant }}
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ material.remaining_material_formatted }} {{ material.material_usage_formatted }}
</TableCell> </TableCell>
</TableRow> <TableCell class="tabular-nums">
</TableBody> {{ material.remaining_material_formatted }}
</Table> </TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
<Table> <div class="space-y-2">
<TableHeader> <h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Produk</h4>
<TableRow> <div class="rounded-md border">
<TableHead colspan="5" class="bg-muted/20"> <Table>
Hasil Produk <TableHeader>
</TableHead> <TableRow>
</TableRow> <TableHead>Varian</TableHead>
<TableRow> <TableHead>Hasil</TableHead>
<TableHead>Produk</TableHead> <TableHead>Gudang</TableHead>
<TableHead>Varian</TableHead> <TableHead>Reject</TableHead>
<TableHead>Hasil</TableHead> </TableRow>
<TableHead>Gudang</TableHead> </TableHeader>
<TableHead>Reject</TableHead> <TableBody>
</TableRow> <TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
</TableHeader> <TableCell colspan="4" class="text-muted-foreground">
<TableBody> Belum ada hasil produk
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`"> </TableCell>
<TableCell colspan="5" class="text-muted-foreground"> </TableRow>
Belum ada hasil produk <template v-else v-for="group in getGroupedResults(cutting.results)" :key="group.productId">
</TableCell> <TableRow class="bg-muted/20 hover:bg-muted/20">
</TableRow> <TableCell colspan="4" class="font-semibold text-foreground">
<TableRow v-for="result in cutting.results" :key="result.id"> {{ group.productName }}
<TableCell class="font-medium"> </TableCell>
{{ result.product_variant?.product?.name }} </TableRow>
</TableCell> <TableRow v-for="result in group.items" :key="result.id">
<TableCell> <TableCell class="pl-6 font-medium">
{{ result.product_variant?.name }} {{ result.product_variant?.name }}
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.cutting_result }} pcs {{ result.cutting_result }} pcs
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.warehouse_stock }} pcs {{ result.warehouse_stock }} pcs
</TableCell> </TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.cutting_reject }} pcs {{ result.cutting_reject }} pcs
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody> </template>
</Table> </TableBody>
</Table>
</div>
</div>
</div> </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 DataTableActions from '@/components/admin/manage/purchases/data-table-actions.vue';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue'; import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue'; import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Empty, Empty,
@ -57,6 +58,36 @@ const paginationSummary = computed(() => {
function rowNumber(index: number): number { function rowNumber(index: number): number {
return (props.firstItem ?? 1) + index; 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> </script>
<template> <template>
@ -102,7 +133,6 @@ function rowNumber(index: number): number {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Bahan Baku</TableHead>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Jumlah</TableHead> <TableHead>Jumlah</TableHead>
<TableHead>Harga Satuan</TableHead> <TableHead>Harga Satuan</TableHead>
@ -111,27 +141,34 @@ function rowNumber(index: number): number {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow v-if="!purchase.items.length" :key="`${purchase.id}-empty`"> <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 Belum ada item
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow v-for="item in purchase.items" :key="item.id"> <template v-else v-for="group in getGroupedItems(purchase.items)" :key="group.rawMaterialId">
<TableCell class="font-medium"> <TableRow class="bg-muted/20 hover:bg-muted/20">
{{ item.raw_material_price?.raw_material?.name }} <TableCell colspan="4" class="font-semibold text-foreground">
</TableCell> {{ group.rawMaterialName }}
<TableCell> <Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
{{ item.raw_material_price?.variant }} {{ group.unitLabel }}
</TableCell> </Badge>
<TableCell class="tabular-nums"> </TableCell>
{{ item.quantity_formatted }} </TableRow>
</TableCell> <TableRow v-for="item in group.items" :key="item.id">
<TableCell class="tabular-nums"> <TableCell class="pl-6 font-medium">
{{ item.unit_price_formatted }} {{ item.raw_material_price?.variant }}
</TableCell> </TableCell>
<TableCell class="tabular-nums font-medium"> <TableCell class="tabular-nums">
{{ item.subtotal_formatted }} {{ item.quantity_formatted }}
</TableCell> </TableCell>
</TableRow> <TableCell class="tabular-nums">
{{ item.unit_price_formatted }}
</TableCell>
<TableCell class="tabular-nums font-medium">
{{ item.subtotal_formatted }}
</TableCell>
</TableRow>
</template>
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@ -17,15 +17,25 @@ export type PurchaseItemListItem = {
export type PurchaseListItem = { export type PurchaseListItem = {
id: number; id: number;
supplier_name: string; supplier_name?: string;
supplier?: {
id: number;
name: string;
} | null;
subtotal_formatted: string; subtotal_formatted: string;
discount_formatted: string; discount_formatted: string;
total_formatted: string; total_formatted: string;
notes: string | null; notes: string | null;
created_at_formatted: string; 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; photos?: MediaItem | null;
items: PurchaseItemListItem[]; items: any[];
}; };
export type PurchaseCartItem = { export type PurchaseCartItem = {