feat: add raw material selection functionality and improve filtering in CuttingPos components for enhanced user experience
This commit is contained in:
parent
da4467b500
commit
0ec51e1ffb
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user