gigafibre-fsm/apps/ops/src/pages/BillingApprovalsPage.vue
louispaulb 512c4a5f1b feat(ops): dispatch auto complet + perf Boîte/rapports + fix session
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).

Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00

100 lines
5.5 KiB
Vue

<template>
<q-page class="q-pa-md">
<div class="row items-center q-mb-md">
<div>
<div class="text-h6">Approbations facturation à l'activation</div>
<div class="text-caption text-grey-7">Installations complétées en attente d'approbation. <b>Approuver</b> active le service ; la facturation prorata s'enclenchera à l'encaissement (cf. plan d'automatisation).</div>
</div>
<q-space />
<q-btn flat round icon="refresh" :loading="loading" @click="load" />
</div>
<DataTable flat bordered dense :rows="rows" :columns="cols" row-key="job" :loading="loading" :pagination="{ rowsPerPage: 20 }">
<template #body-cell-actions="props">
<q-td :props="props" class="q-gutter-xs">
<q-btn flat dense size="sm" color="primary" icon="visibility" no-caps label="Aperçu" @click="openPreview(props.row)" />
<q-btn unelevated dense size="sm" color="green-7" icon="check" no-caps label="Approuver" :loading="approving === props.row.job" @click="approveRow(props.row)" />
</q-td>
</template>
<template #no-data><div class="full-width text-center q-pa-md text-grey-7">Aucune installation en attente d'approbation.</div></template>
</DataTable>
<q-dialog v-model="prev.open">
<q-card style="min-width: 540px; max-width: 96vw">
<q-card-section class="text-h6 row items-center"><q-icon name="receipt_long" color="primary" class="q-mr-sm" />Aperçu prorata — non facturé</q-card-section>
<q-separator />
<q-card-section v-if="prev.data">
<div class="text-caption text-grey-7 q-mb-sm">
Client {{ prev.data.customer }} · période {{ prev.data.period_start }} → {{ prev.data.period_end }}
· {{ prev.data.billed_days }}/{{ prev.data.period_days }} jours<span v-if="prev.data.courtesy_note"> · {{ prev.data.courtesy_note }}</span>
</div>
<q-markup-table flat dense v-if="prev.data.lines && prev.data.lines.length">
<thead><tr><th class="text-left">Ligne</th><th class="text-center">Type</th><th class="text-right">Plein</th><th class="text-right">Proraté</th></tr></thead>
<tbody>
<tr v-for="(l, i) in prev.data.lines" :key="i">
<td class="text-left">{{ l.label }}</td>
<td class="text-center"><q-badge :color="l.recurring ? 'blue-6' : 'grey-6'">{{ l.recurring ? 'récurrent' : l.kind }}</q-badge></td>
<td class="text-right">{{ money(l.full_amount) }}</td>
<td class="text-right">{{ money(l.prorated_amount) }}</td>
</tr>
</tbody>
<tfoot><tr><td colspan="3" class="text-right text-weight-bold">Total proraté</td><td class="text-right text-weight-bold">{{ money(prev.data.prorated_total) }}</td></tr></tfoot>
</q-markup-table>
<div v-else class="text-grey-7">Aucune ligne à facturer (aucun abonnement en attente pour cette adresse).</div>
<q-banner dense class="bg-amber-1 text-body2 rounded-borders q-mt-sm">Aperçu lecture seule aucune facture n'est créée. L'encaissement n'est pas encore armé.</q-banner>
</q-card-section>
<q-card-section v-else class="text-grey-7">Chargement…</q-card-section>
<q-separator />
<q-card-actions align="right"><q-btn flat label="Fermer" v-close-popup /></q-card-actions>
</q-card>
</q-dialog>
</q-page>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useQuasar } from 'quasar'
import DataTable from 'src/components/shared/DataTable.vue'
import { formatMoney as money } from 'src/composables/useFormatters'
import { getApprovals, getApprovalPreview, approveActivation } from 'src/api/billing'
const $q = useQuasar()
const loading = ref(false)
const approving = ref('')
const rows = ref([])
const cols = [
{ name: 'customer', label: 'Client', field: r => r.customer_name || r.customer || '', align: 'left' },
{ name: 'service_location', label: 'Adresse (service)', field: r => r.service_location || '', align: 'left' },
{ name: 'completed_at', label: 'Complété', field: r => (r.completed_at || '').slice(0, 16).replace('T', ' '), align: 'left' },
{ name: 'job', label: 'Job', field: 'job', align: 'left' },
{ name: 'actions', label: '', field: 'job', align: 'right' }
]
// money() unifié via useFormatters (import ci-dessus) — format fr-CA cohérent
async function load () {
loading.value = true
try { rows.value = await getApprovals() } catch (e) { $q.notify({ type: 'negative', message: e.message }) } finally { loading.value = false }
}
const prev = reactive({ open: false, data: null })
async function openPreview (row) {
prev.open = true; prev.data = null
try { prev.data = await getApprovalPreview(row.job) } catch (e) { $q.notify({ type: 'negative', message: e.message }); prev.open = false }
}
async function approveRow (row) {
const who = row.customer_name || row.customer || row.job
if (!window.confirm(`Approuver l'activation pour ${who} ?\nLe service sera activé. Aucune facture ne sera créée tant que l'encaissement n'est pas armé.`)) return
approving.value = row.job
try {
const r = await approveActivation(row.job)
const n = ((r.activation && r.activation.activated) || []).length
const billed = r.activation && r.activation.written ? ` · facture ${r.activation.invoice}` : ''
$q.notify({ type: 'positive', message: `Approuvé ${n} abonnement(s) activé(s)${billed}` })
await load()
} catch (e) { $q.notify({ type: 'negative', message: e.message }) } finally { approving.value = '' }
}
onMounted(load)
</script>