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">
|
<script setup lang="ts">
|
||||||
import { useForm } from '@inertiajs/vue3';
|
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 { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { useVariantList } from '@/composables/useVariantList';
|
import { useVariantList } from '@/composables/useVariantList';
|
||||||
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
|
import { appendMediaToFormData, createMediaUploadState } from '@/types/media';
|
||||||
@ -118,6 +119,33 @@ const {
|
|||||||
|
|
||||||
const copiedPrices = ref<Record<string, string> | null>(null);
|
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>) {
|
function copyPrices(variantPrices: Record<string, string>) {
|
||||||
copiedPrices.value = { ...variantPrices };
|
copiedPrices.value = { ...variantPrices };
|
||||||
}
|
}
|
||||||
@ -230,8 +258,24 @@ function submit() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<ProductVariantSection v-for="(variant, index) in variants" :key="variant.client_id" :form="form"
|
<!-- Section pencarian varian -->
|
||||||
:variant="variant" :index="index" :can-remove="variants.length > 1" :has-copied-prices="!!copiedPrices"
|
<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)"
|
:variant-errors="(clientId, field) => variantErrors(form, clientId, field)"
|
||||||
@remove="confirmRemoveVariant(variant.client_id)"
|
@remove="confirmRemoveVariant(variant.client_id)"
|
||||||
@update:name="setVariantField(variant.client_id, 'name', $event)"
|
@update:name="setVariantField(variant.client_id, 'name', $event)"
|
||||||
@ -243,8 +287,20 @@ function submit() {
|
|||||||
@paste-prices="pastePrices(variant.client_id)"
|
@paste-prices="pastePrices(variant.client_id)"
|
||||||
@apply-to-all-prices="applyToAllPrices(variant.prices as Record<string, string>)" />
|
@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">
|
<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" />
|
<Plus class="size-4" />
|
||||||
Tambah Varian
|
Tambah Varian
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm } from '@inertiajs/vue3';
|
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 { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { FieldError } from '@/components/ui/field';
|
import { FieldError } from '@/components/ui/field';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { useVariantList } from '@/composables/useVariantList';
|
import { useVariantList } from '@/composables/useVariantList';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
@ -121,6 +122,31 @@ function allPricesHaveSameValue(): boolean {
|
|||||||
return prices.value.every((item) => item.price.trim() === first);
|
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() {
|
function addPrice() {
|
||||||
const newPrice: RawMaterialPriceFormItem = {
|
const newPrice: RawMaterialPriceFormItem = {
|
||||||
client_id: createClientId(),
|
client_id: createClientId(),
|
||||||
@ -210,7 +236,31 @@ function submit() {
|
|||||||
<RawMaterialSharedPriceSection :form="form" :prices="prices" :use-same-price="useSamePrice"
|
<RawMaterialSharedPriceSection :form="form" :prices="prices" :use-same-price="useSamePrice"
|
||||||
@toggle-use-same-price="toggleUseSamePrice" @set-shared-price="setSharedPrice" />
|
@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="price" :index="index" :total-prices="prices.length" :use-same-price="useSamePrice"
|
||||||
:price-errors="(clientId, field) => priceErrors(form, clientId, field)"
|
:price-errors="(clientId, field) => priceErrors(form, clientId, field)"
|
||||||
@remove="confirmRemovePrice(price.client_id)" @apply-price-to-all="applyPriceToAllVariants(price.client_id)"
|
@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:stock="setPriceField(price.client_id, 'stock', $event)"
|
||||||
@update:price="setPriceValue(price.client_id, $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')" />
|
<FieldError :errors="formErrors(form, 'prices')" />
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2">
|
<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" />
|
<Plus class="size-4" />
|
||||||
Tambah Varian
|
Tambah Varian
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user