refactor(ops): extract PaymentSection from ClientDetailPage

T7 découpage géants — sort la section « Paiements » unifiée (méthode de
paiement carte/PPA/liens Stripe + transactions Payment Entry + charges
Stripe réelles + ententes) dans components/customer/PaymentSection.vue.
Reste un module de la grille réordonnable : open (v-model:open),
moduleStyle (ordre CSS) et moduleVisible fournis par la fiche ; données +
états de chargement + actions (usePaymentActions + recordPayment/
confirmRefund/loadAllPayments/openModal) passés en props ; colonnes et
formatMoney importés dans l'enfant. ClientDetailPage -78 l. ; imports
morts retirés (paymentMethodCols/paymentCols/arrangementCols).

Vérifié en local sur hub LIVE (C-LPB4) : en-tête « Paiements (3) » + PPA
+ boutons ; v-model expand OK ; Méthode de paiement = carte réelle
MASTERCARD ••7030 (cus_UJNksulWgk5VVX, PPA Oui) ; 3 charges Stripe
« Remboursé ». 0 erreur console.

ClientDetailPage: 2074 → 1646 lignes sur les 3 extractions T7
(DeviceStrip + LocationCard + PaymentSection).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-04 21:37:55 -04:00
parent 925ffc9c3f
commit d4a5644e37
2 changed files with 181 additions and 106 deletions

View File

@ -0,0 +1,153 @@
<template>
<q-expansion-item :model-value="open" @update:model-value="$emit('update:open', $event)"
header-class="section-header" class="q-mb-sm fiche-mod" :style="moduleStyle" v-show="moduleVisible">
<template #header>
<div class="section-title" style="font-size:1rem;width:100%;display:flex;align-items:center">
<q-icon name="payments" size="20px" class="q-mr-xs" />
Paiements ({{ payments.length + stripeCharges.length }}{{ (!paymentsExpanded && payments.length >= 5) ? '+' : '' }})
<span v-if="paymentMethods.some(p => p.is_auto_ppa)" class="ops-badge open q-ml-sm" style="font-size:10px">PPA</span>
<q-space />
<div class="q-gutter-x-xs" @click.stop>
<q-btn flat dense size="sm" icon="send" color="primary" no-caps :loading="sendingLink" @click="sendPaymentLink('both')"><q-tooltip>Envoyer lien de paiement (SMS + Email)</q-tooltip></q-btn>
<q-btn flat dense size="sm" icon="bolt" color="warning" no-caps :loading="chargingCard" @click="chargeCard()"><q-tooltip>Prélever le solde maintenant</q-tooltip></q-btn>
<q-btn flat dense size="sm" icon="add" label="Inscrire" color="primary" no-caps @click="recordPayment()"><q-tooltip>Inscrire un paiement reçu (ouvre ERPNext, pré-rempli)</q-tooltip></q-btn>
</div>
</div>
</template>
<!-- Méthode de paiement (carte, PPA, liens) ex-section « Paiement » -->
<div class="ops-card q-mb-md">
<div class="text-caption text-weight-medium text-grey-7 q-mb-xs"><q-icon name="credit_card" size="14px" color="green-5" /> Méthode de paiement</div>
<div class="row q-gutter-sm q-mb-sm q-pa-sm" style="background:#f8fafc;border-radius:8px">
<q-btn outline dense no-caps color="primary" icon="open_in_new" label="Portail Stripe" size="sm" @click="openPortal"><q-tooltip>Ouvrir le portail Stripe pour gérer la carte</q-tooltip></q-btn>
<q-btn outline dense no-caps color="green-7" icon="add_card" label="Ajouter carte" size="sm" @click="createCheckout"><q-tooltip>Créer un lien Stripe Checkout (copié au presse-papier)</q-tooltip></q-btn>
<q-btn outline dense no-caps icon="sms" label="Lien SMS" size="sm" color="info" :loading="sendingLink" @click="sendPaymentLink('sms')"><q-tooltip>Envoyer lien de paiement par SMS</q-tooltip></q-btn>
<q-btn outline dense no-caps icon="email" label="Lien Email" size="sm" color="purple-6" :loading="sendingLink" @click="sendPaymentLink('email')"><q-tooltip>Envoyer lien de paiement par Email</q-tooltip></q-btn>
</div>
<div v-if="!paymentMethods.length" class="text-center text-grey-6 q-pa-sm">Aucune méthode de paiement</div>
<DataTable v-else :rows="paymentMethods" :columns="paymentMethodCols" row-key="name" flat dense class="ops-table clickable-table" :pagination="{ rowsPerPage: 10 }" @row-click="(_, row) => openModal('Payment Method', row.name, row.provider)">
<template #body-cell-is_auto_ppa="props">
<q-td :props="props" @click.stop>
<q-toggle v-if="props.row.provider === 'Stripe'" dense size="sm" :model-value="!!props.row.is_auto_ppa" :loading="togglingPpa" @update:model-value="v => togglePpa(v).then(ok => { if (ok) props.row.is_auto_ppa = v ? 1 : 0 })" />
<q-icon v-else-if="props.row.is_auto_ppa" name="autorenew" size="16px" color="green-6" />
</q-td>
</template>
<template #body-cell-provider="props">
<q-td :props="props">
<q-icon :name="props.row.provider === 'Stripe' ? 'credit_card' : props.row.provider === 'Bank Draft' ? 'account_balance' : 'payment'" size="14px" class="q-mr-xs" />
{{ props.row.provider }}
</q-td>
</template>
</DataTable>
</div>
<!-- Transactions (Payment Entry ERPNext + charges Stripe réelles) -->
<div class="ops-card q-mb-md">
<div class="text-caption text-weight-medium text-grey-7 q-mb-xs"><q-icon name="receipt_long" size="14px" color="green-5" /> Transactions</div>
<div v-if="!payments.length && !stripeCharges.length" class="text-center text-grey-6 q-pa-sm">Aucune transaction</div>
<template v-else>
<DataTable v-if="payments.length" :rows="payments" :columns="paymentCols" row-key="name"
flat dense class="ops-table clickable-table"
:pagination="{ rowsPerPage: 10, sortBy: 'posting_date', descending: true }"
@row-click="(_, row) => openModal('Payment Entry', row.name, 'Paiement ' + row.name)">
<template #body-cell-paid_amount="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.paid_amount) }}
<q-badge v-if="refundsByPi[props.row.reference_no]" color="amber-1" text-color="amber-9" class="q-ml-xs" style="font-weight:600">
<q-icon name="undo" size="12px" class="q-mr-xs" />remboursé {{ formatMoney(refundsByPi[props.row.reference_no].amount_refunded) }}<span v-if="refundsByPi[props.row.reference_no].refunded_at"> · {{ refundsByPi[props.row.reference_no].refunded_at }}</span>
<q-tooltip>Remboursement Stripe {{ refundsByPi[props.row.reference_no].fully_refunded ? 'complet' : 'partiel' }} · statut {{ refundsByPi[props.row.reference_no].status }} · le délai bancaire est de 5-10 jours ouvrables</q-tooltip>
</q-badge>
</q-td>
</template>
<template #body-cell-actions="props">
<q-td :props="props" @click.stop>
<q-icon v-if="refundsByPi[props.row.reference_no] && refundsByPi[props.row.reference_no].fully_refunded" name="check_circle" color="green-6" size="18px"><q-tooltip>Déjà remboursé via Stripe{{ refundsByPi[props.row.reference_no].refunded_at ? ' le ' + refundsByPi[props.row.reference_no].refunded_at : '' }}</q-tooltip></q-icon>
<q-btn v-else-if="props.row.reference_no && props.row.reference_no.startsWith('pi_')"
flat round dense size="xs" icon="replay" color="warning" :loading="refunding"
@click="confirmRefund(props.row)">
<q-tooltip>{{ refundsByPi[props.row.reference_no] ? 'Rembourser le reste via Stripe' : 'Rembourser via Stripe' }}</q-tooltip>
</q-btn>
</q-td>
</template>
</DataTable>
<div v-if="!paymentsExpanded && payments.length >= 5" class="text-center q-pa-xs">
<q-btn flat dense no-caps color="primary" :loading="loadingMorePayments" label="Voir tous les paiements" icon="expand_more" @click="loadAllPayments" />
</div>
<div v-if="stripeCharges.length" :class="{ 'q-mt-sm': payments.length }" :style="payments.length ? 'border-top:1px solid var(--ops-border,#e5e7eb);padding-top:8px' : ''">
<div class="text-caption text-grey-6 q-mb-xs"><q-icon name="credit_card" size="13px" /> Charges Stripe ({{ stripeCharges.length }})<q-tooltip>Transactions Stripe réelles peuvent ne pas être (encore) enregistrées comme paiement ERPNext</q-tooltip></div>
<q-list dense separator>
<q-item v-for="c in stripeCharges" :key="c.id" :clickable="!!c.receipt_url" tag="a" :href="c.receipt_url || undefined" target="_blank" rel="noopener">
<q-item-section>
<q-item-label>{{ formatMoney(c.amount) }}<span v-if="c.brand" class="text-grey-6"> · {{ c.brand }} {{ c.last4 }}</span></q-item-label>
<q-item-label caption>{{ c.created }}<span v-if="c.description"> · {{ c.description }}</span></q-item-label>
</q-item-section>
<q-item-section side>
<span class="ops-badge" :class="c.refunded ? 'inactive' : (c.paid && c.status === 'succeeded' ? 'paid' : 'open')">{{ c.refunded ? 'Remboursé' : (c.paid && c.status === 'succeeded' ? 'Payé' : c.status) }}</span>
</q-item-section>
</q-item>
</q-list>
</div>
</template>
</div>
<!-- Ententes de paiement ex-section séparée, fusionnée ici -->
<div v-if="arrangements.length" class="ops-card q-mb-md">
<div class="text-caption text-weight-medium text-grey-7 q-mb-xs"><q-icon name="handshake" size="14px" color="green-5" /> Ententes de paiement ({{ arrangements.length }})<span v-if="arrangements.filter(a => a.status === 'Open').length" class="text-caption text-blue q-ml-sm">{{ arrangements.filter(a => a.status === 'Open').length }} actives</span></div>
<DataTable :rows="arrangements" :columns="arrangementCols" row-key="name" flat dense class="ops-table clickable-table" :pagination="{ rowsPerPage: 10, sortBy: 'date_agreed', descending: true }" @row-click="(_, row) => openModal('Payment Arrangement', row.name, 'Entente ' + row.name)">
<template #body-cell-status="props">
<q-td :props="props"><span class="ops-badge" :class="props.row.status === 'Open' ? 'open' : props.row.status === 'Completed' ? 'paid' : 'inactive'">{{ props.row.status }}</span></q-td>
</template>
<template #body-cell-total_amount="props">
<q-td :props="props" class="text-right">{{ formatMoney(props.row.total_amount) }}</q-td>
</template>
</DataTable>
</div>
</q-expansion-item>
</template>
<script setup>
// PaymentSection section « Paiements » unifiée de la fiche : méthode de
// paiement (carte/PPA/liens Stripe) + transactions (Payment Entry ERPNext +
// charges Stripe réelles) + ententes de paiement. Reste un module de la grille
// réordonnable du parent : open (v-model), moduleStyle (ordre CSS) et
// moduleVisible sont fournis par la fiche. Données + états de chargement +
// actions (usePaymentActions + orchestration fiche) passés en props ; colonnes
// et formatMoney importés directement (constantes pures).
import DataTable from 'src/components/shared/DataTable.vue'
import { formatMoney } from 'src/composables/useFormatters'
import { paymentMethodCols, paymentCols, arrangementCols } from 'src/config/table-columns'
defineProps({
open: { type: Boolean, default: false },
moduleStyle: { type: [Object, String], default: () => ({}) },
moduleVisible: { type: Boolean, default: true },
payments: { type: Array, default: () => [] },
stripeCharges: { type: Array, default: () => [] },
paymentMethods: { type: Array, default: () => [] },
arrangements: { type: Array, default: () => [] },
refundsByPi: { type: Object, default: () => ({}) },
paymentsExpanded: { type: Boolean, default: false },
loadingMorePayments: { type: Boolean, default: false },
sendingLink: { type: Boolean, default: false },
chargingCard: { type: Boolean, default: false },
togglingPpa: { type: Boolean, default: false },
refunding: { type: Boolean, default: false },
// Actions (usePaymentActions + fiche)
sendPaymentLink: { type: Function, required: true },
chargeCard: { type: Function, required: true },
togglePpa: { type: Function, required: true },
openPortal: { type: Function, required: true },
createCheckout: { type: Function, required: true },
recordPayment: { type: Function, required: true },
confirmRefund: { type: Function, required: true },
loadAllPayments: { type: Function, required: true },
openModal: { type: Function, required: true },
})
defineEmits(['update:open'])
</script>
<style lang="scss" scoped>
.section-title { font-weight: 700; color: var(--ops-text); display: flex; align-items: center; }
:deep(.section-header) { padding: 8px 0; min-height: unset; background: transparent; border-bottom: 1px solid var(--ops-border, #e2e8f0); }
.clickable-table :deep(thead th) { font-size: 11px; padding: 4px; }
</style>

View File

@ -245,111 +245,32 @@
</div>
</q-expansion-item>
<!-- PAIEMENTS UNIFIÉS : méthode (carte/PPA) + transactions (Payment Entry + charges Stripe) + ententes,
dans une seule section (fusion des ex-sections « Paiement », « Paiements » et « Ententes de paiement »). -->
<q-expansion-item v-model="sectionsOpen.payments" header-class="section-header" class="q-mb-sm fiche-mod" :style="moduleStyle('payments')" v-show="moduleVisible('payments')">
<template #header>
<div class="section-title" style="font-size:1rem;width:100%;display:flex;align-items:center">
<q-icon name="payments" size="20px" class="q-mr-xs" />
Paiements ({{ payments.length + stripeCharges.length }}{{ (!paymentsExpanded && payments.length >= 5) ? '+' : '' }})
<span v-if="paymentMethods.some(p => p.is_auto_ppa)" class="ops-badge open q-ml-sm" style="font-size:10px">PPA</span>
<q-space />
<div class="q-gutter-x-xs" @click.stop>
<q-btn flat dense size="sm" icon="send" color="primary" no-caps :loading="sendingLink" @click="sendPaymentLink('both')"><q-tooltip>Envoyer lien de paiement (SMS + Email)</q-tooltip></q-btn>
<q-btn flat dense size="sm" icon="bolt" color="warning" no-caps :loading="chargingCard" @click="chargeCard()"><q-tooltip>Prélever le solde maintenant</q-tooltip></q-btn>
<q-btn flat dense size="sm" icon="add" label="Inscrire" color="primary" no-caps @click="recordPayment()"><q-tooltip>Inscrire un paiement reçu (ouvre ERPNext, pré-rempli)</q-tooltip></q-btn>
</div>
</div>
</template>
<!-- Méthode de paiement (carte, PPA, liens) ex-section « Paiement » -->
<div class="ops-card q-mb-md">
<div class="text-caption text-weight-medium text-grey-7 q-mb-xs"><q-icon name="credit_card" size="14px" color="green-5" /> Méthode de paiement</div>
<div class="row q-gutter-sm q-mb-sm q-pa-sm" style="background:#f8fafc;border-radius:8px">
<q-btn outline dense no-caps color="primary" icon="open_in_new" label="Portail Stripe" size="sm" @click="openPortal"><q-tooltip>Ouvrir le portail Stripe pour gérer la carte</q-tooltip></q-btn>
<q-btn outline dense no-caps color="green-7" icon="add_card" label="Ajouter carte" size="sm" @click="createCheckout"><q-tooltip>Créer un lien Stripe Checkout (copié au presse-papier)</q-tooltip></q-btn>
<q-btn outline dense no-caps icon="sms" label="Lien SMS" size="sm" color="info" :loading="sendingLink" @click="sendPaymentLink('sms')"><q-tooltip>Envoyer lien de paiement par SMS</q-tooltip></q-btn>
<q-btn outline dense no-caps icon="email" label="Lien Email" size="sm" color="purple-6" :loading="sendingLink" @click="sendPaymentLink('email')"><q-tooltip>Envoyer lien de paiement par Email</q-tooltip></q-btn>
</div>
<div v-if="!paymentMethods.length" class="text-center text-grey-6 q-pa-sm">Aucune méthode de paiement</div>
<DataTable v-else :rows="paymentMethods" :columns="paymentMethodCols" row-key="name" flat dense class="ops-table clickable-table" :pagination="{ rowsPerPage: 10 }" @row-click="(_, row) => openModal('Payment Method', row.name, row.provider)">
<template #body-cell-is_auto_ppa="props">
<q-td :props="props" @click.stop>
<q-toggle v-if="props.row.provider === 'Stripe'" dense size="sm" :model-value="!!props.row.is_auto_ppa" :loading="togglingPpa" @update:model-value="v => togglePpa(v).then(ok => { if (ok) props.row.is_auto_ppa = v ? 1 : 0 })" />
<q-icon v-else-if="props.row.is_auto_ppa" name="autorenew" size="16px" color="green-6" />
</q-td>
</template>
<template #body-cell-provider="props">
<q-td :props="props">
<q-icon :name="props.row.provider === 'Stripe' ? 'credit_card' : props.row.provider === 'Bank Draft' ? 'account_balance' : 'payment'" size="14px" class="q-mr-xs" />
{{ props.row.provider }}
</q-td>
</template>
</DataTable>
</div>
<!-- Transactions (Payment Entry ERPNext + charges Stripe réelles) -->
<div class="ops-card q-mb-md">
<div class="text-caption text-weight-medium text-grey-7 q-mb-xs"><q-icon name="receipt_long" size="14px" color="green-5" /> Transactions</div>
<div v-if="!payments.length && !stripeCharges.length" class="text-center text-grey-6 q-pa-sm">Aucune transaction</div>
<template v-else>
<DataTable v-if="payments.length" :rows="payments" :columns="paymentCols" row-key="name"
flat dense class="ops-table clickable-table"
:pagination="{ rowsPerPage: 10, sortBy: 'posting_date', descending: true }"
@row-click="(_, row) => openModal('Payment Entry', row.name, 'Paiement ' + row.name)">
<template #body-cell-paid_amount="props">
<q-td :props="props" class="text-right">
{{ formatMoney(props.row.paid_amount) }}
<q-badge v-if="refundsByPi[props.row.reference_no]" color="amber-1" text-color="amber-9" class="q-ml-xs" style="font-weight:600">
<q-icon name="undo" size="12px" class="q-mr-xs" />remboursé {{ formatMoney(refundsByPi[props.row.reference_no].amount_refunded) }}<span v-if="refundsByPi[props.row.reference_no].refunded_at"> · {{ refundsByPi[props.row.reference_no].refunded_at }}</span>
<q-tooltip>Remboursement Stripe {{ refundsByPi[props.row.reference_no].fully_refunded ? 'complet' : 'partiel' }} · statut {{ refundsByPi[props.row.reference_no].status }} · le délai bancaire est de 5-10 jours ouvrables</q-tooltip>
</q-badge>
</q-td>
</template>
<template #body-cell-actions="props">
<q-td :props="props" @click.stop>
<q-icon v-if="refundsByPi[props.row.reference_no] && refundsByPi[props.row.reference_no].fully_refunded" name="check_circle" color="green-6" size="18px"><q-tooltip>Déjà remboursé via Stripe{{ refundsByPi[props.row.reference_no].refunded_at ? ' le ' + refundsByPi[props.row.reference_no].refunded_at : '' }}</q-tooltip></q-icon>
<q-btn v-else-if="props.row.reference_no && props.row.reference_no.startsWith('pi_')"
flat round dense size="xs" icon="replay" color="warning" :loading="refunding"
@click="confirmRefund(props.row)">
<q-tooltip>{{ refundsByPi[props.row.reference_no] ? 'Rembourser le reste via Stripe' : 'Rembourser via Stripe' }}</q-tooltip>
</q-btn>
</q-td>
</template>
</DataTable>
<div v-if="!paymentsExpanded && payments.length >= 5" class="text-center q-pa-xs">
<q-btn flat dense no-caps color="primary" :loading="loadingMorePayments" label="Voir tous les paiements" icon="expand_more" @click="loadAllPayments" />
</div>
<div v-if="stripeCharges.length" :class="{ 'q-mt-sm': payments.length }" :style="payments.length ? 'border-top:1px solid var(--ops-border,#e5e7eb);padding-top:8px' : ''">
<div class="text-caption text-grey-6 q-mb-xs"><q-icon name="credit_card" size="13px" /> Charges Stripe ({{ stripeCharges.length }})<q-tooltip>Transactions Stripe réelles peuvent ne pas être (encore) enregistrées comme paiement ERPNext</q-tooltip></div>
<q-list dense separator>
<q-item v-for="c in stripeCharges" :key="c.id" :clickable="!!c.receipt_url" tag="a" :href="c.receipt_url || undefined" target="_blank" rel="noopener">
<q-item-section>
<q-item-label>{{ formatMoney(c.amount) }}<span v-if="c.brand" class="text-grey-6"> · {{ c.brand }} {{ c.last4 }}</span></q-item-label>
<q-item-label caption>{{ c.created }}<span v-if="c.description"> · {{ c.description }}</span></q-item-label>
</q-item-section>
<q-item-section side>
<span class="ops-badge" :class="c.refunded ? 'inactive' : (c.paid && c.status === 'succeeded' ? 'paid' : 'open')">{{ c.refunded ? 'Remboursé' : (c.paid && c.status === 'succeeded' ? 'Payé' : c.status) }}</span>
</q-item-section>
</q-item>
</q-list>
</div>
</template>
</div>
<!-- Ententes de paiement ex-section séparée, fusionnée ici -->
<div v-if="arrangements.length" class="ops-card q-mb-md">
<div class="text-caption text-weight-medium text-grey-7 q-mb-xs"><q-icon name="handshake" size="14px" color="green-5" /> Ententes de paiement ({{ arrangements.length }})<span v-if="arrangements.filter(a => a.status === 'Open').length" class="text-caption text-blue q-ml-sm">{{ arrangements.filter(a => a.status === 'Open').length }} actives</span></div>
<DataTable :rows="arrangements" :columns="arrangementCols" row-key="name" flat dense class="ops-table clickable-table" :pagination="{ rowsPerPage: 10, sortBy: 'date_agreed', descending: true }" @row-click="(_, row) => openModal('Payment Arrangement', row.name, 'Entente ' + row.name)">
<template #body-cell-status="props">
<q-td :props="props"><span class="ops-badge" :class="props.row.status === 'Open' ? 'open' : props.row.status === 'Completed' ? 'paid' : 'inactive'">{{ props.row.status }}</span></q-td>
</template>
<template #body-cell-total_amount="props">
<q-td :props="props" class="text-right">{{ formatMoney(props.row.total_amount) }}</q-td>
</template>
</DataTable>
</div>
</q-expansion-item>
<!-- PAIEMENTS UNIFIÉS (méthode + transactions + ententes) extrait PaymentSection, T7 -->
<PaymentSection
v-model:open="sectionsOpen.payments"
:module-style="moduleStyle('payments')"
:module-visible="moduleVisible('payments')"
:payments="payments"
:stripe-charges="stripeCharges"
:payment-methods="paymentMethods"
:arrangements="arrangements"
:refunds-by-pi="refundsByPi"
:payments-expanded="paymentsExpanded"
:loading-more-payments="loadingMorePayments"
:sending-link="sendingLink"
:charging-card="chargingCard"
:toggling-ppa="togglingPpa"
:refunding="refunding"
:send-payment-link="sendPaymentLink"
:charge-card="chargeCard"
:toggle-ppa="togglePpa"
:open-portal="openPortal"
:create-checkout="createCheckout"
:record-payment="recordPayment"
:confirm-refund="confirmRefund"
:load-all-payments="loadAllPayments"
:open-modal="openModal"
/>
<!-- VoIP Lines -->
<q-expansion-item v-model="sectionsOpen.voip" header-class="section-header" class="q-mb-sm fiche-mod" :style="moduleStyle('voip')" v-show="moduleVisible('voip')">
@ -825,7 +746,7 @@ import { useSubscriptionGroups } from 'src/composables/useSubscriptionGroups'
import { useSubscriptionActions } from 'src/composables/useSubscriptionActions'
import { useCustomerNotes } from 'src/composables/useCustomerNotes'
import { useConversations } from 'src/composables/useConversations'
import { invoiceCols, paymentCols, ticketCols, voipCols, paymentMethodCols, arrangementCols, quotationCols, serviceContractCols } from 'src/config/table-columns'
import { invoiceCols, ticketCols, voipCols, quotationCols, serviceContractCols } from 'src/config/table-columns'
import DetailModal from 'src/components/shared/DetailModal.vue'
import DataTable from 'src/components/shared/DataTable.vue'
import CustomerHeader from 'src/components/customer/CustomerHeader.vue'
@ -848,6 +769,7 @@ import { useUserPrefs } from 'src/composables/useUserPrefs'
import { erpPdfUrl } from 'src/utils/erp-pdf'
import RewardDialog from 'src/components/customer/RewardDialog.vue'
import LocationCard from 'src/components/customer/LocationCard.vue'
import PaymentSection from 'src/components/customer/PaymentSection.vue'
const $q = useQuasar()
const { can } = usePermissions()