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>
100 lines
5.5 KiB
Vue
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>
|