feat: add legacy PDF generation support and invoice discount fields

This commit is contained in:
I-Fun
2026-04-07 07:49:35 +07:00
parent 737ba24293
commit 0e6726b0a6
13 changed files with 710 additions and 45 deletions

View File

@@ -98,16 +98,28 @@
<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>
{{-- Taxable and Discount checkboxes --}}
<div class="mt-2 flex items-center gap-4">
<div class="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 class="flex items-center gap-2" x-show="addDiscount">
<input type="hidden" :name="`items[${index}][is_discountable]`" value="0">
<input type="checkbox" :name="`items[${index}][is_discountable]`" :id="`discountable_${index}`"
x-model="item.is_discountable" @change="recalc()" value="1"
class="w-3.5 h-3.5 accent-pink-400">
<label :for="`discountable_${index}`"
class="text-xs text-gray-500 cursor-pointer select-none">
Discount
</label>
</div>
</div>
</div>
</template>
@@ -116,10 +128,26 @@
{{-- Totals --}}
<div class="bg-pink-50 rounded-2xl p-4 mb-5">
<div class="mb-3 border-b border-pink-100 pb-3 flex flex-col sm:flex-row sm:items-center gap-y-2 gap-x-4 min-h-[32px]">
<label class="flex items-center gap-2 cursor-pointer shrink-0 w-max">
<input type="checkbox" x-model="addDiscount" @change="toggleDiscount()" class="w-4 h-4 accent-pink-400">
<span class="text-sm font-bold text-gray-700">Add Discount</span>
</label>
<div x-show="addDiscount" class="flex items-center gap-3 flex-1 overflow-hidden" style="display: none;">
<input type="range" name="discount_rate" min="5" max="90" step="5" x-model.number="discountRate" @input="recalc()" class="w-32 sm:w-48 accent-pink-500 h-2 bg-pink-100 rounded-lg appearance-none cursor-pointer shrink-0 hover:accent-pink-600 transition-all">
<span class="font-bold text-pink-600 text-sm shrink-0 w-8 text-right"><span x-text="discountRate"></span>%</span>
<span class="text-xs text-pink-500 font-medium tracking-wide truncate border-l border-pink-200 pl-3" x-text="getDiscountLabel()"></span>
</div>
</div>
<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-pink-500" x-show="addDiscount && discountAmount > 0">
<span>Discount (<span x-text="discountRate"></span>%)</span>
<span class="font-semibold">- Rp <span x-text="formatNum(discountAmount)"></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>
@@ -211,19 +239,68 @@
function invoiceForm() {
return {
showCustomFields: {{ old('use_custom_fields', false) ? 'true' : 'false' }},
items: [{ description: @json($invoiceSettings->default_service_description ?? ''), quantity: 1, unit_price: 0, is_taxable: true }],
addDiscount: false,
discountRate: 5,
items: [{ description: @json($invoiceSettings->default_service_description ?? ''), quantity: 1, unit_price: 0, is_taxable: true, is_discountable: false }],
taxRate: {{ $invoiceSettings->default_tax_rate ?? 11 }},
get subtotal() {
return this.items.reduce((s, i) => s + (i.quantity * i.unit_price), 0);
},
get discountAmount() {
if (!this.addDiscount) return 0;
const discountable = this.items
.filter(i => i.is_discountable)
.reduce((s, i) => s + (i.quantity * i.unit_price), 0);
return Math.round(discountable * (this.discountRate / 100));
},
get totalSubtotal() {
return this.subtotal - this.discountAmount;
},
get taxAmount() {
const taxable = this.items
.filter(i => i.is_taxable)
.reduce((s, i) => s + (i.quantity * i.unit_price), 0);
.reduce((s, i) => {
let itemTotal = i.quantity * i.unit_price;
if (this.addDiscount && i.is_discountable) {
itemTotal -= itemTotal * (this.discountRate / 100);
}
return s + itemTotal;
}, 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 }); },
get total() { return this.totalSubtotal; }, // grand total = pre-tax
getDiscountLabel() {
const labels = {
5: 'Small thank-you discount 🙏',
10: 'Appreciation bonus ✨',
15: 'Loyalty perk unlocked 🤝',
20: 'Client appreciation deal 💼',
25: 'Special partner discount ⭐',
30: 'Valued client offer 🎯',
35: 'Priority client savings 📌',
40: 'Strong appreciation rate 💎',
45: 'Preferred client pricing 🏅',
50: 'Half-off partner special 🎉',
55: 'Premium loyalty reward 🧩',
60: 'Major appreciation offer 🚀',
65: 'VIP-level savings 🏆',
70: 'Elite client advantage 👑',
75: 'Strategic partner bonus 🤝✨',
80: 'Executive-tier discount 💼🔥',
85: 'Signature client reward 🖋️',
90: 'Ultimate partnership rate 🌟'
};
return labels[this.discountRate] || '';
},
toggleDiscount() {
if (this.addDiscount) {
this.items.forEach(i => i.is_discountable = true);
} else {
// We can leave them checked or uncheck them, usually uncheck
this.items.forEach(i => i.is_discountable = false);
}
},
addItem() { this.items.push({ description: '', quantity: 1, unit_price: 0, is_taxable: true, is_discountable: this.addDiscount }); },
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
recalc() { },
formatNum(n) { return Number(n).toLocaleString('id-ID'); },