store/resources/js/components/admin/master/raw-materials/RawMaterialForm.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>