gigafibre-fsm/apps/ops/src/components/customer/CustomerHeader.vue
louispaulb 7d7b4fdb06 feat: nested tasks, project wizard, n8n webhooks, inline task editing
Major dispatch/task system overhaul:
- Project templates with 3-step wizard (choose template → edit steps → publish)
- 4 built-in templates: phone service, fiber install, move, repair
- Nested task tree with recursive TaskNode component (parent_job hierarchy)
- n8n webhook integration (on_open_webhook, on_close_webhook per task)
- Inline task editing: status, priority, type, tech assignment, tags, delete
- Tech assignment + tags from ticket modal → jobs appear on dispatch timeline
- ERPNext custom fields: parent_job, on_open_webhook, on_close_webhook, step_order
- Refactored ClientDetailPage, ChatterPanel, DetailModal, dispatch store
- CSS consolidation, dead code cleanup, composable extraction
- Dashboard KPIs with dispatch integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 13:01:20 -04:00

106 lines
4.3 KiB
Vue

<template>
<div class="customer-header-block">
<!-- Main header line -->
<div class="row items-center q-col-gutter-sm">
<div class="col-auto">
<q-btn flat dense round icon="arrow_back" @click="$router.back()" />
</div>
<div class="col">
<div class="text-h5 text-weight-bold" style="line-height:1.2">
<InlineField :value="customer.customer_name" field="customer_name" doctype="Customer" :docname="customer.name"
placeholder="Nom du client" @saved="v => customer.customer_name = v.value" />
</div>
<div class="text-caption text-grey-6 row items-center no-wrap q-gutter-x-xs">
<span>{{ customer.name }}</span>
<template v-if="customer.legacy_customer_id"><span>&middot; Legacy: {{ customer.legacy_customer_id }}</span></template>
<span>&middot;</span>
<q-select v-model="customer.customer_type" dense borderless
:options="['Individual', 'Company']" emit-value map-options
input-class="editable-input text-caption" style="min-width:80px;max-width:110px"
@update:model-value="save('customer_type')" />
<span>&middot;</span>
<q-select v-model="customer.customer_group" dense borderless
:options="customerGroups" emit-value map-options
input-class="editable-input text-caption" style="min-width:90px;max-width:130px"
@update:model-value="save('customer_group')" />
<template v-if="customer.language">
<span>&middot;</span>
<q-select v-model="customer.language" dense borderless
:options="[{label:'Français',value:'fr'},{label:'English',value:'en'}]" emit-value map-options
input-class="editable-input text-caption" style="min-width:70px;max-width:100px"
@update:model-value="save('language')" />
</template>
</div>
</div>
<div class="col-auto text-right">
<span class="ops-badge q-mb-xs" :class="customer.disabled ? 'inactive' : 'active'" style="font-size:0.85rem">
{{ customer.disabled ? 'Inactif' : 'Actif' }}
</span>
<div class="q-mt-xs q-gutter-x-xs">
<q-btn flat dense size="xs" icon="open_in_new" label="ERPNext"
:href="erpDeskUrl + '/app/customer/' + customer.name" target="_blank"
class="text-grey-6" no-caps />
<q-btn flat dense size="xs" icon="manage_accounts" label="Users"
:href="erpDeskUrl + '/app/user?customer=' + customer.name" target="_blank"
class="text-grey-6" no-caps />
</div>
</div>
</div>
<!-- Contact & Info (collapsible) -->
<q-expansion-item v-model="contactOpen" dense header-class="contact-toggle-header" class="q-mt-xs">
<template #header>
<div class="row items-center" style="width:100%;gap:4px">
<q-icon name="contact_phone" size="16px" color="grey-5" />
<span class="text-caption text-weight-medium text-grey-6">Contact & Info</span>
<q-space />
<span class="text-caption text-grey-5">{{ customer.contact_name_legacy || '' }}</span>
</div>
</template>
<div class="q-pt-xs">
<div class="row q-col-gutter-md">
<div class="col-12 col-md-6">
<slot name="contact" />
</div>
<div class="col-12 col-md-6">
<slot name="info" />
</div>
</div>
</div>
</q-expansion-item>
</div>
</template>
<script setup>
import { ref } from 'vue'
import InlineField from 'src/components/shared/InlineField.vue'
import { ERP_DESK_URL as erpDeskUrl } from 'src/config/erpnext'
import { updateDoc } from 'src/api/erp'
const contactOpen = ref(false)
const props = defineProps({
customer: { type: Object, required: true },
customerGroups: { type: Array, default: () => ['Commercial', 'Individual', 'Government', 'Non Profit'] },
})
async function save (field) {
try {
await updateDoc('Customer', props.customer.name, { [field]: props.customer[field] ?? '' })
} catch (e) {
const { Notify } = await import('quasar')
Notify?.create?.({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
}
}
</script>
<style scoped>
.customer-header-block {
padding: 8px 0 4px 0;
}
.contact-toggle-header {
padding: 4px 8px !important;
min-height: 28px !important;
}
</style>