feat: add receipt conversion URLs for media handling across various services and components

This commit is contained in:
Yoga Pangestu 2026-08-12 21:35:46 +07:00
parent ef987360f4
commit 5335e5ad58
26 changed files with 107 additions and 21 deletions

View File

@ -193,6 +193,7 @@ private function formatTransaction(CashTransaction $transaction): array
return $transaction->toArray() + [ return $transaction->toArray() + [
'receipt_key' => null, 'receipt_key' => null,
'receipt_url' => null, 'receipt_url' => null,
'receipt_conversion_url' => null,
]; ];
} }
@ -201,6 +202,7 @@ private function formatTransaction(CashTransaction $transaction): array
return $transaction->toArray() + [ return $transaction->toArray() + [
'receipt_key' => $s3Key, 'receipt_key' => $s3Key,
'receipt_url' => $this->s3Service->getTemporaryUrl($s3Key), 'receipt_url' => $this->s3Service->getTemporaryUrl($s3Key),
'receipt_conversion_url' => $this->s3Service->getTemporaryUrl($media->getPath('thumb')),
]; ];
} }
} }

View File

@ -155,6 +155,7 @@ private function formatExpense(Expense $expense): array
return $expense->toArray() + [ return $expense->toArray() + [
'receipt_key' => null, 'receipt_key' => null,
'receipt_url' => null, 'receipt_url' => null,
'receipt_conversion_url' => null,
]; ];
} }
@ -165,9 +166,15 @@ private function formatExpense(Expense $expense): array
return $this->s3Service->getTemporaryUrl($s3Key); return $this->s3Service->getTemporaryUrl($s3Key);
}); });
$conversionCacheKey = "expense_receipt_conversion_{$media->id}";
$receiptConversionUrl = Cache::remember($conversionCacheKey, now()->addMinutes(55), function () use ($media) {
return $this->s3Service->getTemporaryUrl($media->getPath('thumb'));
});
return $expense->toArray() + [ return $expense->toArray() + [
'receipt_key' => $s3Key, 'receipt_key' => $s3Key,
'receipt_url' => $receiptUrl, 'receipt_url' => $receiptUrl,
'receipt_conversion_url' => $receiptConversionUrl,
]; ];
} }
} }

View File

@ -46,6 +46,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$cutting->photo_url = $cuttingMedia $cutting->photo_url = $cuttingMedia
? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath())
: null; : null;
$cutting->photo_conversion_url = $cuttingMedia
? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath('thumb'))
: null;
$cutting->cuttingMaterials->each(function (CuttingMaterial $material) { $cutting->cuttingMaterials->each(function (CuttingMaterial $material) {
$media = $material->rawMaterialPrice?->getFirstMedia('images'); $media = $material->rawMaterialPrice?->getFirstMedia('images');
@ -53,6 +56,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$material->rawMaterialPrice->photo_url = $media $material->rawMaterialPrice->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath()) ? $this->s3Service->getTemporaryUrl($media->getPath())
: null; : null;
$material->rawMaterialPrice->photo_conversion_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
: null;
} }
}); });
}); });

View File

@ -49,15 +49,21 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$purchase->photo_url = $purchaseMedia $purchase->photo_url = $purchaseMedia
? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath()) ? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath())
: null; : null;
$purchase->photo_conversion_url = $purchaseMedia
? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath('thumb'))
: null;
$purchase->purchaseItems->each(function (PurchaseItem $item) { $purchase->purchaseItems->each(function (PurchaseItem $item) {
if (! $item->rawMaterialPrice) { if (! $item->rawMaterialPrice) {
return; return;
} }
$media = $item->rawMaterialPrice->getMedia('images'); $media = $item->rawMaterialPrice->getFirstMedia('images');
$item->rawMaterialPrice->photo_url = $media->first() $item->rawMaterialPrice->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->first()->getPath()) ? $this->s3Service->getTemporaryUrl($media->getPath())
: null;
$item->rawMaterialPrice->photo_conversion_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
: null; : null;
}); });
}); });

View File

@ -53,6 +53,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$item->productVariant->photo_url = $media $item->productVariant->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath()) ? $this->s3Service->getTemporaryUrl($media->getPath())
: null; : null;
$item->productVariant->photo_conversion_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
: null;
}); });
}); });

View File

@ -79,6 +79,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$order->photo_url = $orderMedia $order->photo_url = $orderMedia
? $this->s3Service->getTemporaryUrl($orderMedia->getPath()) ? $this->s3Service->getTemporaryUrl($orderMedia->getPath())
: null; : null;
$order->photo_conversion_url = $orderMedia
? $this->s3Service->getTemporaryUrl($orderMedia->getPath('thumb'))
: null;
$order->orderItems->each(function (OrderItem $item) { $order->orderItems->each(function (OrderItem $item) {
if (! $item->productVariant) { if (! $item->productVariant) {
@ -89,6 +92,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$item->productVariant->photo_url = $media $item->productVariant->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath()) ? $this->s3Service->getTemporaryUrl($media->getPath())
: null; : null;
$item->productVariant->photo_conversion_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
: null;
}); });
$order->profit = $order->total_amount - $order->cogs; $order->profit = $order->total_amount - $order->cogs;

View File

@ -60,6 +60,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$product->productVariants->each(function ($variant) { $product->productVariants->each(function ($variant) {
$media = $variant->getMedia('images'); $media = $variant->getMedia('images');
$variant->photo_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->getPath()))->toArray(); $variant->photo_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->getPath()))->toArray();
$variant->photo_conversion_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->getPath('thumb')))->toArray();
}); });
}); });

View File

@ -48,10 +48,14 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
$paginator->getCollection()->each(function ($rawMaterial) { $paginator->getCollection()->each(function ($rawMaterial) {
$rawMaterial->rawMaterialPrices->each(function ($price) { $rawMaterial->rawMaterialPrices->each(function ($price) {
$media = $price->getMedia('images'); $media = $price->getFirstMedia('images');
$price->photo_url = $media->first() if ($media) {
? $this->s3Service->getTemporaryUrl($media->first()->getPath()) $price->photo_url = $this->s3Service->getTemporaryUrl($media->getPath());
: null; $price->photo_conversion_url = $this->s3Service->getTemporaryUrl($media->getPath('thumb'));
} else {
$price->photo_url = null;
$price->photo_conversion_url = null;
}
}); });
}); });

View File

@ -3,6 +3,8 @@ import { ImagePreviewModal } from '@/components/dialogs';
type ImagePreviewButtonProps = { type ImagePreviewButtonProps = {
srcs: string[]; srcs: string[];
modalSrc?: string;
modalSrcs?: string[];
title?: string; title?: string;
alt?: string; alt?: string;
className?: string; className?: string;
@ -10,12 +12,16 @@ type ImagePreviewButtonProps = {
export function ImagePreviewButton({ export function ImagePreviewButton({
srcs, srcs,
modalSrc,
modalSrcs,
title, title,
alt, alt,
className = 'h-10 w-10', className = 'h-10 w-10',
}: ImagePreviewButtonProps) { }: ImagePreviewButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const modalSources = modalSrcs ?? (modalSrc ? [modalSrc] : null);
return ( return (
<> <>
<button <button
@ -37,8 +43,8 @@ export function ImagePreviewButton({
<ImagePreviewModal <ImagePreviewModal
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
src={srcs[0]} src={modalSources?.[0] ?? srcs[0]}
sources={srcs} sources={modalSources ?? srcs}
title={title} title={title}
/> />
</> </>

View File

@ -13,6 +13,7 @@ export type CashTransaction = {
description: string; description: string;
receipt_key: string | null; receipt_key: string | null;
receipt_url: string | null; receipt_url: string | null;
receipt_conversion_url: string | null;
created_at: string; created_at: string;
formatted_created_at: string; formatted_created_at: string;
created_by: { created_by: {
@ -113,15 +114,17 @@ export function createTransactionColumns(
id: 'receipt', id: 'receipt',
header: () => <span>Bukti</span>, header: () => <span>Bukti</span>,
cell: ({ row }) => { cell: ({ row }) => {
const receiptConversionUrl = row.original.receipt_conversion_url;
const receiptUrl = row.original.receipt_url; const receiptUrl = row.original.receipt_url;
if (!receiptUrl) { if (!receiptConversionUrl) {
return <span className="text-muted-foreground">-</span>; return <span className="text-muted-foreground">-</span>;
} }
return ( return (
<ImagePreviewButton <ImagePreviewButton
srcs={[receiptUrl]} srcs={[receiptConversionUrl]}
modalSrc={receiptUrl ?? undefined}
title={row.original.description} title={row.original.description}
/> />
); );

View File

@ -10,6 +10,7 @@ export type Expense = {
description: string; description: string;
receipt_key: string | null; receipt_key: string | null;
receipt_url: string | null; receipt_url: string | null;
receipt_conversion_url: string | null;
created_at: string; created_at: string;
formatted_created_at: string; formatted_created_at: string;
created_by: { created_by: {
@ -51,15 +52,17 @@ export function createExpenseColumns(
id: 'receipt', id: 'receipt',
header: () => <span>Bukti</span>, header: () => <span>Bukti</span>,
cell: ({ row }) => { cell: ({ row }) => {
const receiptConversionUrl = row.original.receipt_conversion_url;
const receiptUrl = row.original.receipt_url; const receiptUrl = row.original.receipt_url;
if (!receiptUrl) { if (!receiptConversionUrl) {
return <span className="text-muted-foreground">-</span>; return <span className="text-muted-foreground">-</span>;
} }
return ( return (
<ImagePreviewButton <ImagePreviewButton
srcs={[receiptUrl]} srcs={[receiptConversionUrl]}
modalSrc={receiptUrl ?? undefined}
title={row.original.description} title={row.original.description}
/> />
); );

View File

@ -28,6 +28,7 @@ export type Cutting = {
total_material_cost: number | null; total_material_cost: number | null;
cost_per_unit: number | null; cost_per_unit: number | null;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
created_at: string; created_at: string;
created_by: { created_by: {
id: number; id: number;
@ -48,6 +49,7 @@ export type Cutting = {
price: number; price: number;
stock: number; stock: number;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
raw_material: { raw_material: {
id: number; id: number;
name: string; name: string;

View File

@ -131,7 +131,8 @@ export function CuttingCardRow({
{cutting.photo_url && ( {cutting.photo_url && (
<div className="mt-2"> <div className="mt-2">
<ImagePreviewButton <ImagePreviewButton
srcs={[cutting.photo_url]} srcs={[cutting.photo_conversion_url ?? cutting.photo_url]}
modalSrc={cutting.photo_url}
title="Foto Cutting" title="Foto Cutting"
className="h-16 w-16" className="h-16 w-16"
/> />

View File

@ -127,9 +127,17 @@ acc[name] = [];
<ImagePreviewButton <ImagePreviewButton
srcs={[ srcs={[
item item
.raw_material_price
.photo_conversion_url ??
item
.raw_material_price .raw_material_price
.photo_url, .photo_url,
]} ]}
modalSrc={
item
.raw_material_price
.photo_url
}
title={ title={
item item
.raw_material_price .raw_material_price
@ -222,9 +230,17 @@ acc[name] = [];
<ImagePreviewButton <ImagePreviewButton
srcs={[ srcs={[
item item
.raw_material_price
.photo_conversion_url ??
item
.raw_material_price .raw_material_price
.photo_url, .photo_url,
]} ]}
modalSrc={
item
.raw_material_price
.photo_url
}
title={ title={
item item
.raw_material_price .raw_material_price

View File

@ -17,6 +17,7 @@ export type Purchase = {
total: number; total: number;
notes: string | null; notes: string | null;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
created_at: string; created_at: string;
supplier: { supplier: {
id: number; id: number;
@ -40,6 +41,7 @@ export type Purchase = {
price: number; price: number;
stock: number; stock: number;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
raw_material: { raw_material: {
id: number; id: number;
name: string; name: string;

View File

@ -118,7 +118,8 @@ export function PurchaseCardRow({
{purchase.photo_url && ( {purchase.photo_url && (
<div className="mt-2"> <div className="mt-2">
<ImagePreviewButton <ImagePreviewButton
srcs={[purchase.photo_url]} srcs={[purchase.photo_conversion_url ?? purchase.photo_url]}
modalSrc={purchase.photo_url}
title="Foto Belanja" title="Foto Belanja"
className="h-16 w-16" className="h-16 w-16"
/> />

View File

@ -50,9 +50,9 @@ export function PurchaseItemSubRow({ purchase }: { purchase: Purchase }) {
{item.raw_material_price?.photo_url ? ( {item.raw_material_price?.photo_url ? (
<ImagePreviewButton <ImagePreviewButton
srcs={[ srcs={[
item.raw_material_price item.raw_material_price.photo_conversion_url ?? item.raw_material_price.photo_url,
.photo_url,
]} ]}
modalSrc={item.raw_material_price.photo_url}
title={ title={
item.raw_material_price.variant item.raw_material_price.variant
} }

View File

@ -10,6 +10,7 @@ export type RestockItem = {
id: number; id: number;
name: string; name: string;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
product: { product: {
id: number; id: number;
name: string; name: string;

View File

@ -101,9 +101,17 @@ acc[name] = [];
<ImagePreviewButton <ImagePreviewButton
srcs={[ srcs={[
item item
.product_variant
.photo_conversion_url ??
item
.product_variant .product_variant
.photo_url, .photo_url,
]} ]}
modalSrc={
item
.product_variant
.photo_url
}
title={ title={
item item
.product_variant .product_variant

View File

@ -19,6 +19,7 @@ export type TransactionItem = {
id: number; id: number;
name: string; name: string;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
product: { product: {
id: number; id: number;
name: string; name: string;
@ -50,6 +51,7 @@ export type Transaction = {
total_amount: number; total_amount: number;
notes: string | null; notes: string | null;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
created_at: string; created_at: string;
created_by: { created_by: {
id: number; id: number;

View File

@ -201,7 +201,8 @@ export function TransactionCardRow({
{transaction.photo_url && ( {transaction.photo_url && (
<div className="mt-2"> <div className="mt-2">
<ImagePreviewButton <ImagePreviewButton
srcs={[transaction.photo_url]} srcs={[transaction.photo_conversion_url ?? transaction.photo_url]}
modalSrc={transaction.photo_url}
title="Foto Pesanan" title="Foto Pesanan"
className="h-16 w-16" className="h-16 w-16"
/> />

View File

@ -52,8 +52,9 @@ export function TransactionItemSubRow({ transaction }: { transaction: Transactio
{item.product_variant?.photo_url ? ( {item.product_variant?.photo_url ? (
<ImagePreviewButton <ImagePreviewButton
srcs={[ srcs={[
item.product_variant.photo_url, item.product_variant.photo_conversion_url ?? item.product_variant.photo_url,
]} ]}
modalSrc={item.product_variant.photo_url}
title={item.product_variant.name} title={item.product_variant.name}
/> />
) : ( ) : (

View File

@ -15,6 +15,7 @@ export type ProductVariant = {
reject_stock: number; reject_stock: number;
retail_stock: number; retail_stock: number;
photo_urls: string[]; photo_urls: string[];
photo_conversion_urls: string[];
product_prices: { product_prices: {
id: number; id: number;
type: string; type: string;

View File

@ -90,7 +90,8 @@ export function VariantSubRow({
<TableCell> <TableCell>
{variant.photo_urls?.length > 0 ? ( {variant.photo_urls?.length > 0 ? (
<ImagePreviewButton <ImagePreviewButton
srcs={variant.photo_urls} srcs={variant.photo_conversion_urls?.length > 0 ? variant.photo_conversion_urls : variant.photo_urls}
modalSrcs={variant.photo_urls}
title={variant.name} title={variant.name}
/> />
) : ( ) : (

View File

@ -4,6 +4,7 @@ export type RawMaterialVariant = {
price: number; price: number;
stock: number; stock: number;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null;
}; };
export type RawMaterial = { export type RawMaterial = {

View File

@ -87,7 +87,8 @@ export function RawMaterialVariantSubRow({
<TableCell> <TableCell>
{variant.photo_url ? ( {variant.photo_url ? (
<ImagePreviewButton <ImagePreviewButton
srcs={[variant.photo_url]} srcs={[variant.photo_conversion_url ?? variant.photo_url]}
modalSrc={variant.photo_url}
title={variant.variant} title={variant.variant}
/> />
) : ( ) : (