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:
parent
412b6f49a6
commit
1f47ee4eae
35
apps/ops/src/components/shared/SkillSelect.vue
Normal file
35
apps/ops/src/components/shared/SkillSelect.vue
Normal 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>
|
||||
37
apps/ops/src/components/shared/TechSelect.vue
Normal file
37
apps/ops/src/components/shared/TechSelect.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<!-- Sélecteur de technicien avec autosuggest (typeahead). Réutilisable partout
|
||||
où 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>
|
||||
|
|
@ -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 ?'
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user