feat: add variant search functionality in product and raw material forms, improving user experience by allowing filtering of variants based on name

This commit is contained in:
Yoga Pangestu 2026-07-13 10:33:08 +07:00
parent 5c1f92e48b
commit 9f253dc2d9
2 changed files with 122 additions and 7 deletions

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Plus, Save } from '@lucide/vue';
import { Plus, Save, Search, X } from '@lucide/vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { useCan } from '@/composables/useCan';
import { useVariantList } from '@/composables/useVariantList';
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
@ -118,6 +119,33 @@ const {
const copiedPrices = ref<Record<string, string> | null>(null);
const variantSearchQuery = ref('');
function shouldShowVariant(variant: ProductVariantFormItem, index: number) {
const hasError = Object.keys(form.errors).some((key) => key.startsWith(`variants.${index}.`));
if (hasError) {
return true;
}
const query = variantSearchQuery.value.trim().toLowerCase();
if (!query) {
return true;
}
return variant.name.toLowerCase().includes(query) || variant.name.trim() === '';
}
const hasVisibleVariants = computed(() => {
return variants.value.some((variant, index) => shouldShowVariant(variant, index));
});
function handleAddVariant() {
variantSearchQuery.value = '';
addVariant();
}
function copyPrices(variantPrices: Record<string, string>) {
copiedPrices.value = { ...variantPrices };
}
@ -230,8 +258,24 @@ function submit() {
</CardContent>
</Card>
<ProductVariantSection v-for="(variant, index) in variants" :key="variant.client_id" :form="form"
:variant="variant" :index="index" :can-remove="variants.length > 1" :has-copied-prices="!!copiedPrices"
<!-- Section pencarian varian -->
<div v-if="variants.length > 0" class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<h3 class="text-lg font-semibold tracking-tight">Daftar Varian ({{ variants.length }})</h3>
<div v-if="variants.length > 1" class="relative w-full md:max-w-xs">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input v-model="variantSearchQuery" type="text" placeholder="Cari varian berdasarkan nama..."
class="pl-9 pr-8 h-9 w-full bg-background" />
<button v-if="variantSearchQuery" type="button"
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded-full hover:bg-muted cursor-pointer flex items-center justify-center"
@click="variantSearchQuery = ''">
<X class="size-3.5" />
</button>
</div>
</div>
<ProductVariantSection v-for="(variant, index) in variants" :key="variant.client_id"
v-show="shouldShowVariant(variant, index)" :form="form" :variant="variant" :index="index"
:can-remove="variants.length > 1" :has-copied-prices="!!copiedPrices"
:variant-errors="(clientId, field) => variantErrors(form, clientId, field)"
@remove="confirmRemoveVariant(variant.client_id)"
@update:name="setVariantField(variant.client_id, 'name', $event)"
@ -243,8 +287,20 @@ function submit() {
@paste-prices="pastePrices(variant.client_id)"
@apply-to-all-prices="applyToAllPrices(variant.prices as Record<string, string>)" />
<div v-if="variants.length > 0 && !hasVisibleVariants"
class="flex flex-col items-center justify-center p-8 border border-dashed rounded-lg bg-muted/40 text-center animate-in fade-in duration-200">
<Search class="size-8 text-muted-foreground mb-2" />
<p class="text-sm font-medium">Tidak ada varian yang cocok</p>
<p class="text-xs text-muted-foreground mt-0.5">Tidak ditemukan varian dengan nama "{{
variantSearchQuery }}"</p>
<Button type="button" variant="outline" size="sm" class="mt-4 cursor-pointer"
@click="variantSearchQuery = ''">
Clear Pencarian
</Button>
</div>
<div class="flex items-center justify-between gap-2">
<Button type="button" variant="outline" @click="addVariant">
<Button type="button" variant="outline" @click="handleAddVariant">
<Plus class="size-4" />
Tambah Varian
</Button>

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Plus, Save } from '@lucide/vue';
import { Plus, Save, Search, X } from '@lucide/vue';
import { computed, ref } from 'vue';
import { toast } from 'vue-sonner';
import ConfirmDialog from '@/components/ConfirmDialog.vue';
import { Button } from '@/components/ui/button';
import { FieldError } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { useVariantList } from '@/composables/useVariantList';
import { formErrors } from '@/lib/form';
import { parseRupiah } from '@/lib/rupiah';
@ -121,6 +122,31 @@ function allPricesHaveSameValue(): boolean {
return prices.value.every((item) => item.price.trim() === first);
}
const variantSearchQuery = ref('');
function shouldShowVariant(price: RawMaterialPriceFormItem, index: number) {
const hasError = Object.keys(form.errors).some((key) => key.startsWith(`prices.${index}.`));
if (hasError) {
return true;
}
const query = variantSearchQuery.value.trim().toLowerCase();
if (!query) {
return true;
}
return price.variant.toLowerCase().includes(query) || price.variant.trim() === '';
}
const hasVisiblePrices = computed(() => {
return prices.value.some((price, index) => shouldShowVariant(price, index));
});
function handleAddPrice() {
variantSearchQuery.value = '';
addPrice();
}
function addPrice() {
const newPrice: RawMaterialPriceFormItem = {
client_id: createClientId(),
@ -210,7 +236,31 @@ function submit() {
<RawMaterialSharedPriceSection :form="form" :prices="prices" :use-same-price="useSamePrice"
@toggle-use-same-price="toggleUseSamePrice" @set-shared-price="setSharedPrice" />
<RawMaterialVariantSection v-for="(price, index) in prices" :key="price.client_id" :form="form"
<!-- Section pencarian varian -->
<div v-if="prices.length > 0" class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<h3 class="text-lg font-semibold tracking-tight">Daftar Varian ({{ prices.length }})</h3>
<div v-if="prices.length > 1" class="relative w-full md:max-w-xs">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
v-model="variantSearchQuery"
type="text"
placeholder="Cari varian berdasarkan nama..."
class="pl-9 pr-8 h-9 w-full bg-background"
/>
<button
v-if="variantSearchQuery"
type="button"
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded-full hover:bg-muted cursor-pointer flex items-center justify-center"
@click="variantSearchQuery = ''"
>
<X class="size-3.5" />
</button>
</div>
</div>
<RawMaterialVariantSection v-for="(price, index) in prices" :key="price.client_id"
v-show="shouldShowVariant(price, index)"
:form="form"
:price="price" :index="index" :total-prices="prices.length" :use-same-price="useSamePrice"
:price-errors="(clientId, field) => priceErrors(form, clientId, field)"
@remove="confirmRemovePrice(price.client_id)" @apply-price-to-all="applyPriceToAllVariants(price.client_id)"
@ -218,10 +268,19 @@ function submit() {
@update:stock="setPriceField(price.client_id, 'stock', $event)"
@update:price="setPriceValue(price.client_id, $event)" />
<div v-if="prices.length > 0 && !hasVisiblePrices" class="flex flex-col items-center justify-center p-8 border border-dashed rounded-lg bg-muted/40 text-center animate-in fade-in duration-200">
<Search class="size-8 text-muted-foreground mb-2" />
<p class="text-sm font-medium">Tidak ada varian yang cocok</p>
<p class="text-xs text-muted-foreground mt-0.5">Tidak ditemukan varian dengan nama "{{ variantSearchQuery }}"</p>
<Button type="button" variant="outline" size="sm" class="mt-4 cursor-pointer" @click="variantSearchQuery = ''">
Clear Pencarian
</Button>
</div>
<FieldError :errors="formErrors(form, 'prices')" />
<div class="flex items-center justify-between gap-2">
<Button type="button" variant="outline" @click="addPrice">
<Button type="button" variant="outline" @click="handleAddPrice">
<Plus class="size-4" />
Tambah Varian
</Button>