feat: add legacy PDF generation support and invoice discount fields
This commit is contained in:
@@ -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'); },
|
||||
|
||||
Reference in New Issue
Block a user