refactor: remove image handling from Cutting components; simplify CuttingPosForm, CuttingPosSummaryPanel, and related files to enhance user experience and streamline product creation process

This commit is contained in:
Yoga Pangestu 2026-07-05 15:42:28 +07:00
parent e4c36c8759
commit 58e676e2e1
6 changed files with 120 additions and 46 deletions

View File

@ -23,7 +23,6 @@ const props = defineProps<{
const initialData = computed(() => ({
description: props.cutting.description ?? '',
images: props.cutting.images ?? [],
sewing_cost: String(props.cutting.sewing_cost ?? 0),
other_cost: String(props.cutting.other_cost ?? 0),
materials: props.cutting.materials.map((item) => ({

View File

@ -143,15 +143,7 @@ const groupedResults = computed<GroupedResults[]>(() => {
<p v-if="cutting.description" class="mt-3 text-sm">
{{ cutting.description }}
</p>
<div
v-if="cutting.images && cutting.images.length > 0"
class="mt-3 flex flex-wrap gap-2"
>
<MediaThumbnailCell
:items="cutting.images"
:max-visible="10"
/>
</div>
</div>
<!-- Results Section -->

View File

@ -9,12 +9,7 @@ import type {
CuttingRawMaterialCatalogItem,
CuttingResultCartItem,
} from '@/types/cutting';
import {
appendRootPhotosToFormData,
createMediaUploadState,
} from '@/types/media';
import type { MediaItem } from '@/types/media';
import type { MediaUploadState } from '@/types/media';
import type { CategoryOption } from '@/types/product';
import type { EnumOption } from '@/types/raw-material';
import CuttingPosCartDetailDialog from './CuttingPosCartDetailDialog.vue';
@ -46,8 +41,7 @@ const props = defineProps<{
const isCreateMode = computed(() => props.method === 'post');
const cartDetailOpen = ref(false);
const imageState = ref<MediaUploadState>(createMediaUploadState());
const isUploading = computed(() => imageState.value.pendingUploads > 0);
// No image upload state needed
// Make catalogs mutable so we can add new items from quick-create dialogs
const rawMaterialCatalogState = ref<CuttingRawMaterialCatalogItem[]>([
@ -109,15 +103,12 @@ const {
function populateForm() {
if (!props.initialData) {
imageState.value = createMediaUploadState();
return;
}
form.description = props.initialData.description;
form.sewing_cost = props.initialData.sewing_cost ?? '0';
form.other_cost = props.initialData.other_cost ?? '0';
imageState.value = createMediaUploadState(props.initialData.images ?? []);
setCarts(props.initialData.materials, props.initialData.results);
}
@ -141,7 +132,7 @@ function buildFormData(): FormData {
String(Number.parseInt(parseRupiah(form.other_cost), 10) || 0),
);
appendRootPhotosToFormData(formData, imageState.value);
// No images to append
if (props.method === 'put') {
// Build combination results map
@ -286,9 +277,9 @@ function onProductCreated(product: CuttingProductCatalogItem) {
:categories="categories" :material-cart="materialCart" @decrease-result-qty="decreaseResultQty" @product-created="onProductCreated" />
</div>
<CuttingPosSummaryPanel v-model:image-state="imageState" :form="form" :material-cart="materialCart"
<CuttingPosSummaryPanel :form="form" :material-cart="materialCart"
:result-cart="resultCart" :total-result-pieces="totalResultPieces" :submit-label="submitLabel"
@submit="submit" :is-create-mode="isCreateMode" :is-uploading="isUploading"
@submit="submit" :is-create-mode="isCreateMode"
@open-detail="cartDetailOpen = true" @remove-material="removeMaterial"
@sync-material-field="syncMaterialField" @remove-result="removeResult"
@sync-result-totals="syncResultTotals" @sync-result-field="syncResultField"

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
import { Save, Scissors } from '@lucide/vue';
import MediaDropzone from '@/components/media/MediaDropzone.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -20,7 +19,6 @@ import type {
CuttingMaterialCartItem,
CuttingResultCartItem,
} from '@/types/cutting';
import type { MediaUploadState } from '@/types/media';
import CuttingPosMaterialSummaryItems from './CuttingPosMaterialSummaryItems.vue';
import CuttingPosResultSummaryItems from './CuttingPosResultSummaryItems.vue';
@ -31,7 +29,6 @@ defineProps<{
totalResultPieces: number;
submitLabel: string;
isCreateMode: boolean;
isUploading?: boolean;
}>();
const emit = defineEmits<{
@ -45,10 +42,6 @@ const emit = defineEmits<{
'remove-combination': [combinationId: number];
'sync-combination-result': [combinationId: number, result: number | null];
}>();
const imageState = defineModel<MediaUploadState>('imageState', {
required: true,
});
</script>
<template>
@ -83,9 +76,6 @@ const imageState = defineModel<MediaUploadState>('imageState', {
<FieldError :errors="formErrors(form, 'description')" />
</Field>
<MediaDropzone id="cutting-images" v-model="imageState" label="Foto Cutting" :max-files="10"
:errors="formErrors(form, 'images')" />
<CuttingPosMaterialSummaryItems :form="form" :material-cart="materialCart"
@remove="emit('remove-material', $event)" @sync-field="emit('sync-material-field', $event)"
@remove-combination="emit('remove-combination', $event)"
@ -99,18 +89,11 @@ const imageState = defineModel<MediaUploadState>('imageState', {
@sync-field="emit('sync-result-field', $event)" />
<Button type="submit" class="w-full" :disabled="form.processing ||
isUploading ||
materialCart.length === 0 ||
resultCart.length === 0
">
<Save class="size-4" />
{{
isUploading
? 'Mengunggah...'
: form.processing
? 'Menyimpan...'
: submitLabel
}}
{{ form.processing ? 'Menyimpan...' : submitLabel }}
</Button>
</FieldSet>
</FieldGroup>

View File

@ -182,11 +182,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
<p v-if="cutting.description" class="text-sm text-muted-foreground">
{{ cutting.description }}
</p>
<div v-if="
cutting.images && cutting.images.length > 0
" class="flex flex-wrap gap-2 pt-1">
<MediaThumbnailCell :items="cutting.images" :max-visible="10" />
</div>
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<span>Total Hasil Cutting
<strong class="text-primary">{{

View File

@ -687,6 +687,119 @@ function setupDraftItems(User $user): array
});
});
// ─── Quick Create Product ─────────────────────────────────
describe('Quick Create Product', function () {
test('authenticated user with permission can quick create product without prices', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
'name' => 'Test Product',
'description' => 'Test Description',
'variants' => [
[
'name' => 'Varian 1',
'stock' => 10,
'retail_stock' => 5,
],
],
])
->assertOk()
->assertJsonStructure(['product' => ['id', 'name']]);
$this->assertDatabaseHas('products', ['name' => 'Test Product']);
$this->assertDatabaseHas('product_variants', ['name' => 'Varian 1', 'stock' => 10]);
});
test('quick create product without images is allowed', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
'name' => 'Product No Image',
'variants' => [
[
'name' => 'Varian 1',
'stock' => 5,
'retail_stock' => 2,
],
],
])
->assertOk();
$this->assertDatabaseHas('products', ['name' => 'Product No Image']);
});
test('quick create product requires name', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
'variants' => [
[
'name' => 'Varian 1',
'stock' => 5,
'retail_stock' => 2,
],
],
])
->assertUnprocessable();
});
test('quick create product requires at least one variant', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
'name' => 'Test Product',
'variants' => [],
])
->assertUnprocessable();
});
test('quick create product with categories', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
$category = \App\Models\Category::factory()->create();
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
'name' => 'Product With Category',
'category_ids' => [$category->id],
'variants' => [
[
'name' => 'Varian 1',
'stock' => 5,
'retail_stock' => 2,
],
],
])
->assertOk();
$product = \App\Models\Product::where('name', 'Product With Category')->first();
expect($product)->not->toBeNull();
expect($product->categories)->toHaveCount(1);
});
test('user without permission cannot quick create product', function () {
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW);
$this->actingAs($user)
->postJson('/admin/manage/cuttings/quick-create-product', [
'name' => 'Test Product',
'variants' => [
[
'name' => 'Varian 1',
'stock' => 5,
'retail_stock' => 2,
],
],
])
->assertForbidden();
});
});
// ─── Store with Combination ─────────────────────────────────
describe('Cutting Store with Combination', function () {