feat: enhance cutting materials display in stock management components; implement grouping logic for combinations and improve UI for material usage and results presentation
This commit is contained in:
parent
dbfd7ce48e
commit
5a5d2c6f78
@ -31,6 +31,7 @@ public function getPendingVerificationCuttings(User $user): Collection
|
|||||||
'rejection.rejectedBy.profile',
|
'rejection.rejectedBy.profile',
|
||||||
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
'materials.rawMaterialPrice.media',
|
'materials.rawMaterialPrice.media',
|
||||||
|
'materials.combination',
|
||||||
'results.productVariant.product:id,name',
|
'results.productVariant.product:id,name',
|
||||||
'results.productVariant.media',
|
'results.productVariant.media',
|
||||||
])
|
])
|
||||||
@ -53,6 +54,7 @@ public function getPendingApprovalCuttings(User $user): Collection
|
|||||||
'rejection.rejectedBy.profile',
|
'rejection.rejectedBy.profile',
|
||||||
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
'materials.rawMaterialPrice.media',
|
'materials.rawMaterialPrice.media',
|
||||||
|
'materials.combination',
|
||||||
'results.productVariant.product:id,name',
|
'results.productVariant.product:id,name',
|
||||||
'results.productVariant.media',
|
'results.productVariant.media',
|
||||||
'resultPrices.productVariant:id,product_id,name',
|
'resultPrices.productVariant:id,product_id,name',
|
||||||
@ -236,19 +238,34 @@ private function breakMaterialCircularReference(CuttingMaterial $material): void
|
|||||||
{
|
{
|
||||||
$price = $material->rawMaterialPrice;
|
$price = $material->rawMaterialPrice;
|
||||||
|
|
||||||
|
// Always set these attributes regardless of price
|
||||||
|
$material->setAttribute('material_result', $material->material_result);
|
||||||
|
$material->setAttribute('material_result_input', $material->material_result);
|
||||||
|
$material->setAttribute('combination_id', $material->combination_id);
|
||||||
|
$material->setAttribute('combination_material_result', $material->combination?->material_result);
|
||||||
|
|
||||||
if ($price) {
|
if ($price) {
|
||||||
$rawMaterial = $price->rawMaterial;
|
$rawMaterial = $price->rawMaterial;
|
||||||
|
|
||||||
|
$material->setAttribute('variant', $price->variant);
|
||||||
|
$material->setAttribute('stock_input', $price->stock_input);
|
||||||
|
$material->setAttribute('images', $price->getAttribute('images') ?? []);
|
||||||
|
|
||||||
if ($rawMaterial) {
|
if ($rawMaterial) {
|
||||||
$unitAbbreviation = $rawMaterial->unit->abbreviation();
|
$unitAbbreviation = $rawMaterial->unit->abbreviation();
|
||||||
$price->setAttribute('unit_abbreviation', $unitAbbreviation);
|
$price->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||||
$material->setAttribute('unit_abbreviation', $unitAbbreviation);
|
$material->setAttribute('unit_abbreviation', $unitAbbreviation);
|
||||||
|
$material->setAttribute('unit', $rawMaterial->unit->value);
|
||||||
|
$material->setAttribute('raw_material_id', $rawMaterial->id);
|
||||||
|
$material->setAttribute('raw_material_name', $rawMaterial->name);
|
||||||
|
$material->setAttribute('raw_material_unit_label', $rawMaterial->unit->label());
|
||||||
}
|
}
|
||||||
|
|
||||||
$price->unsetRelation('rawMaterial');
|
$price->unsetRelation('rawMaterial');
|
||||||
}
|
}
|
||||||
|
|
||||||
$material->unsetRelation('rawMaterialPrice');
|
$material->unsetRelation('rawMaterialPrice');
|
||||||
|
$material->unsetRelation('combination');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyProductStockOnVerify(Cutting $cutting): void
|
private function applyProductStockOnVerify(Cutting $cutting): void
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useForm } from '@inertiajs/vue3';
|
|||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -194,6 +195,27 @@ function submitVerify() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cutting.materials && cutting.materials.length > 0" class="space-y-2 mt-4">
|
||||||
|
<p class="text-sm font-medium">Bahan Baku Terpakai</p>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div v-for="material in cutting.materials" :key="material.id"
|
||||||
|
class="flex items-center gap-3 rounded-lg border p-3 bg-muted/10">
|
||||||
|
<div class="shrink-0">
|
||||||
|
<MediaThumbnailCell :items="material.raw_material_price?.images ?? []" :max-visible="1" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-medium text-sm truncate">
|
||||||
|
{{ material.raw_material_name || material.raw_material_price?.raw_material?.name }} - {{ material.variant || material.raw_material_price?.variant }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Pemakaian: {{ material.material_usage_formatted }}
|
||||||
|
<span v-if="material.combination_id" class="text-[10px] text-primary bg-primary/10 px-1.5 py-0.5 rounded ml-2 font-medium">Kombinasi</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FieldError :errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []" />
|
<FieldError :errors="verifyForm.errors.result_prices ? [verifyForm.errors.result_prices] : []" />
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
|
|||||||
@ -12,9 +12,73 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
|
|
||||||
|
interface GroupedCuttingMaterials {
|
||||||
|
rawMaterialId: number;
|
||||||
|
rawMaterialName: string;
|
||||||
|
unitLabel?: string;
|
||||||
|
items: any[];
|
||||||
|
isCombination?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
cuttings: CuttingListItem[];
|
cuttings: CuttingListItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
|
||||||
|
const combinationGroups: Record<number, any[]> = {};
|
||||||
|
const nonCombinationItems: any[] = [];
|
||||||
|
|
||||||
|
materials.forEach((item) => {
|
||||||
|
if (item.combination_id) {
|
||||||
|
if (!combinationGroups[item.combination_id]) {
|
||||||
|
combinationGroups[item.combination_id] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
combinationGroups[item.combination_id].push(item);
|
||||||
|
} else {
|
||||||
|
nonCombinationItems.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: GroupedCuttingMaterials[] = [];
|
||||||
|
|
||||||
|
Object.entries(combinationGroups).forEach(([combinationId, items]) => {
|
||||||
|
const firstItem = items[0];
|
||||||
|
const rawMaterialName = firstItem.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||||
|
const unitLabel = firstItem.raw_material_unit_label;
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
rawMaterialId: Number(combinationId) * -1,
|
||||||
|
rawMaterialName: `Kombinasi`,
|
||||||
|
unitLabel,
|
||||||
|
items,
|
||||||
|
isCombination: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups: Record<number, GroupedCuttingMaterials> = {};
|
||||||
|
|
||||||
|
nonCombinationItems.forEach((item) => {
|
||||||
|
const rawMaterialId = item.raw_material_id ?? 0;
|
||||||
|
const rawMaterialName = item.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||||
|
const unitLabel = item.raw_material_unit_label;
|
||||||
|
|
||||||
|
if (!groups[rawMaterialId]) {
|
||||||
|
groups[rawMaterialId] = {
|
||||||
|
rawMaterialId,
|
||||||
|
rawMaterialName,
|
||||||
|
unitLabel,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[rawMaterialId].items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push(...Object.values(groups));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -46,7 +110,68 @@ defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4 space-y-4">
|
||||||
|
<!-- Bahan Baku Section -->
|
||||||
|
<div v-if="cutting.materials && cutting.materials.length" 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>Foto</TableHead>
|
||||||
|
<TableHead>Pemakaian</TableHead>
|
||||||
|
<TableHead>Hasil</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<template v-for="group in getGroupedMaterials(cutting.materials)" :key="group.rawMaterialId">
|
||||||
|
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||||
|
<TableCell colspan="4" class="font-semibold text-foreground">
|
||||||
|
<template v-if="group.isCombination">
|
||||||
|
<span class="text-primary">{{ group.rawMaterialName }}</span>
|
||||||
|
<Badge variant="default" class="ml-2 font-normal">
|
||||||
|
{{ group.items.length }} bahan
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="group.items[0]?.combination_material_result !== null && group.items[0]?.combination_material_result !== undefined"
|
||||||
|
variant="secondary" class="ml-2 font-normal">
|
||||||
|
Hasil: {{ group.items[0].combination_material_result }} pcs
|
||||||
|
</Badge>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ group.rawMaterialName }}
|
||||||
|
<Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
|
||||||
|
{{ group.unitLabel }}
|
||||||
|
</Badge>
|
||||||
|
</template>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow v-for="material in group.items" :key="material.id">
|
||||||
|
<TableCell class="pl-6 font-medium">
|
||||||
|
{{ material.variant }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="tabular-nums">
|
||||||
|
{{ material.material_usage_formatted }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="tabular-nums">
|
||||||
|
<template v-if="!group.isCombination && material.material_result !== null && material.material_result !== undefined">
|
||||||
|
{{ material.material_result }} pcs
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
-
|
||||||
|
</template>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Verifikasi</h4>
|
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Verifikasi</h4>
|
||||||
<div class="rounded-md border">
|
<div class="rounded-md border">
|
||||||
|
|||||||
@ -11,9 +11,73 @@ import {
|
|||||||
import type { CuttingListItem } from '@/types/cutting';
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
import StockDataTableActions from './data-table-actions.vue';
|
import StockDataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
|
interface GroupedCuttingMaterials {
|
||||||
|
rawMaterialId: number;
|
||||||
|
rawMaterialName: string;
|
||||||
|
unitLabel?: string;
|
||||||
|
items: any[];
|
||||||
|
isCombination?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
cuttings: CuttingListItem[];
|
cuttings: CuttingListItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
|
||||||
|
const combinationGroups: Record<number, any[]> = {};
|
||||||
|
const nonCombinationItems: any[] = [];
|
||||||
|
|
||||||
|
materials.forEach((item) => {
|
||||||
|
if (item.combination_id) {
|
||||||
|
if (!combinationGroups[item.combination_id]) {
|
||||||
|
combinationGroups[item.combination_id] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
combinationGroups[item.combination_id].push(item);
|
||||||
|
} else {
|
||||||
|
nonCombinationItems.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: GroupedCuttingMaterials[] = [];
|
||||||
|
|
||||||
|
Object.entries(combinationGroups).forEach(([combinationId, items]) => {
|
||||||
|
const firstItem = items[0];
|
||||||
|
const rawMaterialName = firstItem.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||||
|
const unitLabel = firstItem.raw_material_unit_label;
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
rawMaterialId: Number(combinationId) * -1,
|
||||||
|
rawMaterialName: `Kombinasi`,
|
||||||
|
unitLabel,
|
||||||
|
items,
|
||||||
|
isCombination: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups: Record<number, GroupedCuttingMaterials> = {};
|
||||||
|
|
||||||
|
nonCombinationItems.forEach((item) => {
|
||||||
|
const rawMaterialId = item.raw_material_id ?? 0;
|
||||||
|
const rawMaterialName = item.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||||
|
const unitLabel = item.raw_material_unit_label;
|
||||||
|
|
||||||
|
if (!groups[rawMaterialId]) {
|
||||||
|
groups[rawMaterialId] = {
|
||||||
|
rawMaterialId,
|
||||||
|
rawMaterialName,
|
||||||
|
unitLabel,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[rawMaterialId].items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push(...Object.values(groups));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -41,7 +105,7 @@ defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4 space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Produk</h4>
|
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Produk</h4>
|
||||||
<div class="rounded-md border">
|
<div class="rounded-md border">
|
||||||
@ -66,7 +130,8 @@ defineProps<{
|
|||||||
{{ result.product_variant?.product?.name }} ({{ result.product_variant?.name }})
|
{{ result.product_variant?.product?.name }} ({{ result.product_variant?.name }})
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" />
|
<MediaThumbnailCell :items="result.product_variant?.images ?? []"
|
||||||
|
:max-visible="1" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="tabular-nums">
|
<TableCell class="tabular-nums">
|
||||||
{{ result.cutting_result }} pcs
|
{{ result.cutting_result }} pcs
|
||||||
@ -82,6 +147,69 @@ defineProps<{
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cutting.materials && cutting.materials.length" 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>Foto</TableHead>
|
||||||
|
<TableHead>Pemakaian</TableHead>
|
||||||
|
<TableHead>Hasil</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<template v-for="group in getGroupedMaterials(cutting.materials)"
|
||||||
|
:key="group.rawMaterialId">
|
||||||
|
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||||
|
<TableCell colspan="4" class="font-semibold text-foreground">
|
||||||
|
<template v-if="group.isCombination">
|
||||||
|
<span class="text-primary">{{ group.rawMaterialName }}</span>
|
||||||
|
<Badge variant="default" class="ml-2 font-normal">
|
||||||
|
{{ group.items.length }} bahan
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="group.items[0]?.combination_material_result !== null && group.items[0]?.combination_material_result !== undefined"
|
||||||
|
variant="secondary" class="ml-2 font-normal">
|
||||||
|
Hasil: {{ group.items[0].combination_material_result }} pcs
|
||||||
|
</Badge>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ group.rawMaterialName }}
|
||||||
|
<Badge v-if="group.unitLabel" variant="secondary"
|
||||||
|
class="ml-2 font-normal">
|
||||||
|
{{ group.unitLabel }}
|
||||||
|
</Badge>
|
||||||
|
</template>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow v-for="material in group.items" :key="material.id">
|
||||||
|
<TableCell class="pl-6 font-medium">
|
||||||
|
{{ material.variant }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="tabular-nums">
|
||||||
|
{{ material.material_usage_formatted }}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="tabular-nums">
|
||||||
|
<template
|
||||||
|
v-if="!group.isCombination && material.material_result !== null && material.material_result !== undefined">
|
||||||
|
{{ material.material_result }} pcs
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
-
|
||||||
|
</template>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -214,6 +214,27 @@ function submitVerify() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cutting.materials && cutting.materials.length > 0" class="space-y-2 mt-4">
|
||||||
|
<p class="text-sm font-medium">Bahan Baku Terpakai</p>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div v-for="material in cutting.materials" :key="material.id"
|
||||||
|
class="flex items-center gap-3 rounded-lg border p-3 bg-muted/10">
|
||||||
|
<div class="shrink-0">
|
||||||
|
<MediaThumbnailCell :items="material.raw_material_price?.images ?? []" :max-visible="1" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-medium text-sm truncate">
|
||||||
|
{{ material.raw_material_name || material.raw_material_price?.raw_material?.name }} - {{ material.variant || material.raw_material_price?.variant }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
Pemakaian: {{ material.material_usage_formatted }}
|
||||||
|
<span v-if="material.combination_id" class="text-[10px] text-primary bg-primary/10 px-1.5 py-0.5 rounded ml-2 font-medium">Kombinasi</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user