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:
parent
5c1f92e48b
commit
9f253dc2d9
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user