375 lines
14 KiB
Vue
375 lines
14 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import { Copy, Plus, Save, Trash2 } from '@lucide/vue';
|
|
import { ref } from 'vue';
|
|
import { toast } from 'vue-sonner';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
Field,
|
|
FieldError,
|
|
FieldGroup,
|
|
FieldLabel,
|
|
FieldSet,
|
|
} from '@/components/ui/field';
|
|
import { Input } from '@/components/ui/input';
|
|
import { RupiahInput } from '@/components/ui/rupiah-input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { parseRupiah } from '@/lib/rupiah';
|
|
import type {
|
|
EnumOption,
|
|
RawMaterialFormData,
|
|
RawMaterialPriceFormItem,
|
|
} from '@/types/raw-material';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
units: EnumOption[];
|
|
initialData?: {
|
|
name?: string;
|
|
unit?: string;
|
|
prices?: Array<{
|
|
id?: number;
|
|
variant?: string;
|
|
price?: string;
|
|
stock?: string;
|
|
}>;
|
|
};
|
|
submitUrl: string;
|
|
method?: 'post' | 'put';
|
|
submitLabel?: string;
|
|
}>(),
|
|
{
|
|
method: 'post',
|
|
submitLabel: 'Simpan',
|
|
},
|
|
);
|
|
|
|
function createClientId(): string {
|
|
return `price-${crypto.randomUUID()}`;
|
|
}
|
|
|
|
function createEmptyPrice(): RawMaterialPriceFormItem {
|
|
return {
|
|
client_id: createClientId(),
|
|
variant: '',
|
|
price: '',
|
|
stock: '0',
|
|
};
|
|
}
|
|
|
|
function buildInitialPrices(): RawMaterialPriceFormItem[] {
|
|
if (!props.initialData?.prices?.length) {
|
|
return [createEmptyPrice()];
|
|
}
|
|
|
|
return props.initialData.prices.map((price) => ({
|
|
client_id: createClientId(),
|
|
id: price.id,
|
|
variant: price.variant ?? '',
|
|
price: price.price ?? '',
|
|
stock: price.stock ?? '0',
|
|
}));
|
|
}
|
|
|
|
const prices = ref<RawMaterialPriceFormItem[]>(buildInitialPrices());
|
|
const useSamePrice = ref(prices.value.length <= 1 || allPricesHaveSameValue(prices.value));
|
|
|
|
const form = useForm({
|
|
name: props.initialData?.name ?? '',
|
|
unit: props.initialData?.unit ?? '',
|
|
});
|
|
|
|
function allPricesHaveSameValue(items: RawMaterialPriceFormItem[]): boolean {
|
|
if (items.length <= 1) {
|
|
return true;
|
|
}
|
|
|
|
const first = items[0].price.trim();
|
|
|
|
return items.every((item) => item.price.trim() === first);
|
|
}
|
|
|
|
function addPrice() {
|
|
const newPrice = createEmptyPrice();
|
|
|
|
if (useSamePrice.value && prices.value[0]) {
|
|
newPrice.price = prices.value[0].price;
|
|
}
|
|
|
|
prices.value = [...prices.value, newPrice];
|
|
}
|
|
|
|
function removePrice(clientId: string) {
|
|
if (prices.value.length <= 1) {
|
|
return;
|
|
}
|
|
|
|
prices.value = prices.value.filter((price) => price.client_id !== clientId);
|
|
}
|
|
|
|
function setPriceField(clientId: string, key: 'variant' | 'stock', value: string) {
|
|
prices.value = prices.value.map((price) =>
|
|
price.client_id === clientId ? { ...price, [key]: value } : price,
|
|
);
|
|
}
|
|
|
|
function setPriceValue(clientId: string, value: string) {
|
|
prices.value = prices.value.map((price) =>
|
|
price.client_id === clientId ? { ...price, price: value } : price,
|
|
);
|
|
}
|
|
|
|
function setSharedPrice(value: string) {
|
|
prices.value = prices.value.map((price) => ({ ...price, price: value }));
|
|
}
|
|
|
|
function toggleUseSamePrice(checked: boolean) {
|
|
useSamePrice.value = checked;
|
|
|
|
if (!checked || !prices.value[0]) {
|
|
return;
|
|
}
|
|
|
|
const sourcePrice = prices.value[0].price;
|
|
prices.value = prices.value.map((price) => ({ ...price, price: sourcePrice }));
|
|
}
|
|
|
|
function applyPriceToAllVariants(sourceClientId: string) {
|
|
const source = prices.value.find((price) => price.client_id === sourceClientId);
|
|
|
|
if (!source) {
|
|
return;
|
|
}
|
|
|
|
prices.value = prices.value.map((price) => ({ ...price, price: source.price }));
|
|
}
|
|
|
|
function parseStockValue(value: string): number {
|
|
const parsed = Number.parseFloat(value.replace(',', '.'));
|
|
|
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
}
|
|
|
|
function buildSubmitPayload(): RawMaterialFormData {
|
|
return {
|
|
name: form.name.trim(),
|
|
unit: form.unit,
|
|
prices: prices.value.map((price) => ({
|
|
...(price.id ? { id: price.id } : {}),
|
|
variant: price.variant.trim(),
|
|
price: Number.parseInt(parseRupiah(price.price), 10) || 0,
|
|
stock: parseStockValue(price.stock),
|
|
})),
|
|
};
|
|
}
|
|
|
|
function formError(key: string): string | undefined {
|
|
return (form.errors as Record<string, string>)[key];
|
|
}
|
|
|
|
function priceError(clientId: string, field: string): string | undefined {
|
|
const index = prices.value.findIndex((price) => price.client_id === clientId);
|
|
|
|
if (index === -1) {
|
|
return undefined;
|
|
}
|
|
|
|
return formError(`prices.${index}.${field}`);
|
|
}
|
|
|
|
function submit() {
|
|
const options = {
|
|
onError: () => {
|
|
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
|
},
|
|
};
|
|
|
|
const payload = buildSubmitPayload();
|
|
|
|
if (props.method === 'put') {
|
|
form.transform(() => payload).put(props.submitUrl, options);
|
|
} else {
|
|
form.transform(() => payload).post(props.submitUrl, options);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<form @submit.prevent="submit">
|
|
<div class="grid gap-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Informasi Bahan Baku</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<FieldGroup>
|
|
<FieldSet class="grid gap-4 md:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel for="name" required>Nama Bahan Baku</FieldLabel>
|
|
<Input
|
|
id="name"
|
|
v-model="form.name"
|
|
type="text"
|
|
placeholder="Masukkan nama bahan baku"
|
|
/>
|
|
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel for="unit" required>Satuan</FieldLabel>
|
|
<Select v-model="form.unit">
|
|
<SelectTrigger id="unit" class="w-full">
|
|
<SelectValue placeholder="Pilih satuan" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem
|
|
v-for="option in units"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FieldError :errors="form.errors.unit ? [form.errors.unit] : []" />
|
|
</Field>
|
|
</FieldSet>
|
|
</FieldGroup>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card v-if="prices.length > 1">
|
|
<CardHeader>
|
|
<CardTitle>Harga Bersama</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<FieldGroup>
|
|
<label class="mb-4 flex cursor-pointer items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
class="size-4 rounded border-input"
|
|
:checked="useSamePrice"
|
|
@change="toggleUseSamePrice(($event.target as HTMLInputElement).checked)"
|
|
>
|
|
<span class="text-sm">Gunakan harga yang sama untuk semua varian</span>
|
|
</label>
|
|
|
|
<Field v-if="useSamePrice && prices[0]">
|
|
<FieldLabel :for="`shared_price_${prices[0].client_id}`" required>
|
|
Harga
|
|
</FieldLabel>
|
|
<RupiahInput
|
|
:id="`shared_price_${prices[0].client_id}`"
|
|
:model-value="prices[0].price"
|
|
@update:model-value="setSharedPrice"
|
|
/>
|
|
<FieldError
|
|
:errors="priceError(prices[0].client_id, 'price') ? [priceError(prices[0].client_id, 'price')!] : []"
|
|
/>
|
|
</Field>
|
|
</FieldGroup>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card v-for="(price, index) in prices" :key="price.client_id">
|
|
<CardHeader class="flex flex-row items-start justify-between gap-4">
|
|
<CardTitle>Varian {{ index + 1 }}</CardTitle>
|
|
<div class="flex items-center gap-2">
|
|
<Button
|
|
v-if="prices.length > 1 && !useSamePrice"
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
@click="applyPriceToAllVariants(price.client_id)"
|
|
>
|
|
<Copy class="size-4" />
|
|
Terapkan Harga ke Semua
|
|
</Button>
|
|
<Button
|
|
v-if="prices.length > 1"
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
class="text-destructive hover:text-destructive size-8"
|
|
@click="removePrice(price.client_id)"
|
|
>
|
|
<Trash2 class="size-4" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<FieldGroup>
|
|
<FieldSet class="grid gap-4 md:grid-cols-3">
|
|
<Field>
|
|
<FieldLabel :for="`variant_${price.client_id}`" required>
|
|
Nama Varian
|
|
</FieldLabel>
|
|
<Input
|
|
:id="`variant_${price.client_id}`"
|
|
:model-value="price.variant"
|
|
type="text"
|
|
placeholder="Contoh: Premium / 40s"
|
|
@update:model-value="setPriceField(price.client_id, 'variant', String($event))"
|
|
/>
|
|
<FieldError
|
|
:errors="priceError(price.client_id, 'variant') ? [priceError(price.client_id, 'variant')!] : []"
|
|
/>
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel :for="`stock_${price.client_id}`" required>
|
|
Stok
|
|
</FieldLabel>
|
|
<Input
|
|
:id="`stock_${price.client_id}`"
|
|
:model-value="price.stock"
|
|
type="number"
|
|
min="0"
|
|
step="0.0001"
|
|
@update:model-value="setPriceField(price.client_id, 'stock', String($event))"
|
|
/>
|
|
<FieldError
|
|
:errors="priceError(price.client_id, 'stock') ? [priceError(price.client_id, 'stock')!] : []"
|
|
/>
|
|
</Field>
|
|
<Field v-if="!useSamePrice || prices.length === 1">
|
|
<FieldLabel :for="`price_${price.client_id}`" required>
|
|
Harga
|
|
</FieldLabel>
|
|
<RupiahInput
|
|
:id="`price_${price.client_id}`"
|
|
:model-value="price.price"
|
|
@update:model-value="setPriceValue(price.client_id, $event)"
|
|
/>
|
|
<FieldError
|
|
:errors="priceError(price.client_id, 'price') ? [priceError(price.client_id, 'price')!] : []"
|
|
/>
|
|
</Field>
|
|
</FieldSet>
|
|
</FieldGroup>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<FieldError :errors="formError('prices') ? [formError('prices')!] : []" />
|
|
|
|
<div class="flex items-center justify-between gap-2">
|
|
<Button type="button" variant="outline" @click="addPrice">
|
|
<Plus class="size-4" />
|
|
Tambah Varian
|
|
</Button>
|
|
|
|
<Button type="submit" :disabled="form.processing">
|
|
<Save class="size-4" />
|
|
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</template>
|