Backend services: - targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas, extract dispatch scoring weights, trim section dividers across 9 files - modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(), consolidate DM query factory, fix duplicate username fill bug, trim headers (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%) Frontend: - useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into 6 focused helpers (processOnlineStatus, processWanIPs, processRadios, processMeshNodes, processClients, checkRadioIssues) - EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments Documentation (17 → 13 files, -1,400 lines): - New consolidated README.md (architecture, services, dependencies, auth) - Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md - Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md - Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md - Update ROADMAP.md with current phase status - Delete CONTEXT.md (absorbed into README) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
10 KiB
Vue
242 lines
10 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<div class="row items-center q-mb-md">
|
|
<q-btn flat dense icon="arrow_back" to="/rapports" class="q-mr-sm" />
|
|
<div class="text-h6 text-weight-bold">Rapport de taxes (TPS / TVQ)</div>
|
|
<q-space />
|
|
<q-btn flat dense icon="download" label="CSV" @click="downloadCSV" :disable="!periods.length" />
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="row q-col-gutter-sm q-mb-md items-end">
|
|
<div class="col-auto">
|
|
<q-input v-model="startDate" type="date" label="Début" dense outlined style="width:160px" />
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-input v-model="endDate" type="date" label="Fin" dense outlined style="width:160px" />
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn-toggle
|
|
v-model="periodType"
|
|
:options="[{ label: 'Mensuel', value: 'monthly' }, { label: 'Trimestriel', value: 'quarterly' }]"
|
|
dense no-caps unelevated toggle-color="primary"
|
|
/>
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn color="primary" label="Générer" icon="play_arrow" @click="loadReport" :loading="loading" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chart -->
|
|
<div v-if="periods.length" class="ops-card q-mb-md" style="height:340px;position:relative">
|
|
<canvas ref="chartCanvas"></canvas>
|
|
</div>
|
|
|
|
<!-- Data table -->
|
|
<q-table
|
|
v-if="periods.length"
|
|
:rows="periods"
|
|
:columns="columns"
|
|
row-key="label"
|
|
flat bordered
|
|
class="ops-table"
|
|
:pagination="{ rowsPerPage: 0 }"
|
|
hide-pagination
|
|
dense
|
|
>
|
|
<!-- Color cells for net amounts -->
|
|
<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>
|
|
</q-table>
|
|
|
|
<!-- Summary for selected period -->
|
|
<div v-if="periods.length" class="q-mt-md">
|
|
<div class="ops-card">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="!loading && !periods.length" class="text-center text-grey-5 q-pa-xl">
|
|
<q-icon name="account_balance" size="64px" class="q-mb-md" />
|
|
<div>Sélectionnez une période et cliquez <b>Générer</b></div>
|
|
<div class="text-caption q-mt-sm">Ce rapport produit les montants TPS/TVQ pour les déclarations fiscales</div>
|
|
</div>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, nextTick } from 'vue'
|
|
import { fetchTaxReport } from 'src/api/reports'
|
|
import { formatMoney } from 'src/composables/useFormatters'
|
|
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 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
|
|
})
|
|
|
|
async function loadReport () {
|
|
loading.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)
|
|
} 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 downloadCSV () {
|
|
if (!periods.value.length) return
|
|
const header = ['Période', 'TPS perçue', 'TPS payée', 'TPS net', 'TVQ perçue', 'TVQ payée', 'TVQ net', 'Total net', 'Revenu']
|
|
const csvRows = 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
|
|
csvRows.push(['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),
|
|
])
|
|
const csv = [header, ...csvRows].map(r => r.join(',')).join('\n')
|
|
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8' })
|
|
const a = document.createElement('a')
|
|
a.href = URL.createObjectURL(blob)
|
|
a.download = `taxes_${startDate.value}_${endDate.value}.csv`
|
|
a.click()
|
|
}
|
|
</script>
|