feat(ui): T5 — DynamicFilter sur Factures fournisseurs + File courriels

- SupplierInvoices : toggle statut → <DynamicFilter> (chips À traiter/Créées/Ignorées + recherche fournisseur/n°/expéditeur, client-side) ; presets 'filter.supplier-invoices'.
- EmailQueue : select statut → chips (Not Sent/Sending/Sent/Error, → rechargement serveur via onFilterChange) + recherche destinataire/référence (client, displayRows) ; presets 'filter.email-queue'.
Build vert, leak 0. DynamicFilter désormais sur 6 pages (Clients/Tickets/Évaluations/RDV/Factures fourn./File courriels).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-07-04 20:02:11 -04:00
parent f6f4303e2e
commit 936152d452
2 changed files with 25 additions and 13 deletions

View File

@ -2,9 +2,8 @@
<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" />
<DynamicFilter class="col q-mx-md" v-model="filterState" search-placeholder="Destinataire, référence"
:chip-groups="chipGroups" preset-key="filter.email-queue" :filtered-count="displayRows.length" @change="onFilterChange" />
<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" />
@ -25,7 +24,7 @@
<DataTable
v-if="loaded"
:rows="rows" :columns="columns" row-key="name"
:rows="displayRows" :columns="columns" row-key="name"
flat bordered dense class="ops-table"
:pagination="{ rowsPerPage: 50, sortBy: 'creation', descending: true }"
:loading="loading"
@ -74,6 +73,7 @@
import { ref, computed, onMounted } from 'vue'
import { formatDateTimeShort as fmtDate } from 'src/composables/useFormatters' // date+heure canonique (source unique)
import DataTable from 'src/components/shared/DataTable.vue'
import DynamicFilter from 'src/components/shared/DynamicFilter.vue'
import { useQuasar } from 'quasar'
import { listEmailQueue, deleteEmailQueueItem, purgeEmailQueue } from 'src/api/emailQueue'
@ -103,6 +103,16 @@ const columns = [
const notSentCount = computed(() => byStatus.value['Not Sent'] || 0)
// Filtre dynamique : chips statut ( rechargement serveur) + recherche destinataire/référence (client, sur les lignes chargées) + presets.
const filterState = ref({ search: '', chips: {} })
const chipGroups = [{ key: 'statut', label: 'Statut', icon: 'label', options: statusOptions.filter(o => o.value !== '') }]
const displayRows = computed(() => {
const q = (filterState.value.search || '').toLowerCase().trim()
if (!q) return rows.value
return rows.value.filter(r => ((r.recipients || []).join(' ') + ' ' + (r.reference_doctype || '') + ' ' + (r.reference_name || '')).toLowerCase().includes(q))
})
function onFilterChange (s) { const ns = s.chips.statut || ''; if (ns !== status.value) { status.value = ns; load() } }
function statusColor (s) {
return { 'Not Sent': 'orange-8', Sending: 'blue-6', Sent: 'green-7', Error: 'negative', Expired: 'grey-6' }[s] || 'grey-7'
}

View File

@ -19,11 +19,8 @@
<div class="row q-col-gutter-md">
<!-- Liste -->
<div :class="selected ? 'col-12 col-md-5' : 'col-12'">
<div class="row items-center q-gutter-sm q-mb-sm">
<q-btn-toggle v-model="filter" :options="[{label:'À traiter',value:'open'},{label:'Créées',value:'Created'},{label:'Ignorées',value:'Ignored'},{label:'Tout',value:'all'}]" dense unelevated toggle-color="primary" color="grey-3" text-color="grey-8" size="sm" no-caps />
<q-space />
<q-badge color="green-1" text-color="green-8">{{ filtered.length }}</q-badge>
</div>
<DynamicFilter v-model="filterState" dense search-placeholder="Fournisseur, n° facture, expéditeur"
:chip-groups="chipGroups" preset-key="filter.supplier-invoices" :filtered-count="filtered.length" class="q-mb-sm" />
<div v-for="r in filtered" :key="r.name" class="si-row" :class="{ sel: selected && selected.name === r.name }" @click="selected = r">
<div class="row items-center no-wrap">
<q-icon :name="statusIcon(r.status)" :color="statusColor(r.status)" size="18px" class="q-mr-sm" />
@ -91,6 +88,7 @@
import { ref, computed, onMounted, watch } from 'vue'
import { useQuasar } from 'quasar'
import { HUB_URL } from 'src/config/hub'
import DynamicFilter from 'src/components/shared/DynamicFilter.vue'
const $q = useQuasar()
const list = ref([])
@ -98,7 +96,9 @@ const loading = ref(false)
const polling = ref(false)
const creating = ref(false)
const selected = ref(null)
const filter = ref('open')
// Filtre dynamique (client) : recherche fournisseur/n°/expéditeur + chips statut + presets.
const filterState = ref({ search: '', chips: { statut: 'open' } })
const chipGroups = [{ key: 'statut', label: 'Statut', icon: 'label', options: [{ value: 'open', label: 'À traiter' }, { value: 'Created', label: 'Créées' }, { value: 'Ignored', label: 'Ignorées' }] }]
const invoiceTo = ref('factures@targointernet.com')
const erpBase = ref('https://erp.gigafibre.ca')
const imgError = ref(false)
@ -106,9 +106,11 @@ const imgError = ref(false)
watch(selected, () => { imgError.value = false })
const filtered = computed(() => {
if (filter.value === 'all') return list.value
if (filter.value === 'open') return list.value.filter(r => r.status === 'New' || r.status === 'Error')
return list.value.filter(r => r.status === filter.value)
const st = filterState.value.chips.statut || 'all'
let arr = st === 'all' ? list.value : (st === 'open' ? list.value.filter(r => r.status === 'New' || r.status === 'Error') : list.value.filter(r => r.status === st))
const q = (filterState.value.search || '').toLowerCase().trim()
if (q) arr = arr.filter(r => (String(r.vendor || '') + ' ' + String(r.invoice_number || '') + ' ' + String(r.email_from || '')).toLowerCase().includes(q))
return arr
})
function fmtMoney (v, cur) { if (v == null || v === '') return '—'; return Number(v).toLocaleString('fr-CA', { style: 'currency', currency: cur || 'CAD' }) }