245 lines
14 KiB
PHP
245 lines
14 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', 'Edit Invoice')
|
||
@section('page-title', 'Edit Invoice 🧾')
|
||
@section('page-subtitle', $invoice->invoice_number)
|
||
|
||
@section('content')
|
||
<div class="max-w-3xl mx-auto">
|
||
<div class="cat-card p-4 lg:p-8" x-data="invoiceEditForm()">
|
||
<form action="{{ route('invoices.update', $invoice) }}" method="POST" enctype="multipart/form-data">
|
||
@csrf @method('PUT')
|
||
|
||
{{-- Header fields --}}
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||
<div class="sm:col-span-2">
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Invoice Title</label>
|
||
<input type="text" name="title" value="{{ old('title', $invoice->title) }}" class="input-soft" required>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Client</label>
|
||
<select name="client_id" class="input-soft">
|
||
<option value="">— No client —</option>
|
||
@foreach($clients as $client)
|
||
<option value="{{ $client->id }}" {{ old('client_id', $invoice->client_id) == $client->id ? 'selected' : '' }}>{{ $client->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Status</label>
|
||
<select name="status" class="input-soft">
|
||
@foreach($statuses as $s)
|
||
<option value="{{ $s->value }}" {{ old('status', $invoice->status->value) === $s->value ? 'selected' : '' }}>{{ $s->label() }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Issue Date</label>
|
||
<input type="date" name="issue_date" value="{{ old('issue_date', $invoice->issue_date->toDateString()) }}" class="input-soft">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Due Date</label>
|
||
<input type="date" name="due_date" value="{{ old('due_date', $invoice->due_date->toDateString()) }}" class="input-soft">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Paid Date</label>
|
||
<input type="date" name="paid_date" value="{{ old('paid_date', $invoice->paid_date?->toDateString()) }}" class="input-soft">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Tax Rate (%)</label>
|
||
<input type="number" name="tax_rate" x-model="taxRate" @input="recalc()"
|
||
value="{{ old('tax_rate', $invoice->tax_rate) }}" class="input-soft" step="0.01">
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Line Items --}}
|
||
<div class="mb-5">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="text-sm font-bold text-gray-700">Line Items</h3>
|
||
<button type="button" @click="addItem()" class="btn-sakura text-xs py-1.5 px-4">+ Add Item</button>
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
<template x-for="(item, index) in items" :key="index">
|
||
<div class="bg-pink-50 rounded-2xl p-3">
|
||
<input type="text" :name="`items[${index}][description]`"
|
||
x-model="item.description"
|
||
class="input-soft text-sm mb-2 bg-white"
|
||
placeholder="Service description" required>
|
||
<div class="flex gap-2 items-center">
|
||
<div class="w-16 shrink-0">
|
||
<label class="text-xs text-gray-400 block mb-0.5">Qty</label>
|
||
<input type="number" :name="`items[${index}][quantity]`"
|
||
x-model.number="item.quantity" @input="recalc()"
|
||
class="input-soft text-sm text-center bg-white px-2" min="1">
|
||
</div>
|
||
<div class="flex-1">
|
||
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
||
<input type="number" :name="`items[${index}][unit_price]`"
|
||
x-model.number="item.unit_price" @input="recalc()"
|
||
class="input-soft text-sm bg-white" min="0" step="1000">
|
||
</div>
|
||
<div class="w-24 shrink-0 text-right">
|
||
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
||
<div class="text-sm font-bold text-gray-700 pt-2">
|
||
Rp <span x-text="formatNum(item.quantity * item.unit_price)"></span>
|
||
</div>
|
||
</div>
|
||
<button type="button" @click="removeItem(index)"
|
||
class="text-red-400 hover:text-red-600 text-lg shrink-0 pt-4">✕</button>
|
||
</div>
|
||
{{-- Taxable checkbox --}}
|
||
<div class="mt-2 flex items-center gap-2">
|
||
<input type="hidden" :name="`items[${index}][is_taxable]`" value="0">
|
||
<input type="checkbox" :name="`items[${index}][is_taxable]`"
|
||
:id="`taxable_${index}`"
|
||
x-model="item.is_taxable" @change="recalc()"
|
||
value="1"
|
||
class="w-3.5 h-3.5 accent-pink-400">
|
||
<label :for="`taxable_${index}`" class="text-xs text-gray-500 cursor-pointer select-none">
|
||
Taxable
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Totals --}}
|
||
<div class="bg-pink-50 rounded-2xl p-4 mb-5">
|
||
<div class="flex justify-between text-sm py-1">
|
||
<span class="text-gray-500">Subtotal (all items)</span>
|
||
<span class="font-semibold">Rp <span x-text="formatNum(subtotal)"></span></span>
|
||
</div>
|
||
<div class="flex justify-between text-sm py-1 text-amber-600">
|
||
<span>Tax (<span x-text="taxRate"></span>% — paid by client)</span>
|
||
<span class="font-semibold">Rp <span x-text="formatNum(taxAmount)"></span></span>
|
||
</div>
|
||
<div class="flex justify-between text-base font-bold py-2 mt-1 border-t border-pink-200">
|
||
<span class="text-gray-700">Grand Total</span>
|
||
<span class="text-pink-600">Rp <span x-text="formatNum(total)"></span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-5 flex items-center gap-2 bg-pink-50/50 p-4 rounded-2xl border border-pink-100">
|
||
<input type="hidden" name="use_custom_fields" value="0">
|
||
<input type="checkbox" id="useCustomFields" name="use_custom_fields" value="1" x-model="showCustomFields" class="w-4 h-4 text-pink-600 bg-white border-pink-300 rounded focus:ring-pink-500 cursor-pointer">
|
||
<label for="useCustomFields" class="text-sm font-bold text-gray-700 cursor-pointer select-none">Show Custom Bank & Signature Settings</label>
|
||
</div>
|
||
|
||
<div x-show="showCustomFields" x-collapse>
|
||
{{-- Bank Info --}}
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-gray-600 mb-1">Bank Name</label>
|
||
<input type="text" name="bank_name" value="{{ old('bank_name', $invoice->bank_name) }}"
|
||
class="input-soft text-sm">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-gray-600 mb-1">Branch Name</label>
|
||
<input type="text" name="branch_name" value="{{ old('branch_name', $invoice->branch_name) }}"
|
||
class="input-soft text-sm" placeholder="e.g. KCP Sudirman">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Number</label>
|
||
<input type="text" name="bank_account" value="{{ old('bank_account', $invoice->bank_account) }}"
|
||
class="input-soft text-sm">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-gray-600 mb-1">Account Holder</label>
|
||
<input type="text" name="bank_holder" value="{{ old('bank_holder', $invoice->bank_holder) }}"
|
||
class="input-soft text-sm">
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Custom Signature Text --}}
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-5">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Name</label>
|
||
<input type="text" name="signer_name" value="{{ old('signer_name', $invoice->signer_name) }}" class="input-soft text-sm" placeholder="e.g. Cathy">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-gray-600 mb-1">Signer Title</label>
|
||
<input type="text" name="signer_title" value="{{ old('signer_title', $invoice->signer_title) }}" class="input-soft text-sm" placeholder="e.g. Director">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-5" x-data="{
|
||
removeSign() {
|
||
if (!confirm('Remove the signature from this invoice?')) return;
|
||
$refs.removeSignInput.value = '1';
|
||
this.$el.closest('form').submit();
|
||
}
|
||
}">
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Custom Signature Image (Optional)</label>
|
||
<p class="text-xs text-gray-400 mb-3">Upload a specific signature for this invoice. Leave blank to keep the current one.</p>
|
||
|
||
@if($invoice->signature_image)
|
||
<div class="flex items-center gap-4 mb-3 p-3 bg-white rounded-xl border border-pink-100">
|
||
<img src="{{ '/storage/' . $invoice->signature_image }}" alt="Signature" class="h-16 object-contain">
|
||
<div>
|
||
<input type="hidden" name="remove_signature" value="0" x-ref="removeSignInput">
|
||
<button type="button" @click="removeSign()" class="text-xs px-3 py-1.5 rounded-full border border-red-200 text-red-400 hover:bg-red-50">
|
||
🗑️ Remove
|
||
</button>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
<input type="file" name="signature_image" accept="image/*" class="w-full text-sm text-gray-600
|
||
file:mr-3 file:py-1.5 file:px-4
|
||
file:rounded-full file:border-0
|
||
file:text-xs file:font-semibold
|
||
file:bg-pink-50 file:text-pink-600
|
||
hover:file:bg-pink-100 cursor-pointer">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-5">
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Notes / Payment Terms</label>
|
||
<textarea name="notes" rows="2" class="input-soft resize-none">{{ old('notes', $invoice->notes) }}</textarea>
|
||
</div>
|
||
|
||
<div class="flex flex-col sm:flex-row gap-3">
|
||
<button type="submit" class="btn-sakura flex-1 py-3">💾 Save Changes</button>
|
||
<a href="{{ route('invoices.show', $invoice) }}"
|
||
class="flex-1 py-3 text-center rounded-full border border-pink-200 text-gray-600 hover:bg-pink-50 font-semibold text-sm">
|
||
Cancel
|
||
</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
@push('scripts')
|
||
<script>
|
||
function invoiceEditForm() {
|
||
@php
|
||
$itemsJson = $invoice->items->map(function($i) {
|
||
return ['description' => $i->description, 'quantity' => $i->quantity, 'unit_price' => (float)$i->unit_price, 'is_taxable' => (bool)$i->is_taxable];
|
||
});
|
||
@endphp
|
||
return {
|
||
showCustomFields: {{ old('use_custom_fields', ($invoice->bank_name || $invoice->signature_image || $invoice->signer_name || $invoice->signer_title) ? true : false) ? 'true' : 'false' }},
|
||
items: @json($itemsJson),
|
||
taxRate: {{ $invoice->tax_rate }},
|
||
get subtotal() {
|
||
return this.items.reduce((s, i) => s + (i.quantity * i.unit_price), 0);
|
||
},
|
||
get taxAmount() {
|
||
const taxable = this.items
|
||
.filter(i => i.is_taxable)
|
||
.reduce((s, i) => s + (i.quantity * i.unit_price), 0);
|
||
return Math.round(taxable * this.taxRate / 100);
|
||
},
|
||
get total() { return this.subtotal; },
|
||
addItem() { this.items.push({ description: '', quantity: 1, unit_price: 0, is_taxable: true }); },
|
||
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
||
recalc() {},
|
||
formatNum(n) { return Number(n).toLocaleString('id-ID'); },
|
||
};
|
||
}
|
||
</script>
|
||
@endpush
|
||
@endsection
|