feat: implement quantity increment/decrement controls and update UI for cutting material and product selection

This commit is contained in:
Yoga Pangestu 2026-06-13 12:28:37 +07:00
parent bd38bbb74e
commit 9329312fd3

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Plus, Save, Scissors, Search, Trash2 } from '@lucide/vue';
import { Minus, Plus, Save, Scissors, Search, Trash2 } from '@lucide/vue';
import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
@ -31,6 +31,9 @@ import {
TableRow,
} from '@/components/ui/table';
import { Textarea } from '@/components/ui/textarea';
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import { FIELD_LIMITS } from '@/lib/field-limits';
import { formErrors } from '@/lib/form';
import type {
@ -108,14 +111,32 @@ const filteredProducts = computed(() => {
);
});
function getMaterialCartItem(priceId: number): CuttingMaterialCartItem | undefined {
return materialCart.value.find((item) => item.raw_material_price_id === priceId);
}
function decreaseMaterialQty(priceId: number) {
const index = materialCart.value.findIndex(
(item) => item.raw_material_price_id === priceId,
);
if (index !== -1) {
const item = materialCart.value[index];
const nextQty = (Number(item.material_usage) || 0) - 1;
if (nextQty <= 0) {
removeMaterial(index);
} else {
item.material_usage = String(nextQty);
}
}
}
function addMaterial(rawMaterial: CuttingRawMaterialCatalogItem, price: CatalogPrice) {
const existing = materialCart.value.find(
(item) => item.raw_material_price_id === price.id,
);
if (existing) {
toast.error('Bahan baku ini sudah ditambahkan.');
existing.material_usage = String((Number(existing.material_usage) || 0) + 1);
return;
}
@ -135,14 +156,37 @@ function removeMaterial(index: number) {
materialCart.value.splice(index, 1);
}
function getResultCartItem(variantId: number): CuttingResultCartItem | undefined {
return resultCart.value.find((item) => item.product_variant_id === variantId);
}
function decreaseResultQty(variantId: number) {
const index = resultCart.value.findIndex(
(item) => item.product_variant_id === variantId,
);
if (index !== -1) {
const item = resultCart.value[index];
const nextQty = (Number(item.cutting_result) || 0) - 1;
if (nextQty <= 0) {
removeResult(index);
} else {
item.cutting_result = String(nextQty);
// also adjust warehouse stock or run sync totals
item.warehouse_stock = String(Math.max(0, (Number(item.warehouse_stock) || 0) - 1));
syncResultTotals(item);
}
}
}
function addResult(product: CuttingProductCatalogItem, variant: CuttingProductCatalogItem['variants'][number]) {
const existing = resultCart.value.find(
(item) => item.product_variant_id === variant.id,
);
if (existing) {
toast.error('Varian produk ini sudah ditambahkan.');
existing.cutting_result = String((Number(existing.cutting_result) || 0) + 1);
existing.warehouse_stock = String((Number(existing.warehouse_stock) || 0) + 1);
syncResultTotals(existing);
return;
}
@ -253,53 +297,53 @@ function submit() {
</Empty>
</div>
<div v-else class="space-y-4">
<div v-for="rawMaterial in filteredRawMaterials" :key="rawMaterial.id"
class="overflow-hidden rounded-md border">
<div class="border-b bg-muted/30 px-4 py-3">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-medium leading-tight">
{{ rawMaterial.name }}
</h3>
<Badge variant="secondary">
{{ rawMaterial.unit_label }}
</Badge>
</div>
</div>
<div v-else class="columns-1 gap-4 sm:columns-2">
<PosCatalogCard v-for="rawMaterial in filteredRawMaterials" :key="rawMaterial.id"
:title="rawMaterial.name" :cover-image="getFirstCoverImage(rawMaterial.prices)">
<template #header-extra>
<Badge variant="secondary" class="mt-1.5">
{{ rawMaterial.unit_label }}
</Badge>
</template>
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Stok</TableHead>
<TableHead>Foto</TableHead>
<TableHead class="w-24 text-right" />
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="price in rawMaterial.prices" :key="price.id"
class="cursor-pointer hover:bg-muted/30"
@click="addMaterial(rawMaterial, price)">
<TableCell class="font-medium">
{{ price.variant }}
</TableCell>
<TableCell class="tabular-nums">
{{ price.stock_formatted }}
</TableCell>
<TableCell>
<MediaThumbnailCell :items="price.images ?? []" />
</TableCell>
<TableCell class="text-right">
<Button type="button" variant="outline" size="sm"
@click.stop="addMaterial(rawMaterial, price)">
<Plus class="size-3.5" />
Tambah
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<p v-if="!rawMaterial.prices.length" class="px-3 py-4 text-sm text-muted-foreground">
Belum ada varian
</p>
<div v-for="price in rawMaterial.prices" :key="price.id"
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200"
:class="[
'cursor-pointer hover:bg-muted/30',
getMaterialCartItem(price.id) ? 'border-2 border-primary bg-primary/5 rounded-md mx-1 my-0.5' : ''
]"
@click="!getMaterialCartItem(price.id) && addMaterial(rawMaterial, price)">
<PosCatalogVariantThumb :items="price.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
{{ price.variant }}
</p>
<p class="text-xs tabular-nums text-muted-foreground">
Stok: {{ price.stock_formatted }}
</p>
</div>
<div v-if="getMaterialCartItem(price.id)" class="flex items-center gap-1.5 shrink-0">
<Button type="button" variant="outline" size="icon-sm"
@click.stop="decreaseMaterialQty(price.id)">
<Minus class="size-3.5" />
</Button>
<span class="text-xs font-semibold min-w-[1.25rem] text-center tabular-nums">
{{ getMaterialCartItem(price.id)!.material_usage }}
</span>
<Button type="button" variant="outline" size="icon-sm"
@click.stop="addMaterial(rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
</div>
<Button v-else type="button" variant="outline" size="icon-sm" class="shrink-0"
@click.stop="addMaterial(rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
</div>
</PosCatalogCard>
</div>
</div>
</CardContent>
@ -327,48 +371,47 @@ function submit() {
</Empty>
</div>
<div v-else class="space-y-4">
<div v-for="product in filteredProducts" :key="product.id"
class="overflow-hidden rounded-md border">
<div class="border-b bg-muted/30 px-4 py-3">
<h3 class="font-medium leading-tight">
{{ product.name }}
</h3>
<div v-else class="columns-1 gap-4 sm:columns-2">
<PosCatalogCard v-for="product in filteredProducts" :key="product.id" :title="product.name"
:cover-image="getFirstCoverImage(product.variants)">
<p v-if="!product.variants.length" class="px-3 py-4 text-sm text-muted-foreground">
Belum ada varian
</p>
<div v-for="variant in product.variants" :key="variant.id"
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200"
:class="[
'cursor-pointer hover:bg-muted/30',
getResultCartItem(variant.id) ? 'border-2 border-primary bg-primary/5 rounded-md mx-1 my-0.5' : ''
]"
@click="!getResultCartItem(variant.id) && addResult(product, variant)">
<PosCatalogVariantThumb :items="variant.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
{{ variant.name }}
</p>
<p class="text-xs text-muted-foreground">
<span class="tabular-nums">Stok: {{ variant.stock }} pcs</span>
</p>
</div>
<div v-if="getResultCartItem(variant.id)" class="flex items-center gap-1.5 shrink-0">
<Button type="button" variant="outline" size="icon-sm"
@click.stop="decreaseResultQty(variant.id)">
<Minus class="size-3.5" />
</Button>
<span class="text-xs font-semibold min-w-[1.25rem] text-center tabular-nums">
{{ getResultCartItem(variant.id)!.cutting_result }}
</span>
<Button type="button" variant="outline" size="icon-sm"
@click.stop="addResult(product, variant)">
<Plus class="size-3.5" />
</Button>
</div>
<Button v-else type="button" variant="outline" size="icon-sm" class="shrink-0"
@click.stop="addResult(product, variant)">
<Plus class="size-3.5" />
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Varian</TableHead>
<TableHead>Stok</TableHead>
<TableHead>Foto</TableHead>
<TableHead class="w-24 text-right" />
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="variant in product.variants" :key="variant.id"
class="cursor-pointer hover:bg-muted/30"
@click="addResult(product, variant)">
<TableCell class="font-medium">
{{ variant.name }}
</TableCell>
<TableCell class="tabular-nums">
{{ variant.stock }} pcs
</TableCell>
<TableCell>
<MediaThumbnailCell :items="variant.images ?? []" />
</TableCell>
<TableCell class="text-right">
<Button type="button" variant="outline" size="sm"
@click.stop="addResult(product, variant)">
<Plus class="size-3.5" />
Tambah
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</PosCatalogCard>
</div>
</div>
</CardContent>