- InlineField component + useInlineEdit composable for Odoo-style dblclick editing - Client search by name, account ID, and legacy_customer_id (or_filters) - SMS/Email notification panel on ContactCard via n8n webhooks - Ticket reply thread via Communication docs - All migration scripts (51 files) now tracked - Client portal and field tech app added to monorepo - README rewritten with full feature list, migration summary, architecture - CHANGELOG updated with all recent work - ROADMAP updated with current completion status - Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN) - .gitignore updated (docker/, .claude/, exports/, .quasar/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
4.0 KiB
Vue
130 lines
4.0 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"
|
|
flat bordered
|
|
class="bg-white"
|
|
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 { useQuasar } from 'quasar'
|
|
import { useCustomerStore } from 'src/stores/customer'
|
|
import { fetchTickets, createTicket } from 'src/api/portal'
|
|
import { useFormatters } from 'src/composables/useFormatters'
|
|
|
|
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>
|