feat: add raw material selection functionality and improve filtering in CuttingPos components for enhanced user experience
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (8.3) (push) Waiting to run
tests / ci (8.4) (push) Waiting to run
tests / ci (8.5) (push) Waiting to run

This commit is contained in:
Yoga Pangestu 2026-07-07 10:18:24 +07:00
parent da4467b500
commit 0ec51e1ffb
4 changed files with 76 additions and 44 deletions

View File

@ -72,6 +72,7 @@ const form = useForm({
const {
materialSearch,
selectedRawMaterialId,
productSearch,
materialCart,
resultCart,
@ -267,6 +268,7 @@ function onProductCreated(product: CuttingProductCatalogItem) {
<div class="grid min-w-0 gap-4 xl:grid-cols-[1fr_400px]">
<div class="min-w-0 space-y-4">
<CuttingPosMaterialCatalogPanel v-model:material-search="materialSearch"
v-model:selected-raw-material-id="selectedRawMaterialId"
:filtered-raw-materials="filteredRawMaterials" :raw-material-catalog="rawMaterialCatalogState"
:material-cart="materialCart" :get-material-cart-item="getMaterialCartItem" :units="units"
@add-material="addMaterial" @decrease-material-qty="decreaseMaterialQty"

View File

@ -13,6 +13,13 @@ import {
EmptyTitle,
} from '@/components/ui/empty';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getFirstCoverImage } from '@/lib/catalog-cover';
import type { CuttingMaterialCartItem, CuttingRawMaterialCatalogItem } from '@/types/cutting';
import type { EnumOption } from '@/types/raw-material';
@ -29,6 +36,7 @@ defineProps<{
}>();
const materialSearch = defineModel<string>('materialSearch', { required: true });
const selectedRawMaterialId = defineModel<string>('selectedRawMaterialId', { required: true });
const emit = defineEmits<{
'add-material': [rawMaterial: CuttingRawMaterialCatalogItem, price: CuttingCatalogPrice];
@ -61,9 +69,24 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt
</CardHeader>
<CardContent>
<div class="space-y-4">
<div class="relative">
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input v-model="materialSearch" placeholder="Cari bahan baku..." class="pl-9" />
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<div class="relative flex-1">
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input v-model="materialSearch" placeholder="Cari varian..." class="pl-9" />
</div>
<div class="w-full sm:w-60">
<Select v-model="selectedRawMaterialId">
<SelectTrigger class="w-full">
<SelectValue placeholder="Semua Bahan Baku" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Semua Bahan Baku</SelectItem>
<SelectItem v-for="item in rawMaterialCatalog" :key="item.id" :value="String(item.id)">
{{ item.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div v-if="filteredRawMaterials.length === 0" class="py-8">
@ -77,7 +100,7 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt
</Empty>
</div>
<div v-else class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<template v-for="rawMaterial in filteredRawMaterials" :key="rawMaterial.id">
<PosCatalogCard v-if="rawMaterial.prices.length > 0"
:title="rawMaterial.name" :cover-image="getFirstCoverImage(rawMaterial.prices)">
@ -106,46 +129,37 @@ function openCombination(rawMaterial: CuttingRawMaterialCatalogItem, price: Cutt
Stok: {{ price.stock_formatted }}
</p>
</div>
<div v-if="getMaterialCartItem(price.id)" class="flex shrink-0 items-center gap-1.5">
<div v-if="getMaterialCartItem(price.id)" class="flex shrink-0 items-center gap-1">
<Button type="button" variant="outline" size="icon-sm"
title="Kurangi Pemakaian"
@click.stop="emit('decrease-material-qty', price.id)">
<Minus class="size-3.5" />
</Button>
<span class="min-w-5 text-center text-xs font-semibold tabular-nums">
{{ getMaterialCartItem(price.id)!.material_usage }}
</span>
<div class="flex shrink-0 items-center gap-1.5">
<div class="flex flex-col items-center gap-0.5">
<Button type="button" variant="outline" size="icon-sm"
@click.stop="emit('add-material', rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
<span class="text-[9px] text-muted-foreground leading-none">Keranjang</span>
</div>
<div class="flex flex-col items-center gap-0.5">
<Button type="button" variant="default" size="icon-sm"
@click.stop="openCombination(rawMaterial, price)">
<Layers class="size-3.5" />
</Button>
<span class="text-[9px] text-muted-foreground leading-none">Kombinasi</span>
</div>
</div>
<Button type="button" variant="outline" size="icon-sm"
title="Tambah ke Keranjang"
@click.stop="emit('add-material', rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
<Button type="button" variant="default" size="icon-sm"
title="Gunakan Kombinasi"
@click.stop="openCombination(rawMaterial, price)">
<Layers class="size-3.5" />
</Button>
</div>
<div v-else class="flex shrink-0 items-center gap-1.5">
<div class="flex flex-col items-center gap-0.5">
<Button type="button" variant="outline" size="icon-sm"
@click.stop="emit('add-material', rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
<span class="text-[9px] text-muted-foreground leading-none">Keranjang</span>
</div>
<div class="flex flex-col items-center gap-0.5">
<Button type="button" variant="default" size="icon-sm"
@click.stop="openCombination(rawMaterial, price)">
<Layers class="size-3.5" />
</Button>
<span class="text-[9px] text-muted-foreground leading-none">Kombinasi</span>
</div>
<div v-else class="flex shrink-0 items-center gap-1">
<Button type="button" variant="outline" size="icon-sm"
title="Tambah ke Keranjang"
@click.stop="emit('add-material', rawMaterial, price)">
<Plus class="size-3.5" />
</Button>
<Button type="button" variant="default" size="icon-sm"
title="Gunakan Kombinasi"
@click.stop="openCombination(rawMaterial, price)">
<Layers class="size-3.5" />
</Button>
</div>
</div>
</PosCatalogCard>

View File

@ -64,7 +64,7 @@ const quickCreateOpen = ref(false);
</Empty>
</div>
<div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div v-else class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<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">

View File

@ -21,6 +21,7 @@ export function useCuttingPosCart(options: {
isCreateMode: MaybeRefOrGetter<boolean>;
}) {
const materialSearch = ref('');
const selectedRawMaterialId = ref('all');
const productSearch = ref('');
const materialCart = ref<CuttingMaterialCartItem[]>([]);
const resultCart = ref<CuttingResultCartItem[]>([]);
@ -61,17 +62,31 @@ export function useCuttingPosCart(options: {
const filteredRawMaterials = computed(() => {
const keyword = materialSearch.value.trim().toLowerCase();
const rawMaterialId = selectedRawMaterialId.value;
const catalog = toValue(options.rawMaterialCatalog);
if (!keyword) {
return catalog;
let items = catalog;
if (rawMaterialId !== 'all') {
const idNum = Number(rawMaterialId);
items = catalog.filter((rawMaterial) => rawMaterial.id === idNum);
}
return catalog.filter(
(rawMaterial) =>
rawMaterial.name.toLowerCase().includes(keyword)
|| rawMaterial.prices.some((price) => price.variant.toLowerCase().includes(keyword)),
);
if (!keyword) {
return items;
}
return items
.map((rawMaterial) => {
const matchingPrices = rawMaterial.prices.filter((price) =>
price.variant.toLowerCase().includes(keyword),
);
return {
...rawMaterial,
prices: matchingPrices,
};
})
.filter((rawMaterial) => rawMaterial.prices.length > 0);
});
const filteredProducts = computed(() => {
@ -502,6 +517,7 @@ export function useCuttingPosCart(options: {
return {
materialSearch,
selectedRawMaterialId,
productSearch,
materialCart,
resultCart,