gigafibre-fsm/apps/ops/src/pages/RapportsPage.vue
louispaulb 893ac8cd4a Déclutter: primitives réutilisables + palette ⌘K + Planif épurée
Ajoute 3 primitives partagées pour standardiser le désencombrement
(philosophie 3 paliers : coup d'œil → survol → détail) :
- OverflowMenu.vue : « ⋮ » standardisé (une action primaire visible,
  le secondaire dans le menu).
- DisclosureSection.vue : section repliée par défaut (résumé + corps
  révélé), état persisté par utilisateur (useUserPrefs).
- CommandPalette.vue + useCommandPalette.js : palette globale ⌘K
  (actions rapides + pages + actions contextuelles de page + recherche
  clients/équipe), registre d'actions par page = escape hatch.

Câblage MainLayout : palette montée + raccourci Cmd/Ctrl+K + indice ⌘K
dans la recherche desktop.

Adoptions :
- RapportsPage : PageHeader + stubs « Opérations » repliés + rapports
  enregistrés dans la palette.
- DashboardPage : bloc admin replié (DisclosureSection) + PPA rétrogradé
  dans un OverflowMenu (anti clic accidentel) + actions dans la palette.
- PlanificationPage : le panneau « Jobs à assigner » n'ouvre plus au
  chargement ; alerte « N à assigner » par jour (clic → panneau filtré) ;
  undo/redo/refresh regroupés dans un OverflowMenu ; actions dans la palette.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 06:53:24 -04:00

158 lines
5.8 KiB
Vue

<template>
<q-page padding>
<PageHeader title="Rapports" :count="financeReports.length" />
<!-- Comptabilité section (rapports opérationnels) -->
<div class="text-subtitle2 text-grey-7 text-weight-bold q-mb-sm">Comptabilité & Finances</div>
<div class="row q-col-gutter-md q-mb-lg">
<div class="col-12 col-md-6 col-lg-4" v-for="report in financeReports" :key="report.title">
<div class="ops-card cursor-pointer" style="min-height:120px" @click="$router.push(report.route)">
<div class="row items-center q-mb-sm">
<q-icon :name="report.icon" size="28px" :color="report.color" class="q-mr-sm" />
<div class="text-subtitle1 text-weight-bold">{{ report.title }}</div>
</div>
<div class="text-caption text-grey-6">{{ report.description }}</div>
</div>
</div>
</div>
<!-- Opérations : rapports pas encore prêts → repliés par défaut (ne polluent pas la vue). -->
<DisclosureSection title="Opérations" icon="insights" persist-key="rapports.ops">
<template #summary>{{ opsReports.length }} rapports — bientôt disponibles</template>
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6 col-lg-4" v-for="report in opsReports" :key="report.title">
<div class="ops-card cursor-pointer" style="min-height:120px;opacity:0.6" @click="report.action">
<div class="row items-center q-mb-sm">
<q-icon :name="report.icon" size="28px" :color="report.color" class="q-mr-sm" />
<div class="text-subtitle1 text-weight-bold">{{ report.title }}</div>
<q-badge class="q-ml-sm" color="grey-4" text-color="grey-7" label="Bientôt" />
</div>
<div class="text-caption text-grey-6">{{ report.description }}</div>
</div>
</div>
</div>
</DisclosureSection>
</q-page>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import PageHeader from 'src/components/shared/PageHeader.vue'
import DisclosureSection from 'src/components/shared/DisclosureSection.vue'
import { useCommandPalette } from 'src/composables/useCommandPalette'
const router = useRouter()
const financeReports = [
{
title: 'Explorateur de revenus',
description: 'MRR net récurrent, regroupé par catégorie, code postal, ville, segment ou plan — critères ajoutables en chips. Un seul rapport unifié.',
icon: 'pivot_table_chart',
color: 'primary',
route: '/rapports/explorateur-revenus',
},
{
title: 'Revenus par compte',
description: 'Évolution mensuelle des revenus par compte GL (4000-5000). Graphique stacked + export CSV.',
icon: 'trending_up',
color: 'green-6',
route: '/rapports/revenus',
},
{
title: 'Ventes détaillées',
description: 'Liste des factures avec sous-total, TPS, TVQ et total. Inclut les notes de crédit.',
icon: 'receipt_long',
color: 'primary',
route: '/rapports/ventes',
},
{
title: 'Rapport de taxes (TPS/TVQ)',
description: 'Taxes perçues vs payées par période mensuelle ou trimestrielle. Pour les déclarations fiscales.',
icon: 'account_balance',
color: 'purple-6',
route: '/rapports/taxes',
},
{
title: 'Âge des comptes',
description: 'Comptes à recevoir par tranche d\'ancienneté: courant, 30, 60, 90, 120+ jours.',
icon: 'account_balance_wallet',
color: 'red-6',
route: '/rapports/ar',
},
{
title: 'Clients qui paient cher — Internet',
description: 'Adresses dont la facture Internet mensuelle (forfait + rabais, hors TV/téléphonie) dépasse un seuil. Pour cibler les forfaits à réviser.',
icon: 'trending_up',
color: 'deep-orange-6',
route: '/rapports/internet-cher',
},
{
title: 'Factures récurrentes négatives',
description: 'Comptes dont le total récurrent mensuel est négatif (rabais > frais) — anomalies de données à corriger. F reste autoritaire.',
icon: 'error',
color: 'red-7',
route: '/rapports/factures-negatives',
},
{
title: 'Services actifs sur comptes résiliés',
description: 'Comptes terminés dans F qui gardent des services récurrents actifs (charges + crédits). Liste de ménage — désactiver au niveau service (sauf employés à internet fourni).',
icon: 'cleaning_services',
color: 'blue-7',
route: '/rapports/resilies-actifs',
},
]
const opsReports = [
{
title: 'Clients par territoire',
description: 'Répartition des clients actifs par zone géographique.',
icon: 'map',
color: 'teal-6',
action: () => {},
},
{
title: 'Tickets par priorité',
description: 'Analyse des tickets ouverts et temps de résolution moyen.',
icon: 'bug_report',
color: 'orange-6',
action: () => {},
},
{
title: 'Équipements déployés',
description: 'Inventaire des ONT, routeurs et modems par statut.',
icon: 'router',
color: 'blue-6',
action: () => {},
},
{
title: 'Abonnements actifs',
description: 'Liste des abonnements avec plan, montant et ancienneté.',
icon: 'subscriptions',
color: 'purple-6',
action: () => {},
},
{
title: 'Dispatch performance',
description: 'Taux de complétion des tâches et temps moyen par technicien.',
icon: 'speed',
color: 'red-6',
action: () => {},
},
]
// Chaque rapport devient joignable par la palette (⌘K) — l'index visuel reste épuré,
// mais rien n'est « caché » : taper le nom d'un rapport l'ouvre directement.
const { registerActions, unregisterActions } = useCommandPalette()
onMounted(() => registerActions('rapports', financeReports.map(r => ({
id: 'rapport-' + r.route,
label: r.title,
caption: 'Rapport',
icon: r.icon,
group: 'Rapports',
keywords: r.description,
run: () => router.push(r.route),
}))))
onUnmounted(() => unregisterActions('rapports'))
</script>