import { reactive } from 'vue' /** * Section classification for subscription grouping. * Maps item_group (legacy category) to display sections with icons. */ export const SECTION_MAP = { 'Internet': { match: ['Mensualités fibre', 'Mensualités sans fil', 'Internet camping', 'Adresse IP Fixe'], icon: 'language' }, 'Téléphonie': { match: ['Téléphonie'], icon: 'phone' }, 'Télévision': { match: ['Mensualités télévision'], icon: 'tv' }, 'Équipement': { match: ['Installation et équipement fibre', 'Installation et équipement télé', 'Installation et équipement internet sans fil', 'Equipement internet fibre', 'Equipement internet sans fil', 'Location point à point', 'Quincaillerie'], icon: 'router' }, 'Hébergement': { match: ['Hébergement', 'Nom de domaine', "Location d'espace", 'Location espace cloud'], icon: 'cloud' }, 'Rabais': { match: ['Rabais'], icon: 'sell' }, } const SECTION_ORDER = ['Internet', 'Téléphonie', 'Télévision', 'Équipement', 'Hébergement', 'Rabais', 'Autre'] /** * Classify a subscription into a section key. */ export function subSection (sub) { const price = parseFloat(sub.actual_price || 0) const group = (sub.item_group || '').trim() const sku = (sub.item_code || '').toUpperCase() const label = (sub.custom_description || sub.item_name || sub.plan_name || '').toLowerCase() // Negative price = rabais if (price < 0 || sku.startsWith('RAB') || label.includes('rabais')) return 'Rabais' // Match by item_group / service_category for (const [section, cfg] of Object.entries(SECTION_MAP)) { if (section === 'Rabais') continue if (cfg.match.some(m => group.includes(m))) return section } // Category-based matching (from Service Subscription.service_category) if (group === 'Internet') return 'Internet' if (group === 'VoIP' || group === 'Téléphonie') return 'Téléphonie' if (group === 'IPTV' || group === 'Télévision') return 'Télévision' if (group === 'Hébergement') return 'Hébergement' if (group === 'Bundle') return 'Internet' // SKU-based fallback (works for both legacy item_codes and SUB-* names) if (/^(FTTH|FTTB|TURBO|FIBRE|FORFERF|FORFBASE|FORFPERF|FORFPOP|FORFMES|SYM|VIP|ENT|COM|FIBCOM|HV)/.test(sku)) return 'Internet' if (/^(TELEP|FAX|SERV911|SERVTEL|TELE_)/.test(sku)) return 'Téléphonie' if (/^(TV|STB|RABTV)/.test(sku)) return 'Télévision' if (/^(LOC|FTT_H|FTTH_LOC)/.test(sku)) return 'Équipement' if (/^(HEB|DOM)/.test(sku)) return 'Hébergement' // Label-based fallback (Service Subscription plan_name) if (/fibre|internet|giga|illimit|turbo|optimum/i.test(label)) return 'Internet' if (/téléphon|voip|phone|fax|911/i.test(label)) return 'Téléphonie' if (/télé|tv|décodeur|stb/i.test(label)) return 'Télévision' if (/location|modem|routeur|appareil|frais.*accès/i.test(label)) return 'Équipement' if (/héberg|domaine|cloud|espace/i.test(label)) return 'Hébergement' return 'Autre' } export function isRebate (sub) { return parseFloat(sub.actual_price || sub.monthly_price || 0) < 0 } export function subMainLabel (sub) { if (sub.custom_description && sub.custom_description.trim()) { return sub.custom_description } return sub.item_name || sub.plan_name || sub.item_code || sub.name } export function subSubLabel (sub) { if (sub.custom_description && sub.custom_description.trim()) return '' return '' } // Only Active subs contribute to the displayed monthly total. Cancelled/ // Suspended/Pending rows are still rendered (with strikethrough) for // audit visibility but they shouldn't inflate the number the rep // communicates to the customer. Was a real bug on C-LPB4: 5 internet // rows summed to 196.05 because the 3 cancelled (Megafibre 80, TEST, // FTTH100) were included in the math even though shown struck-through. const isActiveBilling = sub => (sub.status || 'Active') === 'Active' export function sectionTotal (items) { return items.reduce((s, sub) => isActiveBilling(sub) ? s + parseFloat(sub.actual_price || 0) : s, 0) } export function annualPrice (sub) { return parseFloat(sub.actual_price || 0) * 12 } /** * Composable for managing subscription grouping, section state, and location-level helpers. * @param {import('vue').Ref} subscriptions - Reactive ref of all subscriptions */ export function useSubscriptionGroups (subscriptions) { const subSections = reactive({}) // { "locName:freq": { sectionKey: [subs] } } const openSections = reactive({}) // { "locName:sectionKey": true/false } function locSubs (locName) { return subscriptions.value.filter(s => s.service_location === locName) } function locSubsMonthly (locName) { return locSubs(locName).filter(s => s.billing_frequency !== 'A') } function locSubsAnnual (locName) { return locSubs(locName).filter(s => s.billing_frequency === 'A') } function locSubsMonthlyTotal (locName) { // Same active-only filter as sectionTotal — keep them in sync. return locSubsMonthly(locName).reduce((sum, s) => isActiveBilling(s) ? sum + parseFloat(s.actual_price || 0) : sum, 0) } function locSubsAnnualTotal (locName) { return locSubsAnnual(locName).reduce((sum, s) => isActiveBilling(s) ? sum + annualPrice(s) : sum, 0) } function locSubsSections (locName, freq) { const cacheKey = locName + ':' + freq if (!subSections[cacheKey]) { const subs = freq === 'A' ? locSubsAnnual(locName) : locSubsMonthly(locName) const groups = {} for (const sub of subs) { const sec = subSection(sub) if (!groups[sec]) groups[sec] = [] groups[sec].push(sub) } for (const items of Object.values(groups)) { items.sort((a, b) => parseFloat(b.actual_price || 0) - parseFloat(a.actual_price || 0)) } subSections[cacheKey] = groups } const result = [] for (const key of SECTION_ORDER) { if (subSections[cacheKey][key]?.length) { result.push({ key, label: key, icon: SECTION_MAP[key]?.icon || 'label', items: subSections[cacheKey][key] }) } } for (const [key, items] of Object.entries(subSections[cacheKey])) { if (!SECTION_ORDER.includes(key) && items.length) { result.push({ key, label: key, icon: SECTION_MAP[key]?.icon || 'label', items }) } } return result } function sectionOpen (locName, sectionKey) { const k = locName + ':' + sectionKey if (openSections[k] === undefined) openSections[k] = false return openSections[k] } function toggleSection (locName, sectionKey) { const k = locName + ':' + sectionKey openSections[k] = !sectionOpen(locName, sectionKey) } function invalidateCache (locName) { delete subSections[locName + ':A'] delete subSections[locName + ':M'] } function invalidateAll () { for (const k of Object.keys(subSections)) delete subSections[k] } return { subSections, openSections, locSubs, locSubsMonthly, locSubsAnnual, locSubsMonthlyTotal, locSubsAnnualTotal, locSubsSections, sectionOpen, toggleSection, invalidateCache, invalidateAll, } }