feat: ticket lazy-load, inline editing, search improvements
- Tickets: load 10 initially, "Voir tous les tickets" expands to 500 - Inline editing for ticket status and priority (dblclick → select) - Search: Enter key triggers immediate search and navigates to result - Search: Arrow key navigation for result highlighting - Reset expanded state on customer navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4693bcf60c
commit
a2c59d6528
|
|
@ -45,7 +45,7 @@
|
|||
</q-toolbar>
|
||||
<div v-if="mobileSearchOpen && !isDispatch" class="q-px-sm q-pb-sm" style="background:var(--ops-sidebar-bg)">
|
||||
<q-input v-model="searchQuery" placeholder="Rechercher client, adresse..." dense outlined dark autofocus class="ops-search-dark"
|
||||
@keyup.enter="doSearch" @keydown.escape="closeMobileSearch">
|
||||
@keydown.enter.prevent="doSearch" @keydown.escape="closeMobileSearch">
|
||||
<template #prepend><q-icon name="search" color="grey-5" /></template>
|
||||
<template #append v-if="searchQuery"><q-icon name="close" class="cursor-pointer" color="grey-5" @click="clearSearch" /></template>
|
||||
</q-input>
|
||||
|
|
@ -69,7 +69,8 @@
|
|||
<q-space />
|
||||
<div style="position:relative;width:400px">
|
||||
<q-input v-model="searchQuery" placeholder="Rechercher client, adresse, ticket..." dense outlined class="ops-search"
|
||||
@keyup.enter="doSearch" @keydown.escape="clearSearch"
|
||||
@keydown.enter.prevent="doSearch" @keydown.escape="clearSearch"
|
||||
@keydown.down.prevent="moveHighlight(1)" @keydown.up.prevent="moveHighlight(-1)"
|
||||
@update:model-value="onSearchInput" @blur="onSearchBlur">
|
||||
<template #prepend><q-icon name="search" color="grey-6" /></template>
|
||||
<template #append v-if="searchQuery">
|
||||
|
|
@ -189,12 +190,28 @@ async function runSearch (q) {
|
|||
searchLoading.value = false
|
||||
}
|
||||
|
||||
function doSearch () {
|
||||
function moveHighlight (dir) {
|
||||
if (!searchResults.value.length) return
|
||||
searchDropdownOpen.value = true
|
||||
highlightIdx.value = Math.max(-1, Math.min(searchResults.value.length - 1, highlightIdx.value + dir))
|
||||
}
|
||||
|
||||
async function doSearch () {
|
||||
const q = searchQuery.value?.trim()
|
||||
if (!q || q.length < 2) return
|
||||
|
||||
// Always cancel debounce and run search immediately on Enter
|
||||
if (!searchResults.value.length) {
|
||||
clearTimeout(searchTimer)
|
||||
searchLoading.value = true
|
||||
await runSearch(q)
|
||||
}
|
||||
|
||||
if (searchResults.value.length) {
|
||||
const idx = highlightIdx.value >= 0 ? highlightIdx.value : 0
|
||||
goToResult(searchResults.value[idx])
|
||||
} else if (searchQuery.value.trim()) {
|
||||
router.push({ path: '/clients', query: { q: searchQuery.value.trim() } })
|
||||
} else {
|
||||
router.push({ path: '/clients', query: { q } })
|
||||
clearSearch()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@
|
|||
<template #header>
|
||||
<div class="section-title" style="font-size:1rem;width:100%">
|
||||
<q-icon name="confirmation_number" size="20px" class="q-mr-xs" />
|
||||
Tickets ({{ tickets.length }})
|
||||
Tickets ({{ tickets.length }}{{ !ticketsExpanded ? '+' : '' }})
|
||||
<span v-if="openTicketCount" class="ops-badge open q-ml-sm">{{ openTicketCount }} ouverts</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -316,17 +316,31 @@
|
|||
</template>
|
||||
<template #body-cell-priority="props">
|
||||
<q-td :props="props" style="padding:0 2px">
|
||||
<q-icon v-if="props.row.priority === 'Urgent'" name="priority_high" color="red" size="16px" :title="'Urgent'" />
|
||||
<q-icon v-else-if="props.row.priority === 'High'" name="arrow_upward" color="orange-8" size="14px" :title="'High'" />
|
||||
<q-icon v-else-if="props.row.priority === 'Low'" name="arrow_downward" color="blue-grey-4" size="14px" :title="'Low'" />
|
||||
<InlineField :value="props.row.priority" field="priority" doctype="Issue" :docname="props.row.name"
|
||||
type="select" :options="['Low', 'Medium', 'High', 'Urgent']"
|
||||
@saved="v => props.row.priority = v.value">
|
||||
<template #display>
|
||||
<span class="ops-badge" style="font-size:10px;padding:1px 6px" :class="priorityClass(props.row.priority)">{{ props.row.priority }}</span>
|
||||
</template>
|
||||
</InlineField>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-status="props">
|
||||
<q-td :props="props" style="padding:0 4px">
|
||||
<span class="ops-badge" style="font-size:10px;padding:1px 6px" :class="ticketStatusClass(props.row.status)">{{ props.row.status }}</span>
|
||||
<InlineField :value="props.row.status" field="status" doctype="Issue" :docname="props.row.name"
|
||||
type="select" :options="['Open', 'Replied', 'On Hold', 'Resolved', 'Closed']"
|
||||
@saved="v => props.row.status = v.value">
|
||||
<template #display>
|
||||
<span class="ops-badge" style="font-size:10px;padding:1px 6px" :class="ticketStatusClass(props.row.status)">{{ props.row.status }}</span>
|
||||
</template>
|
||||
</InlineField>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
<div v-if="!ticketsExpanded && tickets.length >= 10" class="text-center q-pa-xs">
|
||||
<q-btn flat dense no-caps color="indigo-6" :loading="loadingMoreTickets"
|
||||
label="Voir tous les tickets" icon="expand_more" @click="loadAllTickets" />
|
||||
</div>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
|
|
@ -470,7 +484,7 @@ import { listDocs, getDoc } from 'src/api/erp'
|
|||
import { authFetch } from 'src/api/auth'
|
||||
import { BASE_URL } from 'src/config/erpnext'
|
||||
import { formatDate, formatMoney, staffColor, staffInitials } from 'src/composables/useFormatters'
|
||||
import { locStatusClass, ticketStatusClass, invStatusClass, deviceColorClass } from 'src/composables/useStatusClasses'
|
||||
import { locStatusClass, ticketStatusClass, invStatusClass, deviceColorClass, priorityClass } from 'src/composables/useStatusClasses'
|
||||
import { useDetailModal } from 'src/composables/useDetailModal'
|
||||
import { useSubscriptionGroups, isRebate, subMainLabel, sectionTotal, annualPrice } from 'src/composables/useSubscriptionGroups'
|
||||
import { useSubscriptionActions } from 'src/composables/useSubscriptionActions'
|
||||
|
|
@ -591,6 +605,7 @@ async function loadCustomer (id) {
|
|||
comments.value = []
|
||||
contact.value = null
|
||||
modalOpen.value = false
|
||||
ticketsExpanded.value = false
|
||||
invoicesExpanded.value = false
|
||||
paymentsExpanded.value = false
|
||||
|
||||
|
|
@ -635,7 +650,7 @@ async function loadCustomer (id) {
|
|||
listDocs('Issue', {
|
||||
filters: custFilter,
|
||||
fields: ['name', 'subject', 'status', 'priority', 'opening_date', 'service_location', 'legacy_ticket_id', 'is_important', 'assigned_staff', 'opened_by_staff', 'issue_type'],
|
||||
limit: 200, orderBy: 'opening_date desc',
|
||||
limit: 10, orderBy: 'is_important desc, opening_date desc',
|
||||
}),
|
||||
listDocs('Sales Invoice', {
|
||||
filters: custFilter,
|
||||
|
|
@ -677,12 +692,29 @@ async function loadCustomer (id) {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Lazy-load more invoices/payments on demand ──
|
||||
// ── Lazy-load more tickets/invoices/payments on demand ──
|
||||
const ticketsExpanded = ref(false)
|
||||
const invoicesExpanded = ref(false)
|
||||
const paymentsExpanded = ref(false)
|
||||
const loadingMoreTickets = ref(false)
|
||||
const loadingMoreInvoices = ref(false)
|
||||
const loadingMorePayments = ref(false)
|
||||
|
||||
async function loadAllTickets () {
|
||||
if (ticketsExpanded.value || !customer.value) return
|
||||
loadingMoreTickets.value = true
|
||||
try {
|
||||
const tix = await listDocs('Issue', {
|
||||
filters: { customer: customer.value.name },
|
||||
fields: ['name', 'subject', 'status', 'priority', 'opening_date', 'service_location', 'legacy_ticket_id', 'is_important', 'assigned_staff', 'opened_by_staff', 'issue_type'],
|
||||
limit: 500, orderBy: 'is_important desc, opening_date desc',
|
||||
})
|
||||
tickets.value = tix
|
||||
ticketsExpanded.value = true
|
||||
} catch {}
|
||||
loadingMoreTickets.value = false
|
||||
}
|
||||
|
||||
async function loadAllInvoices () {
|
||||
if (invoicesExpanded.value || !customer.value) return
|
||||
loadingMoreInvoices.value = true
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user