Surfaces ERPNext's Email Queue in Ops (nav « File courriels ») so ops can see
what's queued — important now that mute_emails=1 + scheduler paused mean nothing
flushes — and delete/purge stale entries without the ERPNext desk.
- hub lib/email-queue.js: GET list (by status, recipients read from each row's
full doc since ERPNext ignores fields on child-doctype REST), DELETE :name,
POST /purge {status}. Wired in server.js.
- ops: api/emailQueue.js + EmailQueuePage.vue (status filter, recipients,
reference, error tooltip, per-row delete + « Purger Not Sent »), route + nav.
Verified live: 13 'Not Sent' (old internal test emails, no invoice refs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
5.7 KiB
Vue
149 lines
5.7 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<div class="row items-center q-mb-md">
|
|
<div class="text-h6 text-weight-bold">File de courriels (ERPNext)</div>
|
|
<q-space />
|
|
<q-select v-model="status" :options="statusOptions" emit-value map-options dense outlined
|
|
label="Statut" style="width:160px" class="q-mr-sm" @update:model-value="load" />
|
|
<q-btn flat dense icon="refresh" :loading="loading" @click="load" class="q-mr-sm" />
|
|
<q-btn color="negative" outline dense icon="delete_sweep" label="Purger « Not Sent »"
|
|
:disable="loading || !notSentCount" @click="confirmPurge" />
|
|
</div>
|
|
|
|
<q-banner dense rounded class="bg-blue-1 text-blue-9 q-mb-md">
|
|
<template #avatar><q-icon name="mark_email_read" /></template>
|
|
ERPNext est en <strong>mute_emails</strong> et le scheduler est en pause : <strong>rien ne s'envoie</strong>.
|
|
Ces entrées sont seulement en file. Tu peux les inspecter et les supprimer en toute sécurité.
|
|
</q-banner>
|
|
|
|
<div class="row q-gutter-sm q-mb-md">
|
|
<q-chip v-for="(n, s) in byStatus" :key="s" :color="statusColor(s)" text-color="white" dense>
|
|
{{ s }} : {{ n }}
|
|
</q-chip>
|
|
<q-chip v-if="!rows.length && loaded" color="grey-4" text-color="grey-8" dense>File vide</q-chip>
|
|
</div>
|
|
|
|
<q-table
|
|
v-if="loaded"
|
|
:rows="rows" :columns="columns" row-key="name"
|
|
flat bordered dense class="ops-table"
|
|
:pagination="{ rowsPerPage: 50, sortBy: 'creation', descending: true }"
|
|
:loading="loading"
|
|
>
|
|
<template #body-cell-status="props">
|
|
<q-td :props="props">
|
|
<q-chip :color="statusColor(props.value)" text-color="white" dense size="sm" :label="props.value" />
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-recipients="props">
|
|
<q-td :props="props">
|
|
<div v-for="(r, i) in (props.row.recipients || [])" :key="i" class="text-caption">{{ r }}</div>
|
|
<span v-if="!(props.row.recipients || []).length" class="text-grey-5">—</span>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-reference="props">
|
|
<q-td :props="props">
|
|
<span v-if="props.row.reference_doctype" class="text-caption">
|
|
{{ props.row.reference_doctype }} · {{ props.row.reference_name }}
|
|
</span>
|
|
<span v-else class="text-grey-5">—</span>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-creation="props">
|
|
<q-td :props="props">{{ fmtDate(props.value) }}</q-td>
|
|
</template>
|
|
<template #body-cell-error="props">
|
|
<q-td :props="props">
|
|
<q-icon v-if="props.value" name="error_outline" color="negative" />
|
|
<q-tooltip v-if="props.value" max-width="480px" class="text-body2">{{ props.value }}</q-tooltip>
|
|
<span v-else class="text-grey-5">—</span>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-actions="props">
|
|
<q-td :props="props">
|
|
<q-btn dense flat round size="sm" icon="delete" color="negative" @click="confirmDelete(props.row)">
|
|
<q-tooltip>Supprimer ce courriel de la file</q-tooltip>
|
|
</q-btn>
|
|
</q-td>
|
|
</template>
|
|
</q-table>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useQuasar } from 'quasar'
|
|
import { listEmailQueue, deleteEmailQueueItem, purgeEmailQueue } from 'src/api/emailQueue'
|
|
|
|
const $q = useQuasar()
|
|
const rows = ref([])
|
|
const loading = ref(false)
|
|
const loaded = ref(false)
|
|
const status = ref('')
|
|
const byStatus = ref({})
|
|
|
|
const statusOptions = [
|
|
{ label: 'Tous', value: '' },
|
|
{ label: 'Not Sent', value: 'Not Sent' },
|
|
{ label: 'Sending', value: 'Sending' },
|
|
{ label: 'Sent', value: 'Sent' },
|
|
{ label: 'Error', value: 'Error' },
|
|
]
|
|
|
|
const columns = [
|
|
{ name: 'status', label: 'Statut', field: 'status', align: 'left', sortable: true },
|
|
{ name: 'recipients', label: 'Destinataire(s)', field: 'recipients', align: 'left' },
|
|
{ name: 'reference', label: 'Référence', field: 'reference_doctype', align: 'left' },
|
|
{ name: 'creation', label: 'Créé', field: 'creation', align: 'left', sortable: true },
|
|
{ name: 'error', label: 'Erreur', field: 'error', align: 'center' },
|
|
{ name: 'actions', label: '', field: 'name', align: 'center' },
|
|
]
|
|
|
|
const notSentCount = computed(() => byStatus.value['Not Sent'] || 0)
|
|
|
|
function statusColor (s) {
|
|
return { 'Not Sent': 'orange-8', Sending: 'blue-6', Sent: 'green-7', Error: 'negative', Expired: 'grey-6' }[s] || 'grey-7'
|
|
}
|
|
function fmtDate (iso) {
|
|
return iso ? new Date(iso.replace(' ', 'T')).toLocaleString('fr-CA', { dateStyle: 'medium', timeStyle: 'short' }) : ''
|
|
}
|
|
|
|
async function load () {
|
|
loading.value = true
|
|
try {
|
|
const data = await listEmailQueue(status.value)
|
|
rows.value = data.rows || []
|
|
byStatus.value = data.by_status || {}
|
|
loaded.value = true
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Erreur : ' + e.message })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDelete (row) {
|
|
$q.dialog({
|
|
title: 'Supprimer',
|
|
message: `Supprimer ce courriel de la file (${(row.recipients || []).join(', ') || row.name}) ?`,
|
|
cancel: true, persistent: true,
|
|
}).onOk(async () => {
|
|
try { await deleteEmailQueueItem(row.name); $q.notify({ type: 'positive', message: 'Supprimé' }); load() }
|
|
catch (e) { $q.notify({ type: 'negative', message: e.message }) }
|
|
})
|
|
}
|
|
|
|
function confirmPurge () {
|
|
$q.dialog({
|
|
title: 'Purger',
|
|
message: `Supprimer les ${notSentCount.value} courriel(s) « Not Sent » de la file ?`,
|
|
cancel: true, persistent: true,
|
|
}).onOk(async () => {
|
|
try { const r = await purgeEmailQueue('Not Sent'); $q.notify({ type: 'positive', message: `${r.deleted} supprimé(s)` }); load() }
|
|
catch (e) { $q.notify({ type: 'negative', message: e.message }) }
|
|
})
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|