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

413 lines
13 KiB
Vue

<template>
<div class="space-y-3">
<div class="flex justify-end">
<UTooltip :text="$t('applicationForms.formElements.table.enlargeTable')">
<UButton
icon="i-lucide-maximize-2"
color="neutral"
variant="ghost"
size="xs"
:aria-label="$t('applicationForms.formElements.table.enlargeTable')"
@click="fullscreenOpen = true"
/>
</UTooltip>
</div>
<!-- Regular table -->
<TheTableContent
:table-data="tableData"
:table-columns="tableColumns"
:data-columns="dataColumns"
:form-options="formOptions"
:disabled="disabled"
:can-modify-rows="canModifyRows"
:get-column-options="getColumnOptions"
:read-only-column-indices="readOnlyColumnIndices"
:is-cell-visible="isCellVisible"
@update:cell="updateCell"
@update:cell-value="updateCellValue"
@update:checkbox-cell="updateCheckboxCell"
@add-row="addRow"
@remove-row="removeRow"
/>
<!-- Fullscreen Modal -->
<UModal
v-model:open="fullscreenOpen"
:title="$t('applicationForms.formElements.table.enlargeTable')"
class="min-w-3/4"
>
<template #content>
<div class="flex flex-col h-full">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 border-b border-default">
<h2 class="text-lg font-semibold">{{ $t('applicationForms.formElements.table.editTable') }}</h2>
<UButton
icon="i-lucide-x"
color="neutral"
variant="ghost"
size="sm"
:aria-label="$t('common.close')"
@click="fullscreenOpen = false"
/>
</div>
<!-- Modal body with full-width table -->
<div class="flex-1 overflow-auto p-4">
<TheTableContent
:table-data="tableData"
:table-columns="tableColumns"
:data-columns="dataColumns"
:form-options="formOptions"
:disabled="disabled"
:can-modify-rows="canModifyRows"
:get-column-options="getColumnOptions"
:read-only-column-indices="readOnlyColumnIndices"
:is-cell-visible="isCellVisible"
add-row-button-class="mt-4"
@update:cell="updateCell"
@update:cell-value="updateCellValue"
@update:checkbox-cell="updateCheckboxCell"
@add-row="addRow"
@remove-row="removeRow"
/>
</div>
</div>
</template>
</UModal>
</div>
</template>
<script setup lang="ts">
import type { FormElementDto, FormOptionDto, TableRowPresetDto } from '~~/.api-client'
import type { TableColumn } from '@nuxt/ui'
import { useTableCrossReferences } from '~/composables/useTableCrossReferences'
import { useFormElementVisibility } from '~/composables/useFormElementVisibility'
const props = defineProps<{
formOptions: FormOptionDto[]
disabled?: boolean
allFormElements?: FormElementDto[]
tableRowPreset?: TableRowPresetDto
}>()
const { isFormOptionVisible } = useFormElementVisibility()
const emit = defineEmits<{
(e: 'update:formOptions', value: FormOptionDto[]): void
}>()
const { getReferencedColumnValues, getConstrainedColumnValues, applyRowPresets } = useTableCrossReferences()
const fullscreenOpen = ref(false)
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
}
// Filter columns based on visibility conditions
interface VisibleColumn {
option: FormOptionDto
originalIndex: number
}
const visibleColumns = computed<VisibleColumn[]>(() => {
return props.formOptions
.map((option, index) => ({ option, originalIndex: index }))
.filter(({ option }) => {
if (!option.visibilityConditions || !props.allFormElements) {
return true
}
return isFormOptionVisible(option.visibilityConditions, props.allFormElements)
})
})
const readOnlyColumnIndices = computed<Set<number>>(() => {
if (!props.allFormElements) return new Set()
return new Set(
props.formOptions
.map((option, index) => ({ option, index }))
.filter(({ option }) => {
const conditions = option.columnConfig?.readOnlyConditions
return conditions && isFormOptionVisible(conditions, props.allFormElements!)
})
.map(({ index }) => index)
)
})
// When columns become read-only, reset their values to the configured default
watch(
readOnlyColumnIndices,
(currentSet, previousSet) => {
const newlyReadOnlyIndices = [...currentSet].filter((i) => !previousSet?.has(i))
if (newlyReadOnlyIndices.length === 0) return
const updatedOptions = props.formOptions.map((option, colIndex) => {
if (!newlyReadOnlyIndices.includes(colIndex)) return option
const columnValues = parseColumnValues(option.value)
const defaultValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
const newValue = JSON.stringify(columnValues.map(() => defaultValue))
return newValue !== option.value ? { ...option, value: newValue } : option
})
if (updatedOptions.some((opt, i) => opt !== props.formOptions[i])) {
emit('update:formOptions', updatedOptions)
}
},
{ immediate: true }
)
const dataColumns = computed<DataColumn[]>(() =>
visibleColumns.value.map(({ originalIndex }) => ({
key: `col_${originalIndex}`,
colIndex: originalIndex
}))
)
const tableColumns = computed<TableColumn<TableRowData>[]>(() => {
const columns: TableColumn<TableRowData>[] = visibleColumns.value.map(({ option, originalIndex }) => ({
accessorKey: `col_${originalIndex}`,
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) => {
const parsed = parseColumnValues(option.value)
// Normalize cell values based on column type
if (isColumnMultipleAllowed(colIndex)) {
return parsed.map((val) => (Array.isArray(val) ? val : []))
}
if (isColumnCheckbox(colIndex)) {
return parsed.map((val) => val === true)
}
return parsed
})
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((_, 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 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 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
const columnValues = parseColumnValues(option.value)
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
const columnValues = parseColumnValues(option.value)
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
const columnValues = parseColumnValues(option.value)
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) => {
const columnValues = parseColumnValues(option.value)
// Determine initial value based on column type
let initialValue: CellValue = ''
if (readOnlyColumnIndices.value.has(colIndex)) {
initialValue = isColumnCheckbox(colIndex) ? false : (option.columnConfig?.readOnlyDefaultValue ?? '')
} else if (isColumnMultipleAllowed(colIndex)) {
initialValue = []
} else if (isColumnCheckbox(colIndex)) {
initialValue = false
}
columnValues.push(initialValue)
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function removeRow(rowIndex: number) {
const updatedOptions = props.formOptions.map((option) => {
const columnValues = parseColumnValues(option.value)
columnValues.splice(rowIndex, 1)
return { ...option, value: JSON.stringify(columnValues) }
})
emit('update:formOptions', updatedOptions)
}
function isCellVisible(colIndex: number, rowData: TableRowData): boolean {
const option = props.formOptions[colIndex]
const rowVisibility = option?.columnConfig?.rowVisibilityCondition
if (!rowVisibility) return true
const { sourceColumnIndex, expectedValues, operator } = rowVisibility
const sourceKey = `col_${sourceColumnIndex}`
const cellValue = rowData[sourceKey]
let sourceValues: string[] = []
if (Array.isArray(cellValue)) {
sourceValues = cellValue
} else if (typeof cellValue === 'string' && cellValue) {
sourceValues = cellValue.split(',').map((v) => v.trim())
}
if (operator === 'CONTAINS' || operator === 'EQUALS') {
return (expectedValues ?? []).some((expected) =>
sourceValues.some((v) => v.toLowerCase() === expected.toLowerCase())
)
}
return true
}
function parseColumnValues(value: string | undefined): CellValue[] {
try {
const parsed = JSON.parse(value || '[]')
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
</script>