feat(ops): gate ticket activation on pending subs + show consolidated proration preview
The "Activer + facture brouillon" button now appears ONLY when the ticket's customer+location has En attente subscriptions (read-only /billing/activation- preview) — defining which completed tickets trigger activation. The confirm shows all services that would activate and the consolidated prorated total (internet+tv+phone in one draft invoice). Preview writes nothing; commit gated by PRORATION_WRITE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1d3212bf7
commit
718698f0d0
|
|
@ -110,6 +110,18 @@ export async function setJobStatusChain (job, status) {
|
|||
return data
|
||||
}
|
||||
|
||||
// Aperçu d'activation (LECTURE SEULE) : abonnements « En attente » du client+lieu + facture prorata consolidée
|
||||
// (TOUS services : internet+tv+téléphone en 1 facture). Sert à SAVOIR si une complétion activerait qqch + montre le total.
|
||||
export async function activationPreview ({ customer, service_location }) {
|
||||
const res = await fetch(`${HUB_URL}/billing/activation-preview`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ customer, service_location, statuses: ['En attente'] }),
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (!res.ok) throw new Error(data.error || ('Billing API ' + res.status))
|
||||
return data
|
||||
}
|
||||
|
||||
export async function createJob (payload) {
|
||||
const res = await authFetch(
|
||||
`${BASE_URL}/api/resource/Dispatch%20Job`,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
<TicketStatusControl doctype="Issue" :docname="docName" :status="doc.status"
|
||||
@changed="p => { if (p && p.status) doc.status = p.status; $emit('reply-sent', docName) }" />
|
||||
<!-- Fulfillment vente : complète l'installation liée → active l'abonnement + facture prorata BROUILLON (flux existant, gaté) -->
|
||||
<q-btn v-if="saleJob" dense unelevated no-caps color="deep-purple-6" icon="paid" label="Activer + facture brouillon" :loading="activating" @click="activateSale">
|
||||
<q-tooltip>Complète l'installation « {{ saleJob.subject || saleJob.name }} » → active l'abonnement en attente + crée une facture prorata (brouillon, à approuver)</q-tooltip>
|
||||
<q-btn v-if="canActivate" dense unelevated no-caps color="deep-purple-6" icon="paid" :label="'Activer + facture brouillon' + (actPreview && actPreview.prorated_total != null ? ' (' + actPreview.prorated_total + '$)' : '')" :loading="activating" @click="activateSale">
|
||||
<q-tooltip>{{ (actPreview.lines || []).length }} abonnement(s) en attente → active + facture prorata BROUILLON (à approuver). Complète l'installation « {{ saleJob.subject || saleJob.name }} ».</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
|
|
@ -343,7 +343,7 @@ import UserAvatar from 'src/components/shared/UserAvatar.vue'
|
|||
import AvailabilityByReason from 'src/components/shared/AvailabilityByReason.vue' // raison → compétence → disponibilité (dénominateur filtré)
|
||||
import { useIdentity } from 'src/composables/useIdentity' // identité unifiée : dé-dup assignation (alias/tech = même personne) + nom complet
|
||||
import TicketStatusMenu from 'src/components/shared/TicketStatusMenu.vue' // bouton « Statut » (Complété/Annulé) — distinct du snooze « Reporter »
|
||||
import { setJobStatusChain } from 'src/api/dispatch' // complète le job lié → active abonnement + facture brouillon (flux existant, gaté)
|
||||
import { setJobStatusChain, activationPreview } from 'src/api/dispatch' // complète le job lié → active abonnement + facture brouillon (flux existant, gaté) + aperçu lecture seule
|
||||
// Modules on-site / dispatch RÉUTILISABLES (mêmes composants que le volet Planification) — montés ici pour la tâche dispatch principale du ticket.
|
||||
import JobMapModule from 'src/components/shared/detail-sections/modules/JobMapModule.vue'
|
||||
import GeofenceTimeline from 'src/components/shared/detail-sections/modules/GeofenceTimeline.vue'
|
||||
|
|
@ -625,6 +625,18 @@ const primaryJob = computed(() => {
|
|||
const activating = ref(false)
|
||||
// Tâche liée encore ouverte (installation à compléter) → cible du bouton « Activer + facture brouillon ».
|
||||
const saleJob = computed(() => (props.dispatchJobs || []).find(j => !['Completed', 'Cancelled'].includes(j.status)) || null)
|
||||
// Aperçu d'activation (lecture seule) : abonnements EN ATTENTE du client+lieu + total prorata consolidé (tous services).
|
||||
// C'est LUI qui définit « quel ticket déclenche une activation » : on n'affiche le bouton que s'il y a du EN ATTENTE.
|
||||
const actPreview = ref(null)
|
||||
async function loadActivationPreview () {
|
||||
const cust = props.doc?.customer; const loc = props.doc?.service_location
|
||||
if (!cust) { actPreview.value = null; return }
|
||||
try { const p = await activationPreview({ customer: cust, service_location: loc }); actPreview.value = (p && (p.lines || []).length) ? p : null }
|
||||
catch (e) { actPreview.value = null }
|
||||
}
|
||||
onMounted(loadActivationPreview)
|
||||
// Le bouton d'activation n'apparaît QUE si (installation liée ouverte) ET (abonnements en attente à activer).
|
||||
const canActivate = computed(() => !!(saleJob.value && actPreview.value))
|
||||
async function onSetStatus (status) {
|
||||
try {
|
||||
await updateDoc('Issue', props.docName, { status })
|
||||
|
|
@ -632,16 +644,18 @@ async function onSetStatus (status) {
|
|||
Notify.create({ type: 'positive', message: 'Statut : ' + status, timeout: 1400 })
|
||||
emit('reply-sent', props.docName)
|
||||
// « Complété » sur une vente avec une installation à compléter → propose l'activation (pas d'auto : l'agent confirme).
|
||||
if (status === 'Resolved' && saleJob.value) setTimeout(activateSale, 300)
|
||||
if (status === 'Resolved' && canActivate.value) setTimeout(activateSale, 300)
|
||||
} catch (e) { Notify.create({ type: 'negative', message: 'Statut : ' + (e.message || e) }) }
|
||||
}
|
||||
// Réutilise le flux hub EXISTANT : compléter le job lié → déblocage chaîne + activation abonnement + facture prorata BROUILLON
|
||||
// (gaté PRORATION_WRITE / BILLING_APPROVAL_GATE). N'AUTO-FACTURE jamais : draft à approuver par la facturation.
|
||||
function activateSale () {
|
||||
const j = saleJob.value; if (!j || activating.value) return
|
||||
const p = actPreview.value || {}
|
||||
const lines = (p.lines || []).map(l => `• ${l.label}${l.amount != null ? ' — ' + l.amount + '$' : ''}`).join('<br>')
|
||||
Dialog.create({
|
||||
title: 'Activer la vente',
|
||||
message: `Compléter l'installation « ${j.subject || j.name} » et activer l'abonnement en attente ?<br><br>Une <b>facture prorata BROUILLON</b> sera créée (à éditer/approuver par la facturation) si la facturation est armée — sinon l'abonnement est activé sans facture.`,
|
||||
message: `Compléter l'installation « ${j.subject || j.name} » et activer <b>${(p.lines || []).length} service(s)</b> en attente ?<br><br>${lines}<br><br><b>Facture prorata consolidée</b>${p.prorated_total != null ? ' ≈ ' + p.prorated_total + '$' : ''}${p.billed_days ? ' (' + p.billed_days + '/' + p.period_days + ' j)' : ''} → créée en <b>BROUILLON</b> (à éditer/approuver par la facturation) si la facturation est armée ; sinon activation sans facture.`,
|
||||
html: true, cancel: 'Annuler', ok: { label: 'Activer', color: 'deep-purple-6' },
|
||||
}).onOk(async () => {
|
||||
activating.value = true
|
||||
|
|
@ -654,7 +668,7 @@ function activateSale () {
|
|||
else if (acts) msg = acts + ' abonnement(s) activé(s) · facture non émise (écriture désarmée)'
|
||||
else msg = 'Installation complétée — aucun abonnement en attente à activer'
|
||||
Notify.create({ type: 'positive', message: msg, timeout: 6000 })
|
||||
j.status = 'Completed'; emit('dispatch-updated', j.name)
|
||||
j.status = 'Completed'; actPreview.value = null; emit('dispatch-updated', j.name)
|
||||
} catch (e) { Notify.create({ type: 'negative', message: 'Activation : ' + (e.message || e), timeout: 5000 }) } finally { activating.value = false }
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user