206 lines
12 KiB
PHP
206 lines
12 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', 'New Invoice')
|
||
@section('page-title', 'Create Invoice 🧾')
|
||
@section('page-subtitle', 'Fill in the invoice details')
|
||
|
||
@section('content')
|
||
<div class="max-w-3xl mx-auto">
|
||
<div class="cat-card p-4 lg:p-8" x-data="invoiceForm()">
|
||
<form action="{{ route('invoices.store') }}" method="POST" enctype="multipart/form-data">
|
||
@csrf
|
||
|
||
{{-- 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 <span
|
||
class="text-pink-400">*</span></label>
|
||
<input type="text" name="title" value="{{ old('title') }}" class="input-soft" required
|
||
placeholder="e.g. Instagram Collaboration - Brand X">
|
||
</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 }}" {{ request('client') == $client->id ? 'selected' : '' }}>
|
||
{{ $client->name }}
|
||
</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 mb-1">Currency</label>
|
||
<select name="currency" class="input-soft">
|
||
@foreach(['IDR' => 'IDR (Rupiah)', 'USD' => 'USD (Dollar)', 'SGD' => 'SGD', 'MYR' => 'MYR (Ringgit)'] as $code => $label)
|
||
<option value="{{ $code }}" {{ old('currency', $invoiceSettings->default_currency ?? 'IDR') === $code ? 'selected' : '' }}>{{ $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', today()->toDateString()) }}"
|
||
class="input-soft" required>
|
||
</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', today()->addDays(14)->toDateString()) }}" class="input-soft" required>
|
||
</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', $invoiceSettings->default_tax_rate ?? 11) }}" 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">
|
||
{{-- Description full width --}}
|
||
<input type="text" :name="`items[${index}][description]`" x-model="item.description"
|
||
class="input-soft text-sm mb-2 bg-white" placeholder="Service description" required>
|
||
{{-- Qty / Price / Total in a row --}}
|
||
<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" placeholder="0">
|
||
</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>
|
||
|
||
{{-- 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', $invoiceSettings->default_bank_name) }}" class="input-soft text-sm"
|
||
placeholder="BCA, Mandiri...">
|
||
</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', $invoiceSettings->default_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', $invoiceSettings->default_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', $invoiceSettings->default_bank_holder) }}"
|
||
class="input-soft text-sm">
|
||
</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"
|
||
placeholder="Payment terms, etc.">{{ old('notes', $invoiceSettings->default_notes) }}</textarea>
|
||
</div>
|
||
|
||
<div class="mb-5">
|
||
<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-2">Leave blank to use your default signature from settings.</p>
|
||
<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 class="flex flex-col sm:flex-row gap-3">
|
||
<button type="submit" class="btn-sakura flex-1 py-3">🧾 Create Invoice</button>
|
||
<a href="{{ route('invoices.index') }}"
|
||
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 invoiceForm() {
|
||
return {
|
||
items: [{ description: @json($invoiceSettings->default_service_description ?? ''), quantity: 1, unit_price: 0, is_taxable: true }],
|
||
taxRate: {{ $invoiceSettings->default_tax_rate ?? 11 }},
|
||
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; }, // grand total = pre-tax (tax borne by client)
|
||
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 |