- Add PostgreSQL performance indexes migration script (1000x faster queries) Sales Invoice: 1,248ms → 28ms, Payment Entry: 443ms → 31ms Indexes on customer/party columns for all major tables - Disable 3CX poller (PBX_ENABLED flag, using Twilio instead) - Add TelephonyPage: full CRUD UI for Routr/Fonoster resources (trunks, agents, credentials, numbers, domains, peers) - Add PhoneModal + usePhone composable (Twilio WebRTC softphone) - Lazy-load invoices/payments (initial 5, expand on demand) - Parallelize all API calls in ClientDetailPage (no waterfall) - Add targo-hub service (SSE relay, SMS, voice, telephony API) - Customer portal: invoice detail, ticket detail, messages pages - Remove dead Ollama nginx upstream Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
133 lines
4.2 KiB
Vue
133 lines
4.2 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<div class="flex items-center justify-between q-mb-md">
|
|
<div class="page-title q-mb-none">Support</div>
|
|
<q-btn color="primary" icon="add" label="Nouveau ticket" no-caps @click="showCreate = true" />
|
|
</div>
|
|
|
|
<q-table
|
|
:rows="tickets"
|
|
:columns="columns"
|
|
row-key="name"
|
|
:loading="loading"
|
|
@row-click="(evt, row) => router.push('/tickets/' + row.name)"
|
|
flat bordered
|
|
class="bg-white clickable-table"
|
|
no-data-label="Aucun ticket"
|
|
:pagination="{ rowsPerPage: 50 }"
|
|
>
|
|
<template #body-cell-creation="props">
|
|
<q-td :props="props">{{ formatShortDate(props.value) }}</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-status="props">
|
|
<q-td :props="props">
|
|
<q-badge :color="statusColor(props.value)" :label="statusLabel(props.value)" />
|
|
</q-td>
|
|
</template>
|
|
|
|
<template #body-cell-priority="props">
|
|
<q-td :props="props">
|
|
<q-badge :color="priorityColor(props.value)" :label="props.value" outline />
|
|
</q-td>
|
|
</template>
|
|
</q-table>
|
|
|
|
<!-- New ticket dialog -->
|
|
<q-dialog v-model="showCreate" persistent>
|
|
<q-card style="min-width: 400px; max-width: 600px">
|
|
<q-card-section>
|
|
<div class="text-h6">Nouveau ticket de support</div>
|
|
</q-card-section>
|
|
|
|
<q-card-section>
|
|
<q-input v-model="newTicket.subject" label="Sujet" outlined class="q-mb-md"
|
|
:rules="[v => !!v || 'Le sujet est requis']" />
|
|
<q-input v-model="newTicket.description" label="Description" outlined type="textarea"
|
|
rows="4" :rules="[v => !!v || 'La description est requise']" />
|
|
</q-card-section>
|
|
|
|
<q-card-actions align="right">
|
|
<q-btn flat label="Annuler" @click="showCreate = false" />
|
|
<q-btn color="primary" label="Envoyer" :loading="creating"
|
|
@click="submitTicket" :disable="!newTicket.subject || !newTicket.description" />
|
|
</q-card-actions>
|
|
</q-card>
|
|
</q-dialog>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useQuasar } from 'quasar'
|
|
import { useCustomerStore } from 'src/stores/customer'
|
|
import { fetchTickets, createTicket } from 'src/api/portal'
|
|
import { useFormatters } from 'src/composables/useFormatters'
|
|
|
|
const router = useRouter()
|
|
const $q = useQuasar()
|
|
const store = useCustomerStore()
|
|
const { formatShortDate } = useFormatters()
|
|
|
|
const tickets = ref([])
|
|
const loading = ref(false)
|
|
const showCreate = ref(false)
|
|
const creating = ref(false)
|
|
const newTicket = ref({ subject: '', description: '' })
|
|
|
|
const columns = [
|
|
{ name: 'name', label: 'No.', field: 'name', align: 'left' },
|
|
{ name: 'subject', label: 'Sujet', field: 'subject', align: 'left' },
|
|
{ name: 'status', label: 'Statut', field: 'status', align: 'center' },
|
|
{ name: 'priority', label: 'Priorité', field: 'priority', align: 'center' },
|
|
{ name: 'creation', label: 'Créé le', field: 'creation', align: 'left' },
|
|
]
|
|
|
|
function statusColor (s) {
|
|
if (s === 'Open') return 'primary'
|
|
if (s === 'Closed' || s === 'Resolved') return 'positive'
|
|
if (s === 'Replied') return 'info'
|
|
return 'grey'
|
|
}
|
|
|
|
function statusLabel (s) {
|
|
const map = { Open: 'Ouvert', Closed: 'Fermé', Resolved: 'Résolu', Replied: 'Répondu' }
|
|
return map[s] || s
|
|
}
|
|
|
|
function priorityColor (p) {
|
|
if (p === 'High' || p === 'Urgent') return 'negative'
|
|
if (p === 'Medium') return 'warning'
|
|
return 'grey'
|
|
}
|
|
|
|
async function loadTickets () {
|
|
loading.value = true
|
|
try {
|
|
tickets.value = await fetchTickets(store.customerId)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function submitTicket () {
|
|
creating.value = true
|
|
try {
|
|
await createTicket(store.customerId, newTicket.value.subject, newTicket.value.description)
|
|
showCreate.value = false
|
|
newTicket.value = { subject: '', description: '' }
|
|
$q.notify({ type: 'positive', message: 'Ticket créé avec succès' })
|
|
await loadTickets()
|
|
} catch (e) {
|
|
$q.notify({ type: 'negative', message: 'Erreur: ' + e.message })
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (store.customerId) loadTickets()
|
|
})
|
|
</script>
|