fix(ui): fiche client — en-tête ne déborde plus en mobile ; assistant = sélecteur recherchable
- CustomerHeader : sous-ligne (nom/legacy/type/langue) était `no-wrap` → débordait sa colonne et chevauchait le bloc statut/liens en mobile. Fix : wrap + colonne `min-width:0` + le bloc statut/ERPNext/Users passe pleine largeur en dessous (<sm) via `col-12 col-sm-auto` ; titre en ellipsis. - Ajouter un assistant : q-select brut → <TechSelect> (recherche typeahead) ; jdTeamOptions = nom · compétences (searchable par nom OU compétence), CAPABLES d'abord (possèdent la compétence du job), value = id ; jdAddAssistant résout le tech par id. Vérifié préview mobile (375px) : sous-ligne wrap, 0 débordement (scrollW==clientW) ; build vert, leak 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
80c8005f0c
commit
112ed0ea2b
|
|
@ -5,12 +5,12 @@
|
|||
<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">
|
||||
<div class="col" style="min-width:0">
|
||||
<div class="text-h5 text-weight-bold ellipsis" 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">
|
||||
<div class="text-caption text-grey-6 row items-center q-gutter-x-xs" style="flex-wrap:wrap">
|
||||
<span>{{ customer.name }}</span>
|
||||
<template v-if="customer.legacy_customer_id"><span>· Legacy: {{ customer.legacy_customer_id }}</span></template>
|
||||
<span>·</span>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto text-right">
|
||||
<div class="col-12 col-sm-auto text-right">
|
||||
<q-select v-model="customerStatus" dense borderless
|
||||
:options="statusOptions" emit-value map-options
|
||||
input-class="editable-input text-weight-bold"
|
||||
|
|
|
|||
|
|
@ -1378,7 +1378,7 @@
|
|||
</div>
|
||||
<div v-else class="text-caption text-grey-6 q-mb-xs">Aucun assistant. Ajoutez un renfort → un bloc <b>hachuré</b> sera réservé dans son horaire (anti double‑booking).</div>
|
||||
<div class="row items-center no-wrap q-gutter-xs">
|
||||
<q-select dense outlined options-dense v-model="jobDetail.teamAdd" :options="jdTeamOptions" emit-value map-options label="Ajouter un assistant" style="flex:1" />
|
||||
<TechSelect v-model="jobDetail.teamAdd" :options="jdTeamOptions" label="Ajouter un assistant (nom · compétence)" style="flex:1;min-width:0" />
|
||||
<q-btn dense unelevated color="deep-purple" icon="group_add" label="Ajouter" :disable="!jobDetail.teamAdd" @click="jdAddAssistant" no-caps />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2186,16 +2186,25 @@ const geoTimeline = computed(() => {
|
|||
const curIdx = order[(gf && gf.state) || 'assigned'] ?? 0
|
||||
return GEO_STEPS.map((s, i) => ({ ...s, at: s.k === 'assigned' ? null : atOf(s.k), done: i <= curIdx, current: i === curIdx }))
|
||||
})
|
||||
// Options du sélecteur d'assistant : tous les techs sauf le lead courant + ceux déjà dans l'équipe.
|
||||
const jdTeamOptions = computed(() => { const taken = new Set([jobDetail.techId, ...(jobDetail.team || []).map(a => a.tech_id)]); return (techs.value || []).filter(t => !taken.has(t.id)).map(t => ({ label: t.name, value: { id: t.id, name: t.name } })) })
|
||||
// Options du sélecteur d'assistant (TechSelect, recherche par nom OU compétence) : tous les techs sauf le lead + l'équipe déjà là.
|
||||
// Label = nom · compétences (searchable) ; CAPABLES d'abord (possèdent la compétence du job) ; value = id (scalaire).
|
||||
const jdTeamOptions = computed(() => {
|
||||
const taken = new Set([jobDetail.techId, ...(jobDetail.team || []).map(a => a.tech_id)])
|
||||
const req = jobDetail.required_skill || jobDetail.skill || ''
|
||||
return (techs.value || []).filter(t => !taken.has(t.id))
|
||||
.map(t => { const sk = (t.skills || []); return { capable: !!(req && sk.includes(req)), label: t.name + (sk.length ? ' · ' + sk.slice(0, 4).join(', ') : ''), value: t.id } })
|
||||
.sort((a, b) => (b.capable - a.capable) || a.label.localeCompare(b.label))
|
||||
.map(({ label, value }) => ({ label, value }))
|
||||
})
|
||||
async function jdAddAssistant () {
|
||||
const t = jobDetail.teamAdd; if (!t || !jobDetail.name) return
|
||||
const id = jobDetail.teamAdd; if (!id || !jobDetail.name) return
|
||||
const tech = (techs.value || []).find(t => t.id === id); if (!tech) return
|
||||
try {
|
||||
await roster.addAssistant(jobDetail.name, { tech_id: t.id, tech_name: t.name, duration_h: 0, pinned: 1 })
|
||||
await roster.addAssistant(jobDetail.name, { tech_id: tech.id, tech_name: tech.name, duration_h: 0, pinned: 1 })
|
||||
jobDetail.teamAdd = null
|
||||
const r = await roster.getJobTeam(jobDetail.name); jobDetail.team = r.assistants || []
|
||||
await reloadOccupancy()
|
||||
$q.notify({ type: 'positive', icon: 'group_add', message: t.name + ' ajouté en renfort · bloc hachuré réservé dans son horaire', timeout: 2600 })
|
||||
$q.notify({ type: 'positive', icon: 'group_add', message: tech.name + ' ajouté en renfort · bloc hachuré réservé dans son horaire', timeout: 2600 })
|
||||
} catch (e) { err(e) }
|
||||
}
|
||||
async function jdRemoveAssistant (techId) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user