gigafibre-fsm/apps/client/src/pages/AccountPage.vue
louispaulb 607ea54b5c 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

306 lines
12 KiB
Vue

<template>
<q-page padding>
<div class="page-title">Mon compte</div>
<div class="row q-col-gutter-md">
<!-- Customer info -->
<div class="col-12 col-md-6">
<div class="portal-card">
<div class="text-subtitle1 text-weight-medium q-mb-md">Informations</div>
<div class="q-gutter-sm">
<div><strong>Nom:</strong> {{ store.customerName }}</div>
<div><strong>Courriel:</strong> {{ store.email }}</div>
<div><strong>No. client:</strong> {{ store.customerId }}</div>
<div v-if="profile"><strong>Langue:</strong> {{ profile.language || 'fr' }}</div>
</div>
</div>
</div>
<!-- Services & Locations -->
<div class="col-12 col-md-6">
<div class="portal-card">
<div class="text-subtitle1 text-weight-medium q-mb-md">Adresses de service</div>
<!-- Monthly total banner (shown when multiple locations) -->
<div v-if="serviceLocations.length > 1 && grandMonthlyTotal !== null" class="monthly-total-banner q-mb-md">
<div class="row items-center justify-between">
<div>
<div class="text-caption text-grey-7">Total mensuel estimé</div>
<div class="text-h5 text-weight-bold" :class="grandMonthlyTotal >= 0 ? 'text-primary' : 'text-positive'">
{{ formatMoney(grandMonthlyTotal) }}<span class="text-caption text-grey-6"> /mois</span>
</div>
</div>
<div class="text-caption text-grey-6">
{{ serviceLocations.length }} adresses · {{ subscriptionCount }} services
</div>
</div>
</div>
<div v-if="loadingServices" class="q-pa-md text-center">
<q-spinner-dots size="28px" color="primary" />
</div>
<div v-else-if="!serviceLocations.length" class="text-grey-6">Aucune adresse enregistrée</div>
<!-- Location cards -->
<div v-else class="q-gutter-sm">
<div v-for="loc in serviceLocations" :key="loc.name" class="location-card">
<!-- Location header -->
<div class="row items-center q-mb-xs">
<q-icon name="place" size="18px" color="primary" class="q-mr-xs" />
<div class="text-weight-medium">{{ loc.location_name || loc.address_line }}</div>
<q-space />
<q-badge v-if="loc.connection_type" outline color="grey-7" :label="loc.connection_type" class="q-mr-xs" />
<q-badge :color="loc.status === 'Active' ? 'positive' : 'grey'" :label="loc.status" />
</div>
<div class="text-caption text-grey-6 q-ml-md q-mb-sm">
{{ loc.address_line }}, {{ loc.city }} {{ loc.postal_code }}
</div>
<!-- Subscriptions for this location -->
<div v-if="loc.subscriptions.length" class="q-ml-md">
<div v-for="sub in loc.subscriptions" :key="sub.name" class="subscription-row">
<div class="row items-center no-wrap">
<q-icon :name="categoryIcon(sub.service_category)" size="16px" :color="categoryColor(sub.service_category)" class="q-mr-sm" />
<div class="col">
<div class="text-body2">{{ sub.plan_name }}</div>
<div v-if="sub.speed_down" class="text-caption text-grey-6">
{{ sub.speed_down }} / {{ sub.speed_up }} Mbps
</div>
</div>
<div class="text-right">
<div class="text-body2 text-weight-medium" :class="sub.monthly_price < 0 ? 'text-positive' : ''">
{{ formatMoney(sub.monthly_price) }}
</div>
<div class="text-caption text-grey-6">/mois</div>
</div>
</div>
</div>
<!-- Location subtotal -->
<div class="location-subtotal q-mt-xs">
<div class="row items-center justify-end">
<span class="text-caption text-grey-7 q-mr-sm">Sous-total:</span>
<span class="text-body2 text-weight-bold">{{ formatMoney(loc.monthly_total) }}/mois</span>
</div>
</div>
</div>
<div v-else class="text-caption text-grey-5 q-ml-md">Aucun service actif</div>
</div>
</div>
</div>
</div>
<!-- Payment methods -->
<div class="col-12">
<div class="portal-card">
<div class="row items-center q-mb-md">
<q-icon name="credit_card" size="24px" color="indigo" class="q-mr-sm" />
<div class="text-subtitle1 text-weight-medium">Paiement</div>
<q-space />
<q-btn outline color="indigo" label="Ajouter une carte" icon="add_card" no-caps size="sm"
:loading="addingCard" @click="addCard" />
<q-btn v-if="hasStripeCards" flat color="grey-7" label="Gerer les cartes" icon="settings" no-caps size="sm"
class="q-ml-sm" :loading="openingPortal" @click="openPortal" />
</div>
<!-- Balance -->
<div v-if="balance !== null" class="q-mb-md">
<div class="row items-center q-gutter-sm">
<span class="text-body2">Solde a payer:</span>
<span class="text-h6 text-weight-bold" :class="balance > 0 ? 'text-negative' : 'text-positive'">
{{ formatMoney(balance) }}
</span>
<q-btn v-if="balance > 0" color="indigo" unelevated label="Payer le solde" icon="payment"
no-caps size="sm" :loading="payingBalance" @click="payBalance" class="q-ml-md" />
</div>
</div>
<!-- Saved cards -->
<div v-if="loadingPayment" class="q-pa-sm">
<q-spinner-dots size="24px" color="indigo" />
</div>
<div v-else-if="cards.length" class="q-mb-md">
<div class="text-caption text-weight-bold text-grey-7 q-mb-xs">Cartes enregistrees</div>
<q-list bordered separator class="rounded-borders">
<q-item v-for="card in cards" :key="card.id">
<q-item-section avatar>
<q-icon :name="cardIcon(card.brand)" size="28px" :color="card.is_default ? 'indigo' : 'grey-6'" />
</q-item-section>
<q-item-section>
<q-item-label>{{ cardBrandLabel(card.brand) }} **** {{ card.last4 }}</q-item-label>
<q-item-label caption>Exp. {{ card.exp_month }}/{{ card.exp_year }}</q-item-label>
</q-item-section>
<q-item-section side v-if="card.is_default">
<q-badge color="indigo" label="Par defaut" />
</q-item-section>
</q-item>
</q-list>
</div>
<div v-else class="text-grey-6 text-body2 q-mb-md">
Aucune carte enregistree. Ajoutez une carte pour activer le paiement automatique.
</div>
<!-- PPA toggle -->
<div class="q-pa-sm rounded-borders" style="background: #f8f9fc;">
<div class="row items-center">
<div class="col">
<div class="text-weight-medium">Paiement automatique (PPA)</div>
<div class="text-caption text-grey-7">
Vos factures seront payees automatiquement avec votre carte par defaut.
</div>
</div>
<div class="col-auto">
<q-toggle v-model="ppaEnabled" color="indigo" :disable="!hasStripeCards || togglingPPA"
@update:model-value="onTogglePPA" />
</div>
</div>
</div>
</div>
</div>
</div>
</q-page>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useQuasar } from 'quasar'
import { useCustomerStore } from 'src/stores/customer'
import { fetchProfile, fetchServiceLocations } from 'src/api/portal'
import { getBalance, getPaymentMethods, checkoutBalance, setupCard, openBillingPortal, togglePPA } from 'src/api/payments'
import { useFormatters } from 'src/composables/useFormatters'
const $q = useQuasar()
const store = useCustomerStore()
const { formatMoney } = useFormatters()
const profile = ref(null)
const serviceLocations = ref([])
const grandMonthlyTotal = ref(null)
const subscriptionCount = ref(0)
const loadingServices = ref(true)
const balance = ref(null)
const cards = ref([])
const ppaEnabled = ref(false)
const loadingPayment = ref(true)
const addingCard = ref(false)
const openingPortal = ref(false)
const payingBalance = ref(false)
const togglingPPA = ref(false)
const hasStripeCards = computed(() => cards.value.length > 0)
function cardIcon (brand) {
const icons = { visa: 'credit_card', mastercard: 'credit_card', amex: 'credit_card' }
return icons[brand] || 'credit_card'
}
function cardBrandLabel (brand) {
const labels = { visa: 'Visa', mastercard: 'Mastercard', amex: 'Amex', discover: 'Discover' }
return labels[brand] || (brand || 'Carte').charAt(0).toUpperCase() + (brand || 'carte').slice(1)
}
async function loadPaymentInfo () {
if (!store.customerId) return
loadingPayment.value = true
try {
const [balRes, methRes] = await Promise.all([
getBalance(store.customerId),
getPaymentMethods(store.customerId),
])
balance.value = balRes.balance || 0
// Extract cards from Stripe methods
const methods = methRes.methods || []
const stripeMethod = methods.find(m => m.provider === 'Stripe')
cards.value = stripeMethod?.stripe_cards || []
ppaEnabled.value = !!(stripeMethod?.is_auto_ppa)
} catch (e) {
console.error('Payment info load error:', e)
} finally {
loadingPayment.value = false
}
}
async function addCard () {
addingCard.value = true
try {
const result = await setupCard(store.customerId)
if (result.url) window.location.href = result.url
} catch (e) {
$q.notify({ message: 'Erreur: ' + e.message, color: 'negative' })
} finally {
addingCard.value = false
}
}
async function openPortal () {
openingPortal.value = true
try {
const result = await openBillingPortal(store.customerId)
if (result.url) window.location.href = result.url
} catch (e) {
$q.notify({ message: 'Erreur: ' + e.message, color: 'negative' })
} finally {
openingPortal.value = false
}
}
async function payBalance () {
payingBalance.value = true
try {
const result = await checkoutBalance(store.customerId)
if (result.url) window.location.href = result.url
} catch (e) {
$q.notify({ message: 'Erreur: ' + e.message, color: 'negative' })
} finally {
payingBalance.value = false
}
}
async function onTogglePPA (val) {
togglingPPA.value = true
try {
await togglePPA(store.customerId, val)
$q.notify({
message: val ? 'Paiement automatique active' : 'Paiement automatique desactive',
color: 'positive', icon: val ? 'check_circle' : 'info',
})
} catch (e) {
ppaEnabled.value = !val // revert
$q.notify({ message: 'Erreur: ' + e.message, color: 'negative' })
} finally {
togglingPPA.value = false
}
}
function categoryIcon (cat) {
const icons = {
Internet: 'wifi', IPTV: 'tv', VoIP: 'phone', Bundle: 'inventory_2',
'Hébergement': 'dns', Autre: 'category',
}
return icons[cat] || 'category'
}
function categoryColor (cat) {
const colors = {
Internet: 'blue', IPTV: 'purple', VoIP: 'green', Bundle: 'orange',
'Hébergement': 'cyan', Autre: 'grey',
}
return colors[cat] || 'grey'
}
onMounted(async () => {
if (!store.customerId) return
const [p, svcResult] = await Promise.all([
fetchProfile(store.customerId),
fetchServiceLocations(store.customerId).catch(() => ({ locations: [], grandTotal: 0, subscriptionCount: 0 })),
])
profile.value = p
serviceLocations.value = svcResult.locations
grandMonthlyTotal.value = svcResult.grandTotal
subscriptionCount.value = svcResult.subscriptionCount
loadingServices.value = false
loadPaymentInfo()
})
</script>