gigafibre-fsm/apps/ops/src/modules/campaigns/pages/CampaignsListPage.vue
louispaulb 512c4a5f1b feat(ops): dispatch auto complet + perf Boîte/rapports + fix session
Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré /
juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis
par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts,
sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle
week-end + placeholder « en attente du quart », clustering + lasso + filtre-date
sur la carte, accès rapide « À assigner » (badge).

Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à
l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive
+ timeout fetch global + authFetch durci → fin des rechargements manuels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:49:35 -04:00

133 lines
5.6 KiB
Vue

<template>
<q-page padding>
<div class="row items-center q-mb-md">
<div class="text-h5">Campagnes</div>
<q-space />
<q-btn flat color="primary" icon="redeem" label="Liens cadeaux" :to="'/campaigns/gifts'" class="q-mr-sm">
<q-tooltip>Inventaire de tous les liens cadeaux (actifs, expirés, réassignables)</q-tooltip>
</q-btn>
<q-btn flat color="primary" icon="palette" label="Éditer le template" :to="'/campaigns/templates/gift-email-fr'" class="q-mr-sm" />
<q-btn unelevated color="primary" icon="add" label="Nouvelle campagne" :to="'/campaigns/new'" />
</div>
<q-card flat bordered v-if="!loading && campaigns.length === 0" class="q-pa-xl text-center text-grey-7">
<q-icon name="card_giftcard" size="48px" class="q-mb-md" />
<div class="text-h6">Aucune campagne pour le moment</div>
<div class="q-mt-sm">
Une campagne envoie des cartes-cadeaux Giftbit par courriel personnalisé.
Importer 2 CSV (export Map + shortlinks Giftbit) et lancer l'envoi via Mailjet.
</div>
<q-btn class="q-mt-lg" color="primary" icon="add" label="Créer la première" :to="'/campaigns/new'" />
</q-card>
<DataTable
v-else
:rows="campaigns"
:columns="columns"
row-key="id"
:loading="loading"
flat bordered
:pagination="{ rowsPerPage: 25, sortBy: 'created_at', descending: true }"
>
<template v-slot:body-cell-status="props">
<q-td :props="props">
<q-chip dense :color="statusColor(props.row.status)" text-color="white" :label="statusLabel(props.row.status)" />
</q-td>
</template>
<template v-slot:body-cell-progress="props">
<q-td :props="props">
<div class="row items-center q-gutter-xs">
<span class="text-grey-7">{{ sentCount(props.row) }} / {{ props.row.total || 0 }}</span>
<q-linear-progress
:value="sentCount(props.row) / Math.max(1, props.row.total || 1)"
size="6px"
:color="props.row.counters?.failed ? 'negative' : 'positive'"
style="min-width:80px"
/>
<span v-if="props.row.counters?.failed" class="text-negative text-caption">
{{ props.row.counters.failed }} échec(s)
</span>
</div>
</q-td>
</template>
<template v-slot:body-cell-actions="props">
<q-td :props="props" class="text-right">
<q-btn flat dense color="primary" icon="visibility" :to="`/campaigns/${props.row.id}`">
<q-tooltip>Voir la campagne</q-tooltip>
</q-btn>
<q-btn flat dense color="negative" icon="delete"
:disable="props.row.status === 'sending'"
@click="confirmDelete(props.row)">
<q-tooltip>{{ props.row.status === 'sending' ? "Envoi en cours — impossible de supprimer" : "Supprimer la campagne" }}</q-tooltip>
</q-btn>
</q-td>
</template>
</DataTable>
</q-page>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import DataTable from 'src/components/shared/DataTable.vue'
import { useQuasar } from 'quasar'
import { listCampaigns, deleteCampaign } from 'src/api/campaigns'
const $q = useQuasar()
const campaigns = ref([])
const loading = ref(true)
const columns = [
{ name: 'name', label: 'Nom', field: 'name', align: 'left', sortable: true },
{ name: 'created_at', label: 'Créée', field: r => new Date(r.created_at).toLocaleString('fr-CA', { dateStyle: 'medium', timeStyle: 'short' }), align: 'left', sortable: true },
{ name: 'status', label: 'Statut', field: 'status', align: 'left' },
{ name: 'progress', label: 'Envois', field: 'counters', align: 'left' },
{ name: 'actions', label: '', field: '', align: 'right' },
]
// "Envoyés" = anything past the SMTP handoff. Webhooks (Mailjet Event API)
// promote 'sent' rows to 'opened' / 'clicked' over time, so counting only
// counters.sent would (incorrectly) drop the displayed count back to 0 once
// recipients start opening their emails. Matches the detail-page convention.
function sentCount (row) {
const c = row?.counters || {}
return (c.sent || 0) + (c.opened || 0) + (c.clicked || 0)
}
function statusColor (s) {
return { draft: 'grey', sending: 'orange', completed: 'positive', failed: 'negative' }[s] || 'grey'
}
function statusLabel (s) {
return { draft: 'Brouillon', sending: 'En cours', completed: 'Terminée', failed: 'Échec' }[s] || s
}
// Hard-delete: removes the JSON on the hub. Giftbit shortlinks already
// issued for this campaign are unaffected (they live on Giftbit's side).
// Used mostly to clean up test runs from the list.
function confirmDelete (row) {
const total = row.total ?? row.counters?.total ?? 0
$q.dialog({
title: 'Supprimer la campagne ?',
message: `<div><strong>${row.name || row.id}</strong> (${total} destinataire${total > 1 ? 's' : ''})<br><br>Cette opération est irréversible — le suivi, les statistiques et le rapport CSV seront perdus. Les liens Giftbit déjà émis restent valides côté Giftbit.</div>`,
html: true,
cancel: { label: 'Annuler', flat: true },
ok: { label: 'Supprimer', color: 'negative', unelevated: true },
persistent: true,
}).onOk(async () => {
try {
await deleteCampaign(row.id)
campaigns.value = campaigns.value.filter(c => c.id !== row.id)
$q.notify({ type: 'positive', message: 'Campagne supprimée' })
} catch (e) {
$q.notify({ type: 'negative', message: 'Erreur : ' + e.message, timeout: 4000 })
}
})
}
async function load () {
loading.value = true
try { campaigns.value = await listCampaigns() }
finally { loading.value = false }
}
onMounted(load)
</script>