- Add PostgreSQL performance indexes migration script (1000x faster queries) Sales Invoice: 1,248ms → 28ms, Payment Entry: 443ms → 31ms Indexes on customer/party columns for all major tables - Disable 3CX poller (PBX_ENABLED flag, using Twilio instead) - Add TelephonyPage: full CRUD UI for Routr/Fonoster resources (trunks, agents, credentials, numbers, domains, peers) - Add PhoneModal + usePhone composable (Twilio WebRTC softphone) - Lazy-load invoices/payments (initial 5, expand on demand) - Parallelize all API calls in ClientDetailPage (no waterfall) - Add targo-hub service (SSE relay, SMS, voice, telephony API) - Customer portal: invoice detail, ticket detail, messages pages - Remove dead Ollama nginx upstream Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
4.0 KiB
Vue
136 lines
4.0 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<div class="page-title">Factures</div>
|
|
|
|
<q-table
|
|
:rows="invoices"
|
|
:columns="columns"
|
|
row-key="name"
|
|
:loading="loading"
|
|
:pagination="pagination"
|
|
@request="onRequest"
|
|
@row-click="(evt, row) => router.push('/invoices/' + row.name)"
|
|
flat bordered
|
|
class="bg-white clickable-table"
|
|
no-data-label="Aucune facture"
|
|
>
|
|
<template #body-cell-posting_date="props">
|
|
<q-td :props="props">{{ formatShortDate(props.value) }}</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-grand_total="props">
|
|
<q-td :props="props" class="text-right">{{ formatMoney(props.value) }}</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-outstanding_amount="props">
|
|
<q-td :props="props" class="text-right">
|
|
<span :class="props.value > 0 ? 'status-unpaid' : 'status-paid'">
|
|
{{ formatMoney(props.value) }}
|
|
</span>
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-status="props">
|
|
<q-td :props="props">
|
|
<q-badge :color="statusColor(props.value)" :label="statusLabel(props.value)" />
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-actions="props">
|
|
<q-td :props="props">
|
|
<q-btn flat dense round icon="picture_as_pdf" color="primary"
|
|
@click="downloadPDF(props.row.name)" :loading="downloading === props.row.name" />
|
|
</q-td>
|
|
</template>
|
|
</q-table>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useCustomerStore } from 'src/stores/customer'
|
|
import { fetchInvoices, countInvoices, fetchInvoicePDF } from 'src/api/portal'
|
|
import { useFormatters } from 'src/composables/useFormatters'
|
|
|
|
const router = useRouter()
|
|
const store = useCustomerStore()
|
|
const { formatShortDate, formatMoney } = useFormatters()
|
|
|
|
const invoices = ref([])
|
|
const loading = ref(false)
|
|
const downloading = ref(null)
|
|
const totalCount = ref(0)
|
|
|
|
const pagination = ref({
|
|
page: 1,
|
|
rowsPerPage: 20,
|
|
rowsNumber: 0,
|
|
sortBy: 'posting_date',
|
|
descending: true,
|
|
})
|
|
|
|
const columns = [
|
|
{ name: 'name', label: 'No.', field: 'name', align: 'left', sortable: true },
|
|
{ name: 'posting_date', label: 'Date', field: 'posting_date', align: 'left', sortable: true },
|
|
{ name: 'grand_total', label: 'Total', field: 'grand_total', align: 'right', sortable: true },
|
|
{ name: 'outstanding_amount', label: 'Solde', field: 'outstanding_amount', align: 'right', sortable: true },
|
|
{ name: 'status', label: 'Statut', field: 'status', align: 'center' },
|
|
{ name: 'actions', label: '', field: 'actions', align: 'center' },
|
|
]
|
|
|
|
function statusColor (s) {
|
|
if (s === 'Paid') return 'positive'
|
|
if (s === 'Overdue') return 'negative'
|
|
if (s === 'Unpaid') return 'warning'
|
|
return 'grey'
|
|
}
|
|
|
|
function statusLabel (s) {
|
|
const map = { Paid: 'Payée', Unpaid: 'Impayée', Overdue: 'En retard', 'Partly Paid': 'Partielle' }
|
|
return map[s] || s
|
|
}
|
|
|
|
async function loadPage (page, pageSize) {
|
|
loading.value = true
|
|
try {
|
|
const data = await fetchInvoices(store.customerId, { page, pageSize })
|
|
invoices.value = data
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function onRequest (props) {
|
|
const { page, rowsPerPage } = props.pagination
|
|
await loadPage(page, rowsPerPage)
|
|
pagination.value.page = page
|
|
pagination.value.rowsPerPage = rowsPerPage
|
|
pagination.value.rowsNumber = totalCount.value
|
|
}
|
|
|
|
async function downloadPDF (name) {
|
|
downloading.value = name
|
|
try {
|
|
const blob = await fetchInvoicePDF(name)
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `${name}.pdf`
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
} catch (e) {
|
|
console.error('PDF download failed:', e)
|
|
} finally {
|
|
downloading.value = null
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!store.customerId) return
|
|
totalCount.value = await countInvoices(store.customerId)
|
|
pagination.value.rowsNumber = totalCount.value
|
|
await loadPage(1, 20)
|
|
})
|
|
</script>
|