Compare commits

...

2 Commits

Author SHA1 Message Date
Yoga Pangestu
0ec51e1ffb 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
2026-07-07 10:18:24 +07:00
Yoga Pangestu
da4467b500 refactor: improve layout and styling in cutting forms for better user experience and responsiveness 2026-07-07 10:03:42 +07:00
7 changed files with 79 additions and 47 deletions

View File

@ -31,7 +31,7 @@ const page = usePage();
<UserNav />
</div>
</header>
<div class="flex min-w-0 flex-1 flex-col gap-4 overflow-y-auto p-4 pb-24 lg:pb-4">
<div class="flex min-w-0 flex-1 flex-col gap-4 overflow-y-auto overflow-x-hidden p-4 pb-24 lg:pb-4">
<slot />
</div>
</SidebarInset>

View File

@ -72,6 +72,7 @@ const form = useForm({
const {
materialSearch,
selectedRawMaterialId,
productSearch,
materialCart,
resultCart,
@ -265,8 +266,9 @@ function onProductCreated(product: CuttingProductCatalogItem) {
<template>
<div class="grid min-w-0 gap-4 xl:grid-cols-[1fr_400px]">
<div class="space-y-4">
<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">
@ -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

@ -98,7 +98,7 @@ function setCombinationResult(combinationId: number | undefined, value: string)
Belum ada bahan baku dipilih.
</div>
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overflow-x-hidden overscroll-y-contain">
<template v-for="group in materialGroups" :key="group.combinationId ?? `single-${group.items[0]?.index}`">
<div v-if="group.type === 'combination'"
class="rounded-lg border-2 border-dashed border-primary/30 p-3">

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

@ -41,7 +41,7 @@ const emit = defineEmits<{
Belum ada produk hasil dipilih.
</div>
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overflow-x-hidden overscroll-y-contain">
<div v-for="(item, index) in resultCart" :key="item.product_variant_id" class="rounded-lg border p-3">
<div class="mb-2 flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">

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,