gigafibre-fsm/apps/ops/src/pages/ReportTaxesPage.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

181 lines
9.9 KiB
Vue

<template>
<ReportScaffold
title="Rapport de taxes (TPS / TVQ)"
:loading="loading" :error="error" :has-run="hasRun"
:cards="cards" :columns="columns" :rows="periods" row-key="label"
:pagination="{ rowsPerPage: 0 }"
empty="Aucune donnée de taxe pour cette période."
@retry="loadReport" @export="doExport">
<template #filters>
<q-input v-model="startDate" type="date" label="Début" dense outlined stack-label style="width:150px;max-width:100%" />
<q-input v-model="endDate" type="date" label="Fin" dense outlined stack-label style="width:150px;max-width:100%" />
<q-btn-toggle v-model="periodType" :options="[{ label: 'Mensuel', value: 'monthly' }, { label: 'Trimestriel', value: 'quarterly' }]" dense no-caps unelevated toggle-color="primary" />
<q-btn color="primary" label="Générer" icon="play_arrow" @click="loadReport" :loading="loading" />
</template>
<template #chart>
<div v-if="periods.length" class="ops-card q-mb-md" style="height:340px;position:relative"><canvas ref="chartCanvas"></canvas></div>
</template>
<template #body-cell-tps_net="props"><q-td :props="props"><span class="text-weight-bold" :class="props.value >= 0 ? 'text-negative' : 'text-positive'">{{ formatMoney(props.value) }}</span></q-td></template>
<template #body-cell-tvq_net="props"><q-td :props="props"><span class="text-weight-bold" :class="props.value >= 0 ? 'text-negative' : 'text-positive'">{{ formatMoney(props.value) }}</span></q-td></template>
<template #body-cell-total_tax_net="props"><q-td :props="props"><span class="text-weight-bold" :class="props.value >= 0 ? 'text-negative' : 'text-positive'">{{ formatMoney(props.value) }}</span></q-td></template>
<template #body-cell-revenue="props"><q-td :props="props"><span class="text-weight-bold text-positive">{{ formatMoney(props.value) }}</span></q-td></template>
<template #below>
<div v-if="periods.length" class="ops-card q-mt-md">
<div class="text-subtitle2 text-weight-bold q-mb-sm">Résumé — Période complète</div>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<table class="full-width" style="border-collapse:collapse">
<tr class="text-weight-bold" style="border-bottom:1px solid #e0e0e0">
<td class="q-pa-xs"></td>
<td class="q-pa-xs text-right">Perçue</td>
<td class="q-pa-xs text-right">Payée</td>
<td class="q-pa-xs text-right">Net à remettre</td>
</tr>
<tr style="border-bottom:1px solid #f0f0f0">
<td class="q-pa-xs text-weight-bold">TPS (5%)</td>
<td class="q-pa-xs text-right">{{ formatMoney(totals.tps_collected) }}</td>
<td class="q-pa-xs text-right">{{ formatMoney(totals.tps_paid) }}</td>
<td class="q-pa-xs text-right text-weight-bold">{{ formatMoney(totals.tps_net) }}</td>
</tr>
<tr style="border-bottom:1px solid #f0f0f0">
<td class="q-pa-xs text-weight-bold">TVQ (9.975%)</td>
<td class="q-pa-xs text-right">{{ formatMoney(totals.tvq_collected) }}</td>
<td class="q-pa-xs text-right">{{ formatMoney(totals.tvq_paid) }}</td>
<td class="q-pa-xs text-right text-weight-bold">{{ formatMoney(totals.tvq_net) }}</td>
</tr>
<tr class="text-weight-bold" style="border-top:2px solid #333">
<td class="q-pa-xs">Total taxes</td>
<td class="q-pa-xs text-right">{{ formatMoney(totals.tps_collected + totals.tvq_collected) }}</td>
<td class="q-pa-xs text-right">{{ formatMoney(totals.tps_paid + totals.tvq_paid) }}</td>
<td class="q-pa-xs text-right text-negative">{{ formatMoney(totals.total_tax_net) }}</td>
</tr>
</table>
</div>
<div class="col-12 col-md-6">
<div class="text-caption text-grey-6 q-mb-xs">Revenu total (comptes 4000+)</div>
<div class="text-h5 text-weight-bold text-positive">{{ formatMoney(totals.revenue) }}</div>
<div class="text-caption text-grey-5 q-mt-sm">
Le montant « Net à remettre » correspond à la taxe perçue sur les ventes moins la taxe payée sur les achats. Un montant positif = à remettre au gouvernement.
</div>
</div>
</div>
</div>
</template>
</ReportScaffold>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { fetchTaxReport } from 'src/api/reports'
import { formatMoney } from 'src/composables/useFormatters'
import { exportCsv } from 'src/composables/useCsvExport'
import ReportScaffold from 'src/components/shared/ReportScaffold.vue'
import Chart from 'chart.js/auto'
const now = new Date()
const startDate = ref(new Date(now.getFullYear(), 0, 1).toISOString().slice(0, 10))
const endDate = ref(now.toISOString().slice(0, 10))
const periodType = ref('monthly')
const loading = ref(false)
const error = ref('')
const hasRun = ref(false)
const periods = ref([])
const chartCanvas = ref(null)
let chartInstance = null
const columns = [
{ name: 'label', label: 'Période', field: 'label', align: 'left' },
{ name: 'tps_collected', label: 'TPS perçue', field: 'tps_collected', align: 'right', format: v => formatMoney(v) },
{ name: 'tps_paid', label: 'TPS payée', field: 'tps_paid', align: 'right', format: v => formatMoney(v) },
{ name: 'tps_net', label: 'TPS net', field: 'tps_net', align: 'right' },
{ name: 'tvq_collected', label: 'TVQ perçue', field: 'tvq_collected', align: 'right', format: v => formatMoney(v) },
{ name: 'tvq_paid', label: 'TVQ payée', field: 'tvq_paid', align: 'right', format: v => formatMoney(v) },
{ name: 'tvq_net', label: 'TVQ net', field: 'tvq_net', align: 'right' },
{ name: 'total_tax_net', label: 'Total net', field: 'total_tax_net', align: 'right' },
{ name: 'revenue', label: 'Revenu', field: 'revenue', align: 'right' },
]
const totals = computed(() => {
const t = { tps_collected: 0, tps_paid: 0, tps_net: 0, tvq_collected: 0, tvq_paid: 0, tvq_net: 0, total_tax_net: 0, revenue: 0 }
for (const p of periods.value) {
t.tps_collected += p.tps_collected; t.tps_paid += p.tps_paid; t.tps_net += p.tps_net
t.tvq_collected += p.tvq_collected; t.tvq_paid += p.tvq_paid; t.tvq_net += p.tvq_net
t.total_tax_net += p.total_tax_net; t.revenue += p.revenue
}
return t
})
const cards = computed(() => {
if (!periods.value.length) return []
const t = totals.value
return [
{ label: 'TPS net', value: formatMoney(t.tps_net), valueClass: t.tps_net >= 0 ? 'text-negative' : 'text-positive' },
{ label: 'TVQ net', value: formatMoney(t.tvq_net), valueClass: t.tvq_net >= 0 ? 'text-negative' : 'text-positive' },
{ label: 'Total à remettre', value: formatMoney(t.total_tax_net), valueClass: 'text-negative' },
{ label: 'Revenu', value: formatMoney(t.revenue), valueClass: 'text-positive' },
]
})
async function loadReport () {
loading.value = true
error.value = ''
hasRun.value = true
try {
const res = await fetchTaxReport(startDate.value, endDate.value, periodType.value)
periods.value = res.periods || []
await nextTick()
renderChart()
} catch (e) {
console.error('Tax report error:', e)
error.value = 'Échec du chargement du rapport — réessayez.'
} finally {
loading.value = false
}
}
function renderChart () {
if (chartInstance) chartInstance.destroy()
if (!chartCanvas.value || !periods.value.length) return
const labels = periods.value.map(p => p.label)
chartInstance = new Chart(chartCanvas.value, {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'TPS perçue', data: periods.value.map(p => p.tps_collected), backgroundColor: '#6366f188', borderColor: '#6366f1', borderWidth: 1 },
{ label: 'TPS payée', data: periods.value.map(p => -p.tps_paid), backgroundColor: '#6366f144', borderColor: '#6366f1', borderWidth: 1, borderDash: [4, 4] },
{ label: 'TVQ perçue', data: periods.value.map(p => p.tvq_collected), backgroundColor: '#10b98188', borderColor: '#10b981', borderWidth: 1 },
{ label: 'TVQ payée', data: periods.value.map(p => -p.tvq_paid), backgroundColor: '#10b98144', borderColor: '#10b981', borderWidth: 1, borderDash: [4, 4] },
{ label: 'Revenu', data: periods.value.map(p => p.revenue), type: 'line', borderColor: '#f59e0b', backgroundColor: 'transparent', borderWidth: 2, yAxisID: 'y1', tension: 0.3, pointRadius: 3 },
],
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 11 } } },
tooltip: { callbacks: { label: ctx => ctx.dataset.label + ': ' + formatMoney(Math.abs(ctx.parsed.y)) } },
},
scales: {
x: { grid: { display: false } },
y: { position: 'left', ticks: { callback: v => formatMoney(v) }, grid: { color: 'rgba(0,0,0,0.06)' } },
y1: { position: 'right', ticks: { callback: v => (v / 1000).toFixed(0) + 'k$' }, grid: { display: false } },
},
},
})
}
function doExport () {
if (!periods.value.length) return
const headers = ['Période', 'TPS perçue', 'TPS payée', 'TPS net', 'TVQ perçue', 'TVQ payée', 'TVQ net', 'Total net', 'Revenu']
const data = periods.value.map(p => [p.label, p.tps_collected.toFixed(2), p.tps_paid.toFixed(2), p.tps_net.toFixed(2), p.tvq_collected.toFixed(2), p.tvq_paid.toFixed(2), p.tvq_net.toFixed(2), p.total_tax_net.toFixed(2), p.revenue.toFixed(2)])
const t = totals.value
const totalRow = ['TOTAL', t.tps_collected.toFixed(2), t.tps_paid.toFixed(2), t.tps_net.toFixed(2), t.tvq_collected.toFixed(2), t.tvq_paid.toFixed(2), t.tvq_net.toFixed(2), t.total_tax_net.toFixed(2), t.revenue.toFixed(2)]
exportCsv(`taxes_${startDate.value}_${endDate.value}`, headers, data, { totalRow })
}
</script>