feat: update photo handling to support multiple images in PurchaseRequest and related components
This commit is contained in:
parent
018c3adf12
commit
8c26f942bf
@ -41,7 +41,8 @@ public function rules(): array
|
||||
'discount' => ['nullable', 'integer', 'min:0'],
|
||||
'shipping_cost' => ['nullable', 'integer', 'min:0'],
|
||||
'notes' => ['nullable', 'string', 'max:100'],
|
||||
'photo_key' => ['nullable', 'string', 'max:500'],
|
||||
'photo_keys' => ['nullable', 'array', 'max:5'],
|
||||
'photo_keys.*' => ['required', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -63,7 +64,8 @@ public function attributes(): array
|
||||
'discount' => 'diskon',
|
||||
'shipping_cost' => 'ongkir',
|
||||
'notes' => 'keterangan',
|
||||
'photo_key' => 'foto',
|
||||
'photo_keys' => 'foto',
|
||||
'photo_keys.*' => 'foto',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\S3PresignedService;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PurchaseService
|
||||
@ -49,19 +50,19 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->paginate($perPage);
|
||||
|
||||
$paginator->getCollection()->each(function (Purchase $purchase) {
|
||||
$purchaseMedia = $purchase->getFirstMedia('photos');
|
||||
$purchase->photo_url = $purchaseMedia
|
||||
? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath())
|
||||
: null;
|
||||
$purchase->photo_conversion_url = $purchaseMedia
|
||||
? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath('thumb'))
|
||||
: null;
|
||||
$purchaseMedia = $purchase->getMedia('photos');
|
||||
$purchase->photo_urls = $purchaseMedia->map(
|
||||
fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath())
|
||||
)->toArray();
|
||||
$purchase->photo_conversion_urls = $purchaseMedia->map(
|
||||
fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
|
||||
)->toArray();
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function getItems(Purchase $purchase): \Illuminate\Support\Collection
|
||||
public function getItems(Purchase $purchase): Collection
|
||||
{
|
||||
return $purchase->purchaseItems()
|
||||
->select(['id', 'purchase_id', 'raw_material_price_id', 'quantity', 'unit_price', 'subtotal'])
|
||||
@ -146,11 +147,13 @@ public function getForEdit(Purchase $purchase): array
|
||||
->whereIn('raw_material_price_id', $items->pluck('raw_material_price_id'))
|
||||
->exists();
|
||||
|
||||
$purchaseMedia = $purchase->getFirstMedia('photos');
|
||||
$purchasePhotoKey = $purchaseMedia?->getCustomProperty('s3_key') ?? $purchaseMedia?->file_name;
|
||||
$purchasePhotoUrl = $purchaseMedia
|
||||
? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath())
|
||||
: null;
|
||||
$purchaseMedia = $purchase->getMedia('photos');
|
||||
$purchasePhotoKeys = $purchaseMedia->map(
|
||||
fn ($media) => $media->getCustomProperty('s3_key') ?? $media->file_name
|
||||
)->toArray();
|
||||
$purchasePhotoUrls = $purchaseMedia->map(
|
||||
fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath())
|
||||
)->toArray();
|
||||
|
||||
return [
|
||||
'id' => $purchase->id,
|
||||
@ -160,8 +163,8 @@ public function getForEdit(Purchase $purchase): array
|
||||
'discount' => $purchase->discount,
|
||||
'shipping_cost' => $purchase->shipping_cost,
|
||||
'notes' => $purchase->notes,
|
||||
'photo_key' => $purchasePhotoKey,
|
||||
'photo_url' => $purchasePhotoUrl,
|
||||
'photo_keys' => $purchasePhotoKeys,
|
||||
'photo_urls' => $purchasePhotoUrls,
|
||||
'variants' => $variants,
|
||||
'default_mode' => $singleMaterial && ! $sharedWithOther ? 'new' : 'existing',
|
||||
'existing_material_name' => $singleMaterial ? $materials->first()->name : null,
|
||||
@ -228,12 +231,11 @@ private function storeFromExisting(array $data): Purchase
|
||||
->increment('stock', (int) $item['quantity']);
|
||||
}
|
||||
|
||||
if (! empty($data['photo_key'])) {
|
||||
$this->registerMedia(
|
||||
if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) {
|
||||
$this->registerPhotos(
|
||||
model: $purchase,
|
||||
s3Key: $data['photo_key'],
|
||||
photoKeys: $data['photo_keys'],
|
||||
collectionName: 'photos',
|
||||
orderColumn: 1,
|
||||
);
|
||||
}
|
||||
|
||||
@ -318,12 +320,11 @@ private function storeNew(array $data): Purchase
|
||||
|
||||
DB::table('purchase_items')->insert($purchaseItems);
|
||||
|
||||
if (! empty($data['photo_key'])) {
|
||||
$this->registerMedia(
|
||||
if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) {
|
||||
$this->registerPhotos(
|
||||
model: $purchase,
|
||||
s3Key: $data['photo_key'],
|
||||
photoKeys: $data['photo_keys'],
|
||||
collectionName: 'photos',
|
||||
orderColumn: 1,
|
||||
);
|
||||
}
|
||||
|
||||
@ -474,7 +475,7 @@ public function update(Purchase $purchase, array $data): Purchase
|
||||
}
|
||||
DB::table('purchase_items')->insert($itemRows);
|
||||
|
||||
$this->syncPhoto($purchase, $data);
|
||||
$this->syncPhotos($purchase, $data['photo_keys'] ?? [], 'photos');
|
||||
|
||||
return $purchase;
|
||||
});
|
||||
|
||||
@ -11,7 +11,7 @@ export type PurchaseDraftData = {
|
||||
variant: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
photo?: string;
|
||||
photo_keys?: string[];
|
||||
}>;
|
||||
mode?: 'new' | 'existing';
|
||||
selectedMaterialName?: string;
|
||||
|
||||
@ -16,8 +16,8 @@ export type Purchase = {
|
||||
shipping_cost: number;
|
||||
total: number;
|
||||
notes: string | null;
|
||||
photo_url: string | null;
|
||||
photo_conversion_url: string | null;
|
||||
photo_urls: string[];
|
||||
photo_conversion_urls: string[];
|
||||
created_at: string;
|
||||
variants_count: number;
|
||||
total_qty: number;
|
||||
@ -65,8 +65,8 @@ export type PurchaseForEdit = {
|
||||
discount: number;
|
||||
shipping_cost: number;
|
||||
notes: string | null;
|
||||
photo_key: string | null;
|
||||
photo_url: string | null;
|
||||
photo_keys: string[];
|
||||
photo_urls: string[];
|
||||
variants: {
|
||||
id: number;
|
||||
variant: string;
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ConfirmDialog } from '@/components/dialogs';
|
||||
import { FileUpload } from '@/components/inputs';
|
||||
import { FileUpload, FileUploadMultiple } from '@/components/inputs';
|
||||
import { ImagePreviewModal } from '@/components/dialogs';
|
||||
import { InputError } from '@/components/ui';
|
||||
import { NumberInput } from '@/components/inputs';
|
||||
@ -114,10 +114,15 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
|
||||
const [discount, setDiscount] = useState(draft?.discount ?? 0);
|
||||
const [shippingCost, setShippingCost] = useState(draft?.shippingCost ?? 0);
|
||||
const [notes, setNotes] = useState(draft?.notes ?? '');
|
||||
const [photo, setPhoto] = useState<string | null>(draft?.photo ?? null);
|
||||
const [photoUrl, setPhotoUrl] = useState<string | null>(
|
||||
draft?.photo ? getTemporaryUrl(draft.photo) : null,
|
||||
);
|
||||
const [photos, setPhotos] = useState<{ key: string; url: string | null }[]>(() => {
|
||||
if (draft?.photo_keys && draft.photo_keys.length > 0) {
|
||||
return draft.photo_keys.map((key: string) => ({
|
||||
key,
|
||||
url: getTemporaryUrl(key),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const [mode, setMode] = useState<'new' | 'existing'>(draft?.mode ?? 'new');
|
||||
@ -158,7 +163,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
|
||||
qty,
|
||||
]),
|
||||
),
|
||||
photo: photo ?? undefined,
|
||||
photo_keys: photos.map((p) => p.key),
|
||||
}),
|
||||
[
|
||||
name,
|
||||
@ -171,7 +176,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
|
||||
mode,
|
||||
selectedMaterialName,
|
||||
quantities,
|
||||
photo,
|
||||
photos,
|
||||
],
|
||||
);
|
||||
|
||||
@ -390,7 +395,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
|
||||
discount,
|
||||
shipping_cost: shippingCost,
|
||||
notes: notes || null,
|
||||
photo_key: photo,
|
||||
photo_keys: photos.map((p) => p.key),
|
||||
};
|
||||
|
||||
if (mode === 'existing') {
|
||||
@ -1099,21 +1104,15 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Foto</Label>
|
||||
<FileUpload
|
||||
value={photo}
|
||||
onChange={(key) => {
|
||||
setPhoto(key);
|
||||
const url = key
|
||||
? `https://dstpabuaran.s3.ap-southeast-1.amazonaws.com/${key}`
|
||||
: null;
|
||||
setPhotoUrl(url);
|
||||
}}
|
||||
<FileUploadMultiple
|
||||
value={photos}
|
||||
onChange={setPhotos}
|
||||
folder="purchase"
|
||||
existingUrl={photoUrl}
|
||||
maxItems={5}
|
||||
onUploadingChange={setUploading}
|
||||
/>
|
||||
<InputError
|
||||
message={errors.photo_key}
|
||||
message={errors.photo_keys as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ConfirmDialog } from '@/components/dialogs';
|
||||
import { FileUpload } from '@/components/inputs';
|
||||
import { FileUploadMultiple } from '@/components/inputs';
|
||||
import { ImagePreviewModal } from '@/components/dialogs';
|
||||
import { InputError } from '@/components/ui';
|
||||
import { NumberInput } from '@/components/inputs';
|
||||
@ -73,8 +73,15 @@ export default function PurchaseEdit({
|
||||
const [discount, setDiscount] = useState(purchase.discount);
|
||||
const [shippingCost, setShippingCost] = useState(purchase.shipping_cost);
|
||||
const [notes, setNotes] = useState(purchase.notes ?? '');
|
||||
const [photo, setPhoto] = useState<string | null>(purchase.photo_key);
|
||||
const [photoUrl, setPhotoUrl] = useState<string | null>(purchase.photo_url);
|
||||
const [photos, setPhotos] = useState<{ key: string; url: string | null }[]>(() => {
|
||||
if (purchase.photo_keys && purchase.photo_keys.length > 0) {
|
||||
return purchase.photo_keys.map((key: string, index: number) => ({
|
||||
key,
|
||||
url: purchase.photo_urls?.[index] ?? getTemporaryUrl(key),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const [selectedMaterialName, setSelectedMaterialName] = useState(
|
||||
@ -185,7 +192,7 @@ export default function PurchaseEdit({
|
||||
discount,
|
||||
shipping_cost: shippingCost,
|
||||
notes: notes || null,
|
||||
photo_key: photo,
|
||||
photo_keys: photos.map((p) => p.key),
|
||||
existing_items: Object.entries(quantities)
|
||||
.map(([priceId, quantity]) => ({
|
||||
raw_material_price_id: Number(priceId),
|
||||
@ -538,21 +545,15 @@ export default function PurchaseEdit({
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Foto</Label>
|
||||
<FileUpload
|
||||
value={photo}
|
||||
onChange={(key) => {
|
||||
setPhoto(key);
|
||||
const url = key
|
||||
? `https://dstpabuaran.s3.ap-southeast-1.amazonaws.com/${key}`
|
||||
: null;
|
||||
setPhotoUrl(url);
|
||||
}}
|
||||
<FileUploadMultiple
|
||||
value={photos}
|
||||
onChange={setPhotos}
|
||||
folder="purchase"
|
||||
existingUrl={photoUrl}
|
||||
maxItems={5}
|
||||
onUploadingChange={setUploading}
|
||||
/>
|
||||
<InputError
|
||||
message={errors.photo_key}
|
||||
message={errors.photo_keys as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -110,11 +110,11 @@ export function PurchaseCardRow({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{purchase.photo_url && (
|
||||
{purchase.photo_urls && purchase.photo_urls.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<ImagePreviewButton
|
||||
srcs={[purchase.photo_conversion_url ?? purchase.photo_url]}
|
||||
modalSrc={purchase.photo_url}
|
||||
srcs={purchase.photo_conversion_urls?.length ? purchase.photo_conversion_urls : purchase.photo_urls}
|
||||
modalSrcs={purchase.photo_urls}
|
||||
title={purchase.supplier.name}
|
||||
description={formatDateTime(purchase.created_at)}
|
||||
className="h-16 w-16"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user