gigafibre-fsm/apps/client/src/pages/DashboardPage.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

97 lines
3.4 KiB
Vue

<template>
<q-page padding>
<div class="page-title">Bonjour, {{ store.customerName }}</div>
<div class="row q-col-gutter-md">
<!-- Unpaid invoices -->
<div class="col-12 col-sm-6 col-md-4">
<div class="portal-card">
<div class="text-caption text-grey-7">Factures impayées</div>
<div class="summary-value">{{ unpaidCount }}</div>
<div v-if="unpaidTotal > 0" class="text-body2 text-grey-7">
{{ formatMoney(unpaidTotal) }} à payer
</div>
<q-btn flat color="primary" label="Voir les factures" to="/invoices" class="q-mt-sm" no-caps />
</div>
</div>
<!-- Open tickets -->
<div class="col-12 col-sm-6 col-md-4">
<div class="portal-card">
<div class="text-caption text-grey-7">Tickets ouverts</div>
<div class="summary-value">{{ openTickets }}</div>
<q-btn flat color="primary" label="Voir le support" to="/tickets" class="q-mt-sm" no-caps />
</div>
</div>
<!-- Quick action -->
<div class="col-12 col-sm-6 col-md-4">
<div class="portal-card">
<div class="text-caption text-grey-7">Besoin d'aide?</div>
<div class="text-h6 q-mt-xs">Contactez-nous</div>
<q-btn flat color="primary" label="Nouveau ticket" to="/tickets" class="q-mt-sm" no-caps icon="add" />
</div>
</div>
</div>
<!-- Recent invoices -->
<div v-if="recentInvoices.length" class="q-mt-lg">
<div class="text-subtitle1 text-weight-medium q-mb-sm">Dernières factures</div>
<q-list bordered separator class="rounded-borders bg-white">
<q-item v-for="inv in recentInvoices" :key="inv.name" clickable @click="$router.push('/invoices/' + inv.name)">
<q-item-section>
<q-item-label>{{ inv.name }}</q-item-label>
<q-item-label caption>{{ formatDate(inv.posting_date) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label>{{ formatMoney(inv.grand_total) }}</q-item-label>
<q-item-label caption :class="statusClass(inv.status)">
{{ inv.status }}
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</q-page>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useCustomerStore } from 'src/stores/customer'
import { fetchInvoices, fetchTickets } from 'src/api/portal'
import { useFormatters } from 'src/composables/useFormatters'
const store = useCustomerStore()
const { formatDate, formatMoney } = useFormatters()
const recentInvoices = ref([])
const allTickets = ref([])
const unpaidCount = computed(() =>
recentInvoices.value.filter(i => i.outstanding_amount > 0).length,
)
const unpaidTotal = computed(() =>
recentInvoices.value.filter(i => i.outstanding_amount > 0)
.reduce((sum, i) => sum + i.outstanding_amount, 0),
)
const openTickets = computed(() =>
allTickets.value.filter(t => t.status === 'Open').length,
)
function statusClass (status) {
if (status === 'Paid') return 'status-paid'
if (status === 'Unpaid' || status === 'Overdue') return 'status-unpaid'
return ''
}
onMounted(async () => {
if (!store.customerId) return
const [invoices, tickets] = await Promise.all([
fetchInvoices(store.customerId, { pageSize: 5 }),
fetchTickets(store.customerId),
])
recentInvoices.value = invoices
allTickets.value = tickets
})
</script>