feat: add shipping cost field to purchase management, including validation, calculations, and UI updates for total cost
This commit is contained in:
parent
db3f6887ea
commit
2a2cc0fb96
@ -28,6 +28,7 @@ public function rules(): array
|
||||
$rules = [
|
||||
'supplier_id' => ['required', 'integer', Rule::exists('suppliers', 'id')->whereNull('deleted_at')],
|
||||
'discount' => ['nullable', 'integer', 'min:0'],
|
||||
'shipping_cost' => ['nullable', 'integer', 'min:0'],
|
||||
'notes' => ['nullable', 'string', 'max:100'],
|
||||
...$this->photoRules(),
|
||||
];
|
||||
@ -53,6 +54,7 @@ public function attributes(): array
|
||||
return [
|
||||
'supplier_id' => 'supplier',
|
||||
'discount' => 'diskon',
|
||||
'shipping_cost' => 'ongkir',
|
||||
'notes' => 'keterangan',
|
||||
'items' => 'bahan baku',
|
||||
'items.*.raw_material_price_id' => 'bahan baku',
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
#[Appends([
|
||||
'subtotal_formatted',
|
||||
'discount_formatted',
|
||||
'shipping_cost_formatted',
|
||||
'total_formatted',
|
||||
'created_at_formatted',
|
||||
])]
|
||||
@ -33,6 +34,7 @@ protected function casts(): array
|
||||
return [
|
||||
'subtotal' => 'integer',
|
||||
'discount' => 'integer',
|
||||
'shipping_cost' => 'integer',
|
||||
'total' => 'integer',
|
||||
];
|
||||
}
|
||||
@ -66,6 +68,13 @@ public function discountFormatted(): Attribute
|
||||
);
|
||||
}
|
||||
|
||||
public function shippingCostFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp '.number_format($this->shipping_cost, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
public function subtotalFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@ -223,13 +223,15 @@ public function create(array $validated, User $user): Purchase
|
||||
|
||||
$subtotal = $draftItems->sum('subtotal');
|
||||
$discount = (int) ($validated['discount'] ?? 0);
|
||||
$total = max($subtotal - $discount, 0);
|
||||
$shippingCost = (int) ($validated['shipping_cost'] ?? 0);
|
||||
$total = max($subtotal - $discount + $shippingCost, 0);
|
||||
|
||||
$purchase = Purchase::create([
|
||||
'supplier_id' => $validated['supplier_id'],
|
||||
'created_by_id' => $user->id,
|
||||
'subtotal' => $subtotal,
|
||||
'discount' => $discount,
|
||||
'shipping_cost' => $shippingCost,
|
||||
'total' => $total,
|
||||
'notes' => $validated['notes'] ?? null,
|
||||
]);
|
||||
@ -273,11 +275,13 @@ public function update(Purchase $purchase, array $validated): void
|
||||
$lineItems = $this->buildLineItems($validated['items']);
|
||||
$subtotal = array_sum(array_column($lineItems, 'subtotal'));
|
||||
$discount = (int) ($validated['discount'] ?? 0);
|
||||
$total = max($subtotal - $discount, 0);
|
||||
$shippingCost = (int) ($validated['shipping_cost'] ?? 0);
|
||||
$total = max($subtotal - $discount + $shippingCost, 0);
|
||||
|
||||
$purchase->supplier_id = $validated['supplier_id'];
|
||||
$purchase->subtotal = $subtotal;
|
||||
$purchase->discount = $discount;
|
||||
$purchase->shipping_cost = $shippingCost;
|
||||
$purchase->total = $total;
|
||||
$purchase->notes = $validated['notes'] ?? null;
|
||||
$purchase->save();
|
||||
|
||||
@ -191,7 +191,6 @@ public function update(RawMaterial $rawMaterial, array $validated): void
|
||||
{
|
||||
DB::transaction(function () use ($validated, $rawMaterial): void {
|
||||
$rawMaterial->name = $validated['name'];
|
||||
$rawMaterial->unit = $validated['unit'];
|
||||
$rawMaterial->save();
|
||||
|
||||
$submittedPriceIds = collect($validated['prices'])
|
||||
|
||||
@ -16,13 +16,15 @@ public function definition(): array
|
||||
{
|
||||
$subtotal = fake()->numberBetween(100_000, 10_000_000);
|
||||
$discount = fake()->numberBetween(0, (int) ($subtotal * 0.1));
|
||||
$shippingCost = fake()->numberBetween(0, 100_000);
|
||||
|
||||
return [
|
||||
'supplier_id' => Supplier::factory(),
|
||||
'created_by_id' => User::factory(),
|
||||
'subtotal' => $subtotal,
|
||||
'discount' => $discount,
|
||||
'total' => $subtotal - $discount,
|
||||
'shipping_cost' => $shippingCost,
|
||||
'total' => $subtotal - $discount + $shippingCost,
|
||||
'notes' => fake()->optional()->sentence(3),
|
||||
];
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ public function up(): void
|
||||
|
||||
$table->unsignedBigInteger('subtotal');
|
||||
$table->unsignedBigInteger('discount')->default(0);
|
||||
$table->unsignedBigInteger('shipping_cost')->default(0);
|
||||
$table->unsignedBigInteger('total');
|
||||
$table->string('notes', 100)->nullable();
|
||||
|
||||
|
||||
@ -53,7 +53,7 @@ function destroyAttendance() {
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-xl">
|
||||
<DialogContent class="sm:max-w-xl max-h-[90vh] overflow-y-auto scrollbar-thin">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail Presensi</DialogTitle>
|
||||
<DialogDescription v-if="attendance">
|
||||
@ -64,7 +64,7 @@ function destroyAttendance() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="attendance" class="space-y-4">
|
||||
<div v-if="attendance" class="space-y-4 pb-2">
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
<div>
|
||||
<dt class="text-muted-foreground">
|
||||
|
||||
@ -117,6 +117,8 @@ function getGroupedItems(items: any[]): GroupedPurchaseItems[] {
|
||||
}}</strong></span>
|
||||
<span>Diskon <strong class="text-foreground">{{ purchase.discount_formatted
|
||||
}}</strong></span>
|
||||
<span v-if="purchase.shipping_cost > 0">Ongkir <strong class="text-foreground">{{
|
||||
purchase.shipping_cost_formatted }}</strong></span>
|
||||
<span>Total <strong class="text-primary">{{ purchase.total_formatted }}</strong></span>
|
||||
</div>
|
||||
<p v-if="purchase.notes" class="text-muted-foreground text-sm">
|
||||
|
||||
@ -49,6 +49,7 @@ const props = defineProps<{
|
||||
initialData?: {
|
||||
supplier_id: string;
|
||||
discount: string;
|
||||
shipping_cost: string;
|
||||
notes: string;
|
||||
items: PurchaseCartItem[];
|
||||
photos?: MediaItem | null;
|
||||
@ -69,6 +70,7 @@ const currentPhotoUrl = computed(() => props.initialData?.photos?.url ?? null);
|
||||
const form = useForm({
|
||||
supplier_id: '',
|
||||
discount: '',
|
||||
shipping_cost: '',
|
||||
notes: '',
|
||||
photos: [] as File[],
|
||||
remove_media_ids: [] as number[],
|
||||
@ -88,6 +90,7 @@ function populateForm() {
|
||||
|
||||
form.supplier_id = props.initialData.supplier_id;
|
||||
form.discount = props.initialData.discount;
|
||||
form.shipping_cost = props.initialData.shipping_cost ?? '0';
|
||||
form.notes = props.initialData.notes;
|
||||
form.photos = [];
|
||||
form.remove_media_ids = [];
|
||||
@ -153,8 +156,9 @@ const subtotal = computed(() =>
|
||||
);
|
||||
|
||||
const discountAmount = computed(() => Number(parseRupiah(form.discount)) || 0);
|
||||
const shippingAmount = computed(() => Number(parseRupiah(form.shipping_cost)) || 0);
|
||||
|
||||
const total = computed(() => Math.max(subtotal.value - discountAmount.value, 0));
|
||||
const total = computed(() => Math.max(subtotal.value - discountAmount.value + shippingAmount.value, 0));
|
||||
|
||||
function getCartItem(priceId: number): PurchaseCartItem | undefined {
|
||||
return cart.value.find((item) => item.raw_material_price_id === priceId);
|
||||
@ -290,6 +294,7 @@ function buildFormData(): FormData {
|
||||
|
||||
formData.append('supplier_id', form.supplier_id);
|
||||
formData.append('discount', parseRupiah(form.discount));
|
||||
formData.append('shipping_cost', parseRupiah(form.shipping_cost));
|
||||
formData.append('notes', form.notes);
|
||||
|
||||
if (props.method === 'put') {
|
||||
@ -511,6 +516,11 @@ function submit() {
|
||||
<RupiahInput id="discount" v-model="form.discount" placeholder="0" />
|
||||
<FieldError :errors="formErrors(form, 'discount')" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="shipping_cost">Ongkir</FieldLabel>
|
||||
<RupiahInput id="shipping_cost" v-model="form.shipping_cost" placeholder="0" />
|
||||
<FieldError :errors="formErrors(form, 'shipping_cost')" />
|
||||
</Field>
|
||||
<div class="flex justify-between text-base font-semibold">
|
||||
<span>Total</span>
|
||||
<span class="text-primary">Rp {{ formatRupiah(total) }}</span>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Copy, Plus, Save, Trash2 } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { DecimalInput } from '@/components/form/decimal-input';
|
||||
import { MultipleImageUploadField } from '@/components/form/image-upload-field';
|
||||
@ -94,6 +94,14 @@ const form = useForm({
|
||||
unit: props.initialData?.unit ?? '',
|
||||
});
|
||||
|
||||
const isEditMode = computed(() => props.method === 'put');
|
||||
|
||||
const unitLabel = computed(() => {
|
||||
const option = props.units.find((item) => item.value === form.unit);
|
||||
|
||||
return option?.label ?? form.unit;
|
||||
});
|
||||
|
||||
function allPricesHaveSameValue(items: RawMaterialPriceFormItem[]): boolean {
|
||||
if (items.length <= 1) {
|
||||
return true;
|
||||
@ -237,7 +245,10 @@ function submit() {
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="unit" required>Satuan</FieldLabel>
|
||||
<Select v-model="form.unit">
|
||||
<Select
|
||||
v-if="!isEditMode"
|
||||
v-model="form.unit"
|
||||
>
|
||||
<SelectTrigger id="unit" class="w-full">
|
||||
<SelectValue placeholder="Pilih satuan" />
|
||||
</SelectTrigger>
|
||||
@ -247,6 +258,16 @@ function submit() {
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div
|
||||
v-else
|
||||
id="unit"
|
||||
class="flex h-9 w-full items-center rounded-md border bg-muted/40 px-3 text-sm"
|
||||
>
|
||||
{{ unitLabel }}
|
||||
</div>
|
||||
<p v-if="isEditMode" class="text-xs text-muted-foreground">
|
||||
Satuan tidak dapat diubah agar stok tetap konsisten.
|
||||
</p>
|
||||
<FieldError :errors="formErrors(form, 'unit')" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
@ -16,6 +16,7 @@ const props = defineProps<{
|
||||
const initialData = computed(() => ({
|
||||
supplier_id: String(props.purchase.supplier_id),
|
||||
discount: String(props.purchase.discount),
|
||||
shipping_cost: String(props.purchase.shipping_cost ?? 0),
|
||||
notes: props.purchase.notes ?? '',
|
||||
items: props.purchase.items.map((item) => ({
|
||||
raw_material_price_id: item.raw_material_price_id,
|
||||
|
||||
@ -24,6 +24,8 @@ export type PurchaseListItem = {
|
||||
} | null;
|
||||
subtotal_formatted: string;
|
||||
discount_formatted: string;
|
||||
shipping_cost: number;
|
||||
shipping_cost_formatted: string;
|
||||
total_formatted: string;
|
||||
notes: string | null;
|
||||
created_at_formatted: string;
|
||||
@ -61,6 +63,7 @@ export type PurchaseEditItem = {
|
||||
id: number;
|
||||
supplier_id: number;
|
||||
discount: number;
|
||||
shipping_cost: number;
|
||||
notes: string | null;
|
||||
photos?: MediaItem | null;
|
||||
items: Array<{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user