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:
parent
925ffc9c3f
commit
d4a5644e37
153
apps/ops/src/components/customer/PaymentSection.vue
Normal file
153
apps/ops/src/components/customer/PaymentSection.vue
Normal 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>
|
||||||
|
|
@ -245,111 +245,32 @@
|
||||||
</div>
|
</div>
|
||||||
</q-expansion-item>
|
</q-expansion-item>
|
||||||
|
|
||||||
<!-- PAIEMENTS UNIFIÉS : méthode (carte/PPA) + transactions (Payment Entry + charges Stripe) + ententes,
|
<!-- PAIEMENTS UNIFIÉS (méthode + transactions + ententes) — extrait → PaymentSection, T7 -->
|
||||||
dans une seule section (fusion des ex-sections « Paiement », « Paiements » et « Ententes de paiement »). -->
|
<PaymentSection
|
||||||
<q-expansion-item v-model="sectionsOpen.payments" header-class="section-header" class="q-mb-sm fiche-mod" :style="moduleStyle('payments')" v-show="moduleVisible('payments')">
|
v-model:open="sectionsOpen.payments"
|
||||||
<template #header>
|
:module-style="moduleStyle('payments')"
|
||||||
<div class="section-title" style="font-size:1rem;width:100%;display:flex;align-items:center">
|
:module-visible="moduleVisible('payments')"
|
||||||
<q-icon name="payments" size="20px" class="q-mr-xs" />
|
:payments="payments"
|
||||||
Paiements ({{ payments.length + stripeCharges.length }}{{ (!paymentsExpanded && payments.length >= 5) ? '+' : '' }})
|
:stripe-charges="stripeCharges"
|
||||||
<span v-if="paymentMethods.some(p => p.is_auto_ppa)" class="ops-badge open q-ml-sm" style="font-size:10px">PPA</span>
|
:payment-methods="paymentMethods"
|
||||||
<q-space />
|
:arrangements="arrangements"
|
||||||
<div class="q-gutter-x-xs" @click.stop>
|
:refunds-by-pi="refundsByPi"
|
||||||
<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>
|
:payments-expanded="paymentsExpanded"
|
||||||
<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>
|
:loading-more-payments="loadingMorePayments"
|
||||||
<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>
|
:sending-link="sendingLink"
|
||||||
</div>
|
:charging-card="chargingCard"
|
||||||
</div>
|
:toggling-ppa="togglingPpa"
|
||||||
</template>
|
:refunding="refunding"
|
||||||
|
:send-payment-link="sendPaymentLink"
|
||||||
<!-- ── Méthode de paiement (carte, PPA, liens) — ex-section « Paiement » ── -->
|
:charge-card="chargeCard"
|
||||||
<div class="ops-card q-mb-md">
|
:toggle-ppa="togglePpa"
|
||||||
<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>
|
:open-portal="openPortal"
|
||||||
<div class="row q-gutter-sm q-mb-sm q-pa-sm" style="background:#f8fafc;border-radius:8px">
|
:create-checkout="createCheckout"
|
||||||
<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>
|
:record-payment="recordPayment"
|
||||||
<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>
|
:confirm-refund="confirmRefund"
|
||||||
<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>
|
:load-all-payments="loadAllPayments"
|
||||||
<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>
|
:open-modal="openModal"
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- VoIP Lines -->
|
<!-- 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')">
|
<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 { useSubscriptionActions } from 'src/composables/useSubscriptionActions'
|
||||||
import { useCustomerNotes } from 'src/composables/useCustomerNotes'
|
import { useCustomerNotes } from 'src/composables/useCustomerNotes'
|
||||||
import { useConversations } from 'src/composables/useConversations'
|
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 DetailModal from 'src/components/shared/DetailModal.vue'
|
||||||
import DataTable from 'src/components/shared/DataTable.vue'
|
import DataTable from 'src/components/shared/DataTable.vue'
|
||||||
import CustomerHeader from 'src/components/customer/CustomerHeader.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 { erpPdfUrl } from 'src/utils/erp-pdf'
|
||||||
import RewardDialog from 'src/components/customer/RewardDialog.vue'
|
import RewardDialog from 'src/components/customer/RewardDialog.vue'
|
||||||
import LocationCard from 'src/components/customer/LocationCard.vue'
|
import LocationCard from 'src/components/customer/LocationCard.vue'
|
||||||
|
import PaymentSection from 'src/components/customer/PaymentSection.vue'
|
||||||
|
|
||||||
const $q = useQuasar()
|
const $q = useQuasar()
|
||||||
const { can } = usePermissions()
|
const { can } = usePermissions()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user