Ops cohérence: composants partagés TechSelect (autosuggest) + SkillSelect (chips)

Corrige les manques signalés: champ technicien (congés) → autosuggest typeahead; compétences
(demande de shift + éditeur équipe) → chips au lieu de texte libre. Composants réutilisables
pour une UX cohérente partout (et le copilote/réassignation à venir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-04 11:57:58 -04:00
parent 412b6f49a6
commit 1f47ee4eae
3 changed files with 77 additions and 3 deletions

View File

@ -0,0 +1,35 @@
<template>
<!-- Saisie de compétences en CHIPS (cohérent partout). Stocke/émet une chaîne
CSV ("fibre,cuivre") pour rester compatible avec le backend existant.
Tape + Entrée = nouvelle compétence ; suggestions proposées. -->
<q-select
dense outlined multiple use-input use-chips hide-dropdown-icon
new-value-mode="add-unique" input-debounce="0"
:model-value="arr"
@update:model-value="onChange"
:options="filtered" @filter="onFilter"
:label="label" :style="style"
placeholder="fibre, cuivre…" />
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
modelValue: { type: [String, Array], default: '' },
suggestions: { type: Array, default: () => ['fibre', 'cuivre', 'aérien', 'souterrain', 'TV', 'téléphonie', 'installation', 'réparation', 'épissure'] },
label: { type: String, default: 'Compétences' },
style: { type: String, default: 'min-width:150px' },
})
const emit = defineEmits(['update:modelValue'])
const arr = computed(() => Array.isArray(props.modelValue)
? props.modelValue
: String(props.modelValue || '').split(',').map(s => s.trim()).filter(Boolean))
const filtered = ref(props.suggestions)
function onFilter (val, update) {
update(() => {
const n = (val || '').toLowerCase()
filtered.value = props.suggestions.filter(s => s.toLowerCase().includes(n))
})
}
function onChange (v) { emit('update:modelValue', (v || []).join(',')) }
</script>

View File

@ -0,0 +1,37 @@
<template>
<!-- Sélecteur de technicien avec autosuggest (typeahead). Réutilisable partout
on choisit un tech, pour une UX cohérente. options = [{label, value}]. -->
<q-select
dense outlined
:model-value="modelValue"
@update:model-value="v => $emit('update:modelValue', v)"
:options="filtered"
use-input input-debounce="200" fill-input hide-selected
emit-value map-options clearable
:label="label" :style="style" behavior="menu"
@filter="onFilter">
<template #no-option>
<q-item><q-item-section class="text-grey-6">Aucun technicien</q-item-section></q-item>
</template>
</q-select>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: { type: [String, null], default: null },
options: { type: Array, default: () => [] }, // [{label, value}]
label: { type: String, default: 'Technicien' },
style: { type: String, default: 'min-width:200px' },
})
defineEmits(['update:modelValue'])
const filtered = ref(props.options)
watch(() => props.options, (v) => { filtered.value = v }, { immediate: true })
function onFilter (val, update) {
update(() => {
const n = (val || '').toLowerCase()
filtered.value = !n ? props.options
: props.options.filter(o => (o.label || '').toLowerCase().includes(n) || String(o.value || '').toLowerCase().includes(n))
})
}
</script>

View File

@ -61,7 +61,7 @@
<tr v-for="(d, i) in demand" :key="i">
<td><q-select dense options-dense outlined v-model="d.shift" :options="tplOptions" emit-value map-options style="min-width:150px" @update:model-value="saveDemand" /></td>
<td><q-input dense outlined v-model="d.zone" style="width:120px" @update:model-value="saveDemand" /></td>
<td><q-input dense outlined v-model="d.skills" placeholder="fibre,cuivre" style="width:130px" @update:model-value="saveDemand" /></td>
<td><SkillSelect v-model="d.skills" style="min-width:150px" @update:model-value="saveDemand" /></td>
<td><q-input dense outlined type="number" step="0.5" v-model.number="d.job_h" placeholder="0" style="width:80px" @update:model-value="saveDemand" /></td>
<td><q-input dense outlined type="number" v-model.number="d.weekday" style="width:70px" @update:model-value="saveDemand" /></td>
<td><q-input dense outlined type="number" v-model.number="d.weekend" style="width:70px" @update:model-value="saveDemand" /></td>
@ -197,7 +197,7 @@
<q-separator class="q-my-md" />
<div class="text-caption text-weight-bold q-mb-xs">Nouvelle demande</div>
<div class="row items-center q-gutter-sm">
<q-select dense outlined v-model="newLeave.technician" :options="techOptions" emit-value map-options label="Technicien" style="width:200px" />
<TechSelect v-model="newLeave.technician" :options="techOptions" label="Technicien" style="width:200px" />
<q-select dense outlined v-model="newLeave.availability_type" :options="['Congé', 'Pause', 'Indisponible', 'Maladie']" label="Type" style="width:130px" />
<q-input dense outlined type="date" v-model="newLeave.from_date" label="Du" style="width:140px" />
<q-input dense outlined type="date" v-model="newLeave.to_date" label="Au" style="width:140px" />
@ -220,7 +220,7 @@
<tbody>
<tr v-for="t in editTechs" :key="t.id">
<td>{{ t.name }}<span v-if="t.group" class="grp">{{ t.group }}</span></td>
<td><q-input dense outlined v-model="t.skills" placeholder="fibre,cuivre" style="width:140px" @blur="saveSkills(t)" /></td>
<td><SkillSelect v-model="t.skills" style="min-width:160px" @update:model-value="saveSkills(t)" /></td>
<td><q-input dense outlined type="number" step="0.05" v-model.number="t.efficiency" style="width:80px" @blur="saveEff(t)" /></td>
<td><q-input dense outlined type="number" step="0.5" v-model.number="t.salary" style="width:80px" @blur="saveCost(t)" /></td>
<td><q-input dense outlined type="number" step="1" v-model.number="t.charges" style="width:80px" @blur="saveCost(t)" /></td>
@ -257,6 +257,8 @@ import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useQuasar } from 'quasar'
import * as roster from 'src/api/roster'
import TechSelect from 'src/components/shared/TechSelect.vue'
import SkillSelect from 'src/components/shared/SkillSelect.vue'
const $q = useQuasar()
const DIRTY_MSG = 'Vous avez des modifications non publiées. Les abandonner ?'