gigafibre-fsm/apps/ops/src/pages/ReportTaxesPage.vue
louispaulb 10d0854e51 refactor: reduce token count, DRY code, consolidate docs
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>
2026-04-13 08:39:58 -04:00

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>