Compare commits

...

2 Commits

Author SHA1 Message Date
Yoga Pangestu
fc9a8b1f0e fix: update error keys in CuttingService, ProductService, and RawMaterialService to use 's3_keys' instead of 'images'
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (8.3) (push) Waiting to run
tests / ci (8.4) (push) Waiting to run
tests / ci (8.5) (push) Waiting to run
2026-07-19 19:06:58 +07:00
Yoga Pangestu
6271bca490 feat: enhance PurchaseGroupedTable to display product information and item numbering 2026-07-19 18:56:56 +07:00
4 changed files with 44 additions and 18 deletions

View File

@ -984,7 +984,7 @@ public function quickCreateProduct(array $validated): array
null,
$maxVariantImages,
required: false,
errorKey: "variants.{$index}.images",
errorKey: "variants.{$index}.s3_keys",
s3Keys: $variantData['s3_keys'] ?? null,
);

View File

@ -815,7 +815,7 @@ private function syncVariantImages(
$variantData['remove_media_ids'] ?? null,
self::MAX_VARIANT_IMAGES,
required: true,
errorKey: "variants.{$index}.images",
errorKey: "variants.{$index}.s3_keys",
s3Keys: $variantData['s3_keys'] ?? null,
);
}
@ -835,7 +835,7 @@ private function syncRequestVariantImages(
$variantData['remove_media_ids'] ?? null,
self::MAX_VARIANT_IMAGES,
required: $required,
errorKey: "variants.{$index}.images",
errorKey: "variants.{$index}.s3_keys",
s3Keys: $variantData['s3_keys'] ?? null,
);
}

View File

@ -515,7 +515,7 @@ private function syncPriceImages(RawMaterialPrice $price, array $priceData, int
$priceData['remove_media_ids'] ?? null,
self::MAX_VARIANT_IMAGES,
required: true,
errorKey: "prices.{$index}.images",
errorKey: "prices.{$index}.s3_keys",
s3Keys: $priceData['s3_keys'] ?? null,
);
}
@ -535,7 +535,7 @@ private function syncRequestPriceImages(
$priceData['remove_media_ids'] ?? null,
self::MAX_VARIANT_IMAGES,
required: $required,
errorKey: "prices.{$index}.images",
errorKey: "prices.{$index}.s3_keys",
s3Keys: $priceData['s3_keys'] ?? null,
);
}

View File

@ -48,15 +48,37 @@ function rowNumber(index: number): number {
return groupedTableRowNumber(props.firstItem, index);
}
interface ProductInfo {
name: string;
unitLabel?: string;
}
function productInfos(items: PurchaseItemDetail[]): ProductInfo[] {
const map = new Map<number, ProductInfo>();
items.forEach((item) => {
const id = item.raw_material_id ?? 0;
if (id && !map.has(id)) {
map.set(id, {
name: item.raw_material_name || 'Bahan Baku Tidak Diketahui',
unitLabel: item.raw_material_unit_label,
});
}
});
return [...map.values()];
}
interface GroupedPurchaseItems {
rawMaterialId: number;
rawMaterialName: string;
unitLabel?: string;
items: PurchaseItemDetail[];
items: (PurchaseItemDetail & { item_number: number })[];
}
function getGroupedItems(items: PurchaseItemDetail[]): GroupedPurchaseItems[] {
const groups: Record<number, GroupedPurchaseItems> = {};
let itemCounter = 0;
items.forEach((item) => {
const rawMaterialId = item.raw_material_id ?? 0;
@ -72,7 +94,8 @@ function getGroupedItems(items: PurchaseItemDetail[]): GroupedPurchaseItems[] {
};
}
groups[rawMaterialId].items.push(item);
itemCounter++;
groups[rawMaterialId].items.push({ ...item, item_number: itemCounter });
});
return Object.values(groups);
@ -102,9 +125,16 @@ function openVerificationDetail(requestId: number | undefined) {
{{ rowNumber(index) }}
</span>
<div class="min-w-0 space-y-2">
<h3 class="font-medium leading-tight">
{{ purchase.supplier?.name }}
<h3 class="font-medium leading-tight flex flex-wrap items-center gap-x-1">
<template v-for="(product, pIdx) in productInfos(purchase.items)" :key="pIdx">
<span>{{ product.name }}</span>
<Badge v-if="product.unitLabel" variant="secondary" class="font-normal">{{ product.unitLabel }}</Badge>
<span v-if="pIdx < productInfos(purchase.items).length - 1" class="text-muted-foreground">,&nbsp;</span>
</template>
</h3>
<p class="text-muted-foreground text-sm">
Supplier: {{ purchase.supplier?.name }}
</p>
<p v-if="purchase.has_pending_request" class="text-sm text-amber-600">
{{ purchase.pending_request_submitted_by_name }} mengajukan {{ purchase.pending_request_action_label?.toLowerCase() }} belanja ini
<button type="button" class="underline-offset-2 hover:underline" @click="openVerificationDetail(purchase.pending_request_id)">lihat</button>
@ -138,6 +168,7 @@ function openVerificationDetail(requestId: number | undefined) {
<Table>
<TableHeader>
<TableRow>
<TableHead class="w-8">#</TableHead>
<TableHead>Varian</TableHead>
<TableHead>Jumlah</TableHead>
<TableHead>Harga Satuan</TableHead>
@ -146,20 +177,15 @@ function openVerificationDetail(requestId: number | undefined) {
</TableHeader>
<TableBody>
<TableRow v-if="!purchase.items.length" :key="`${purchase.id}-empty`">
<TableCell colspan="4" class="text-muted-foreground">
<TableCell colspan="5" class="text-muted-foreground">
Belum ada item
</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 text-center text-muted-foreground tabular-nums">
{{ item.item_number }}
</TableCell>
<TableCell class="pl-6 font-medium">
{{ item.variant }}
</TableCell>