- DynamicFilter.vue : recherche + groupes de chips (tag/statut/…) + PRESETS sauvegardés par utilisateur
(useUserPrefs, clé presetKey) ; contrôleur d'état ({search,chips}) émis → le parent applique (client OU serveur).
- ClientsPage : remplace search+toggle par <DynamicFilter> ; AJOUTE chips Statut/Groupe/Territoire (avant : statut seul) → pilotent la requête serveur ; presets 'filter.clients'.
- fix(erp): countDocs comptait FAUX avec or_filters (get_count ignore or_filters → 0 pendant une recherche) → compte via get_list quand or_filters présent. Vérifié : « bourdon » 0→33.
Vérifié préview : chips (Actifs 9008), recherche (bourdon 33 = lignes), menu presets. Build vert, leak 0. Réutilisable : Tickets/Network/Évaluations/Boîte ensuite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
6.1 KiB
Vue
146 lines
6.1 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<!-- Recherche + filtres dynamiques réutilisables (recherche + chips statut/groupe/territoire + presets sauvegardés par utilisateur) -->
|
|
<div class="q-mb-md">
|
|
<DynamicFilter v-model="filterState" search-placeholder="Nom, ID legacy, numéro de compte…"
|
|
:chip-groups="chipGroups" preset-key="filter.clients" :filtered-count="total" @change="onFilterChange" />
|
|
</div>
|
|
|
|
<!-- Client table — DataTable : passe en cartes sous 1024px (sinon la table déborde sur mobile) -->
|
|
<DataTable
|
|
:rows="clients" :columns="columns" row-key="name"
|
|
class="ops-table"
|
|
:loading="loading"
|
|
:pagination="pagination"
|
|
@request="onRequest"
|
|
@row-click="(_, row) => $router.push('/clients/' + row.name)"
|
|
style="cursor:pointer"
|
|
>
|
|
<template #body-cell-status="props">
|
|
<q-td :props="props">
|
|
<span class="ops-badge" :class="props.row.disabled ? 'inactive' : 'active'">
|
|
{{ props.row.disabled ? 'Inactif' : 'Actif' }}
|
|
</span>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-customer_name="props">
|
|
<q-td :props="props">
|
|
<div class="text-weight-medium" @dblclick.stop>
|
|
<InlineField :value="props.row.customer_name" field="customer_name" doctype="Customer" :docname="props.row.name"
|
|
placeholder="Nom" @saved="v => props.row.customer_name = v.value" />
|
|
</div>
|
|
<div class="text-caption text-grey-6">
|
|
{{ props.row.name }}
|
|
<template v-if="props.row.legacy_customer_id"> · {{ props.row.legacy_customer_id }}</template>
|
|
</div>
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-customer_type="props">
|
|
<q-td :props="props" @dblclick.stop>
|
|
<InlineField :value="props.row.customer_type" field="customer_type" doctype="Customer" :docname="props.row.name"
|
|
type="select" :options="['Individual', 'Company']"
|
|
@saved="v => props.row.customer_type = v.value" />
|
|
</q-td>
|
|
</template>
|
|
<template #body-cell-customer_group="props">
|
|
<q-td :props="props" @dblclick.stop>
|
|
<InlineField :value="props.row.customer_group" field="customer_group" doctype="Customer" :docname="props.row.name"
|
|
type="select" :options="customerGroups"
|
|
@saved="v => props.row.customer_group = v.value" />
|
|
</q-td>
|
|
</template>
|
|
</DataTable>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, watch } from 'vue'
|
|
import { Notify } from 'quasar'
|
|
import { useRoute } from 'vue-router'
|
|
import { listDocs, countDocs } from 'src/api/erp'
|
|
import InlineField from 'src/components/shared/InlineField.vue'
|
|
import DataTable from 'src/components/shared/DataTable.vue'
|
|
import DynamicFilter from 'src/components/shared/DynamicFilter.vue'
|
|
import { territoryOptions } from 'src/config/nav'
|
|
|
|
const route = useRoute()
|
|
const customerGroups = ['Commercial', 'Individual', 'Government', 'Non Profit']
|
|
const filterState = ref({ search: route.query.q || '', chips: {} }) // recherche + chips (statut/groupe/territoire) → pilote la requête serveur
|
|
const chipGroups = [
|
|
{ key: 'statut', label: 'Statut', icon: 'toggle_on', options: [{ value: 'active', label: 'Actifs' }, { value: 'disabled', label: 'Inactifs' }] },
|
|
{ key: 'group', label: 'Groupe', icon: 'category', options: customerGroups.map(g => ({ value: g, label: g })) },
|
|
{ key: 'territory', label: 'Territoire', icon: 'place', options: territoryOptions.map(t => ({ value: t.value, label: t.label })) },
|
|
]
|
|
const clients = ref([])
|
|
const loading = ref(false)
|
|
const total = ref(0)
|
|
const pagination = ref({ page: 1, rowsPerPage: 25, rowsNumber: 0 })
|
|
let searchTimer = null
|
|
|
|
const columns = [
|
|
{ name: 'customer_name', label: 'Client', field: 'customer_name', align: 'left', sortable: true },
|
|
{ name: 'legacy_customer_id', label: 'ID Legacy', field: 'legacy_customer_id', align: 'left' },
|
|
{ name: 'customer_type', label: 'Type', field: 'customer_type', align: 'left' },
|
|
{ name: 'customer_group', label: 'Groupe', field: 'customer_group', align: 'left' },
|
|
{ name: 'territory', label: 'Territoire', field: 'territory', align: 'left' },
|
|
{ name: 'status', label: 'Statut', align: 'center', field: row => (row.disabled ? 'Inactif' : 'Actif') },
|
|
]
|
|
|
|
// DynamicFilter a émis un nouvel état → recharge (débounce léger pour la frappe ; les chips passent aussi par ici).
|
|
function onFilterChange () {
|
|
clearTimeout(searchTimer)
|
|
searchTimer = setTimeout(() => { pagination.value.page = 1; doSearch() }, 300)
|
|
}
|
|
|
|
async function doSearch () {
|
|
loading.value = true
|
|
const filters = {}
|
|
let or_filters
|
|
const c = filterState.value.chips || {}
|
|
if (c.statut === 'active') filters.disabled = 0
|
|
else if (c.statut === 'disabled') filters.disabled = 1
|
|
if (c.group) filters.customer_group = c.group
|
|
if (c.territory) filters.territory = c.territory
|
|
|
|
const q = (filterState.value.search || '').trim()
|
|
if (q) {
|
|
const like = '%' + q + '%'
|
|
or_filters = [
|
|
['customer_name', 'like', like],
|
|
['name', 'like', like],
|
|
['legacy_customer_id', 'like', like],
|
|
]
|
|
}
|
|
|
|
try {
|
|
const [data, count] = await Promise.all([
|
|
listDocs('Customer', {
|
|
filters,
|
|
or_filters,
|
|
fields: ['name', 'customer_name', 'customer_type', 'customer_group', 'territory', 'disabled', 'legacy_customer_id'],
|
|
limit: pagination.value.rowsPerPage,
|
|
offset: (pagination.value.page - 1) * pagination.value.rowsPerPage,
|
|
orderBy: 'customer_name asc',
|
|
}),
|
|
countDocs('Customer', filters, or_filters),
|
|
])
|
|
clients.value = data
|
|
total.value = count
|
|
pagination.value.rowsNumber = count
|
|
} catch (e) {
|
|
Notify.create({ type: 'negative', message: 'Échec du chargement des clients — réessayez.' })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onRequest (props) {
|
|
pagination.value.page = props.pagination.page
|
|
pagination.value.rowsPerPage = props.pagination.rowsPerPage
|
|
doSearch()
|
|
}
|
|
|
|
onMounted(() => doSearch())
|
|
watch(() => route.query.q, (q) => { if (q) { filterState.value = { ...filterState.value, search: q }; doSearch() } })
|
|
</script>
|