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>
181 lines
9.9 KiB
Vue
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>
|