feat: fix raw material photo display in Cutting and Purchase pages by adding conversion URLs

This commit is contained in:
Yoga Pangestu 2026-08-15 20:30:04 +07:00
parent ef136bd1e7
commit 6fa9224983
8 changed files with 99 additions and 27 deletions

View File

@ -24,10 +24,22 @@ public function getForCutting(): array
->select(['id', 'name', 'unit', 'is_active'])
->with([
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
'rawMaterialPrices.media',
])
->active()
->orderBy('name')
->get()
->each(function (RawMaterial $rawMaterial) {
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
$media = $price->getFirstMedia('images');
$price->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath())
: null;
$price->photo_conversion_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
: null;
});
})
->toArray();
}

View File

@ -0,0 +1,49 @@
# Fix: Foto Bahan Baku Tidak Muncul di Cutting & Belanja
## Tujuan
Memperbaiki foto bahan baku (raw material) yang tidak muncul di halaman create/edit Cutting dan Belanja (Purchasing). Menampilkan foto conversion (thumbnail) bukan original.
## File yang Dibaca
- `app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php``getForCutting()` method (root cause backend)
- `resources/js/pages/admin/manage/cutting/columns.tsx``CuttingCreateData` type (root cause frontend)
- `resources/js/pages/admin/manage/purchase/columns.tsx``PurchaseCreateData` type (root cause frontend)
- `resources/js/pages/admin/master/raw-material/columns.tsx` → reference type pattern
## Root Cause
### Backend
`getForCutting()` tidak load media atau set `photo_url`/`photo_conversion_url` pada `RawMaterialPrice`.
### Frontend
Type `CuttingCreateData` dan `PurchaseCreateData` tidak include `photo_conversion_url` di `raw_material_prices` → TypeScript strip property → frontend tidak bisa akses.
## Perubahan
### 1. Backend — `RawMaterialVariantService::getForCutting()`
- Tambah eager load `rawMaterialPrices.media`
- Iterasi setiap `RawMaterialPrice` untuk set `photo_url` + `photo_conversion_url`
### 2. Frontend Types — `columns.tsx`
- `CuttingCreateData.raw_material_prices` → tambah `photo_conversion_url: string | null`
- `PurchaseCreateData.raw_material_prices` → tambah `photo_conversion_url: string | null`
### 3. Frontend Components — 4 files
- Tambah `photo_conversion_url` ke `MaterialState` type (cutting create/edit)
- Map `photo_conversion_url` dari backend data
- `<img src={price.photo_url}``<img src={price.photo_conversion_url ?? price.photo_url}`
- `photoUrl: price.photo_url``photoUrl: price.photo_conversion_url ?? price.photo_url` (purchase cart items)
## Files Changed
| File | Change |
|------|--------|
| `RawMaterialVariantService.php` | Load media + set photo_url/photo_conversion_url |
| `cutting/columns.tsx` | Add `photo_conversion_url` to type |
| `purchase/columns.tsx` | Add `photo_conversion_url` to type |
| `cutting/create.tsx` | Type + mapping + img tags |
| `cutting/edit.tsx` | Type + mapping + img tags |
| `purchase/create.tsx` | Cart photoUrl + img tag |
| `purchase/edit.tsx` | Cart photoUrl + img tag |
## Dampak
- Create/Edit Cutting → foto bahan baku muncul (conversion/thumbnail)
- Create/Edit Belanja → foto bahan baku muncul (conversion/thumbnail)

View File

@ -105,6 +105,7 @@ export type CuttingCreateData = {
price: number;
stock: number;
photo_url: string | null;
photo_conversion_url: string | null;
}[];
}[];
};

View File

@ -34,6 +34,7 @@ type MaterialState = {
material_name: string;
unit: string;
photo_url: string | null;
photo_conversion_url: string | null;
};
type CombinationState = {
@ -61,6 +62,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
material_name: m.material_name,
unit: m.unit,
photo_url: m.photo_url,
photo_conversion_url: m.photo_conversion_url,
}));
}
@ -130,6 +132,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
material_name: m.material_name,
unit: m.unit,
photo_url: m.photo_url,
photo_conversion_url: m.photo_conversion_url,
})),
combinations: combinations.map((c) => ({
material_result: c.material_result,
@ -192,6 +195,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
material_name: rawMaterial.name,
unit: rawMaterial.unit,
photo_url: price.photo_url,
photo_conversion_url: price.photo_conversion_url,
},
];
});
@ -242,6 +246,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
material_name: foundMaterial?.name ?? '',
unit: foundMaterial?.unit ?? '',
photo_url: foundPrice?.photo_url ?? null,
photo_conversion_url: foundPrice?.photo_conversion_url ?? null,
};
});
@ -393,8 +398,8 @@ export default function CuttingCreate({ rawMaterials }: Props) {
return (
<div key={price.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3 ${isAdded ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
{price.photo_conversion_url ?? price.photo_url ? (
<img src={price.photo_conversion_url ?? price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
@ -439,8 +444,8 @@ export default function CuttingCreate({ rawMaterials }: Props) {
return (
<div key={price.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3 ${isAdded ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
{price.photo_conversion_url ?? price.photo_url ? (
<img src={price.photo_conversion_url ?? price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
@ -623,9 +628,9 @@ export default function CuttingCreate({ rawMaterials }: Props) {
<div key={cartKey} className="space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
{m.photo_url ? (
{m.photo_conversion_url ?? m.photo_url ? (
<button type="button" onClick={() => setPreviewKey(cartKey)} className="block h-8 w-8 shrink-0 overflow-hidden rounded-md border transition-opacity hover:opacity-80">
<img src={m.photo_url} alt={m.variant} className="h-full w-full object-cover" />
<img src={m.photo_conversion_url ?? m.photo_url} alt={m.variant} className="h-full w-full object-cover" />
</button>
) : (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
@ -744,7 +749,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
if (p) {
variantName = p.variant;
materialName = rm.name;
photoUrl = p.photo_url;
photoUrl = p.photo_conversion_url ?? p.photo_url;
break;
}
}
@ -782,8 +787,8 @@ export default function CuttingCreate({ rawMaterials }: Props) {
return (
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
{price.photo_conversion_url ?? price.photo_url ? (
<img src={price.photo_conversion_url ?? price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}

View File

@ -33,6 +33,7 @@ type MaterialState = {
material_name: string;
unit: string;
photo_url: string | null;
photo_conversion_url: string | null;
};
type CombinationState = {
@ -73,6 +74,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
material_name: rawMaterial?.name ?? '',
unit: rawMaterial?.unit ?? '',
photo_url: price?.photo_url ?? m.photo_url ?? null,
photo_conversion_url: price?.photo_conversion_url ?? m.photo_conversion_url ?? null,
};
});
});
@ -175,6 +177,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
material_name: rawMaterial.name,
unit: rawMaterial.unit,
photo_url: price.photo_url,
photo_conversion_url: price.photo_conversion_url,
},
];
});
@ -225,6 +228,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
material_name: foundMaterial?.name ?? '',
unit: foundMaterial?.unit ?? '',
photo_url: foundPrice?.photo_url ?? null,
photo_conversion_url: foundPrice?.photo_conversion_url ?? null,
};
});
@ -371,8 +375,8 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
return (
<div key={price.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3 ${isAdded ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
{price.photo_conversion_url ?? price.photo_url ? (
<img src={price.photo_conversion_url ?? price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
@ -417,8 +421,8 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
return (
<div key={price.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3 ${isAdded ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
{price.photo_conversion_url ?? price.photo_url ? (
<img src={price.photo_conversion_url ?? price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
@ -601,9 +605,9 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
<div key={cartKey} className="space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
{m.photo_url ? (
{m.photo_conversion_url ?? m.photo_url ? (
<button type="button" onClick={() => setPreviewKey(cartKey)} className="block h-8 w-8 shrink-0 overflow-hidden rounded-md border transition-opacity hover:opacity-80">
<img src={m.photo_url} alt={m.variant} className="h-full w-full object-cover" />
<img src={m.photo_conversion_url ?? m.photo_url} alt={m.variant} className="h-full w-full object-cover" />
</button>
) : (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
@ -722,7 +726,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
if (p) {
variantName = p.variant;
materialName = rm.name;
photoUrl = p.photo_url;
photoUrl = p.photo_conversion_url ?? p.photo_url;
break;
}
}
@ -760,8 +764,8 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
return (
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
{price.photo_conversion_url ?? price.photo_url ? (
<img src={price.photo_conversion_url ?? price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}

View File

@ -98,6 +98,7 @@ export type PurchaseCreateData = {
price: number;
stock: number;
photo_url: string | null;
photo_conversion_url: string | null;
}[];
}[];
};

View File

@ -354,7 +354,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
if (price && material) {
lines.push({
key: `existing-${id}`,
photoUrl: price.photo_url,
photoUrl: price.photo_conversion_url ?? price.photo_url,
title: `${material.name}${price.variant}`,
subtitle: `${formatCurrency(price.price)} / ${material.unit}`,
price: price.price,
@ -876,10 +876,10 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
}
>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
{price.photo_conversion_url ?? price.photo_url ? (
<img
src={
price.photo_url
price.photo_conversion_url ?? price.photo_url
}
alt={
price.variant

View File

@ -166,7 +166,7 @@ export default function PurchaseEdit({
if (price && material) {
lines.push({
key: `existing-${id}`,
photoUrl: price.photo_url,
photoUrl: price.photo_conversion_url ?? price.photo_url,
title: `${material.name}${price.variant}`,
subtitle: `${formatCurrency(price.price)} / ${material.unit}`,
price: price.price,
@ -319,10 +319,10 @@ export default function PurchaseEdit({
}
>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
{price.photo_conversion_url ?? price.photo_url ? (
<img
src={
price.photo_url
price.photo_conversion_url ?? price.photo_url
}
alt={
price.variant