gigafibre-fsm/apps/client/src/pages/InvoicesPage.vue
louispaulb 656f9d638c feat: telephony UI, performance indexes, Twilio softphone, lazy-load invoices
- 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>
2026-04-02 13:59:59 -04:00

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>