diff --git a/app/Http/Requests/Admin/Manage/CuttingRequest.php b/app/Http/Requests/Admin/Manage/CuttingRequest.php index a731048..5d2dd64 100644 --- a/app/Http/Requests/Admin/Manage/CuttingRequest.php +++ b/app/Http/Requests/Admin/Manage/CuttingRequest.php @@ -28,7 +28,8 @@ public function rules(): array 'materials.*.combination_index' => ['nullable', 'integer'], 'combinations' => ['nullable', 'array'], 'combinations.*.material_result' => ['nullable', 'integer'], - 'photo_key' => ['nullable', 'string', 'max:500'], + 'photo_keys' => ['nullable', 'array', 'max:5'], + 'photo_keys.*' => ['required', 'string', 'max:500'], ]; } @@ -47,7 +48,8 @@ public function attributes(): array 'materials.*.combination_index' => 'indeks kombinasi', 'combinations' => 'kombinasi', 'combinations.*.material_result' => 'hasil kombinasi', - 'photo_key' => 'foto', + 'photo_keys' => 'foto', + 'photo_keys.*' => 'foto', ]; } } diff --git a/app/Services/Admin/Manage/CuttingService.php b/app/Services/Admin/Manage/CuttingService.php index 95a5d22..397ac34 100644 --- a/app/Services/Admin/Manage/CuttingService.php +++ b/app/Services/Admin/Manage/CuttingService.php @@ -52,13 +52,13 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = ->paginate($perPage); $paginator->getCollection()->each(function (Cutting $cutting) { - $cuttingMedia = $cutting->getFirstMedia('images'); - $cutting->photo_url = $cuttingMedia - ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) - : null; - $cutting->photo_conversion_url = $cuttingMedia - ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath('thumb')) - : null; + $cuttingMedia = $cutting->getMedia('images'); + $cutting->photo_urls = $cuttingMedia->map( + fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath()) + )->toArray(); + $cutting->photo_conversion_urls = $cuttingMedia->map( + fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath('thumb')) + )->toArray(); }); return $paginator; @@ -118,13 +118,13 @@ public function getForShow(Cutting $cutting): array 'cuttingMaterialCombinations:id,cutting_id,material_result', ]); - $cuttingMedia = $cutting->getFirstMedia('images'); - $cutting->photo_url = $cuttingMedia - ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) - : null; - $cutting->photo_conversion_url = $cuttingMedia - ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath('thumb')) - : null; + $cuttingMedia = $cutting->getMedia('images'); + $cutting->photo_urls = $cuttingMedia->map( + fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath()) + )->toArray(); + $cutting->photo_conversion_urls = $cuttingMedia->map( + fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath('thumb')) + )->toArray(); $cutting->cuttingMaterials->each(function (CuttingMaterial $material) { $media = $material->rawMaterialPrice?->getFirstMedia('images'); @@ -149,8 +149,8 @@ public function getForShow(Cutting $cutting): array 'formatted_cost_per_unit' => $cutting->formatted_cost_per_unit, 'created_at' => $cutting->created_at, 'formatted_created_at' => $cutting->formatted_created_at, - 'photo_url' => $cutting->photo_url, - 'photo_conversion_url' => $cutting->photo_conversion_url, + 'photo_urls' => $cutting->photo_urls, + 'photo_conversion_urls' => $cutting->photo_conversion_urls, 'created_by' => [ 'id' => $cutting->createdBy->id, 'user_profile' => [ @@ -228,11 +228,13 @@ public function getForEdit(Cutting $cutting): array ]; }); - $cuttingMedia = $cutting->getFirstMedia('images'); - $photoKey = $cuttingMedia?->getCustomProperty('s3_key') ?? $cuttingMedia?->file_name; - $photoUrl = $cuttingMedia - ? $this->s3Service->getTemporaryUrl($cuttingMedia->getPath()) - : null; + $cuttingMedia = $cutting->getMedia('images'); + $photoKeys = $cuttingMedia->map( + fn ($media) => $media->getCustomProperty('s3_key') ?? $media->file_name + )->toArray(); + $photoUrls = $cuttingMedia->map( + fn ($media) => $this->s3Service->getTemporaryUrl($media->getPath()) + )->toArray(); return [ 'id' => $cutting->id, @@ -244,8 +246,8 @@ public function getForEdit(Cutting $cutting): array 'cutting_result' => $result?->cutting_result ?? 0, 'materials' => $materials, 'combinations' => $combinations, - 'photo_key' => $photoKey, - 'photo_url' => $photoUrl, + 'photo_keys' => $photoKeys, + 'photo_urls' => $photoUrls, ]; } @@ -338,12 +340,11 @@ public function store(array $data): Cutting 'updated_at' => $now, ]); - if (! empty($data['photo_key'])) { - $this->registerMedia( + if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) { + $this->registerPhotos( model: $cutting, - s3Key: $data['photo_key'], + photoKeys: $data['photo_keys'], collectionName: 'images', - orderColumn: 1, ); } @@ -458,7 +459,7 @@ public function update(Cutting $cutting, array $data): Cutting 'updated_at' => $now, ]); - $this->syncPhoto($cutting, $data); + $this->syncPhotos($cutting, $data['photo_keys'] ?? [], 'images'); return $cutting; }); diff --git a/resources/js/lib/cutting-draft.ts b/resources/js/lib/cutting-draft.ts index 54aaa97..f102436 100644 --- a/resources/js/lib/cutting-draft.ts +++ b/resources/js/lib/cutting-draft.ts @@ -19,7 +19,7 @@ export type CuttingDraftData = { material_result: number | null; }>; selectedMaterialName?: string; - photo?: string; + photo_keys?: string[]; }; export const cuttingDraftStore = diff --git a/resources/js/pages/admin/manage/cutting/columns.tsx b/resources/js/pages/admin/manage/cutting/columns.tsx index 24c9a28..9f31f56 100644 --- a/resources/js/pages/admin/manage/cutting/columns.tsx +++ b/resources/js/pages/admin/manage/cutting/columns.tsx @@ -29,8 +29,8 @@ export type Cutting = { formatted_total_material_cost: string | null; cost_per_unit: number | null; formatted_cost_per_unit: string | null; - photo_url: string | null; - photo_conversion_url: string | null; + photo_urls: string[]; + photo_conversion_urls: string[]; created_at: string; formatted_created_at: string; materials_count: number; @@ -89,8 +89,8 @@ export type CuttingForEdit = { material_result: number | null; material_indices: number[]; }[]; - photo_key: string | null; - photo_url: string | null; + photo_keys: string[]; + photo_urls: string[]; }; export type CuttingCreateData = { diff --git a/resources/js/pages/admin/manage/cutting/create.tsx b/resources/js/pages/admin/manage/cutting/create.tsx index 86c69ec..be86706 100644 --- a/resources/js/pages/admin/manage/cutting/create.tsx +++ b/resources/js/pages/admin/manage/cutting/create.tsx @@ -5,7 +5,7 @@ import { ArrowLeft, Check, Layers, Plus, Search, ShoppingCart, Trash2 } from 'lu import { useCallback, useMemo, useRef, 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'; @@ -83,10 +83,15 @@ export default function CuttingCreate({ rawMaterials }: Props) { const [originalOutsideSample, setOriginalOutsideSample] = useState(draft?.originalOutsideSample ?? 0); const cuttingResult = sample + originalOutsideSample; const [notes, setNotes] = useState(draft?.notes ?? ''); - const [photo, setPhoto] = useState(draft?.photo ?? null); - const [photoUrl, setPhotoUrl] = useState( - 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 submittingRef = useRef(false); @@ -130,9 +135,9 @@ export default function CuttingCreate({ rawMaterials }: Props) { material_result: c.material_result, })), selectedMaterialName, - photo: photo ?? undefined, + photo_keys: photos.map((p) => p.key), }), - [productName, sample, originalOutsideSample, notes, materials, combinations, selectedMaterialName, photo], + [productName, sample, originalOutsideSample, notes, materials, combinations, selectedMaterialName, photos], ); useCuttingDraftSave('create', draftData, userId); @@ -306,7 +311,7 @@ export default function CuttingCreate({ rawMaterials }: Props) { combinations: combinations.map((c) => ({ material_result: c.material_result, })), - photo_key: photo, + photo_keys: photos.map((p) => p.key), }; } @@ -527,10 +532,14 @@ export default function CuttingCreate({ rawMaterials }: Props) {
- { - setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null); - }} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} /> - + +