Files
gremiumhub/legalconsenthub/app/components/formelements/TheTable.vue

385 lines
12 KiB
Vue

<template>
<div class="space-y-3">
<UTable :data="tableData" :columns="tableColumns" class="w-full" :ui="{ td: 'p-2' }">
<template v-for="col in dataColumns" :key="col.key" #[`${col.key}-cell`]="slotProps">
<!-- Column with cross-reference -->
<USelectMenu
v-if="hasColumnReference(col.colIndex) && !isColumnReadOnly(col.colIndex)"
:model-value="getCellValueForSelect(slotProps.row as TableRow<TableRowData>, col.key, col.colIndex)"
:items="getColumnOptions(col.colIndex, (slotProps.row as TableRow<TableRowData>).original)"
:disabled="disabled"
:placeholder="$t('applicationForms.formElements.table.selectValue')"
:multiple="isColumnMultipleAllowed(col.colIndex)"
class="w-full min-w-32"
@update:model-value="
(val: string | string[]) =>
updateCellValue((slotProps.row as TableRow<TableRowData>).index, col.key, col.colIndex, val)
"
/>
<!-- Read-only column -->
<span v-else-if="isColumnReadOnly(col.colIndex)" class="text-muted px-2 py-1">
{{ formatCellDisplay(slotProps.row as any, col.key, col.colIndex) }}
</span>
<!-- Checkbox column -->
<div v-else-if="isColumnCheckbox(col.colIndex)" class="flex justify-center">
<UCheckbox
:model-value="getCellValueForCheckbox(slotProps.row as TableRow<TableRowData>, col.key)"
:disabled="disabled"
@update:model-value="
(val: boolean | 'indeterminate') =>
updateCheckboxCell((slotProps.row as TableRow<TableRowData>).index, col.colIndex, val === true)
"
/>
</div>
<!-- Regular text input -->
<UInput
v-else
:model-value="getCellValue(slotProps.row as TableRow<TableRowData>, col.key)"
:disabled="disabled"
class="w-full min-w-32"
@update:model-value="
(val: string | number) => updateCell((slotProps.row as TableRow<TableRowData>).index, col.key, String(val))
"
/>
</template>
<template v-if="canModifyRows" #actions-cell="{ row }">
<UButton
v-if="!disabled"
icon="i-lucide-trash-2"
color="error"
variant="ghost"
size="xs"
:aria-label="$t('applicationForms.formElements.table.removeRow')"
@click="removeRow(row.index)"
/>
</template>
</UTable>
<div v-if="tableData.length === 0" class="text-center text-dimmed py-4">
{{ $t('applicationForms.formElements.table.noData') }}
</div>
<UButton v-if="!disabled && canModifyRows" variant="outline" size="sm" leading-icon="i-lucide-plus" @click="addRow">
{{ $t('applicationForms.formElements.table.addRow') }}
</UButton>
</div>
</template>
<script setup lang="ts">
import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client'
import type { TableColumn, TableRow } from '@nuxt/ui'
import { useTableCrossReferences } from '~/composables/useTableCrossReferences'
const props = defineProps<{
formOptions: FormOptionDto[]
disabled?: boolean
allFormElements?: FormElementDto[]
tableRowPreset?: TableRowPresetDto
}>()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
const { getReferencedColumnValues, getConstrainedColumnValues, applyRowPresets } = useTableCrossReferences()
const canModifyRows = computed(() => {
if (!props.tableRowPreset) return true
return props.tableRowPreset.canAddRows !== false
})
// Watch for changes in source table and apply row presets reactively
const sourceTableOptions = computed(() => {
if (!props.tableRowPreset?.sourceTableReference || !props.allFormElements) return null
const sourceTable = props.allFormElements.find(
(el) => el.reference === props.tableRowPreset?.sourceTableReference && el.type === 'TABLE'
)
return sourceTable?.options
})
watch(
sourceTableOptions,
() => {
if (!sourceTableOptions.value || !props.tableRowPreset || !props.allFormElements) return
const updatedOptions = applyRowPresets(props.tableRowPreset, props.formOptions, props.allFormElements)
const hasChanges = updatedOptions.some((opt, idx) => opt.value !== props.formOptions[idx]?.value)
if (hasChanges) {
emit('update:formOptions', updatedOptions)
}
},
{ immediate: true, deep: true }
)
type CellValue = string | string[] | boolean
type TableRowData = Record<string, CellValue>
interface DataColumn {
key: string
colIndex: number
}
const dataColumns = computed<DataColumn[]>(() =>
props.formOptions.map((_, index) => ({
key: `col_${index}`,
colIndex: index
}))
)
const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
const columns: TableColumn<TableRowData>[] = props.formOptions.map((option, index) => ({
accessorKey: `col_${index}`,
header: option.label || ''
}))
// Only show actions column if not disabled AND rows can be modified
if (!props.disabled && canModifyRows.value) {
columns.push({
id: 'actions',
header: ''
})
}
return columns
})
const tableData = computed<TableRowData[]>(() => {
if (props.formOptions.length === 0) return []
const columnData: CellValue[][] = props.formOptions.map((option, colIndex) => {
try {
const parsed = JSON.parse(option.value || '[]')
if (!Array.isArray(parsed)) return []
// For multi-select columns, each cell value is already an array
// For checkbox columns, each cell value is a boolean
// For single-select columns, each cell value is a string
if (isColumnMultipleAllowed(colIndex)) {
return parsed.map((val: CellValue) => (Array.isArray(val) ? val : []))
}
if (isColumnCheckbox(colIndex)) {
return parsed.map((val: CellValue) => val === true)
}
return parsed
} catch {
return []
}
})
const rowCount = Math.max(...columnData.map((col) => col.length), 0)
const rows: TableRowData[] = []
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row: TableRowData = {}
props.formOptions.forEach((option, colIndex) => {
const cellValue = columnData[colIndex]?.[rowIndex]
if (isColumnMultipleAllowed(colIndex)) {
row[`col_${colIndex}`] = Array.isArray(cellValue) ? cellValue : []
} else if (isColumnCheckbox(colIndex)) {
row[`col_${colIndex}`] = cellValue === true
} else {
row[`col_${colIndex}`] = typeof cellValue === 'string' ? cellValue : ''
}
})
rows.push(row)
}
return rows
})
function hasColumnReference(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return !!option?.columnConfig?.sourceTableReference
}
function isColumnReadOnly(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isReadOnly === true
}
function isColumnMultipleAllowed(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isMultipleAllowed === true
}
function isColumnCheckbox(colIndex: number): boolean {
const option = props.formOptions[colIndex]
return option?.columnConfig?.isCheckbox === true
}
function getColumnOptions(colIndex: number, currentRowData?: TableRowData): string[] {
const option = props.formOptions[colIndex]
if (!option?.columnConfig || !props.allFormElements) {
return []
}
const { columnConfig } = option
const { rowConstraint } = columnConfig
// If row constraint is configured, filter values based on current row's key value
if (rowConstraint?.constraintTableReference && currentRowData) {
const currentRowAsRecord: Record<string, string> = {}
for (const [key, value] of Object.entries(currentRowData)) {
currentRowAsRecord[key] =
typeof value === 'string' ? value : Array.isArray(value) ? value.join(',') : String(value)
}
return getConstrainedColumnValues(
columnConfig,
currentRowAsRecord,
rowConstraint.constraintTableReference,
rowConstraint.constraintKeyColumnIndex ?? 0,
rowConstraint.constraintValueColumnIndex ?? 1,
props.allFormElements,
rowConstraint.currentRowKeyColumnIndex
)
}
return getReferencedColumnValues(columnConfig, props.allFormElements)
}
function getCellValue(row: TableRow<TableRowData>, columnKey: string): string {
const value = row.original[columnKey]
return typeof value === 'string' ? value : ''
}
function getCellValueForSelect(row: TableRow<TableRowData>, columnKey: string, colIndex: number): string | string[] {
const value = row.original[columnKey]
if (isColumnMultipleAllowed(colIndex)) {
return Array.isArray(value) ? value : []
}
return typeof value === 'string' ? value : ''
}
function getCellValueForCheckbox(row: TableRow<TableRowData>, columnKey: string): boolean {
const value = row.original[columnKey]
return value === true
}
function formatCellDisplay(row: TableRow<TableRowData>, columnKey: string, colIndex: number): string {
const value = row.original[columnKey]
if (isColumnMultipleAllowed(colIndex) && Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : '-'
}
return (typeof value === 'string' ? value : '') || '-'
}
function updateCell(rowIndex: number, columnKey: string, value: string) {
const colIndex = parseInt(columnKey.replace('col_', ''), 10)
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
while (columnValues.length <= rowIndex) {
columnValues.push('')
}
columnValues[rowIndex] = value
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function updateCellValue(rowIndex: number, columnKey: string, colIndex: number, value: string | string[]) {
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
const isMultiple = isColumnMultipleAllowed(colIndex)
while (columnValues.length <= rowIndex) {
columnValues.push(isMultiple ? [] : '')
}
columnValues[rowIndex] = value
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function updateCheckboxCell(rowIndex: number, colIndex: number, value: boolean) {
const updatedOptions = props.formOptions.map((option, index) => {
if (index !== colIndex) return option
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
while (columnValues.length <= rowIndex) {
columnValues.push(false)
}
columnValues[rowIndex] = value
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function addRow() {
const updatedOptions = props.formOptions.map((option, colIndex) => {
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
// For multi-select columns, initialize with empty array
// For checkbox columns, initialize with false
// Otherwise empty string
let emptyValue: CellValue = ''
if (isColumnMultipleAllowed(colIndex)) {
emptyValue = []
} else if (isColumnCheckbox(colIndex)) {
emptyValue = false
}
columnValues.push(emptyValue)
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function removeRow(rowIndex: number) {
const updatedOptions = props.formOptions.map((option) => {
let columnValues: CellValue[]
try {
columnValues = JSON.parse(option.value || '[]')
if (!Array.isArray(columnValues)) columnValues = []
} catch {
columnValues = []
}
columnValues.splice(rowIndex, 1)
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
</script>