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

View File

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

View File

@ -64,7 +64,7 @@ const quickCreateOpen = ref(false);
</Empty> </Empty>
</div> </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" <PosCatalogCard v-for="product in filteredProducts" :key="product.id" :title="product.name"
:cover-image="getFirstCoverImage(product.variants)"> :cover-image="getFirstCoverImage(product.variants)">
<p v-if="!product.variants.length" class="px-3 py-4 text-sm text-muted-foreground"> <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>; isCreateMode: MaybeRefOrGetter<boolean>;
}) { }) {
const materialSearch = ref(''); const materialSearch = ref('');
const selectedRawMaterialId = ref('all');
const productSearch = ref(''); const productSearch = ref('');
const materialCart = ref<CuttingMaterialCartItem[]>([]); const materialCart = ref<CuttingMaterialCartItem[]>([]);
const resultCart = ref<CuttingResultCartItem[]>([]); const resultCart = ref<CuttingResultCartItem[]>([]);
@ -61,17 +62,31 @@ export function useCuttingPosCart(options: {
const filteredRawMaterials = computed(() => { const filteredRawMaterials = computed(() => {
const keyword = materialSearch.value.trim().toLowerCase(); const keyword = materialSearch.value.trim().toLowerCase();
const rawMaterialId = selectedRawMaterialId.value;
const catalog = toValue(options.rawMaterialCatalog); const catalog = toValue(options.rawMaterialCatalog);
if (!keyword) { let items = catalog;
return catalog; if (rawMaterialId !== 'all') {
const idNum = Number(rawMaterialId);
items = catalog.filter((rawMaterial) => rawMaterial.id === idNum);
} }
return catalog.filter( if (!keyword) {
(rawMaterial) => return items;
rawMaterial.name.toLowerCase().includes(keyword) }
|| rawMaterial.prices.some((price) => price.variant.toLowerCase().includes(keyword)),
); 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(() => { const filteredProducts = computed(() => {
@ -502,6 +517,7 @@ export function useCuttingPosCart(options: {
return { return {
materialSearch, materialSearch,
selectedRawMaterialId,
productSearch, productSearch,
materialCart, materialCart,
resultCart, resultCart,