store/resources/js/components/modal/RetailStockTransferModal.vue

243 lines
8.3 KiB
Vue

<script setup lang="ts">
import { Save } from '@lucide/vue';
import { computed, reactive, ref, watch } from 'vue';
import { toast } from 'vue-sonner';
import { NumberInput } from '@/components/form/number-input';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
FieldSet,
} from '@/components/ui/field';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { apiFetch } from '@/lib/api';
import { transfer } from '@/routes/admin/manage/retail-stock';
interface VariantOption {
id: number;
name: string;
stock: number;
retail_stock: number;
}
const open = defineModel<boolean>('open', { default: false });
const props = defineProps<{
variantId: number;
variantName: string;
productName: string;
goodStock: number;
retailStock: number;
variants?: VariantOption[];
}>();
const emit = defineEmits<{
submitted: [];
}>();
const processing = ref(false);
const errors = reactive<Record<string, string>>({});
const selectedVariantId = ref(0);
const hasMultipleVariants = computed(() => (props.variants?.length ?? 0) > 1);
const activeVariant = computed(() => {
if (hasMultipleVariants.value && selectedVariantId.value > 0) {
return props.variants?.find((v) => v.id === selectedVariantId.value);
}
return undefined;
});
const displayVariantName = computed(() => activeVariant.value?.name ?? props.variantName);
const displayGoodStock = computed(() => activeVariant.value?.stock ?? props.goodStock);
const displayRetailStock = computed(() => activeVariant.value?.retail_stock ?? props.retailStock);
const form = reactive({
quantity: '',
notes: '',
});
function resetForm() {
form.quantity = '';
form.notes = '';
Object.keys(errors).forEach((key) => delete errors[key]);
if (hasMultipleVariants.value) {
selectedVariantId.value = 0;
}
}
watch(open, (isOpen) => {
if (isOpen) {
resetForm();
if (!hasMultipleVariants.value && props.variantId > 0) {
selectedVariantId.value = props.variantId;
}
}
});
const submitVariantId = computed(() => {
if (hasMultipleVariants.value) {
return selectedVariantId.value;
}
return props.variantId;
});
async function submit() {
Object.keys(errors).forEach((key) => delete errors[key]);
processing.value = true;
try {
await apiFetch(transfer.url(), {
method: 'POST',
body: JSON.stringify({
product_variant_id: submitVariantId.value,
quantity: Number(form.quantity),
notes: form.notes || null,
}),
});
emit('submitted');
open.value = false;
toast.success('Pengajuan transfer stok ecer berhasil dikirim. Menunggu verifikasi owner.');
} catch (error) {
const message =
error instanceof Error ? error.message : 'Gagal menyimpan data.';
if (message.includes('jumlah') || message.includes('Jumlah')) {
errors.quantity = message;
} else if (message.includes('Stok')) {
errors.quantity = message;
} else if (message.includes('pengajuan') || message.includes('menunggu')) {
errors.quantity = message;
} else {
toast.error(message);
}
} finally {
processing.value = false;
}
}
function formatNumber(value: number): string {
return value.toLocaleString('id-ID');
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
Transfer Stok Ecer
</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div class="rounded-lg border p-3 space-y-1.5">
<p class="text-sm font-medium">{{ productName }}</p>
<Field v-if="hasMultipleVariants">
<FieldLabel for="transfer-variant" required>Pilih Varian</FieldLabel>
<Select :model-value="selectedVariantId ? String(selectedVariantId) : undefined" @update:model-value="selectedVariantId = Number($event)">
<SelectTrigger id="transfer-variant" class="w-full">
<SelectValue placeholder="Pilih varian" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="variant in variants"
:key="variant.id"
:value="String(variant.id)"
>
{{ variant.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
<p v-if="!hasMultipleVariants" class="text-xs text-muted-foreground">Varian: {{ displayVariantName }}</p>
<div class="flex justify-between text-xs pt-1">
<span class="text-muted-foreground">Stok Bagus</span>
<span class="font-medium tabular-nums">{{ formatNumber(displayGoodStock) }}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-muted-foreground">Stok Ecer</span>
<span class="font-medium tabular-nums text-blue-600">{{ formatNumber(displayRetailStock) }}</span>
</div>
</div>
<p class="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-md px-3 py-2">
Transfer stok memerlukan persetujuan owner. Stok akan berubah setelah disetujui.
</p>
<form @submit.prevent="submit">
<FieldGroup>
<FieldSet class="grid gap-4">
<Field>
<FieldLabel for="retail-quantity" required>Jumlah Transfer</FieldLabel>
<NumberInput
id="retail-quantity"
v-model="form.quantity"
:max="displayGoodStock"
placeholder="Masukkan jumlah"
/>
<FieldError
v-if="errors.quantity"
:errors="[errors.quantity]"
/>
</Field>
<Field>
<FieldLabel for="retail-notes">Catatan</FieldLabel>
<Textarea
id="retail-notes"
v-model="form.notes"
rows="2"
placeholder="Contoh: Transfer untuk kebutuhan kasir"
/>
</Field>
</FieldSet>
</FieldGroup>
<DialogFooter class="mt-6">
<Button
type="button"
variant="outline"
:disabled="processing"
@click="open = false"
>
Batal
</Button>
<Button type="submit" :disabled="processing || !form.quantity || Number(form.quantity) < 1 || (hasMultipleVariants && !selectedVariantId)">
<Save class="size-4" />
{{ processing ? 'Mengirim...' : 'Ajukan Transfer' }}
</Button>
</DialogFooter>
</form>
</div>
</DialogContent>
</Dialog>
</template>