New Ops report to surface clients whose net monthly Internet bill exceeds a threshold — for spotting plans that should be revised. Hub (lib/legacy-reports.js — new module, read-only MariaDB): - GET /reports/legacy/overpriced-internet (+ .csv variant) - Queries the legacy gestionclient DB directly via a small mysql2 pool (reuses cfg.LEGACY_DB_* — same vars as auth.js sync-legacy; added LEGACY_DB_PASS to the hub .env which was previously unset). - Grain = delivery (service address), NOT account: a multi-unit building (account 13166 has 82 doors / 205 services) would otherwise show a single bogus $2117 line instead of ~45 per door. - Net monthly Internet = SUM of effective per-line price across Internet categories (32 fibre, 4 wireless, 23 camping + optional add-ons 16/17/21), discounts included (products with price<0 are recurring credits like RAB24M -15$). - Effective price = service.hijack ? hijack_price : product.price. - Only recurring lines (product.price_recurr_type=1) — excludes one-time equipment/install charges. - Annual plans (SKU LIKE '%ANN', e.g. FTTH_ANN @ 480$/yr) normalized /12 so they compare correctly against a monthly threshold (was falsely showing $480 → now $40, drops below 90$). - Excludes TV (33,34) and téléphonie (9) entirely. Validated counts at 90$/mo: 983 residential, 297 commercial addresses. Ops UI: - src/pages/ReportInternetCherPage.vue — threshold/segment/add-ons filters, summary cards (count, total monthly, avg, discounts), sortable+filterable table (client, address, net, gross, discount, plan detail with full tooltip, contact), CSV download. - Card on the Rapports hub + route /rapports/internet-cher. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
4.0 KiB
JavaScript
61 lines
4.0 KiB
JavaScript
import { route } from 'quasar/wrappers'
|
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
|
|
const routes = [
|
|
// Tech mobile view (sent via SMS to field techs)
|
|
{
|
|
path: '/j',
|
|
component: () => import('src/layouts/TechLayout.vue'),
|
|
children: [
|
|
{ path: '', name: 'tech-tasks', component: () => import('src/modules/tech/pages/TechTasksPage.vue') },
|
|
{ path: 'job/:name', name: 'tech-job', component: () => import('src/modules/tech/pages/TechJobDetailPage.vue'), props: true },
|
|
{ path: 'scan', name: 'tech-scan', component: () => import('src/modules/tech/pages/TechScanPage.vue') },
|
|
{ path: 'device/:serial', name: 'tech-device', component: () => import('src/modules/tech/pages/TechDevicePage.vue'), props: true },
|
|
{ path: 'diagnostic', name: 'tech-diag', component: () => import('src/modules/tech/pages/TechDiagnosticPage.vue') },
|
|
{ path: 'more', name: 'tech-more', component: () => import('src/modules/tech/pages/TechMorePage.vue') },
|
|
// Magic link: /j/{jwt-token} — must be LAST to not capture static paths above
|
|
{ path: ':token', name: 'tech-magic', component: () => import('src/modules/tech/pages/TechTasksPage.vue'), props: true },
|
|
],
|
|
},
|
|
// Ops staff desktop view
|
|
{
|
|
path: '/',
|
|
component: () => import('src/layouts/MainLayout.vue'),
|
|
children: [
|
|
{ path: '', component: () => import('src/pages/DashboardPage.vue') },
|
|
{ path: 'clients', component: () => import('src/pages/ClientsPage.vue') },
|
|
{ path: 'clients/:id', component: () => import('src/pages/ClientDetailPage.vue'), props: true },
|
|
{ path: 'tickets', component: () => import('src/pages/TicketsPage.vue') },
|
|
{ path: 'equipe', component: () => import('src/pages/EquipePage.vue') },
|
|
{ path: 'rapports', component: () => import('src/pages/RapportsPage.vue') },
|
|
{ path: 'rapports/revenus', component: () => import('src/pages/ReportRevenuPage.vue') },
|
|
{ path: 'rapports/ventes', component: () => import('src/pages/ReportVentesPage.vue') },
|
|
{ path: 'rapports/taxes', component: () => import('src/pages/ReportTaxesPage.vue') },
|
|
{ path: 'rapports/ar', component: () => import('src/pages/ReportARPage.vue') },
|
|
{ path: 'rapports/internet-cher', component: () => import('src/pages/ReportInternetCherPage.vue') },
|
|
{ path: 'ocr', component: () => import('src/pages/OcrPage.vue') },
|
|
{ path: 'settings', component: () => import('src/pages/SettingsPage.vue') },
|
|
{ path: 'telephony', component: () => import('src/pages/TelephonyPage.vue') },
|
|
{ path: 'dispatch', component: () => import('src/pages/DispatchPage.vue') },
|
|
{ path: 'agent-flows', component: () => import('src/pages/AgentFlowsPage.vue') },
|
|
{ path: 'network', component: () => import('src/pages/NetworkPage.vue') },
|
|
// Gift campaigns — list, new wizard (CSV upload + matching), per-campaign detail with live SSE updates
|
|
{ path: 'campaigns', component: () => import('src/modules/campaigns/pages/CampaignsListPage.vue') },
|
|
{ path: 'campaigns/new', component: () => import('src/modules/campaigns/pages/CampaignNewPage.vue') },
|
|
// Template editor + gifts inventory routes must be ABOVE /campaigns/:id
|
|
// otherwise the ':id' wildcard captures the literal paths and shows
|
|
// the detail page instead.
|
|
{ path: 'campaigns/templates/:name?', component: () => import('src/modules/campaigns/pages/TemplateEditorPage.vue'), props: true },
|
|
{ path: 'campaigns/gifts', component: () => import('src/modules/campaigns/pages/GiftsInventoryPage.vue') },
|
|
{ path: 'campaigns/:id', component: () => import('src/modules/campaigns/pages/CampaignDetailPage.vue'), props: true },
|
|
],
|
|
},
|
|
]
|
|
|
|
export default route(function () {
|
|
return createRouter({
|
|
history: createWebHashHistory(process.env.VUE_ROUTER_BASE),
|
|
routes,
|
|
})
|
|
})
|