feat: include and display media images for raw material prices and product variants in cutting management

This commit is contained in:
Yoga Pangestu 2026-06-30 21:22:17 +07:00
parent e76fd8107b
commit 9f0676c923
4 changed files with 61 additions and 28 deletions

View File

@ -35,8 +35,9 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
'createdBy.profile', 'createdBy.profile',
'rejection.rejectedBy.profile', 'rejection.rejectedBy.profile',
'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'results.productVariant.product:id,name', 'results.productVariant.product:id,name',
'results.productVariant:id,product_id,name', 'results.productVariant.media',
]) ])
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search']; $search = $tableQuery['search'];
@ -62,6 +63,7 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user)); $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
$cutting->setAttribute('is_editable', $cutting->status->isEditable()); $cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting); $this->appendCostPreview($cutting);
$this->appendPhotos($cutting);
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m)); $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
return $cutting; return $cutting;
@ -75,8 +77,9 @@ public function getInProgressCuttings(User $user): Collection
'createdBy.profile', 'createdBy.profile',
'rejection.rejectedBy.profile', 'rejection.rejectedBy.profile',
'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'results.productVariant.product:id,name', 'results.productVariant.product:id,name',
'results.productVariant:id,product_id,name', 'results.productVariant.media',
]) ])
->inProgress() ->inProgress()
->latest() ->latest()
@ -85,6 +88,7 @@ public function getInProgressCuttings(User $user): Collection
$cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user)); $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
$cutting->setAttribute('is_editable', $cutting->status->isEditable()); $cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting); $this->appendCostPreview($cutting);
$this->appendPhotos($cutting);
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m)); $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
}); });
} }
@ -96,8 +100,9 @@ public function getCompletedCuttings(User $user): Collection
'createdBy.profile', 'createdBy.profile',
'rejection.rejectedBy.profile', 'rejection.rejectedBy.profile',
'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'results.productVariant.product:id,name', 'results.productVariant.product:id,name',
'results.productVariant:id,product_id,name', 'results.productVariant.media',
]) ])
->completed() ->completed()
->latest() ->latest()
@ -106,6 +111,7 @@ public function getCompletedCuttings(User $user): Collection
$cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user)); $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
$cutting->setAttribute('is_editable', $cutting->status->isEditable()); $cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting); $this->appendCostPreview($cutting);
$this->appendPhotos($cutting);
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m)); $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
}); });
} }
@ -187,14 +193,37 @@ public function findForEdit(Cutting $cutting): Cutting
'rejection.rejectedBy.profile', 'rejection.rejectedBy.profile',
]); ]);
$this->appendPhotos($cutting);
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
return $cutting;
}
public function findForShare(Cutting $cutting): Cutting
{
$cutting->load([
'createdBy.profile',
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'materials.rawMaterialPrice.media',
'results.productVariant.product:id,name',
'results.productVariant.media',
]);
$this->appendCostPreview($cutting);
$this->appendPhotos($cutting);
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
return $cutting;
}
private function appendPhotos(Cutting $cutting): void
{
$cutting->materials->each(function (CuttingMaterial $material): void { $cutting->materials->each(function (CuttingMaterial $material): void {
$price = $material->rawMaterialPrice; $price = $material->rawMaterialPrice;
if ($price) { if ($price) {
$price->setAttribute('images', MediaPresenter::collection($price, 'images')); $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
} }
$this->breakMaterialCircularReference($material);
}); });
$cutting->results->each(function (CuttingResult $result): void { $cutting->results->each(function (CuttingResult $result): void {
@ -207,23 +236,6 @@ public function findForEdit(Cutting $cutting): Cutting
); );
} }
}); });
return $cutting;
}
public function findForShare(Cutting $cutting): Cutting
{
$cutting->load([
'createdBy.profile',
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
'results.productVariant.product:id,name',
'results.productVariant:id,product_id,name',
]);
$this->appendCostPreview($cutting);
$cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
return $cutting;
} }
public function draftMaterialsForUser(User $user): array public function draftMaterialsForUser(User $user): array

View File

@ -10,6 +10,7 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status'; import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import type { CuttingListItem } from '@/types/cutting'; import type { CuttingListItem } from '@/types/cutting';
const props = defineProps<{ const props = defineProps<{
@ -120,13 +121,14 @@ const groupedResults = computed<GroupedResults[]>(() => {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Foto</TableHead>
<TableHead class="text-right">Pemakaian</TableHead> <TableHead class="text-right">Pemakaian</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<template v-for="group in groupedMaterials" :key="group.key"> <template v-for="group in groupedMaterials" :key="group.key">
<TableRow class="bg-muted/20 hover:bg-muted/20"> <TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="2" class="font-semibold"> <TableCell colspan="3" class="font-semibold">
{{ group.name }} {{ group.name }}
<Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal"> <Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
{{ group.unitLabel }} {{ group.unitLabel }}
@ -137,6 +139,9 @@ const groupedResults = computed<GroupedResults[]>(() => {
<TableCell class="pl-6"> <TableCell class="pl-6">
{{ material.variant }} {{ material.variant }}
</TableCell> </TableCell>
<TableCell>
<MediaThumbnailCell :items="material.images ?? []" />
</TableCell>
<TableCell class="text-right tabular-nums"> <TableCell class="text-right tabular-nums">
{{ material.material_usage_formatted }} {{ material.material_usage_formatted }}
</TableCell> </TableCell>
@ -155,6 +160,7 @@ const groupedResults = computed<GroupedResults[]>(() => {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Foto</TableHead>
<TableHead class="text-right">Hasil</TableHead> <TableHead class="text-right">Hasil</TableHead>
<TableHead class="text-right">Sample</TableHead> <TableHead class="text-right">Sample</TableHead>
<TableHead class="text-right">Diluar Sample</TableHead> <TableHead class="text-right">Diluar Sample</TableHead>
@ -163,7 +169,7 @@ const groupedResults = computed<GroupedResults[]>(() => {
<TableBody> <TableBody>
<template v-for="group in groupedResults" :key="group.key"> <template v-for="group in groupedResults" :key="group.key">
<TableRow class="bg-muted/20 hover:bg-muted/20"> <TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="4" class="font-semibold"> <TableCell colspan="5" class="font-semibold">
{{ group.name }} {{ group.name }}
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -171,6 +177,9 @@ const groupedResults = computed<GroupedResults[]>(() => {
<TableCell class="pl-6"> <TableCell class="pl-6">
{{ result.product_variant?.name }} {{ result.product_variant?.name }}
</TableCell> </TableCell>
<TableCell>
<MediaThumbnailCell :items="result.product_variant?.images ?? []" />
</TableCell>
<TableCell class="text-right tabular-nums"> <TableCell class="text-right tabular-nums">
{{ result.cutting_result }} pcs {{ result.cutting_result }} pcs
</TableCell> </TableCell>

View File

@ -15,6 +15,7 @@ import {
} from '@/components/ui/table'; } from '@/components/ui/table';
import { usePaginationSummary } from '@/composables/usePaginationSummary'; import { usePaginationSummary } from '@/composables/usePaginationSummary';
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status'; import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
import { groupedTableRowNumber } from '@/lib/grouped-table'; import { groupedTableRowNumber } from '@/lib/grouped-table';
import type { CuttingListItem } from '@/types/cutting'; import type { CuttingListItem } from '@/types/cutting';
import type { import type {
@ -155,19 +156,20 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Foto</TableHead>
<TableHead>Pemakaian</TableHead> <TableHead>Pemakaian</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`"> <TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
<TableCell colspan="2" class="text-muted-foreground"> <TableCell colspan="3" class="text-muted-foreground">
Belum ada bahan baku Belum ada bahan baku
</TableCell> </TableCell>
</TableRow> </TableRow>
<template v-else v-for="group in getGroupedMaterials(cutting.materials)" <template v-else v-for="group in getGroupedMaterials(cutting.materials)"
:key="group.rawMaterialId"> :key="group.rawMaterialId">
<TableRow class="bg-muted/20 hover:bg-muted/20"> <TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="2" class="font-semibold text-foreground"> <TableCell colspan="3" class="font-semibold text-foreground">
{{ group.rawMaterialName }} {{ group.rawMaterialName }}
<Badge v-if="group.unitLabel" variant="secondary" <Badge v-if="group.unitLabel" variant="secondary"
class="ml-2 font-normal"> class="ml-2 font-normal">
@ -179,6 +181,9 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
<TableCell class="pl-6 font-medium"> <TableCell class="pl-6 font-medium">
{{ material.variant }} {{ material.variant }}
</TableCell> </TableCell>
<TableCell>
<MediaThumbnailCell :items="material.images ?? []" />
</TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ material.material_usage_formatted }} {{ material.material_usage_formatted }}
</TableCell> </TableCell>
@ -196,6 +201,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead>Foto</TableHead>
<TableHead>Hasil</TableHead> <TableHead>Hasil</TableHead>
<TableHead>Sample</TableHead> <TableHead>Sample</TableHead>
<TableHead>Diluar Sample</TableHead> <TableHead>Diluar Sample</TableHead>
@ -203,14 +209,14 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`"> <TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
<TableCell colspan="4" class="text-muted-foreground"> <TableCell colspan="5" class="text-muted-foreground">
Belum ada hasil produk Belum ada hasil produk
</TableCell> </TableCell>
</TableRow> </TableRow>
<template v-else v-for="group in getGroupedResults(cutting.results)" <template v-else v-for="group in getGroupedResults(cutting.results)"
:key="group.productId"> :key="group.productId">
<TableRow class="bg-muted/20 hover:bg-muted/20"> <TableRow class="bg-muted/20 hover:bg-muted/20">
<TableCell colspan="4" class="font-semibold text-foreground"> <TableCell colspan="5" class="font-semibold text-foreground">
{{ group.productName }} {{ group.productName }}
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -218,6 +224,9 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
<TableCell class="pl-6 font-medium"> <TableCell class="pl-6 font-medium">
{{ result.product_variant?.name }} {{ result.product_variant?.name }}
</TableCell> </TableCell>
<TableCell>
<MediaThumbnailCell :items="result.product_variant?.images ?? []" />
</TableCell>
<TableCell class="tabular-nums"> <TableCell class="tabular-nums">
{{ result.cutting_result }} pcs {{ result.cutting_result }} pcs
</TableCell> </TableCell>

View File

@ -18,9 +18,11 @@ export type CuttingMaterialListItem = {
raw_material_id?: number; raw_material_id?: number;
raw_material_name?: string; raw_material_name?: string;
raw_material_unit_label?: string; raw_material_unit_label?: string;
images?: MediaItem[];
raw_material_price?: { raw_material_price?: {
id: number; id: number;
variant: string; variant: string;
images?: MediaItem[];
raw_material?: { raw_material?: {
id: number; id: number;
name: string; name: string;
@ -50,6 +52,7 @@ export type CuttingResultListItem = {
product_variant?: { product_variant?: {
id: number; id: number;
name: string; name: string;
images?: MediaItem[];
product?: { product?: {
id: number; id: number;
name: string; name: string;