feat: implement quantity increment/decrement controls and update UI for cutting material and product selection
This commit is contained in:
parent
bd38bbb74e
commit
9329312fd3
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user