calculator2026/src/features/shared/components/ServiceCheckboxSelector.vue

141 lines
4.9 KiB
Vue

<script setup lang="ts">
import { computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
interface ServiceItem {
id: string
code: string
name: string
}
const props = defineProps<{
services: ServiceItem[]
serviceRows?: string[][]
modelValue: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const { t } = useI18n()
const selectedSet = computed(() => new Set(props.modelValue))
const serviceById = computed(() => new Map(props.services.map(item => [item.id, item])))
const firstRowIds = computed(() => {
const rows = Array.isArray(props.serviceRows) ? props.serviceRows : []
if (rows.length === 0) return [] as string[]
const firstRow = Array.isArray(rows[0]) ? rows[0] : []
return firstRow.filter(id => serviceById.value.has(id))
})
const firstRowIdSet = computed(() => new Set(firstRowIds.value))
const groupedRows = computed(() => {
const rows = Array.isArray(props.serviceRows) ? props.serviceRows : []
if (rows.length === 0) return [] as ServiceItem[][]
const used = new Set<string>()
const grouped = rows
.map(row => row.map(id => serviceById.value.get(id)).filter((item): item is ServiceItem => Boolean(item)))
.map(row => row.filter(item => {
if (used.has(item.id)) return false
used.add(item.id)
return true
}))
.filter(row => row.length > 0)
const leftovers = props.services.filter(item => !used.has(item.id))
if (leftovers.length > 0) grouped.push(leftovers)
return grouped
})
const toggleService = (id: string, checked: boolean) => {
const next = new Set(props.modelValue)
if (checked) {
if (firstRowIdSet.value.has(id)) {
firstRowIds.value.forEach(firstId => next.delete(firstId))
}
next.add(id)
} else {
next.delete(id)
}
emit('update:modelValue', props.services.map(item => item.id).filter(itemId => next.has(itemId)))
}
const isFirstRowDisabled = (id: string) => {
if (!firstRowIdSet.value.has(id)) return false
for (const selectedId of props.modelValue) {
if (selectedId !== id && firstRowIdSet.value.has(selectedId)) return true
}
return false
}
watch(
() => [props.modelValue, firstRowIds.value] as const,
() => {
const firstSelected = props.modelValue.filter(id => firstRowIdSet.value.has(id))
if (firstSelected.length <= 1) return
const keepId = firstRowIds.value.find(id => firstSelected.includes(id)) || firstSelected[0]
const next = props.modelValue.filter(id => !firstRowIdSet.value.has(id) || id === keepId)
emit('update:modelValue', next)
},
{ immediate: true, deep: true }
)
const clearAll = () => {
emit('update:modelValue', [])
}
</script>
<template>
<div class="rounded-lg border bg-card p-2.5 shadow-sm">
<div class="mb-1 flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-1.5">
<label class="block shrink-0 text-sm font-semibold leading-none text-slate-900">{{ t('serviceSelector.title') }}</label>
<span class="min-w-0 text-xs leading-5 text-muted-foreground">{{ t('serviceSelector.titleHint') }}</span>
</div>
<button
type="button"
class="cursor-pointer h-6 rounded-md border px-2 text-[13px] text-muted-foreground transition hover:bg-accent"
@click="clearAll"
>
{{ t('serviceSelector.clear') }}
</button>
</div>
<div class="rounded-md border p-1.5">
<div v-if="groupedRows.length > 0" class="flex flex-col gap-1.5">
<div
v-for="(row, rowIndex) in groupedRows"
:key="`service-row-${rowIndex}`"
class="flex flex-wrap items-start gap-1 border-b border-slate-200 pb-1.5 last:border-b-0 last:pb-0"
>
<label
v-for="item in row"
:key="item.id"
:class="[
'inline-flex w-fit max-w-full items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 transition',
isFirstRowDisabled(item.id)
? 'cursor-not-allowed border-slate-300 bg-slate-100/80 text-slate-400 opacity-80'
: 'cursor-pointer hover:bg-muted/60'
]"
>
<input
type="checkbox"
:class="[
'mt-0.5',
isFirstRowDisabled(item.id) ? 'cursor-not-allowed accent-slate-300' : 'cursor-pointer'
]"
:checked="selectedSet.has(item.id)"
:disabled="isFirstRowDisabled(item.id)"
@change="toggleService(item.id, ($event.target as HTMLInputElement).checked)"
/>
<span class="text-muted-foreground shrink-0">{{ item.code }}</span>
<span class="text-foreground break-words">{{ item.name }}</span>
</label>
</div>
</div>
<div v-if="props.services.length === 0" class="px-2 py-4 text-center text-xs text-muted-foreground">
{{ t('serviceSelector.empty') }}
</div>
</div>
</div>
</template>