This commit is contained in:
@@ -90,24 +90,32 @@ const isDraft = invoice.status === 'draft';
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{(invoice.status === 'sent' && invoice.type === 'invoice') && (
|
||||
{(invoice.status !== 'paid' && invoice.status !== 'void' && invoice.type === 'invoice') && (
|
||||
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
|
||||
<input type="hidden" name="status" value="paid" />
|
||||
<button type="submit" class="btn btn-success text-white">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<Icon name="heroicons:check" class="w-5 h-5" />
|
||||
Mark Paid
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{(invoice.status === 'sent' && invoice.type === 'quote') && (
|
||||
{(invoice.status !== 'accepted' && invoice.status !== 'declined' && invoice.status !== 'void' && invoice.type === 'quote') && (
|
||||
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
|
||||
<input type="hidden" name="status" value="accepted" />
|
||||
<button type="submit" class="btn btn-success text-white">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<Icon name="heroicons:check" class="w-5 h-5" />
|
||||
Mark Accepted
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{(invoice.type === 'quote' && invoice.status === 'accepted') && (
|
||||
<form method="POST" action={`/api/invoices/${invoice.id}/convert`}>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<Icon name="heroicons:document-duplicate" class="w-5 h-5" />
|
||||
Convert to Invoice
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div role="button" tabindex="0" class="btn btn-square btn-ghost border border-base-300">
|
||||
<Icon name="heroicons:ellipsis-horizontal" class="w-6 h-6" />
|
||||
@@ -125,12 +133,6 @@ const isDraft = invoice.status === 'draft';
|
||||
Download PDF
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" onclick="window.print()">
|
||||
<Icon name="heroicons:printer" class="w-4 h-4" />
|
||||
Print
|
||||
</button>
|
||||
</li>
|
||||
{invoice.status !== 'void' && invoice.status !== 'draft' && (
|
||||
<li>
|
||||
<form method="POST" action={`/api/invoices/${invoice.id}/status`}>
|
||||
@@ -196,7 +198,19 @@ const isDraft = invoice.status === 'draft';
|
||||
{client ? (
|
||||
<div>
|
||||
<div class="font-bold text-lg">{client.name}</div>
|
||||
<div class="text-base-content/70">{client.email}</div>
|
||||
{client.email && <div class="text-base-content/70">{client.email}</div>}
|
||||
{client.phone && <div class="text-base-content/70">{client.phone}</div>}
|
||||
{(client.street || client.city || client.state || client.zip || client.country) && (
|
||||
<div class="text-sm text-base-content/70 mt-2 space-y-0.5">
|
||||
{client.street && <div>{client.street}</div>}
|
||||
{(client.city || client.state || client.zip) && (
|
||||
<div>
|
||||
{[client.city, client.state, client.zip].filter(Boolean).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{client.country && <div>{client.country}</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div class="italic text-base-content/40">Client deleted</div>
|
||||
@@ -280,9 +294,16 @@ const isDraft = invoice.status === 'draft';
|
||||
<span class="text-base-content/60">Subtotal</span>
|
||||
<span class="font-medium">{formatCurrency(invoice.subtotal)}</span>
|
||||
</div>
|
||||
{(invoice.taxRate ?? 0) > 0 && (
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-base-content/60">Tax ({invoice.taxRate}%)</span>
|
||||
{((invoice.taxRate ?? 0) > 0 || isDraft) && (
|
||||
<div class="flex justify-between text-sm items-center group">
|
||||
<span class="text-base-content/60 flex items-center gap-2">
|
||||
Tax ({invoice.taxRate ?? 0}%)
|
||||
{isDraft && (
|
||||
<button type="button" onclick="document.getElementById('tax_modal').showModal()" class="btn btn-ghost btn-xs btn-square opacity-0 group-hover:opacity-100 transition-opacity" title="Edit Tax Rate">
|
||||
<Icon name="heroicons:pencil" class="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
<span class="font-medium">{formatCurrency(invoice.taxAmount)}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -305,10 +326,42 @@ const isDraft = invoice.status === 'draft';
|
||||
{/* Edit Notes (Draft Only) - Simplistic approach */}
|
||||
{isDraft && !invoice.notes && (
|
||||
<div class="mt-8 text-center">
|
||||
<a href={`/dashboard/invoices/${invoice.id}/edit`} class="btn btn-sm btn-ghost">Add Notes</a>
|
||||
<a href={`/dashboard/invoices/${invoice.id}/edit`} class="btn btn-sm btn-primary">Edit Details</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax Modal -->
|
||||
<dialog id="tax_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Update Tax Rate</h3>
|
||||
<p class="py-4">Enter the tax percentage to apply to the subtotal.</p>
|
||||
<form method="POST" action={`/api/invoices/${invoice.id}/update-tax`}>
|
||||
<div class="form-control mb-6">
|
||||
<label class="label">
|
||||
<span class="label-text">Tax Rate (%)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="taxRate"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
class="input input-bordered w-full"
|
||||
value={invoice.taxRate ?? 0}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick="document.getElementById('tax_modal').close()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</DashboardLayout>
|
||||
|
||||
@@ -99,7 +99,9 @@ const dueDateStr = invoice.dueDate.toISOString().split('T')[0];
|
||||
<!-- Due Date -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Due Date</span>
|
||||
<span class="label-text font-semibold">
|
||||
{invoice.type === 'quote' ? 'Valid Until' : 'Due Date'}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -128,7 +130,7 @@ const dueDateStr = invoice.dueDate.toISOString().split('T')[0];
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="form-control">
|
||||
<div class="form-control flex flex-col">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Notes / Terms</span>
|
||||
</label>
|
||||
|
||||
@@ -109,7 +109,7 @@ const getStatusColor = (status: string) => {
|
||||
|
||||
<div class="card bg-base-100 shadow-xl border border-base-200">
|
||||
<div class="card-body p-0">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto md:overflow-visible pb-32 md:pb-0">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr class="bg-base-200/50">
|
||||
|
||||
@@ -47,7 +47,9 @@ if (lastInvoice) {
|
||||
const match = lastInvoice.number.match(/(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]) + 1;
|
||||
const prefix = lastInvoice.number.replace(match[0], '');
|
||||
let prefix = lastInvoice.number.replace(match[0], '');
|
||||
// Ensure we don't carry over an EST- prefix to an invoice
|
||||
if (prefix === 'EST-') prefix = 'INV-';
|
||||
nextInvoiceNumber = prefix + num.toString().padStart(match[0].length, '0');
|
||||
}
|
||||
}
|
||||
@@ -68,7 +70,9 @@ if (lastQuote) {
|
||||
const match = lastQuote.number.match(/(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]) + 1;
|
||||
const prefix = lastQuote.number.replace(match[0], '');
|
||||
let prefix = lastQuote.number.replace(match[0], '');
|
||||
// Ensure we don't carry over an INV- prefix to a quote
|
||||
if (prefix === 'INV-') prefix = 'EST-';
|
||||
nextQuoteNumber = prefix + num.toString().padStart(match[0].length, '0');
|
||||
}
|
||||
}
|
||||
@@ -167,7 +171,7 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
|
||||
<!-- Due Date -->
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Due Date</span>
|
||||
<span class="label-text font-semibold" id="dueDateLabel">Due Date</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -212,22 +216,26 @@ const defaultDueDate = nextMonth.toISOString().split('T')[0];
|
||||
// Update number based on document type
|
||||
const typeRadios = document.querySelectorAll('input[name="type"]');
|
||||
const numberInput = document.getElementById('documentNumber') as HTMLInputElement | null;
|
||||
const dueDateLabel = document.getElementById('dueDateLabel');
|
||||
|
||||
if (numberInput) {
|
||||
const invoiceNumber = numberInput.dataset.invoiceNumber || 'INV-001';
|
||||
const quoteNumber = numberInput.dataset.quoteNumber || 'EST-001';
|
||||
const invoiceNumber = numberInput?.dataset.invoiceNumber || 'INV-001';
|
||||
const quoteNumber = numberInput?.dataset.quoteNumber || 'EST-001';
|
||||
|
||||
typeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (numberInput) {
|
||||
if (target.value === 'quote') {
|
||||
numberInput.value = quoteNumber;
|
||||
} else if (target.value === 'invoice') {
|
||||
numberInput.value = invoiceNumber;
|
||||
}
|
||||
typeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
if (numberInput) {
|
||||
if (target.value === 'quote') {
|
||||
numberInput.value = quoteNumber;
|
||||
} else if (target.value === 'invoice') {
|
||||
numberInput.value = invoiceNumber;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (dueDateLabel) {
|
||||
dueDateLabel.textContent = target.value === 'quote' ? 'Valid Until' : 'Due Date';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user