- Remove apps/dispatch/ (100% replaced by ops dispatch module, unmaintained) - Commit services/targo-hub/lib/ (24 modules, 6290 lines — was never tracked) - Commit services/docuseal + services/legacy-db docker-compose configs - Extract client app composables: useOTP, useAddressSearch, catalog data, format utils - Refactor CartPage.vue 630→175 lines, CatalogPage.vue 375→95 lines - Clean hardcoded credentials from config.js fallback values - Add client portal: catalog, cart, checkout, OTP verification, address search - Add ops: NetworkPage, AgentFlowsPage, ConversationPanel, UnifiedCreateModal - Add ops composables: useBestTech, useConversations, usePermissions, useScanner - Add field app: scanner composable, docker/nginx configs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
167 lines
6.2 KiB
Vue
167 lines
6.2 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>· Legacy: {{ customer.legacy_customer_id }}</span></template>
|
|
<span>·</span>
|
|
<q-select v-model="customer.customer_group" dense borderless
|
|
:options="accountTypeOptions" emit-value map-options
|
|
input-class="editable-input text-caption" style="min-width:90px;max-width:140px"
|
|
@update:model-value="saveAccountType" />
|
|
<template v-if="customer.language">
|
|
<span>·</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">
|
|
<q-select v-model="customerStatus" dense borderless
|
|
:options="statusOptions" emit-value map-options
|
|
input-class="editable-input text-weight-bold"
|
|
:input-style="{ color: statusColor }"
|
|
style="min-width:100px;max-width:140px;text-align:right"
|
|
@update:model-value="saveStatus" />
|
|
<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, computed } 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 },
|
|
})
|
|
|
|
const accountTypeOptions = [
|
|
{ label: 'Résidentiel', value: 'Individual' },
|
|
{ label: 'Commercial', value: 'Commercial' },
|
|
]
|
|
|
|
const statusOptions = [
|
|
{ label: 'Actif', value: 'active' },
|
|
{ label: 'Inactif', value: 'disabled' },
|
|
{ label: 'Prospect', value: 'prospect' },
|
|
{ label: 'Suspendu', value: 'suspended' },
|
|
]
|
|
|
|
const customerStatus = computed({
|
|
get () {
|
|
if (props.customer.customer_group === 'Prospect') return 'prospect'
|
|
if (props.customer.disabled) return 'disabled'
|
|
if (props.customer.customer_status === 'suspended') return 'suspended'
|
|
return 'active'
|
|
},
|
|
set () { /* handled by saveStatus */ }
|
|
})
|
|
|
|
const statusColor = computed(() => {
|
|
const colors = { active: '#2e7d32', disabled: '#c62828', prospect: '#ef6c00', suspended: '#7b1fa2' }
|
|
return colors[customerStatus.value] || '#333'
|
|
})
|
|
|
|
async function saveAccountType (val) {
|
|
try {
|
|
// Sync customer_type: Commercial → Company, Résidentiel → Individual
|
|
const customerType = val === 'Commercial' ? 'Company' : 'Individual'
|
|
await updateDoc('Customer', props.customer.name, { customer_group: val, customer_type: customerType })
|
|
props.customer.customer_type = customerType
|
|
} catch (e) {
|
|
const { Notify } = await import('quasar')
|
|
Notify?.create?.({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
|
|
}
|
|
}
|
|
|
|
async function saveStatus (val) {
|
|
try {
|
|
const updates = {}
|
|
if (val === 'disabled') {
|
|
updates.disabled = 1
|
|
} else if (val === 'prospect') {
|
|
updates.disabled = 0
|
|
updates.customer_group = 'Prospect'
|
|
props.customer.customer_group = 'Prospect'
|
|
} else if (val === 'suspended') {
|
|
updates.disabled = 0
|
|
updates.customer_status = 'suspended'
|
|
} else {
|
|
updates.disabled = 0
|
|
updates.customer_status = ''
|
|
}
|
|
await updateDoc('Customer', props.customer.name, updates)
|
|
props.customer.disabled = updates.disabled ?? props.customer.disabled
|
|
if (updates.customer_status !== undefined) props.customer.customer_status = updates.customer_status
|
|
} catch (e) {
|
|
const { Notify } = await import('quasar')
|
|
Notify?.create?.({ type: 'negative', message: 'Erreur: ' + e.message, timeout: 3000 })
|
|
}
|
|
}
|
|
|
|
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>
|