fix: syntax error in Invoices.vue
This commit is contained in:
467
src/views/Invoices.vue
Normal file
467
src/views/Invoices.vue
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Invoices</h1>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="view = 'list'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 rounded-lg transition-colors',
|
||||||
|
view === 'list'
|
||||||
|
? 'bg-amber text-background'
|
||||||
|
: 'border border-border text-text-primary hover:bg-surface-elevated'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
List
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="view = 'create'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 rounded-lg transition-colors',
|
||||||
|
view === 'create'
|
||||||
|
? 'bg-amber text-background'
|
||||||
|
: 'border border-border text-text-primary hover:bg-surface-elevated'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<div v-if="view === 'list'" class="bg-surface border border-border rounded-lg overflow-hidden">
|
||||||
|
<div v-if="invoicesStore.invoices.length > 0" class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-surface-elevated">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-medium text-text-secondary">Invoice #</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-medium text-text-secondary">Client</th>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-medium text-text-secondary">Date</th>
|
||||||
|
<th class="px-4 py-3 text-right text-sm font-medium text-text-secondary">Amount</th>
|
||||||
|
<th class="px-4 py-3 text-center text-sm font-medium text-text-secondary">Status</th>
|
||||||
|
<th class="px-4 py-3 text-center text-sm font-medium text-text-secondary">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-border">
|
||||||
|
<tr
|
||||||
|
v-for="invoice in invoicesStore.invoices"
|
||||||
|
:key="invoice.id"
|
||||||
|
class="hover:bg-surface-elevated transition-colors"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 text-text-primary font-medium">
|
||||||
|
{{ invoice.invoice_number }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-text-primary">
|
||||||
|
{{ getClientName(invoice.client_id) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-text-secondary">
|
||||||
|
{{ formatDate(invoice.date) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right text-text-primary font-mono">
|
||||||
|
${{ invoice.total.toFixed(2) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'px-2 py-1 text-xs font-medium rounded',
|
||||||
|
invoice.status === 'paid' ? 'bg-green-900 text-green-200' :
|
||||||
|
invoice.status === 'pending' ? 'bg-yellow-900 text-yellow-200' :
|
||||||
|
'bg-red-900 text-red-200'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ invoice.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="viewInvoice(invoice)"
|
||||||
|
class="p-2 text-text-secondary hover:text-amber transition-colors"
|
||||||
|
title="View"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="exportPDF(invoice)"
|
||||||
|
class="p-2 text-text-secondary hover:text-amber transition-colors"
|
||||||
|
title="Export PDF"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirmDelete(invoice)"
|
||||||
|
class="p-2 text-text-secondary hover:text-error transition-colors"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-text-secondary py-12">
|
||||||
|
No invoices yet. Create your first invoice!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create View -->
|
||||||
|
<div v-else-if="view === 'create'" class="bg-surface border border-border rounded-lg p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Create Invoice</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleCreate" class="space-y-4">
|
||||||
|
<!-- Client -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-text-secondary mb-1">Client *</label>
|
||||||
|
<select
|
||||||
|
v-model="createForm.client_id"
|
||||||
|
required
|
||||||
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-text-primary focus:outline-none focus:border-amber"
|
||||||
|
>
|
||||||
|
<option :value="0">Select a client</option>
|
||||||
|
<option
|
||||||
|
v-for="client in clientsStore.clients"
|
||||||
|
:key="client.id"
|
||||||
|
:value="client.id"
|
||||||
|
>
|
||||||
|
{{ client.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-text-secondary mb-1">Invoice Date *</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-text-primary focus:outline-none focus:border-amber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-text-secondary mb-1">Due Date</label>
|
||||||
|
<input
|
||||||
|
v-model="createForm.due_date"
|
||||||
|
type="date"
|
||||||
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-text-primary focus:outline-none focus:border-amber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tax Rate -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-text-secondary mb-1">Tax Rate (%)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="createForm.tax_rate"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.01"
|
||||||
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-text-primary focus:outline-none focus:border-amber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-text-secondary mb-1">Discount ($)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="createForm.discount"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-text-primary focus:outline-none focus:border-amber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-text-secondary mb-1">Notes</label>
|
||||||
|
<textarea
|
||||||
|
v-model="createForm.notes"
|
||||||
|
rows="3"
|
||||||
|
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-text-primary focus:outline-none focus:border-amber"
|
||||||
|
placeholder="Additional notes for the invoice"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calculated Total -->
|
||||||
|
<div class="bg-surface-elevated rounded-lg p-4">
|
||||||
|
<div class="flex justify-between text-text-secondary mb-2">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span>${{ calculateSubtotal().toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-text-secondary mb-2">
|
||||||
|
<span>Tax ({{ createForm.tax_rate }}%):</span>
|
||||||
|
<span>${{ calculateTax().toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-text-secondary mb-2">
|
||||||
|
<span>Discount:</span>
|
||||||
|
<span>-${{ createForm.discount.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-text-primary font-bold text-lg pt-2 border-t border-border">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span>${{ calculateTotal().toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="view = 'list'"
|
||||||
|
class="px-4 py-2 border border-border text-text-primary rounded-lg hover:bg-surface-elevated transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 bg-amber text-background font-medium rounded-lg hover:bg-amber-hover transition-colors"
|
||||||
|
>
|
||||||
|
Create Invoice
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Invoice Detail View -->
|
||||||
|
<div
|
||||||
|
v-if="showDetailDialog"
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
@click.self="showDetailDialog = false"
|
||||||
|
>
|
||||||
|
<div class="bg-surface border border-border rounded-lg w-full max-w-2xl mx-4 p-6 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="flex items-start justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-text-primary">Invoice {{ selectedInvoice?.invoice_number }}</h2>
|
||||||
|
<p class="text-text-secondary">{{ getClientName(selectedInvoice?.client_id || 0) }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="showDetailDialog = false"
|
||||||
|
class="p-2 text-text-secondary hover:text-text-primary transition-colors"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-text-secondary">Invoice Date</p>
|
||||||
|
<p class="text-text-primary">{{ formatDate(selectedInvoice?.date || '') }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-text-secondary">Due Date</p>
|
||||||
|
<p class="text-text-primary">{{ selectedInvoice?.due_date ? formatDate(selectedInvoice.due_date) : '-' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-border pt-4">
|
||||||
|
<div class="flex justify-between text-text-secondary mb-2">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span>${{ (selectedInvoice?.subtotal || 0).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-text-secondary mb-2">
|
||||||
|
<span>Tax ({{ selectedInvoice?.tax_rate || 0 }}%):</span>
|
||||||
|
<span>${{ (selectedInvoice?.tax_amount || 0).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-text-secondary mb-2">
|
||||||
|
<span>Discount:</span>
|
||||||
|
<span>-${{ (selectedInvoice?.discount || 0).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-text-primary font-bold text-lg pt-2 border-t border-border">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span>${{ (selectedInvoice?.total || 0).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedInvoice?.notes" class="border-t border-border pt-4">
|
||||||
|
<p class="text-sm text-text-secondary mb-1">Notes</p>
|
||||||
|
<p class="text-text-primary">{{ selectedInvoice.notes }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
@click="exportPDF(selectedInvoice!)"
|
||||||
|
class="px-4 py-2 bg-amber text-background font-medium rounded-lg hover:bg-amber-hover transition-colors"
|
||||||
|
>
|
||||||
|
Export PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<div
|
||||||
|
v-if="showDeleteDialog"
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
@click.self="showDeleteDialog = false"
|
||||||
|
>
|
||||||
|
<div class="bg-surface border border-border rounded-lg w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-2">Delete Invoice</h2>
|
||||||
|
<p class="text-text-secondary mb-6">
|
||||||
|
Are you sure you want to delete invoice "{{ invoiceToDelete?.invoice_number }}"? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
@click="showDeleteDialog = false"
|
||||||
|
class="px-4 py-2 border border-border text-text-primary rounded-lg hover:bg-surface-elevated transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleDelete"
|
||||||
|
class="px-4 py-2 bg-error text-white font-medium rounded-lg hover:opacity-90 transition-opacity"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { useInvoicesStore, type Invoice } from '../stores/invoices'
|
||||||
|
import { useClientsStore } from '../stores/clients'
|
||||||
|
|
||||||
|
const invoicesStore = useInvoicesStore()
|
||||||
|
const clientsStore = useClientsStore()
|
||||||
|
|
||||||
|
// View state
|
||||||
|
const view = ref<'list' | 'create'>('list')
|
||||||
|
|
||||||
|
// Dialog state
|
||||||
|
const showDetailDialog = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const selectedInvoice = ref<Invoice | null>(null)
|
||||||
|
const invoiceToDelete = ref<Invoice | null>(null)
|
||||||
|
|
||||||
|
// Create form
|
||||||
|
const createForm = reactive({
|
||||||
|
client_id: 0,
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
due_date: '',
|
||||||
|
tax_rate: 0,
|
||||||
|
discount: 0,
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get client name by ID
|
||||||
|
function getClientName(clientId: number): string {
|
||||||
|
const client = clientsStore.clients.find(c => c.id === clientId)
|
||||||
|
return client?.name || 'Unknown Client'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
if (!dateString) return '-'
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate subtotal (simplified - would need line items in a real app)
|
||||||
|
function calculateSubtotal(): number {
|
||||||
|
// For now, just return 0 - would need time entries or line items
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate tax
|
||||||
|
function calculateTax(): number {
|
||||||
|
return calculateSubtotal() * (createForm.tax_rate / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total
|
||||||
|
function calculateTotal(): number {
|
||||||
|
return calculateSubtotal() + calculateTax() - createForm.discount
|
||||||
|
}
|
||||||
|
|
||||||
|
// View invoice details
|
||||||
|
function viewInvoice(invoice: Invoice) {
|
||||||
|
selectedInvoice.value = invoice
|
||||||
|
showDetailDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm delete
|
||||||
|
function confirmDelete(invoice: Invoice) {
|
||||||
|
invoiceToDelete.value = invoice
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle delete
|
||||||
|
async function handleDelete() {
|
||||||
|
if (invoiceToDelete.value?.id) {
|
||||||
|
await invoicesStore.deleteInvoice(invoiceToDelete.value.id)
|
||||||
|
}
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
invoiceToDelete.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle create
|
||||||
|
async function handleCreate() {
|
||||||
|
if (!createForm.client_id) {
|
||||||
|
alert('Please select a client')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtotal = calculateSubtotal()
|
||||||
|
const taxAmount = subtotal * (createForm.tax_rate / 100)
|
||||||
|
const total = subtotal + taxAmount - createForm.discount
|
||||||
|
|
||||||
|
const invoice: Invoice = {
|
||||||
|
client_id: createForm.client_id,
|
||||||
|
invoice_number: `INV-${Date.now()}`,
|
||||||
|
date: createForm.date,
|
||||||
|
due_date: createForm.due_date || undefined,
|
||||||
|
subtotal,
|
||||||
|
tax_rate: createForm.tax_rate,
|
||||||
|
tax_amount: taxAmount,
|
||||||
|
discount: createForm.discount,
|
||||||
|
total,
|
||||||
|
notes: createForm.notes || undefined,
|
||||||
|
status: 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
|
await invoicesStore.createInvoice(invoice)
|
||||||
|
view.value = 'list'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export PDF (placeholder - would need PDF generation)
|
||||||
|
async function exportPDF(invoice: Invoice) {
|
||||||
|
try {
|
||||||
|
await invoke('export_invoice_pdf', { invoiceId: invoice.id })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export PDF:', error)
|
||||||
|
alert('Failed to export PDF. This feature requires backend implementation.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
invoicesStore.fetchInvoices(),
|
||||||
|
clientsStore.fetchClients()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user