gigafibre-fsm/apps/client/src/pages/DashboardPage.vue
louispaulb 101faa21f1 feat: inline editing, search, notifications + full repo cleanup
- 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>
2026-03-31 07:34:41 -04:00

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>