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