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