feat(ops): redesign client ticket list (TicketCard, mobile-first)
La section Tickets de la fiche utilisait un q-table dont le mode carte mobile empilait des valeurs non étiquetées (« 0 », n°, sujet, avatars, date…) = archaïque et peu lisible (cf. capture agent terrain). Nouveau composant components/customer/TicketCard.vue : sujet en évidence + badges statut/priorité colorés (édition inline conservée via InlineField) + n° (legacy ou ISS) + date + avatars ouvreur/assigné empilés ; rangée responsive (flex-wrap) lisible sur mobile ET desktop. Remplace le DataTable dans la section Tickets ; imports morts retirés (ticketCols, ticketStatusClass, priorityClass, staffColor, staffInitials). Vérifié live (C-LPB4) : 5 cartes, sujet + Open/Medium + #ISS-… + date, aucun élément > viewport à 375px, rendu propre à 1280px, 0 erreur. Note : la réutilisation d'icônes de compétences dans les tickets est reportée — les Issues (surtout legacy) n'ont pas de signal compétence fiable (0 Dispatch Job correspondant) ; à faire quand un lien ticket→compétence existe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c193bcbf1f
commit
eb4d098bf0
72
apps/ops/src/components/customer/TicketCard.vue
Normal file
72
apps/ops/src/components/customer/TicketCard.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div class="tk-card clickable-row" @click="$emit('open', ticket)">
|
||||
<div class="tk-row">
|
||||
<q-icon v-if="ticket.is_important" name="star" color="warning" size="16px" class="tk-star" />
|
||||
<div class="tk-subject">{{ ticket.subject || '(sans sujet)' }}</div>
|
||||
<span class="tk-badge-wrap" @click.stop @dblclick.stop>
|
||||
<InlineField :value="ticket.status" field="status" doctype="Issue" :docname="ticket.name"
|
||||
type="select" :options="['Open', 'Replied', 'On Hold', 'Resolved', 'Closed']"
|
||||
@saved="v => ticket.status = v.value">
|
||||
<template #display>
|
||||
<span class="ops-badge tk-badge" :class="ticketStatusClass(ticket.status)">{{ ticket.status }}</span>
|
||||
</template>
|
||||
</InlineField>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tk-meta">
|
||||
<span class="tk-badge-wrap" @click.stop @dblclick.stop>
|
||||
<InlineField :value="ticket.priority" field="priority" doctype="Issue" :docname="ticket.name"
|
||||
type="select" :options="['Low', 'Medium', 'High', 'Urgent']"
|
||||
@saved="v => ticket.priority = v.value">
|
||||
<template #display>
|
||||
<span class="ops-badge tk-badge" :class="priorityClass(ticket.priority)">{{ ticket.priority }}</span>
|
||||
</template>
|
||||
</InlineField>
|
||||
</span>
|
||||
<span class="tk-id">#{{ ticket.legacy_ticket_id || ticket.name }}</span>
|
||||
<span v-if="ticket.opening_date" class="tk-date"><q-icon name="event" size="12px" /> {{ ticket.opening_date }}</span>
|
||||
<q-space />
|
||||
<div v-if="ticket.assigned_staff || ticket.opened_by_staff" class="avatar-stack">
|
||||
<q-avatar v-if="ticket.opened_by_staff && ticket.opened_by_staff !== ticket.assigned_staff"
|
||||
size="22px" class="avatar-chip" :style="{ background: staffColor(ticket.opened_by_staff), zIndex: 1 }">
|
||||
{{ staffInitials(ticket.opened_by_staff) }}
|
||||
<q-tooltip>Ouvert par {{ ticket.opened_by_staff }}</q-tooltip>
|
||||
</q-avatar>
|
||||
<q-avatar v-if="ticket.assigned_staff"
|
||||
size="22px" class="avatar-chip" :style="{ background: staffColor(ticket.assigned_staff), zIndex: 2 }">
|
||||
{{ staffInitials(ticket.assigned_staff) }}
|
||||
<q-tooltip>Assigné à {{ ticket.assigned_staff }}</q-tooltip>
|
||||
</q-avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// TicketCard — rangée de ticket lisible et responsive (remplace le mode carte
|
||||
// archaïque du q-table sur mobile). Sujet en évidence + badges statut/priorité
|
||||
// colorés (édition inline conservée) + n° + date + avatars ouvreur/assigné.
|
||||
import InlineField from 'src/components/shared/InlineField.vue'
|
||||
import { ticketStatusClass, priorityClass } from 'src/composables/useStatusClasses'
|
||||
import { staffColor, staffInitials } from 'src/composables/useFormatters'
|
||||
|
||||
defineProps({ ticket: { type: Object, required: true } })
|
||||
defineEmits(['open'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tk-card { padding: 9px 8px; border-radius: 8px; margin: 0 -4px; }
|
||||
.tk-card:not(:last-child) { border-bottom: 1px solid #eef1f5; }
|
||||
.tk-row { display: flex; align-items: flex-start; gap: 8px; }
|
||||
.tk-star { flex-shrink: 0; margin-top: 1px; }
|
||||
.tk-subject { flex: 1; min-width: 0; font-size: 0.9rem; font-weight: 600; color: #1e293b; line-height: 1.3; word-break: break-word; }
|
||||
.tk-badge-wrap { flex-shrink: 0; }
|
||||
.tk-badge { font-size: 10px; padding: 1px 8px; }
|
||||
.tk-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; margin-top: 6px; padding-left: 0; }
|
||||
.tk-id { font-size: 11px; color: #94a3b8; font-variant-numeric: tabular-nums; }
|
||||
.tk-date { font-size: 11px; color: #94a3b8; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; }
|
||||
.avatar-stack { display: flex; align-items: center; }
|
||||
.avatar-stack .avatar-chip { color: #fff; font-size: 9px; font-weight: 600; border: 1.5px solid #fff; margin-left: -8px; }
|
||||
.avatar-stack .avatar-chip:first-child { margin-left: 0; }
|
||||
/* clickable-row (hover) est global (app.scss) */
|
||||
</style>
|
||||
|
|
@ -125,69 +125,8 @@
|
|||
</template>
|
||||
<div v-if="!tickets.length" class="ops-card text-center text-grey-6 q-pa-md q-mb-md">Aucun ticket</div>
|
||||
<div v-else class="ops-card q-mb-md">
|
||||
<DataTable :rows="visibleTickets" :columns="ticketCols" row-key="name"
|
||||
flat dense class="ops-table clickable-table"
|
||||
:pagination="{ rowsPerPage: 0, sortBy: null }"
|
||||
@row-click="(_, row) => openModal('Issue', row.name, row.subject)">
|
||||
<template #body-cell-important="props">
|
||||
<q-td :props="props" style="padding:0 2px">
|
||||
<q-icon v-if="props.row.is_important" name="star" color="warning" size="14px" />
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-legacy_id="props">
|
||||
<q-td :props="props" style="padding:0 4px">
|
||||
<span style="font-size:11px;color:#9e9e9e">{{ props.row.legacy_ticket_id || props.row.name }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-subject="props">
|
||||
<q-td :props="props" class="ticket-subject-cell">
|
||||
<div style="font-size:12.5px;line-height:1.3">{{ props.row.subject }}</div>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-assigned="props">
|
||||
<q-td :props="props" style="padding:0 2px">
|
||||
<div class="avatar-stack">
|
||||
<q-avatar v-if="props.row.opened_by_staff && props.row.opened_by_staff !== props.row.assigned_staff"
|
||||
size="22px" class="avatar-chip" :style="{ background: staffColor(props.row.opened_by_staff), zIndex: 1 }">
|
||||
{{ staffInitials(props.row.opened_by_staff) }}
|
||||
<q-tooltip>{{ props.row.opened_by_staff }}</q-tooltip>
|
||||
</q-avatar>
|
||||
<q-avatar v-if="props.row.assigned_staff"
|
||||
size="22px" class="avatar-chip" :style="{ background: staffColor(props.row.assigned_staff), zIndex: 2 }">
|
||||
{{ staffInitials(props.row.assigned_staff) }}
|
||||
<q-tooltip>{{ props.row.assigned_staff }}</q-tooltip>
|
||||
</q-avatar>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-opening_date="props">
|
||||
<q-td :props="props" style="padding:0 4px">
|
||||
<span style="font-size:11px;color:#9e9e9e;white-space:nowrap">{{ props.row.opening_date }}</span>
|
||||
</q-td>
|
||||
</template>
|
||||
<template #body-cell-priority="props">
|
||||
<q-td :props="props" style="padding:0 2px">
|
||||
<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">
|
||||
<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>
|
||||
</DataTable>
|
||||
<TicketCard v-for="t in visibleTickets" :key="t.name" :ticket="t"
|
||||
@open="openModal('Issue', t.name, t.subject)" />
|
||||
<div v-if="!ticketsExpanded && tickets.length > 5" class="text-center q-pa-xs">
|
||||
<q-btn flat dense no-caps color="primary" :loading="loadingMoreTickets"
|
||||
label="Voir tous les tickets" icon="expand_more"
|
||||
|
|
@ -739,14 +678,14 @@ import { deleteDoc, createDoc, listDocs } from 'src/api/erp'
|
|||
import { authFetch } from 'src/api/auth'
|
||||
import { BASE_URL, ERP_DESK_URL } from 'src/config/erpnext'
|
||||
import { HUB_URL } from 'src/config/hub'
|
||||
import { formatDate, formatDateTime, formatMoney, staffColor, staffInitials, noteAuthorName } from 'src/composables/useFormatters'
|
||||
import { ticketStatusClass, invStatusClass, priorityClass } from 'src/composables/useStatusClasses'
|
||||
import { formatDate, formatDateTime, formatMoney, noteAuthorName } from 'src/composables/useFormatters'
|
||||
import { invStatusClass } from 'src/composables/useStatusClasses'
|
||||
import { useDetailModal } from 'src/composables/useDetailModal'
|
||||
import { useSubscriptionGroups } from 'src/composables/useSubscriptionGroups'
|
||||
import { useSubscriptionActions } from 'src/composables/useSubscriptionActions'
|
||||
import { useCustomerNotes } from 'src/composables/useCustomerNotes'
|
||||
import { useConversations } from 'src/composables/useConversations'
|
||||
import { invoiceCols, ticketCols, voipCols, quotationCols, serviceContractCols } from 'src/config/table-columns'
|
||||
import { invoiceCols, voipCols, quotationCols, serviceContractCols } from 'src/config/table-columns'
|
||||
import DetailModal from 'src/components/shared/DetailModal.vue'
|
||||
import DataTable from 'src/components/shared/DataTable.vue'
|
||||
import CustomerHeader from 'src/components/customer/CustomerHeader.vue'
|
||||
|
|
@ -770,6 +709,7 @@ import { erpPdfUrl } from 'src/utils/erp-pdf'
|
|||
import RewardDialog from 'src/components/customer/RewardDialog.vue'
|
||||
import LocationCard from 'src/components/customer/LocationCard.vue'
|
||||
import PaymentSection from 'src/components/customer/PaymentSection.vue'
|
||||
import TicketCard from 'src/components/customer/TicketCard.vue'
|
||||
|
||||
const $q = useQuasar()
|
||||
const { can } = usePermissions()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user