feat: enhance Cutting module by adding image upload functionality and improving form handling; update CuttingPosForm and related components to support image management and improve user experience
This commit is contained in:
parent
c3db522c87
commit
33afec2513
@ -23,6 +23,7 @@ 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) => ({
|
||||
@ -52,7 +53,9 @@ const initialData = computed(() => ({
|
||||
<Head title="Ubah Cutting" />
|
||||
|
||||
<AdminLayout>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div
|
||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Ubah Cutting</h2>
|
||||
</div>
|
||||
@ -60,9 +63,15 @@ const initialData = computed(() => ({
|
||||
<BackButton :href="index.url()" />
|
||||
</div>
|
||||
|
||||
<CuttingPosForm :raw-material-catalog="rawMaterialCatalog" :product-catalog="productCatalog"
|
||||
:categories="categories" :units="units"
|
||||
:initial-data="initialData" :submit-url="update.url(props.cutting.id)" method="put"
|
||||
submit-label="Perbarui" />
|
||||
<CuttingPosForm
|
||||
:raw-material-catalog="rawMaterialCatalog"
|
||||
:product-catalog="productCatalog"
|
||||
:categories="categories"
|
||||
:units="units"
|
||||
:initial-data="initialData"
|
||||
:submit-url="update.url(props.cutting.id)"
|
||||
method="put"
|
||||
submit-label="Perbarui"
|
||||
/>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@ -38,7 +38,12 @@ const groupedMaterials = computed<GroupedMaterials[]>(() => {
|
||||
const name = item.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||
|
||||
if (!groups[key]) {
|
||||
groups[key] = { key, name, unitLabel: item.raw_material_unit_label, items: [] };
|
||||
groups[key] = {
|
||||
key,
|
||||
name,
|
||||
unitLabel: item.raw_material_unit_label,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
groups[key].items.push(item);
|
||||
@ -71,8 +76,12 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
||||
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8 text-center">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Detail Cutting</h1>
|
||||
<p class="mt-1 text-muted-foreground">Rincian data proses cutting</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Detail Cutting
|
||||
</h1>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
Rincian data proses cutting
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Card -->
|
||||
@ -80,18 +89,37 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
||||
<!-- Cutting Info Header -->
|
||||
<div class="border-b bg-muted/30 p-6">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-xl font-semibold">Cutting #{{ cutting.id }}</h2>
|
||||
<Badge :variant="cuttingStatusBadgeVariant(cutting.status)">
|
||||
<h2 class="text-xl font-semibold">
|
||||
Cutting #{{ cutting.id }}
|
||||
</h2>
|
||||
<Badge
|
||||
:variant="cuttingStatusBadgeVariant(cutting.status)"
|
||||
>
|
||||
{{ cutting.status_label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="mt-3 space-y-1 text-sm text-muted-foreground">
|
||||
<p>{{ cutting.created_at_formatted }}</p>
|
||||
<p>Oleh {{ cutting.created_by?.profile?.full_name ?? cutting.created_by?.username }}</p>
|
||||
<p>
|
||||
Oleh
|
||||
{{
|
||||
cutting.created_by?.profile?.full_name ??
|
||||
cutting.created_by?.username
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Summary -->
|
||||
@ -99,15 +127,26 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
||||
<h3 class="mb-3 text-sm font-semibold">Ringkasan</h3>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-lg border p-3">
|
||||
<p class="text-xs text-muted-foreground">Total Hasil Cutting</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Total Hasil Cutting
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-semibold text-primary">
|
||||
{{ cutting.total_result_pieces ?? 0 }} pcs
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="cutting.total_material_usage_summary_formatted" class="rounded-lg border p-3">
|
||||
<p class="text-xs text-muted-foreground">Total Pemakaian Bahan</p>
|
||||
<div
|
||||
v-if="
|
||||
cutting.total_material_usage_summary_formatted
|
||||
"
|
||||
class="rounded-lg border p-3"
|
||||
>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Total Pemakaian Bahan
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-semibold text-primary">
|
||||
{{ cutting.total_material_usage_summary_formatted }}
|
||||
{{
|
||||
cutting.total_material_usage_summary_formatted
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,28 +161,52 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
||||
<TableRow>
|
||||
<TableHead>Varian</TableHead>
|
||||
<TableHead>Foto</TableHead>
|
||||
<TableHead class="text-right">Pemakaian</TableHead>
|
||||
<TableHead class="text-right"
|
||||
>Pemakaian</TableHead
|
||||
>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-for="group in groupedMaterials" :key="group.key">
|
||||
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||
<TableCell colspan="3" class="font-semibold">
|
||||
<template
|
||||
v-for="group in groupedMaterials"
|
||||
:key="group.key"
|
||||
>
|
||||
<TableRow
|
||||
class="bg-muted/20 hover:bg-muted/20"
|
||||
>
|
||||
<TableCell
|
||||
colspan="3"
|
||||
class="font-semibold"
|
||||
>
|
||||
{{ group.name }}
|
||||
<Badge v-if="group.unitLabel" variant="secondary" class="ml-2 font-normal">
|
||||
<Badge
|
||||
v-if="group.unitLabel"
|
||||
variant="secondary"
|
||||
class="ml-2 font-normal"
|
||||
>
|
||||
{{ group.unitLabel }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="material in group.items" :key="material.id">
|
||||
<TableRow
|
||||
v-for="material in group.items"
|
||||
:key="material.id"
|
||||
>
|
||||
<TableCell class="pl-6">
|
||||
{{ material.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" />
|
||||
<MediaThumbnailCell
|
||||
:items="material.images ?? []"
|
||||
:max-visible="1"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ material.material_usage_formatted }}
|
||||
<TableCell
|
||||
class="text-right tabular-nums"
|
||||
>
|
||||
{{
|
||||
material.material_usage_formatted
|
||||
}}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
@ -161,33 +224,63 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
||||
<TableRow>
|
||||
<TableHead>Varian</TableHead>
|
||||
<TableHead>Foto</TableHead>
|
||||
<TableHead class="text-right">Hasil</TableHead>
|
||||
<TableHead class="text-right">Sample</TableHead>
|
||||
<TableHead class="text-right">Diluar Sample</TableHead>
|
||||
<TableHead class="text-right"
|
||||
>Hasil</TableHead
|
||||
>
|
||||
<TableHead class="text-right"
|
||||
>Sample</TableHead
|
||||
>
|
||||
<TableHead class="text-right"
|
||||
>Diluar Sample</TableHead
|
||||
>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-for="group in groupedResults" :key="group.key">
|
||||
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||
<TableCell colspan="5" class="font-semibold">
|
||||
<template
|
||||
v-for="group in groupedResults"
|
||||
:key="group.key"
|
||||
>
|
||||
<TableRow
|
||||
class="bg-muted/20 hover:bg-muted/20"
|
||||
>
|
||||
<TableCell
|
||||
colspan="5"
|
||||
class="font-semibold"
|
||||
>
|
||||
{{ group.name }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="result in group.items" :key="result.id">
|
||||
<TableRow
|
||||
v-for="result in group.items"
|
||||
:key="result.id"
|
||||
>
|
||||
<TableCell class="pl-6">
|
||||
{{ result.product_variant?.name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" />
|
||||
<MediaThumbnailCell
|
||||
:items="
|
||||
result.product_variant
|
||||
?.images ?? []
|
||||
"
|
||||
:max-visible="1"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
<TableCell
|
||||
class="text-right tabular-nums"
|
||||
>
|
||||
{{ result.cutting_result }} pcs
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
<TableCell
|
||||
class="text-right tabular-nums"
|
||||
>
|
||||
{{ result.sample }} pcs
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ result.original_outside_sample }} pcs
|
||||
<TableCell
|
||||
class="text-right tabular-nums"
|
||||
>
|
||||
{{ result.original_outside_sample }}
|
||||
pcs
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
|
||||
@ -11,6 +11,12 @@ import type {
|
||||
} from '@/types/cutting';
|
||||
import type { CategoryOption } from '@/types/product';
|
||||
import type { EnumOption } from '@/types/raw-material';
|
||||
import type { MediaItem } from '@/types/media';
|
||||
import {
|
||||
appendRootPhotosToFormData,
|
||||
createMediaUploadState,
|
||||
} from '@/types/media';
|
||||
import type { MediaUploadState } from '@/types/media';
|
||||
import CuttingPosCartDetailDialog from './CuttingPosCartDetailDialog.vue';
|
||||
import CuttingPosMaterialCatalogPanel from './CuttingPosMaterialCatalogPanel.vue';
|
||||
import CuttingPosResultCatalogPanel from './CuttingPosResultCatalogPanel.vue';
|
||||
@ -24,6 +30,7 @@ const props = defineProps<{
|
||||
units: EnumOption[];
|
||||
initialData?: {
|
||||
description: string;
|
||||
images?: MediaItem[];
|
||||
sewing_cost?: string;
|
||||
other_cost?: string;
|
||||
materials: CuttingMaterialCartItem[];
|
||||
@ -39,12 +46,29 @@ const props = defineProps<{
|
||||
const isCreateMode = computed(() => props.method === 'post');
|
||||
const cartDetailOpen = ref(false);
|
||||
|
||||
// Make catalogs mutable so we can add new items from quick-create dialogs
|
||||
const rawMaterialCatalogState = ref<CuttingRawMaterialCatalogItem[]>([...props.rawMaterialCatalog]);
|
||||
const productCatalogState = ref<CuttingProductCatalogItem[]>([...props.productCatalog]);
|
||||
const imageState = ref<MediaUploadState>(createMediaUploadState());
|
||||
const isUploading = computed(() => imageState.value.pendingUploads > 0);
|
||||
|
||||
watch(() => props.rawMaterialCatalog, (val) => { rawMaterialCatalogState.value = [...val]; });
|
||||
watch(() => props.productCatalog, (val) => { productCatalogState.value = [...val]; });
|
||||
// Make catalogs mutable so we can add new items from quick-create dialogs
|
||||
const rawMaterialCatalogState = ref<CuttingRawMaterialCatalogItem[]>([
|
||||
...props.rawMaterialCatalog,
|
||||
]);
|
||||
const productCatalogState = ref<CuttingProductCatalogItem[]>([
|
||||
...props.productCatalog,
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.rawMaterialCatalog,
|
||||
(val) => {
|
||||
rawMaterialCatalogState.value = [...val];
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.productCatalog,
|
||||
(val) => {
|
||||
productCatalogState.value = [...val];
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
description: '',
|
||||
@ -83,39 +107,68 @@ 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);
|
||||
}
|
||||
|
||||
watch(() => props.initialData, populateForm, { immediate: true });
|
||||
loadDraftItems(props.draftMaterials ?? [], props.draftResults ?? []);
|
||||
|
||||
function buildPayload() {
|
||||
const payload: Record<string, unknown> = {
|
||||
description: form.description,
|
||||
sewing_cost: Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
|
||||
other_cost: Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
|
||||
};
|
||||
function buildFormData(): FormData {
|
||||
const formData = new FormData();
|
||||
|
||||
if (!isCreateMode.value) {
|
||||
payload.materials = materialCart.value.map((item) => ({
|
||||
raw_material_price_id: item.raw_material_price_id,
|
||||
material_usage: item.material_usage,
|
||||
}));
|
||||
payload.results = resultCart.value.map((item) => ({
|
||||
product_variant_id: item.product_variant_id,
|
||||
cutting_result: item.cutting_result,
|
||||
sample: item.sample,
|
||||
original_outside_sample: item.original_outside_sample,
|
||||
}));
|
||||
if (props.method === 'put') {
|
||||
formData.append('_method', 'PUT');
|
||||
}
|
||||
|
||||
return payload;
|
||||
formData.append('description', form.description);
|
||||
formData.append(
|
||||
'sewing_cost',
|
||||
String(Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0),
|
||||
);
|
||||
formData.append(
|
||||
'other_cost',
|
||||
String(Number.parseInt(parseRupiah(form.other_cost), 10) || 0),
|
||||
);
|
||||
|
||||
appendRootPhotosToFormData(formData, imageState.value);
|
||||
|
||||
if (props.method === 'put') {
|
||||
materialCart.value.forEach((item, index) => {
|
||||
formData.append(
|
||||
`materials[${index}][raw_material_price_id]`,
|
||||
String(item.raw_material_price_id),
|
||||
);
|
||||
formData.append(
|
||||
`materials[${index}][material_usage]`,
|
||||
item.material_usage,
|
||||
);
|
||||
});
|
||||
resultCart.value.forEach((item, index) => {
|
||||
formData.append(
|
||||
`results[${index}][product_variant_id]`,
|
||||
String(item.product_variant_id),
|
||||
);
|
||||
formData.append(
|
||||
`results[${index}][cutting_result]`,
|
||||
String(item.cutting_result),
|
||||
);
|
||||
formData.append(`results[${index}][sample]`, String(item.sample));
|
||||
formData.append(
|
||||
`results[${index}][original_outside_sample]`,
|
||||
String(item.original_outside_sample),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
@ -131,21 +184,10 @@ function submit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildPayload();
|
||||
|
||||
if (props.method === 'put') {
|
||||
form.transform(() => payload).put(props.submitUrl, {
|
||||
onError: (errors: Record<string, string>) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
const payload = buildFormData();
|
||||
|
||||
form.transform(() => payload).post(props.submitUrl, {
|
||||
forceFormData: true,
|
||||
onError: (errors: Record<string, string>) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
@ -182,29 +224,53 @@ function onProductCreated(product: CuttingProductCatalogItem) {
|
||||
<template>
|
||||
<div class="grid gap-4 xl:grid-cols-[1fr_400px]">
|
||||
<div class="space-y-4">
|
||||
<CuttingPosMaterialCatalogPanel v-model:material-search="materialSearch"
|
||||
:filtered-raw-materials="filteredRawMaterials" :get-material-cart-item="getMaterialCartItem"
|
||||
<CuttingPosMaterialCatalogPanel
|
||||
v-model:material-search="materialSearch"
|
||||
:filtered-raw-materials="filteredRawMaterials"
|
||||
:get-material-cart-item="getMaterialCartItem"
|
||||
:units="units"
|
||||
@add-material="addMaterial" @decrease-material-qty="decreaseMaterialQty"
|
||||
@raw-material-created="onRawMaterialCreated" />
|
||||
@add-material="addMaterial"
|
||||
@decrease-material-qty="decreaseMaterialQty"
|
||||
@raw-material-created="onRawMaterialCreated"
|
||||
/>
|
||||
|
||||
<CuttingPosResultCatalogPanel v-model:product-search="productSearch" :filtered-products="filteredProducts"
|
||||
:get-result-cart-item="getResultCartItem" @add-result="addResult"
|
||||
<CuttingPosResultCatalogPanel
|
||||
v-model:product-search="productSearch"
|
||||
:filtered-products="filteredProducts"
|
||||
:get-result-cart-item="getResultCartItem"
|
||||
@add-result="addResult"
|
||||
:is-create-mode="isCreateMode"
|
||||
:categories="categories"
|
||||
@decrease-result-qty="decreaseResultQty"
|
||||
@product-created="onProductCreated" />
|
||||
@product-created="onProductCreated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CuttingPosSummaryPanel :form="form" :material-cart="materialCart" :result-cart="resultCart"
|
||||
:total-result-pieces="totalResultPieces" :submit-label="submitLabel" @submit="submit"
|
||||
<CuttingPosSummaryPanel
|
||||
v-model:image-state="imageState"
|
||||
:form="form"
|
||||
:material-cart="materialCart"
|
||||
:result-cart="resultCart"
|
||||
:total-result-pieces="totalResultPieces"
|
||||
:submit-label="submitLabel"
|
||||
@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" />
|
||||
:is-uploading="isUploading"
|
||||
@open-detail="cartDetailOpen = true"
|
||||
@remove-material="removeMaterial"
|
||||
@sync-material-field="syncMaterialField"
|
||||
@remove-result="removeResult"
|
||||
@sync-result-totals="syncResultTotals"
|
||||
@sync-result-field="syncResultField"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CuttingPosCartDetailDialog v-model:open="cartDetailOpen" :material-cart="materialCart" :result-cart="resultCart"
|
||||
:material-line-cost="materialLineCost" :total-material-cost="totalMaterialCost"
|
||||
:is-create-mode="isCreateMode" />
|
||||
<CuttingPosCartDetailDialog
|
||||
v-model:open="cartDetailOpen"
|
||||
:material-cart="materialCart"
|
||||
:result-cart="resultCart"
|
||||
:material-line-cost="materialLineCost"
|
||||
:total-material-cost="totalMaterialCost"
|
||||
:is-create-mode="isCreateMode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -14,9 +14,14 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||
import { formErrors, type FormWithErrors } from '@/lib/form';
|
||||
import type { CuttingMaterialCartItem, CuttingResultCartItem } from '@/types/cutting';
|
||||
import type {
|
||||
CuttingMaterialCartItem,
|
||||
CuttingResultCartItem,
|
||||
} from '@/types/cutting';
|
||||
import CuttingPosMaterialSummaryItems from './CuttingPosMaterialSummaryItems.vue';
|
||||
import CuttingPosResultSummaryItems from './CuttingPosResultSummaryItems.vue';
|
||||
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||
import type { MediaUploadState } from '@/types/media';
|
||||
|
||||
defineProps<{
|
||||
form: FormWithErrors & { description: string; processing?: boolean };
|
||||
@ -25,6 +30,7 @@ defineProps<{
|
||||
totalResultPieces: number;
|
||||
submitLabel: string;
|
||||
isCreateMode: boolean;
|
||||
isUploading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -36,12 +42,18 @@ const emit = defineEmits<{
|
||||
'sync-result-totals': [item: CuttingResultCartItem];
|
||||
'sync-result-field': [index: number];
|
||||
}>();
|
||||
|
||||
const imageState = defineModel<MediaUploadState>('imageState', {
|
||||
required: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="h-fit xl:sticky xl:top-4">
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="flex items-center justify-between gap-2 text-base">
|
||||
<CardTitle
|
||||
class="flex items-center justify-between gap-2 text-base"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Scissors class="size-4" />
|
||||
Ringkasan Cutting
|
||||
@ -55,7 +67,11 @@ const emit = defineEmits<{
|
||||
>
|
||||
Lihat Detail
|
||||
</button>
|
||||
<Badge v-if="!isCreateMode && totalResultPieces > 0" variant="secondary" class="tabular-nums font-semibold">
|
||||
<Badge
|
||||
v-if="!isCreateMode && totalResultPieces > 0"
|
||||
variant="secondary"
|
||||
class="font-semibold tabular-nums"
|
||||
>
|
||||
Total: {{ totalResultPieces }} pcs
|
||||
</Badge>
|
||||
</span>
|
||||
@ -66,7 +82,9 @@ const emit = defineEmits<{
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="description">Keterangan</FieldLabel>
|
||||
<FieldLabel for="description"
|
||||
>Keterangan</FieldLabel
|
||||
>
|
||||
<Textarea
|
||||
id="description"
|
||||
v-model="form.description"
|
||||
@ -74,9 +92,19 @@ const emit = defineEmits<{
|
||||
rows="2"
|
||||
:maxlength="FIELD_LIMITS.description"
|
||||
/>
|
||||
<FieldError :errors="formErrors(form, 'description')" />
|
||||
<FieldError
|
||||
:errors="formErrors(form, 'description')"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<MediaDropzone
|
||||
id="cutting-images"
|
||||
v-model="imageState"
|
||||
label="Foto Cutting"
|
||||
:max-files="10"
|
||||
:errors="formErrors(form, 'images')"
|
||||
/>
|
||||
|
||||
<CuttingPosMaterialSummaryItems
|
||||
:material-cart="materialCart"
|
||||
@remove="emit('remove-material', $event)"
|
||||
@ -97,10 +125,21 @@ const emit = defineEmits<{
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
:disabled="form.processing || materialCart.length === 0 || resultCart.length === 0"
|
||||
:disabled="
|
||||
form.processing ||
|
||||
isUploading ||
|
||||
materialCart.length === 0 ||
|
||||
resultCart.length === 0
|
||||
"
|
||||
>
|
||||
<Save class="size-4" />
|
||||
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
||||
{{
|
||||
isUploading
|
||||
? 'Mengunggah...'
|
||||
: form.processing
|
||||
? 'Menyimpan...'
|
||||
: submitLabel
|
||||
}}
|
||||
</Button>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
|
||||
@ -37,7 +37,11 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const showingCount = computed(() => props.cuttings.length);
|
||||
const paginationSummary = usePaginationSummary(() => props.pagination, showingCount, 'cutting');
|
||||
const paginationSummary = usePaginationSummary(
|
||||
() => props.pagination,
|
||||
showingCount,
|
||||
'cutting',
|
||||
);
|
||||
|
||||
function rowNumber(index: number): number {
|
||||
return groupedTableRowNumber(props.firstItem, index);
|
||||
@ -61,7 +65,8 @@ function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
|
||||
|
||||
materials.forEach((item) => {
|
||||
const rawMaterialId = item.raw_material_id ?? 0;
|
||||
const rawMaterialName = item.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||
const rawMaterialName =
|
||||
item.raw_material_name || 'Bahan Baku Tidak Diketahui';
|
||||
const unitLabel = item.raw_material_unit_label;
|
||||
|
||||
if (!groups[rawMaterialId]) {
|
||||
@ -100,59 +105,126 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
|
||||
return Object.values(groups);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<DataTableToolbar v-model:search="search" @filters-reset="emit('filters-reset')" />
|
||||
<DataTableToolbar
|
||||
v-model:search="search"
|
||||
@filters-reset="emit('filters-reset')"
|
||||
/>
|
||||
|
||||
<div v-if="cuttings.length" class="space-y-4">
|
||||
<div v-for="(cutting, index) in cuttings" :key="cutting.id" class="overflow-hidden rounded-md border">
|
||||
<div
|
||||
v-for="(cutting, index) in cuttings"
|
||||
:key="cutting.id"
|
||||
class="overflow-hidden rounded-md border"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between"
|
||||
>
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<span class="text-muted-foreground w-8 shrink-0 pt-0.5 text-center text-sm tabular-nums">
|
||||
<span
|
||||
class="w-8 shrink-0 pt-0.5 text-center text-sm text-muted-foreground tabular-nums"
|
||||
>
|
||||
{{ rowNumber(index) }}
|
||||
</span>
|
||||
<div class="min-w-0 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="font-medium leading-tight">
|
||||
<h3 class="leading-tight font-medium">
|
||||
Cutting #{{ cutting.id }}
|
||||
</h3>
|
||||
<Badge :variant="cuttingStatusBadgeVariant(cutting.status)">
|
||||
<Badge
|
||||
:variant="
|
||||
cuttingStatusBadgeVariant(
|
||||
cutting.status,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ cutting.status_label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-sm">
|
||||
<div
|
||||
class="space-y-1 text-sm text-muted-foreground"
|
||||
>
|
||||
<p>{{ cutting.created_at_formatted }}</p>
|
||||
<p>Oleh {{ cutting.created_by?.profile?.full_name ?? cutting.created_by?.username }}</p>
|
||||
<p>
|
||||
Oleh
|
||||
{{
|
||||
cutting.created_by?.profile
|
||||
?.full_name ??
|
||||
cutting.created_by?.username
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="cutting.description" class="text-muted-foreground text-sm">
|
||||
<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">{{ cutting.total_result_pieces ??
|
||||
0 }} pcs</strong></span>
|
||||
<span v-if="cutting.total_material_usage_summary_formatted">Total Pemakaian Bahan
|
||||
<strong class="text-primary">{{ cutting.total_material_usage_summary_formatted
|
||||
}}</strong></span>
|
||||
<span>Biaya Bahan <strong class="text-primary">{{
|
||||
cutting.total_material_cost_formatted ?? 'Rp 0' }}</strong></span>
|
||||
<span>Biaya per Produk <strong class="text-primary">{{
|
||||
cutting.material_cost_per_product_formatted ?? 'Rp 0' }}</strong></span>
|
||||
<span
|
||||
>Total Hasil Cutting
|
||||
<strong class="text-primary"
|
||||
>{{
|
||||
cutting.total_result_pieces ?? 0
|
||||
}}
|
||||
pcs</strong
|
||||
></span
|
||||
>
|
||||
<span
|
||||
v-if="
|
||||
cutting.total_material_usage_summary_formatted
|
||||
"
|
||||
>Total Pemakaian Bahan
|
||||
<strong class="text-primary">{{
|
||||
cutting.total_material_usage_summary_formatted
|
||||
}}</strong></span
|
||||
>
|
||||
<span
|
||||
>Biaya Bahan
|
||||
<strong class="text-primary">{{
|
||||
cutting.total_material_cost_formatted ??
|
||||
'Rp 0'
|
||||
}}</strong></span
|
||||
>
|
||||
<span
|
||||
>Biaya per Produk
|
||||
<strong class="text-primary">{{
|
||||
cutting.material_cost_per_product_formatted ??
|
||||
'Rp 0'
|
||||
}}</strong></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
|
||||
<div
|
||||
class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5"
|
||||
>
|
||||
<DataTableActions :cutting="cutting" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 p-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-semibold tracking-tight text-foreground">Bahan Baku</h4>
|
||||
<h4
|
||||
class="text-sm font-semibold tracking-tight text-foreground"
|
||||
>
|
||||
Bahan Baku
|
||||
</h4>
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@ -163,31 +235,60 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
|
||||
<TableCell colspan="3" class="text-muted-foreground">
|
||||
<TableRow
|
||||
v-if="!cutting.materials.length"
|
||||
:key="`${cutting.id}-material-empty`"
|
||||
>
|
||||
<TableCell
|
||||
colspan="3"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
Belum ada bahan baku
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<template v-else v-for="group in getGroupedMaterials(cutting.materials)"
|
||||
:key="group.rawMaterialId">
|
||||
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||
<TableCell colspan="3" class="font-semibold text-foreground">
|
||||
<template
|
||||
v-else
|
||||
v-for="group in getGroupedMaterials(
|
||||
cutting.materials,
|
||||
)"
|
||||
:key="group.rawMaterialId"
|
||||
>
|
||||
<TableRow
|
||||
class="bg-muted/20 hover:bg-muted/20"
|
||||
>
|
||||
<TableCell
|
||||
colspan="3"
|
||||
class="font-semibold text-foreground"
|
||||
>
|
||||
{{ group.rawMaterialName }}
|
||||
<Badge v-if="group.unitLabel" variant="secondary"
|
||||
class="ml-2 font-normal">
|
||||
<Badge
|
||||
v-if="group.unitLabel"
|
||||
variant="secondary"
|
||||
class="ml-2 font-normal"
|
||||
>
|
||||
{{ group.unitLabel }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="material in group.items" :key="material.id">
|
||||
<TableRow
|
||||
v-for="material in group.items"
|
||||
:key="material.id"
|
||||
>
|
||||
<TableCell class="pl-6 font-medium">
|
||||
{{ material.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="material.images ?? []" :max-visible="1" />
|
||||
<MediaThumbnailCell
|
||||
:items="
|
||||
material.images ?? []
|
||||
"
|
||||
:max-visible="1"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ material.material_usage_formatted }}
|
||||
{{
|
||||
material.material_usage_formatted
|
||||
}}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
@ -197,7 +298,11 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h4 class="text-sm font-semibold tracking-tight text-foreground">Hasil Produk</h4>
|
||||
<h4
|
||||
class="text-sm font-semibold tracking-tight text-foreground"
|
||||
>
|
||||
Hasil Produk
|
||||
</h4>
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@ -210,24 +315,51 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
|
||||
<TableCell colspan="5" class="text-muted-foreground">
|
||||
<TableRow
|
||||
v-if="!cutting.results.length"
|
||||
:key="`${cutting.id}-result-empty`"
|
||||
>
|
||||
<TableCell
|
||||
colspan="5"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
Belum ada hasil produk
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<template v-else v-for="group in getGroupedResults(cutting.results)"
|
||||
:key="group.productId">
|
||||
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||
<TableCell colspan="5" class="font-semibold text-foreground">
|
||||
<template
|
||||
v-else
|
||||
v-for="group in getGroupedResults(
|
||||
cutting.results,
|
||||
)"
|
||||
:key="group.productId"
|
||||
>
|
||||
<TableRow
|
||||
class="bg-muted/20 hover:bg-muted/20"
|
||||
>
|
||||
<TableCell
|
||||
colspan="5"
|
||||
class="font-semibold text-foreground"
|
||||
>
|
||||
{{ group.productName }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="result in group.items" :key="result.id">
|
||||
<TableRow
|
||||
v-for="result in group.items"
|
||||
:key="result.id"
|
||||
>
|
||||
<TableCell class="pl-6 font-medium">
|
||||
{{ result.product_variant?.name }}
|
||||
{{
|
||||
result.product_variant?.name
|
||||
}}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="result.product_variant?.images ?? []" :max-visible="1" />
|
||||
<MediaThumbnailCell
|
||||
:items="
|
||||
result.product_variant
|
||||
?.images ?? []
|
||||
"
|
||||
:max-visible="1"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ result.cutting_result }} pcs
|
||||
@ -236,7 +368,10 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
{{ result.sample }} pcs
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ result.original_outside_sample }} pcs
|
||||
{{
|
||||
result.original_outside_sample
|
||||
}}
|
||||
pcs
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
@ -248,8 +383,15 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTableEmpty v-else description="Silakan lakukan pencarian untuk menemukan data yang Anda cari." />
|
||||
<DataTableEmpty
|
||||
v-else
|
||||
description="Silakan lakukan pencarian untuk menemukan data yang Anda cari."
|
||||
/>
|
||||
|
||||
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||
<GroupedTableFooter
|
||||
:summary="paginationSummary"
|
||||
:pagination="pagination"
|
||||
:pagination-links="paginationLinks"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user