- 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>
97 lines
3.4 KiB
Vue
97 lines
3.4 KiB
Vue
<template>
|
|
<q-page padding>
|
|
<div class="page-title">Bonjour, {{ store.customerName }}</div>
|
|
|
|
<div class="row q-col-gutter-md">
|
|
<!-- Unpaid invoices -->
|
|
<div class="col-12 col-sm-6 col-md-4">
|
|
<div class="portal-card">
|
|
<div class="text-caption text-grey-7">Factures impayées</div>
|
|
<div class="summary-value">{{ unpaidCount }}</div>
|
|
<div v-if="unpaidTotal > 0" class="text-body2 text-grey-7">
|
|
{{ formatMoney(unpaidTotal) }} à payer
|
|
</div>
|
|
<q-btn flat color="primary" label="Voir les factures" to="/invoices" class="q-mt-sm" no-caps />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Open tickets -->
|
|
<div class="col-12 col-sm-6 col-md-4">
|
|
<div class="portal-card">
|
|
<div class="text-caption text-grey-7">Tickets ouverts</div>
|
|
<div class="summary-value">{{ openTickets }}</div>
|
|
<q-btn flat color="primary" label="Voir le support" to="/tickets" class="q-mt-sm" no-caps />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick action -->
|
|
<div class="col-12 col-sm-6 col-md-4">
|
|
<div class="portal-card">
|
|
<div class="text-caption text-grey-7">Besoin d'aide?</div>
|
|
<div class="text-h6 q-mt-xs">Contactez-nous</div>
|
|
<q-btn flat color="primary" label="Nouveau ticket" to="/tickets" class="q-mt-sm" no-caps icon="add" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent invoices -->
|
|
<div v-if="recentInvoices.length" class="q-mt-lg">
|
|
<div class="text-subtitle1 text-weight-medium q-mb-sm">Dernières factures</div>
|
|
<q-list bordered separator class="rounded-borders bg-white">
|
|
<q-item v-for="inv in recentInvoices" :key="inv.name" clickable @click="$router.push('/invoices')">
|
|
<q-item-section>
|
|
<q-item-label>{{ inv.name }}</q-item-label>
|
|
<q-item-label caption>{{ formatDate(inv.posting_date) }}</q-item-label>
|
|
</q-item-section>
|
|
<q-item-section side>
|
|
<q-item-label>{{ formatMoney(inv.grand_total) }}</q-item-label>
|
|
<q-item-label caption :class="statusClass(inv.status)">
|
|
{{ inv.status }}
|
|
</q-item-label>
|
|
</q-item-section>
|
|
</q-item>
|
|
</q-list>
|
|
</div>
|
|
</q-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useCustomerStore } from 'src/stores/customer'
|
|
import { fetchInvoices, fetchTickets } from 'src/api/portal'
|
|
import { useFormatters } from 'src/composables/useFormatters'
|
|
|
|
const store = useCustomerStore()
|
|
const { formatDate, formatMoney } = useFormatters()
|
|
|
|
const recentInvoices = ref([])
|
|
const allTickets = ref([])
|
|
|
|
const unpaidCount = computed(() =>
|
|
recentInvoices.value.filter(i => i.outstanding_amount > 0).length,
|
|
)
|
|
const unpaidTotal = computed(() =>
|
|
recentInvoices.value.filter(i => i.outstanding_amount > 0)
|
|
.reduce((sum, i) => sum + i.outstanding_amount, 0),
|
|
)
|
|
const openTickets = computed(() =>
|
|
allTickets.value.filter(t => t.status === 'Open').length,
|
|
)
|
|
|
|
function statusClass (status) {
|
|
if (status === 'Paid') return 'status-paid'
|
|
if (status === 'Unpaid' || status === 'Overdue') return 'status-unpaid'
|
|
return ''
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!store.customerId) return
|
|
const [invoices, tickets] = await Promise.all([
|
|
fetchInvoices(store.customerId, { pageSize: 5 }),
|
|
fetchTickets(store.customerId),
|
|
])
|
|
recentInvoices.value = invoices
|
|
allTickets.value = tickets
|
|
})
|
|
</script>
|