refactor: improve raw material filtering logic in CuttingPosCombinationDialog to return only matching prices, enhancing search efficiency
This commit is contained in:
parent
a3954faab6
commit
b8e9aaa128
@ -5,78 +5,63 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import type { FormWithErrors } from '@/lib/form';
|
||||
import type { CuttingMaterialCartItem, CuttingResultCartItem } from '@/types/cutting';
|
||||
import CuttingPosMaterialSummaryItems from './CuttingPosMaterialSummaryItems.vue';
|
||||
import CuttingPosResultSummaryItems from './CuttingPosResultSummaryItems.vue';
|
||||
|
||||
defineProps<{
|
||||
form: FormWithErrors & { description: string; processing?: boolean };
|
||||
materialCart: CuttingMaterialCartItem[];
|
||||
resultCart: CuttingResultCartItem[];
|
||||
materialLineCost: (item: CuttingMaterialCartItem) => number;
|
||||
totalMaterialCost: number;
|
||||
totalResultPieces: number;
|
||||
isCreateMode: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'remove-material': [index: number];
|
||||
'sync-material-field': [index: number];
|
||||
'remove-combination': [combinationId: number];
|
||||
'sync-combination-result': [combinationId: number, result: number | null];
|
||||
'remove-result': [index: number];
|
||||
'sync-result-totals': [item: CuttingResultCartItem];
|
||||
'sync-result-field': [index: number];
|
||||
}>();
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogContent class="sm:max-w-lg min-w-0 max-h-[90dvh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail Keranjang</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="scrollbar-thin max-h-96 space-y-4 overflow-y-auto overscroll-y-contain">
|
||||
<div class="scrollbar-thin flex-1 overflow-y-auto overscroll-y-contain pr-1 space-y-4">
|
||||
<div v-if="materialCart.length > 0">
|
||||
<p class="mb-2 text-xs font-medium text-muted-foreground">Bahan Baku</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="item in materialCart" :key="`detail-mat-${item.raw_material_price_id}`"
|
||||
class="rounded-lg border p-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium">{{ item.raw_material_name }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">
|
||||
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 text-xs font-medium tabular-nums">
|
||||
{{ item.material_usage }} {{ item.unit_abbreviation }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1.5 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
||||
<span class="font-medium text-foreground">{{ formatRupiah(materialLineCost(item))
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CuttingPosMaterialSummaryItems :form="form" :material-cart="materialCart"
|
||||
@remove="emit('remove-material', $event)" @sync-field="emit('sync-material-field', $event)"
|
||||
@remove-combination="emit('remove-combination', $event)"
|
||||
@sync-combination-result="(combinationId, result) => emit('sync-combination-result', combinationId, result)" />
|
||||
</div>
|
||||
<Separator v-if="materialCart.length > 0 && resultCart.length > 0" />
|
||||
<div v-if="resultCart.length > 0">
|
||||
<p class="mb-2 text-xs font-medium text-muted-foreground">Hasil Produk</p>
|
||||
<div class="space-y-2">
|
||||
<div v-for="item in resultCart" :key="`detail-res-${item.product_variant_id}`"
|
||||
class="rounded-lg border p-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
||||
<p class="truncate text-xs text-muted-foreground">
|
||||
{{ item.variant_name }} · Stok {{ item.stock }} pcs
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isCreateMode" class="mt-1.5 flex items-center gap-3 text-xs tabular-nums">
|
||||
<span>Hasil: <strong>{{ item.cutting_result }}</strong></span>
|
||||
<span class="text-muted-foreground">·</span>
|
||||
<span>Sample: <strong>{{ item.sample }}</strong></span>
|
||||
<span class="text-muted-foreground">·</span>
|
||||
<span>Diluar Sample: <strong>{{ item.original_outside_sample }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CuttingPosResultSummaryItems :form="form" :result-cart="resultCart"
|
||||
:total-result-pieces="totalResultPieces" :is-create-mode="isCreateMode"
|
||||
@remove="emit('remove-result', $event)" @sync-totals="emit('sync-result-totals', $event)"
|
||||
@sync-field="emit('sync-result-field', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t pt-3">
|
||||
<div class="flex items-center justify-between text-sm font-semibold">
|
||||
<span>Total Biaya Bahan</span>
|
||||
<span class="text-primary">{{ formatRupiah(totalMaterialCost) }}</span>
|
||||
<span class="text-primary">Rp {{ formatRupiah(totalMaterialCost) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -62,11 +62,18 @@ const filteredRawMaterials = computed(() => {
|
||||
return catalog;
|
||||
}
|
||||
|
||||
return catalog.filter(
|
||||
(rawMaterial) =>
|
||||
rawMaterial.name.toLowerCase().includes(keyword)
|
||||
|| rawMaterial.prices.some((price) => price.variant.toLowerCase().includes(keyword)),
|
||||
);
|
||||
return catalog
|
||||
.map((rawMaterial) => {
|
||||
const matchingPrices = rawMaterial.prices.filter((price) =>
|
||||
price.variant.toLowerCase().includes(keyword),
|
||||
);
|
||||
|
||||
return {
|
||||
...rawMaterial,
|
||||
prices: matchingPrices,
|
||||
};
|
||||
})
|
||||
.filter((rawMaterial) => rawMaterial.prices.length > 0);
|
||||
});
|
||||
|
||||
function isSelected(priceId: number): boolean {
|
||||
@ -186,7 +193,7 @@ watch(open, (value) => {
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<PosCatalogVariantThumb :items="item.images" :title="`${item.raw_material_name} - ${item.variant}`" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium">
|
||||
<p class="text-sm font-medium">
|
||||
{{ item.raw_material_name }} - {{ item.variant }}
|
||||
</p>
|
||||
<Badge variant="outline" class="text-xs mt-0.5 select-none font-normal">
|
||||
@ -240,7 +247,7 @@ watch(open, (value) => {
|
||||
]" @click="!isSelected(price.id) && toggleVariant(rawMaterial, price)">
|
||||
<PosCatalogVariantThumb :items="price.images" :title="`${rawMaterial.name} - ${price.variant}`" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm">{{ price.variant }}</p>
|
||||
<p class="text-sm">{{ price.variant }}</p>
|
||||
<p class="text-xs tabular-nums text-muted-foreground">
|
||||
Stok: {{ price.stock_formatted }}
|
||||
</p>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ShoppingCart } from '@lucide/vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { parseRupiah } from '@/lib/rupiah';
|
||||
import type {
|
||||
@ -16,6 +17,7 @@ import CuttingPosCartDetailDialog from './CuttingPosCartDetailDialog.vue';
|
||||
import CuttingPosMaterialCatalogPanel from './CuttingPosMaterialCatalogPanel.vue';
|
||||
import CuttingPosResultCatalogPanel from './CuttingPosResultCatalogPanel.vue';
|
||||
import CuttingPosSummaryPanel from './CuttingPosSummaryPanel.vue';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { useCuttingPosCart } from './useCuttingPosCart';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -102,6 +104,65 @@ const {
|
||||
isCreateMode: () => isCreateMode.value,
|
||||
});
|
||||
|
||||
// Confirmation Dialog states
|
||||
const showDeleteConfirm = ref(false);
|
||||
const deleteType = ref<'material' | 'result' | 'combination' | null>(null);
|
||||
const deleteIndex = ref<number | null>(null);
|
||||
const deleteCombinationId = ref<number | null>(null);
|
||||
|
||||
const deleteConfirmTitle = computed(() => {
|
||||
if (deleteType.value === 'combination') {
|
||||
return 'Hapus Kombinasi?';
|
||||
}
|
||||
if (deleteType.value === 'material') {
|
||||
return 'Hapus Bahan Baku?';
|
||||
}
|
||||
return 'Hapus Hasil Produk?';
|
||||
});
|
||||
|
||||
const deleteConfirmDescription = computed(() => {
|
||||
if (deleteType.value === 'combination') {
|
||||
return 'Apakah Anda yakin ingin menghapus kombinasi bahan baku ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
|
||||
}
|
||||
if (deleteType.value === 'material') {
|
||||
return 'Apakah Anda yakin ingin menghapus bahan baku ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
|
||||
}
|
||||
return 'Apakah Anda yakin ingin menghapus hasil produk ini dari keranjang? Tindakan ini tidak dapat dibatalkan.';
|
||||
});
|
||||
|
||||
function confirmRemoveMaterial(index: number) {
|
||||
deleteType.value = 'material';
|
||||
deleteIndex.value = index;
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmRemoveResult(index: number) {
|
||||
deleteType.value = 'result';
|
||||
deleteIndex.value = index;
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmRemoveCombination(combinationId: number) {
|
||||
deleteType.value = 'combination';
|
||||
deleteCombinationId.value = combinationId;
|
||||
showDeleteConfirm.value = true;
|
||||
}
|
||||
|
||||
async function handleConfirmDelete() {
|
||||
if (deleteType.value === 'material' && deleteIndex.value !== null) {
|
||||
await removeMaterial(deleteIndex.value);
|
||||
} else if (deleteType.value === 'result' && deleteIndex.value !== null) {
|
||||
await removeResult(deleteIndex.value);
|
||||
} else if (deleteType.value === 'combination' && deleteCombinationId.value !== null) {
|
||||
await removeCombination(deleteCombinationId.value);
|
||||
}
|
||||
|
||||
showDeleteConfirm.value = false;
|
||||
deleteType.value = null;
|
||||
deleteIndex.value = null;
|
||||
deleteCombinationId.value = null;
|
||||
}
|
||||
|
||||
function populateForm() {
|
||||
if (!props.initialData) {
|
||||
return;
|
||||
@ -266,7 +327,7 @@ function onProductCreated(product: CuttingProductCatalogItem) {
|
||||
|
||||
<template>
|
||||
<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 order-last xl:order-first">
|
||||
<CuttingPosMaterialCatalogPanel v-model:material-search="materialSearch"
|
||||
v-model:selected-raw-material-id="selectedRawMaterialId"
|
||||
:filtered-raw-materials="filteredRawMaterials" :raw-material-catalog="rawMaterialCatalogState"
|
||||
@ -279,16 +340,47 @@ function onProductCreated(product: CuttingProductCatalogItem) {
|
||||
:categories="categories" :material-cart="materialCart" @decrease-result-qty="decreaseResultQty" @product-created="onProductCreated" />
|
||||
</div>
|
||||
|
||||
<CuttingPosSummaryPanel :form="form" :material-cart="materialCart"
|
||||
<CuttingPosSummaryPanel class="order-first xl:order-last" :form="form" :material-cart="materialCart"
|
||||
:result-cart="resultCart" :total-result-pieces="totalResultPieces" :submit-label="submitLabel"
|
||||
@submit="submit" :is-create-mode="isCreateMode"
|
||||
@open-detail="cartDetailOpen = true" @remove-material="removeMaterial"
|
||||
@sync-material-field="syncMaterialField" @remove-result="removeResult"
|
||||
@open-detail="cartDetailOpen = true" @remove-material="confirmRemoveMaterial"
|
||||
@sync-material-field="syncMaterialField" @remove-result="confirmRemoveResult"
|
||||
@sync-result-totals="syncResultTotals" @sync-result-field="syncResultField"
|
||||
@remove-combination="removeCombination" @sync-combination-result="syncCombinationResult" />
|
||||
@remove-combination="confirmRemoveCombination" @sync-combination-result="syncCombinationResult" />
|
||||
</div>
|
||||
|
||||
<CuttingPosCartDetailDialog v-model:open="cartDetailOpen" :material-cart="materialCart" :result-cart="resultCart"
|
||||
:material-line-cost="materialLineCost" :total-material-cost="totalMaterialCost"
|
||||
:is-create-mode="isCreateMode" />
|
||||
<CuttingPosCartDetailDialog v-model:open="cartDetailOpen" :form="form" :material-cart="materialCart" :result-cart="resultCart"
|
||||
:material-line-cost="materialLineCost" :total-material-cost="totalMaterialCost" :total-result-pieces="totalResultPieces"
|
||||
:is-create-mode="isCreateMode"
|
||||
@remove-material="confirmRemoveMaterial"
|
||||
@sync-material-field="syncMaterialField"
|
||||
@remove-combination="confirmRemoveCombination"
|
||||
@sync-combination-result="syncCombinationResult"
|
||||
@remove-result="confirmRemoveResult"
|
||||
@sync-result-totals="syncResultTotals"
|
||||
@sync-result-field="syncResultField" />
|
||||
|
||||
<!-- Floating Cart button on mobile -->
|
||||
<button
|
||||
v-if="materialCart.length + resultCart.length > 0"
|
||||
type="button"
|
||||
class="fixed top-50 right-6 z-50 flex size-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-105 active:scale-95 xl:hidden"
|
||||
@click="cartDetailOpen = true"
|
||||
>
|
||||
<ShoppingCart class="size-6" />
|
||||
<span class="absolute -right-1 -top-1 flex size-6 items-center justify-center rounded-full bg-destructive text-[11px] font-bold text-destructive-foreground">
|
||||
{{ (materialCart.length + resultCart.length) > 99 ? '99+' : (materialCart.length + resultCart.length) }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Confirm deletion dialog -->
|
||||
<ConfirmDialog
|
||||
v-model:open="showDeleteConfirm"
|
||||
:title="deleteConfirmTitle"
|
||||
:description="deleteConfirmDescription"
|
||||
confirm-label="Hapus"
|
||||
cancel-label="Batal"
|
||||
destructive
|
||||
@confirm="handleConfirmDelete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -53,11 +53,6 @@ const emit = defineEmits<{
|
||||
Ringkasan Cutting
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<button v-if="materialCart.length > 0 || resultCart.length > 0" type="button"
|
||||
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
|
||||
@click="emit('open-detail')">
|
||||
Lihat Detail
|
||||
</button>
|
||||
<Badge v-if="!isCreateMode && totalResultPieces > 0" variant="secondary"
|
||||
class="font-semibold tabular-nums">
|
||||
Total: {{ totalResultPieces }} pcs
|
||||
|
||||
Loading…
Reference in New Issue
Block a user