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>
101 lines
4.6 KiB
Vue
101 lines
4.6 KiB
Vue
<template>
|
|
<ReportScaffold
|
|
title="Rapport de ventes"
|
|
:loading="loading" :error="error" :has-run="hasRun"
|
|
:cards="cards" :columns="columns" :rows="rows" row-key="name" searchable
|
|
:pagination="{ rowsPerPage: 50 }"
|
|
empty="Aucune facture pour cette période."
|
|
@retry="loadReport" @export="doExport">
|
|
<template #filters>
|
|
<q-input v-model="startDate" type="date" label="Début" dense outlined stack-label style="width:150px;max-width:100%" />
|
|
<q-input v-model="endDate" type="date" label="Fin" dense outlined stack-label style="width:150px;max-width:100%" />
|
|
<q-btn color="primary" label="Générer" icon="play_arrow" @click="loadReport" :loading="loading" />
|
|
</template>
|
|
|
|
<template #body-cell-name="props">
|
|
<q-td :props="props"><a :href="erpLink('Sales Invoice', props.value)" target="_blank" class="text-primary">{{ props.value }}</a></q-td>
|
|
</template>
|
|
<template #body-cell-total="props">
|
|
<q-td :props="props"><span :class="props.row.is_return ? 'text-negative' : 'text-weight-bold'">{{ formatMoney(props.value) }}</span></q-td>
|
|
</template>
|
|
<template #body-cell-status="props">
|
|
<q-td :props="props"><q-badge :color="statusColor(props.value)" :label="props.value" /></q-td>
|
|
</template>
|
|
</ReportScaffold>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { fetchSalesReport } from 'src/api/reports'
|
|
import { formatMoney, formatDateShort, erpLink } from 'src/composables/useFormatters'
|
|
import { exportCsv } from 'src/composables/useCsvExport'
|
|
import ReportScaffold from 'src/components/shared/ReportScaffold.vue'
|
|
|
|
const now = new Date()
|
|
const startDate = ref(new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10))
|
|
const endDate = ref(now.toISOString().slice(0, 10))
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const hasRun = ref(false)
|
|
const rows = ref([])
|
|
const summary = ref(null)
|
|
|
|
const columns = [
|
|
{ name: 'name', label: '#Facture', field: 'name', align: 'left', sortable: true },
|
|
{ name: 'date', label: 'Date', field: 'date', align: 'left', sortable: true, format: v => formatDateShort(v) },
|
|
{ name: 'customer_name', label: 'Client', field: 'customer_name', align: 'left', sortable: true },
|
|
{ name: 'subtotal', label: 'Sous-total', field: 'subtotal', align: 'right', sortable: true, format: v => formatMoney(v) },
|
|
{ name: 'tps', label: 'TPS', field: 'tps', align: 'right', sortable: true, format: v => formatMoney(v) },
|
|
{ name: 'tvq', label: 'TVQ', field: 'tvq', align: 'right', sortable: true, format: v => formatMoney(v) },
|
|
{ name: 'total', label: 'Total', field: 'total', align: 'right', sortable: true },
|
|
{ name: 'outstanding', label: 'Impayé', field: 'outstanding', align: 'right', sortable: true, format: v => v > 0 ? formatMoney(v) : '—' },
|
|
{ name: 'status', label: 'Statut', field: 'status', align: 'center', sortable: true },
|
|
]
|
|
|
|
const cards = computed(() => {
|
|
const s = summary.value
|
|
if (!s) return []
|
|
const c = [
|
|
{ label: 'Factures', value: s.count },
|
|
{ label: 'Sous-total', value: formatMoney(s.subtotal) },
|
|
{ label: 'TPS', value: formatMoney(s.tps) },
|
|
{ label: 'TVQ', value: formatMoney(s.tvq) },
|
|
{ label: 'Total', value: formatMoney(s.total), valueClass: 'text-positive' },
|
|
]
|
|
if (s.returns) c.push({ label: 'Notes de crédit', value: s.returns, valueClass: 'text-negative' })
|
|
return c
|
|
})
|
|
|
|
function statusColor (s) {
|
|
if (s === 'Paid') return 'positive'
|
|
if (s === 'Overdue') return 'negative'
|
|
if (s === 'Return') return 'purple'
|
|
if (s === 'Cancelled') return 'grey'
|
|
return 'warning'
|
|
}
|
|
|
|
async function loadReport () {
|
|
loading.value = true
|
|
error.value = ''
|
|
hasRun.value = true
|
|
try {
|
|
const res = await fetchSalesReport(startDate.value, endDate.value)
|
|
rows.value = res.rows || []
|
|
summary.value = res.summary || null
|
|
} catch (e) {
|
|
console.error('Sales report error:', e)
|
|
error.value = 'Échec du chargement du rapport — réessayez.'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function doExport () {
|
|
if (!rows.value.length) return
|
|
const headers = ['#Facture', 'Date', '#Client', 'Client', 'SousTotal', 'TPS', 'TVQ', 'Total', 'Impayé', 'Statut']
|
|
const data = rows.value.map(r => [r.name, r.date, r.customer, r.customer_name, r.subtotal.toFixed(2), r.tps.toFixed(2), r.tvq.toFixed(2), r.total.toFixed(2), r.outstanding.toFixed(2), r.status])
|
|
const totalRow = summary.value ? ['', '', '', 'TOTAL', summary.value.subtotal.toFixed(2), summary.value.tps.toFixed(2), summary.value.tvq.toFixed(2), summary.value.total.toFixed(2), '', ''] : null
|
|
exportCsv(`ventes_${startDate.value}_${endDate.value}`, headers, data, { totalRow })
|
|
}
|
|
</script>
|