220 lines
8.4 KiB
Vue
220 lines
8.4 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import AutocompleteInput from '~/components/AutocompleteInput.vue'
|
||
import type { ConfigurationCriteria, ParameterCriteria } from '#domain/configuration/configuration'
|
||
|
||
interface Props {
|
||
modelValue: ConfigurationCriteria
|
||
}
|
||
interface Emits {
|
||
(e: 'update:modelValue', value: ConfigurationCriteria): void
|
||
}
|
||
|
||
const props = defineProps<Props>()
|
||
const emit = defineEmits<Emits>()
|
||
const { t } = useI18n()
|
||
|
||
const allowedFields = ['manufacturer', 'oui', 'productClass', 'serialNumber'] as const
|
||
|
||
const suggestions = ref<Record<string, string[]>>({
|
||
manufacturer: [],
|
||
oui: [],
|
||
productClass: [],
|
||
serialNumber: [],
|
||
modelName: [],
|
||
hardwareVersion: [],
|
||
softwareVersion: [],
|
||
})
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
const res = await fetch('/internal/devices/criteria-values')
|
||
if (res.ok) {
|
||
const data = await res.json()
|
||
// API returns plural keys, map to singular keys used by the form fields
|
||
suggestions.value = {
|
||
...suggestions.value,
|
||
manufacturer: data.manufacturers ?? [],
|
||
oui: data.ouis ?? [],
|
||
productClass: data.productClasses ?? [],
|
||
serialNumber: data.serialNumbers ?? [],
|
||
modelName: data.modelNames ?? [],
|
||
hardwareVersion: data.hardwareVersions ?? [],
|
||
softwareVersion: data.softwareVersions ?? [],
|
||
}
|
||
}
|
||
} catch {
|
||
// silently fail
|
||
}
|
||
})
|
||
|
||
function updateCriteria(field: string, value: string) {
|
||
const updated = { ...props.modelValue }
|
||
if (value) {
|
||
updated[field as keyof ConfigurationCriteria] = value as any
|
||
} else {
|
||
delete updated[field as keyof ConfigurationCriteria]
|
||
}
|
||
emit('update:modelValue', updated)
|
||
}
|
||
|
||
// ── Parameter Criteria ──────────────────────────────────────────────────────
|
||
|
||
const presetParams: { labelKey: string; tr181: string; tr098: string; placeholderKey: string; suggestionsKey: string }[] = [
|
||
{ labelKey: 'criteriaEditor.presets.modelName', tr181: 'Device.DeviceInfo.ModelName', tr098: 'InternetGatewayDevice.DeviceInfo.ModelName', placeholderKey: 'criteriaEditor.presets.modelNamePlaceholder', suggestionsKey: 'modelName' },
|
||
{ labelKey: 'criteriaEditor.presets.hardwareVersion', tr181: 'Device.DeviceInfo.HardwareVersion', tr098: 'InternetGatewayDevice.DeviceInfo.HardwareVersion', placeholderKey: 'criteriaEditor.presets.hardwareVersionPlaceholder', suggestionsKey: 'hardwareVersion' },
|
||
{ labelKey: 'criteriaEditor.presets.softwareVersion', tr181: 'Device.DeviceInfo.SoftwareVersion', tr098: 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', placeholderKey: 'criteriaEditor.presets.softwareVersionPlaceholder', suggestionsKey: 'softwareVersion' },
|
||
]
|
||
|
||
// Paths already covered by preset fields — excluded from custom conditions suggestions
|
||
const presetPaths = new Set(presetParams.flatMap((p) => [p.tr181, p.tr098]))
|
||
|
||
const customSuggestions = computed(() =>
|
||
(suggestions.value.parameterNames || []).filter((name) => !presetPaths.has(name))
|
||
)
|
||
|
||
function getPresetValue(preset: typeof presetParams[0]): string {
|
||
const list = props.modelValue.parameterCriteria || []
|
||
const entry = list.find((p) => p.name === preset.tr181 || p.name === preset.tr098)
|
||
return entry ? String(entry.value) : ''
|
||
}
|
||
|
||
function updatePresetParam(preset: typeof presetParams[0], value: string) {
|
||
const updated = { ...props.modelValue }
|
||
const list = [...(updated.parameterCriteria || [])]
|
||
const idx = list.findIndex((p) => p.name === preset.tr181 || p.name === preset.tr098)
|
||
if (value) {
|
||
if (idx >= 0) {
|
||
list[idx] = { name: preset.tr181, value }
|
||
} else {
|
||
list.push({ name: preset.tr181, value })
|
||
}
|
||
} else {
|
||
if (idx >= 0) list.splice(idx, 1)
|
||
}
|
||
updated.parameterCriteria = list.length > 0 ? list : undefined
|
||
emit('update:modelValue', updated)
|
||
}
|
||
|
||
const paramCriteria = computed<ParameterCriteria[]>(() => props.modelValue.parameterCriteria || [])
|
||
|
||
function addParamCriteria() {
|
||
const updated = { ...props.modelValue }
|
||
updated.parameterCriteria = [...(updated.parameterCriteria || []), { name: '', value: '' }]
|
||
emit('update:modelValue', updated)
|
||
}
|
||
|
||
function removeParamCriteria(index: number) {
|
||
const updated = { ...props.modelValue }
|
||
const list = [...(updated.parameterCriteria || [])]
|
||
list.splice(index, 1)
|
||
updated.parameterCriteria = list.length > 0 ? list : undefined
|
||
emit('update:modelValue', updated)
|
||
}
|
||
|
||
function updateParamCriteria(index: number, field: 'name' | 'value', value: string) {
|
||
const updated = { ...props.modelValue }
|
||
const list = [...(updated.parameterCriteria || [])]
|
||
list[index] = { ...list[index], [field]: value }
|
||
updated.parameterCriteria = list
|
||
emit('update:modelValue', updated)
|
||
}
|
||
|
||
function knownValuesLabel(count: number): string {
|
||
return count > 1 ? t('criteriaEditor.knownValues') : t('criteriaEditor.knownValue')
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- DeviceId criteria -->
|
||
<p class="fw-semibold small mb-2">{{ t('criteriaEditor.deviceId') }}</p>
|
||
<div class="row g-3">
|
||
<div v-for="field in allowedFields" :key="field" class="col-md-6">
|
||
<label :for="`criteria-${field}`" class="form-label">{{ t(`criteriaEditor.fields.${field}.label`) }}</label>
|
||
<AutocompleteInput
|
||
:id="`criteria-${field}`"
|
||
:model-value="modelValue[field] || ''"
|
||
:suggestions="suggestions[field]"
|
||
:placeholder="t(`criteriaEditor.fields.${field}.placeholder`)"
|
||
monospace
|
||
@update:model-value="updateCriteria(field, $event)"
|
||
/>
|
||
<div class="form-text">
|
||
{{ t(`criteriaEditor.fields.${field}.help`) }}
|
||
<span v-if="suggestions[field]?.length" class="text-muted">
|
||
· {{ suggestions[field].length }} {{ knownValuesLabel(suggestions[field].length) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Parameter Criteria — Preset fields -->
|
||
<div class="mt-4">
|
||
<p class="fw-semibold small mb-2">{{ t('criteriaEditor.standardParams') }}</p>
|
||
<div class="row g-3 mb-3">
|
||
<div v-for="preset in presetParams" :key="preset.tr181" class="col-md-6">
|
||
<label class="form-label">{{ t(preset.labelKey) }}</label>
|
||
<AutocompleteInput
|
||
:model-value="getPresetValue(preset)"
|
||
:suggestions="suggestions[preset.suggestionsKey]"
|
||
:placeholder="t(preset.placeholderKey)"
|
||
monospace
|
||
@update:model-value="updatePresetParam(preset, $event)"
|
||
/>
|
||
<div class="form-text text-muted" style="font-size:0.7rem">
|
||
{{ preset.tr181 }}
|
||
<span v-if="suggestions[preset.suggestionsKey]?.length" class="ms-1">
|
||
· {{ suggestions[preset.suggestionsKey].length }} {{ knownValuesLabel(suggestions[preset.suggestionsKey].length) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Parameter Criteria — conditions libres -->
|
||
<div class="mt-4">
|
||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||
<div>
|
||
<span class="fw-semibold small">{{ t('criteriaEditor.customCriteria') }}</span>
|
||
<span class="form-text ms-2">{{ t('criteriaEditor.customCriteriaHelp') }}</span>
|
||
</div>
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addParamCriteria">
|
||
{{ t('criteriaEditor.addCondition') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="paramCriteria.length === 0" class="text-muted small fst-italic">
|
||
{{ t('criteriaEditor.noConditions') }}
|
||
</div>
|
||
|
||
<div v-for="(pc, idx) in paramCriteria" :key="idx" class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="flex-grow-1">
|
||
<AutocompleteInput
|
||
:model-value="pc.name"
|
||
:suggestions="customSuggestions"
|
||
placeholder="e.g. Device.DeviceInfo.Manufacturer"
|
||
monospace
|
||
@update:model-value="updateParamCriteria(idx, 'name', $event)"
|
||
/>
|
||
</div>
|
||
<span class="text-muted small">=</span>
|
||
<input
|
||
type="text"
|
||
class="form-control form-control-sm font-monospace"
|
||
style="max-width: 160px"
|
||
:value="String(pc.value)"
|
||
placeholder="e.g. true"
|
||
@input="updateParamCriteria(idx, 'value', ($event.target as HTMLInputElement).value)"
|
||
/>
|
||
<button type="button" class="btn btn-outline-danger btn-sm" :title="t('button.delete')" @click="removeParamCriteria(idx)">
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="paramCriteria.length > 0" class="form-text mt-1">
|
||
ℹ️ {{ t('criteriaEditor.paramCriteriaWarning') }}
|
||
</div>
|
||
</div>
|
||
</template>
|