From ded2d13140f9491b931c990dbd5de9e912f9cbe4 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 3 Oct 2025 13:33:37 +0300 Subject: [PATCH 01/31] KAAV-3388 Filter deprecated vahvista_paattyy attributes from confirmed fields in projectSaga and generateConfirmedFields --- src/sagas/projectSaga.js | 8 ++++---- src/utils/generateConfirmedFields.js | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/sagas/projectSaga.js b/src/sagas/projectSaga.js index 75af6abd1..2bd4452e7 100644 --- a/src/sagas/projectSaga.js +++ b/src/sagas/projectSaga.js @@ -756,8 +756,8 @@ function* validateProjectTimetable() { const phaseNames = [ 'periaatteet', 'oas', - 'luonnos', - 'ehdotus', + 'kaavaluonnos', + 'kaavaehdotus', 'tarkistettu_ehdotus' ]; //Find confirmed fields from attribute_data so backend knows not to edit them @@ -853,8 +853,8 @@ function* saveProjectTimetable(action,retryCount = 0) { const phaseNames = [ 'periaatteet', 'oas', - 'luonnos', - 'ehdotus', + 'kaavaluonnos', + 'kaavaehdotus', 'tarkistettu_ehdotus' ]; diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index da3aae6d5..c22f3bec9 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,8 +1,13 @@ export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { + // Filter out deprecated vahvista_x_paattyy attributes before processing + const filteredConfirmationAttributeNames = confirmationAttributeNames.filter( + key => key.includes('_alkaa') || key.includes('_lautakunnassa') + ); + const confirmedFields = []; const seenPhases = new Set(); - confirmationAttributeNames.forEach((confirmationKey) => { + filteredConfirmationAttributeNames.forEach((confirmationKey) => { if (!attributeData[confirmationKey]) return; const rawKey = confirmationKey.replace(/^vahvista_/, ''); @@ -24,10 +29,8 @@ export function generateConfirmedFields(attributeData, confirmationAttributeName // Special case like vahvista_periaatteet_lautakunnassa const field1 = `milloin_${phase}_${base}${finalSuffix}`; const field2 = `${phase}_lautakunta_aineiston_maaraaika${finalSuffix}`; - confirmedFields.push(field1); confirmedFields.push(field2); - return; } From 24ecada3185f6895929dbef73a1e2464b746f2ad Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 3 Oct 2025 14:41:01 +0300 Subject: [PATCH 02/31] KAAV-3388 Add fix for SonarCloud issue --- src/utils/generateConfirmedFields.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index c22f3bec9..effba07a6 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,5 +1,4 @@ export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - // Filter out deprecated vahvista_x_paattyy attributes before processing const filteredConfirmationAttributeNames = confirmationAttributeNames.filter( key => key.includes('_alkaa') || key.includes('_lautakunnassa') ); @@ -7,12 +6,12 @@ export function generateConfirmedFields(attributeData, confirmationAttributeName const confirmedFields = []; const seenPhases = new Set(); - filteredConfirmationAttributeNames.forEach((confirmationKey) => { - if (!attributeData[confirmationKey]) return; + for (const confirmationKey of filteredConfirmationAttributeNames) { + if (!attributeData[confirmationKey]) continue; const rawKey = confirmationKey.replace(/^vahvista_/, ''); const phase = phaseNames.find((p) => rawKey === p || rawKey.startsWith(p + '_')); - if (!phase) return; + if (!phase) continue; const suffixMatch = rawKey.match(/(_\d+)$/); const suffix = suffixMatch ? suffixMatch[1] : ''; @@ -31,7 +30,7 @@ export function generateConfirmedFields(attributeData, confirmationAttributeName const field2 = `${phase}_lautakunta_aineiston_maaraaika${finalSuffix}`; confirmedFields.push(field1); confirmedFields.push(field2); - return; + continue; } // Regular case @@ -40,17 +39,17 @@ export function generateConfirmedFields(attributeData, confirmationAttributeName const paattyy = `milloin_${phase}_${group}_paattyy${finalSuffix}`; const mielipiteet = `viimeistaan_mielipiteet_${phase}`; - [aineisto, alkaa, paattyy].forEach((key) => { + for (const key of [aineisto, alkaa, paattyy]) { if (key in attributeData) { confirmedFields.push(key); } - }); + } if (!seenPhases.has(phase) && mielipiteet in attributeData) { confirmedFields.push(mielipiteet); seenPhases.add(phase); } - }); + } return [...new Set(confirmedFields)]; } \ No newline at end of file From 1dc42b64c8d2afde460abc3da98a323878f8ea6c Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 3 Oct 2025 14:47:33 +0300 Subject: [PATCH 03/31] KAAV-3388 Reduce cognitive complexity of generateConfirmedFields --- src/utils/generateConfirmedFields.js | 38 +++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index effba07a6..08d20cd57 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,3 +1,19 @@ +function getSpecialCaseFields(phase, base, finalSuffix) { + return [ + `milloin_${phase}_${base}${finalSuffix}`, + `${phase}_lautakunta_aineiston_maaraaika${finalSuffix}`, + ]; +} + +function getRegularFields(phase, group, finalSuffix, attributeData) { + const fields = [ + `${phase}_${group}_aineiston_maaraaika${finalSuffix}`, + `milloin_${phase}_${group}_alkaa${finalSuffix}`, + `milloin_${phase}_${group}_paattyy${finalSuffix}`, + ]; + return fields.filter(key => key in attributeData); +} + export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { const filteredConfirmationAttributeNames = confirmationAttributeNames.filter( key => key.includes('_alkaa') || key.includes('_lautakunnassa') @@ -21,30 +37,16 @@ export function generateConfirmedFields(attributeData, confirmationAttributeName const base = keyWithoutSuffix.replace(`${phase}_`, ''); const parts = base.split('_'); - let group = parts[0]; - let type = parts[1]; + const group = parts[0]; if (parts.length === 1) { - // Special case like vahvista_periaatteet_lautakunnassa - const field1 = `milloin_${phase}_${base}${finalSuffix}`; - const field2 = `${phase}_lautakunta_aineiston_maaraaika${finalSuffix}`; - confirmedFields.push(field1); - confirmedFields.push(field2); + confirmedFields.push(...getSpecialCaseFields(phase, base, finalSuffix)); continue; } - // Regular case - const aineisto = `${phase}_${group}_aineiston_maaraaika${finalSuffix}`; - const alkaa = `milloin_${phase}_${group}_alkaa${finalSuffix}`; - const paattyy = `milloin_${phase}_${group}_paattyy${finalSuffix}`; - const mielipiteet = `viimeistaan_mielipiteet_${phase}`; - - for (const key of [aineisto, alkaa, paattyy]) { - if (key in attributeData) { - confirmedFields.push(key); - } - } + confirmedFields.push(...getRegularFields(phase, group, finalSuffix, attributeData)); + const mielipiteet = `viimeistaan_mielipiteet_${phase}`; if (!seenPhases.has(phase) && mielipiteet in attributeData) { confirmedFields.push(mielipiteet); seenPhases.add(phase); From 889d9f43d2866791843b1978900cc451d6e0bbd8 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 3 Oct 2025 14:53:33 +0300 Subject: [PATCH 04/31] KAAV-3388 Refactor more to reduce cognitive complexity --- src/utils/generateConfirmedFields.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 08d20cd57..9e72d92ce 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -14,6 +14,14 @@ function getRegularFields(phase, group, finalSuffix, attributeData) { return fields.filter(key => key in attributeData); } +function addMielipiteetField(confirmedFields, seenPhases, phase, attributeData) { + const mielipiteet = `viimeistaan_mielipiteet_${phase}`; + if (!seenPhases.has(phase) && mielipiteet in attributeData) { + confirmedFields.push(mielipiteet); + seenPhases.add(phase); + } +} + export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { const filteredConfirmationAttributeNames = confirmationAttributeNames.filter( key => key.includes('_alkaa') || key.includes('_lautakunnassa') @@ -41,16 +49,12 @@ export function generateConfirmedFields(attributeData, confirmationAttributeName if (parts.length === 1) { confirmedFields.push(...getSpecialCaseFields(phase, base, finalSuffix)); + addMielipiteetField(confirmedFields, seenPhases, phase, attributeData); continue; } confirmedFields.push(...getRegularFields(phase, group, finalSuffix, attributeData)); - - const mielipiteet = `viimeistaan_mielipiteet_${phase}`; - if (!seenPhases.has(phase) && mielipiteet in attributeData) { - confirmedFields.push(mielipiteet); - seenPhases.add(phase); - } + addMielipiteetField(confirmedFields, seenPhases, phase, attributeData); } return [...new Set(confirmedFields)]; From 60daa1575ec5135009feed143135ee0cc108bed1 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 10 Oct 2025 12:47:30 +0300 Subject: [PATCH 05/31] KAAV-3388 Add missign phaseNames --- src/sagas/projectSaga.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sagas/projectSaga.js b/src/sagas/projectSaga.js index 2bd4452e7..b8a4df3d5 100644 --- a/src/sagas/projectSaga.js +++ b/src/sagas/projectSaga.js @@ -1,4 +1,5 @@ import axios from 'axios' +import React from 'react'; import { eventChannel } from 'redux-saga'; import { take, takeLatest, put, all, call, select, takeEvery, delay, race } from 'redux-saga/effects' import { isEqual, isEmpty, isArray } from 'lodash' @@ -756,6 +757,8 @@ function* validateProjectTimetable() { const phaseNames = [ 'periaatteet', 'oas', + 'luonnos', + 'ehdotus', 'kaavaluonnos', 'kaavaehdotus', 'tarkistettu_ehdotus' @@ -853,6 +856,8 @@ function* saveProjectTimetable(action,retryCount = 0) { const phaseNames = [ 'periaatteet', 'oas', + 'luonnos', + 'ehdotus', 'kaavaluonnos', 'kaavaehdotus', 'tarkistettu_ehdotus' From f70b623894a592513fe94c3d7cc6387b2354db69 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 10 Oct 2025 12:49:41 +0300 Subject: [PATCH 06/31] KAAV-3388 Support numeric and special suffixes in generateConfirmedFields utility --- src/utils/generateConfirmedFields.js | 121 +++++++++++++++++++-------- 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 9e72d92ce..9822902ba 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,61 +1,108 @@ -function getSpecialCaseFields(phase, base, finalSuffix) { - return [ - `milloin_${phase}_${base}${finalSuffix}`, - `${phase}_lautakunta_aineiston_maaraaika${finalSuffix}`, - ]; -} +const SUFFIXES = ['', '_2', '_3', '_4', '_pieni', '_iso']; +const GROUPS = ['esillaolo', 'nahtaville', 'lautakunta', 'kylk']; +const MIELIPITEET_SUFFIXES = ['', 'sta', 'ista']; +const SPECIAL_CASES = [ + 'ehdotus_nahtaville_aineiston_maaraaika', + 'milloin_ehdotuksen_nahtavilla_paattyy', + 'viimeistaan_lausunnot_ehdotuksesta', + 'luonnosaineiston_maaraaika', + 'kaavaluonnos_kylk_aineiston_maaraaika', + 'tarkistettu_ehdotus_kylk_maaraaika' +]; -function getRegularFields(phase, group, finalSuffix, attributeData) { - const fields = [ - `${phase}_${group}_aineiston_maaraaika${finalSuffix}`, - `milloin_${phase}_${group}_alkaa${finalSuffix}`, - `milloin_${phase}_${group}_paattyy${finalSuffix}`, +// Helper to get all possible vaihe fields for a phase (with and without underscore) +function getVaiheFields(phase, attributeData) { + const variants = [ + `${phase}vaihe_alkaa_pvm`, + `${phase}vaihe_paattyy_pvm`, + `${phase}_vaihe_alkaa_pvm`, + `${phase}_vaihe_paattyy_pvm` ]; - return fields.filter(key => key in attributeData); + return variants.filter(key => key in attributeData); } +// Helper to add mielipiteet fields with all suffixes function addMielipiteetField(confirmedFields, seenPhases, phase, attributeData) { - const mielipiteet = `viimeistaan_mielipiteet_${phase}`; - if (!seenPhases.has(phase) && mielipiteet in attributeData) { - confirmedFields.push(mielipiteet); - seenPhases.add(phase); + const nums = ['', '_2', '_3', '_4']; + for (const suffix of MIELIPITEET_SUFFIXES) { + for (const num of nums) { + const key = `viimeistaan_mielipiteet_${phase}${suffix}${num}`; + if (!seenPhases.has(key) && key in attributeData) { + confirmedFields.push(key); + seenPhases.add(key); + } + } } } -export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - const filteredConfirmationAttributeNames = confirmationAttributeNames.filter( - key => key.includes('_alkaa') || key.includes('_lautakunnassa') - ); +// Helper to add all *_fieldset fields if present +function addFieldsetFields(confirmedFields, attributeData) { + Object.keys(attributeData).forEach(key => { + if (key.endsWith('_fieldset') && !confirmedFields.includes(key)) { + confirmedFields.push(key); + } + }); +} +// Main function +export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { const confirmedFields = []; const seenPhases = new Set(); - for (const confirmationKey of filteredConfirmationAttributeNames) { - if (!attributeData[confirmationKey]) continue; + for (const confirmationKey of confirmationAttributeNames) { + if (!confirmationKey.startsWith('vahvista_')) continue; - const rawKey = confirmationKey.replace(/^vahvista_/, ''); - const phase = phaseNames.find((p) => rawKey === p || rawKey.startsWith(p + '_')); + let rawKey = confirmationKey.replace(/^vahvista_/, ''); + let phase = phaseNames.find((p) => rawKey === p || rawKey.startsWith(p + '_')); if (!phase) continue; - const suffixMatch = rawKey.match(/(_\d+)$/); - const suffix = suffixMatch ? suffixMatch[1] : ''; - const finalSuffix = suffix === '_1' ? '' : suffix; - - const keyWithoutSuffix = suffix ? rawKey.slice(0, -suffix.length) : rawKey; - const base = keyWithoutSuffix.replace(`${phase}_`, ''); + // Try all suffixes and groups + for (const group of GROUPS) { + for (const suffix of SUFFIXES) { + const possibleKeys = [ + `${phase}_${group}_aineiston_maaraaika${suffix}`, + `milloin_${phase}_${group}_alkaa${suffix}`, + `milloin_${phase}_${group}_paattyy${suffix}`, + `milloin_${phase}_${group}_alkaa_pieni`, + `milloin_${phase}_${group}_alkaa_iso`, + `${phase}_${group}_aineiston_maaraaika_pieni`, + `${phase}_${group}_aineiston_maaraaika_iso` + ]; + for (const key of possibleKeys) { + if (key in attributeData && !confirmedFields.includes(key)) { + confirmedFields.push(key); + } + } + } + } - const parts = base.split('_'); - const group = parts[0]; + // Add vaihe fields if present + for (const key of getVaiheFields(phase, attributeData)) { + if (!confirmedFields.includes(key)) { + confirmedFields.push(key); + } + } - if (parts.length === 1) { - confirmedFields.push(...getSpecialCaseFields(phase, base, finalSuffix)); - addMielipiteetField(confirmedFields, seenPhases, phase, attributeData); - continue; + // Add special cases for this phase + for (const specialKey of SPECIAL_CASES) { + if (specialKey.includes(phase) && specialKey in attributeData && !confirmedFields.includes(specialKey)) { + confirmedFields.push(specialKey); + } } - confirmedFields.push(...getRegularFields(phase, group, finalSuffix, attributeData)); + // Add mielipiteet field if present addMielipiteetField(confirmedFields, seenPhases, phase, attributeData); } + // Also add any other special cases you see in your data + for (const specialKey of SPECIAL_CASES) { + if (specialKey in attributeData && !confirmedFields.includes(specialKey)) { + confirmedFields.push(specialKey); + } + } + + // Add all *_fieldset fields if present + addFieldsetFields(confirmedFields, attributeData); + return [...new Set(confirmedFields)]; } \ No newline at end of file From 3da9406e4e99ffb685ac46c984f4b321fe4d0e98 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 10 Oct 2025 12:59:19 +0300 Subject: [PATCH 07/31] KAAV-3388 Refactor to reduce cognitive complexity --- src/utils/generateConfirmedFields.js | 123 +++++++++++++-------------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 9822902ba..348af257e 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -10,7 +10,7 @@ const SPECIAL_CASES = [ 'tarkistettu_ehdotus_kylk_maaraaika' ]; -// Helper to get all possible vaihe fields for a phase (with and without underscore) +// Get all possible vaihe fields for a phase (with and without underscore) function getVaiheFields(phase, attributeData) { const variants = [ `${phase}vaihe_alkaa_pvm`, @@ -21,88 +21,87 @@ function getVaiheFields(phase, attributeData) { return variants.filter(key => key in attributeData); } -// Helper to add mielipiteet fields with all suffixes -function addMielipiteetField(confirmedFields, seenPhases, phase, attributeData) { +// Get all timetable fields for a phase/group/suffix combination +function getGroupSuffixFields(phase, attributeData) { + const fields = []; + for (const group of GROUPS) { + for (const suffix of SUFFIXES) { + [ + `${phase}_${group}_aineiston_maaraaika${suffix}`, + `milloin_${phase}_${group}_alkaa${suffix}`, + `milloin_${phase}_${group}_paattyy${suffix}`, + `milloin_${phase}_${group}_alkaa_pieni`, + `milloin_${phase}_${group}_alkaa_iso`, + `${phase}_${group}_aineiston_maaraaika_pieni`, + `${phase}_${group}_aineiston_maaraaika_iso` + ].forEach(key => { + if (key in attributeData) fields.push(key); + }); + } + } + return fields; +} + +// Get special case fields for a phase +function getSpecialCaseFields(phase, attributeData) { + return SPECIAL_CASES.filter( + key => key.includes(phase) && key in attributeData + ); +} + +// Get all mielipiteet fields for a phase (with sta/ista/numeric suffixes) +function getMielipiteetFields(phase, attributeData, seenPhases) { + const fields = []; const nums = ['', '_2', '_3', '_4']; for (const suffix of MIELIPITEET_SUFFIXES) { for (const num of nums) { const key = `viimeistaan_mielipiteet_${phase}${suffix}${num}`; if (!seenPhases.has(key) && key in attributeData) { - confirmedFields.push(key); + fields.push(key); seenPhases.add(key); } } } + return fields; } -// Helper to add all *_fieldset fields if present -function addFieldsetFields(confirmedFields, attributeData) { - Object.keys(attributeData).forEach(key => { - if (key.endsWith('_fieldset') && !confirmedFields.includes(key)) { - confirmedFields.push(key); - } - }); +// Get all *_fieldset fields present in attributeData +function getFieldsetFields(attributeData, confirmedFields) { + return Object.keys(attributeData).filter( + key => key.endsWith('_fieldset') && !confirmedFields.includes(key) + ); } -// Main function +// Collect all fields that should be confirmed/locked for the backend export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { const confirmedFields = []; const seenPhases = new Set(); - for (const confirmationKey of confirmationAttributeNames) { - if (!confirmationKey.startsWith('vahvista_')) continue; - - let rawKey = confirmationKey.replace(/^vahvista_/, ''); - let phase = phaseNames.find((p) => rawKey === p || rawKey.startsWith(p + '_')); - if (!phase) continue; - - // Try all suffixes and groups - for (const group of GROUPS) { - for (const suffix of SUFFIXES) { - const possibleKeys = [ - `${phase}_${group}_aineiston_maaraaika${suffix}`, - `milloin_${phase}_${group}_alkaa${suffix}`, - `milloin_${phase}_${group}_paattyy${suffix}`, - `milloin_${phase}_${group}_alkaa_pieni`, - `milloin_${phase}_${group}_alkaa_iso`, - `${phase}_${group}_aineiston_maaraaika_pieni`, - `${phase}_${group}_aineiston_maaraaika_iso` - ]; - for (const key of possibleKeys) { - if (key in attributeData && !confirmedFields.includes(key)) { - confirmedFields.push(key); - } - } - } - } - - // Add vaihe fields if present - for (const key of getVaiheFields(phase, attributeData)) { - if (!confirmedFields.includes(key)) { - confirmedFields.push(key); - } - } + confirmationAttributeNames.forEach(confirmationKey => { + if (!confirmationKey.startsWith('vahvista_')) return; + const rawKey = confirmationKey.replace(/^vahvista_/, ''); + const phase = phaseNames.find(p => rawKey === p || rawKey.startsWith(p + '_')); + if (!phase) return; - // Add special cases for this phase - for (const specialKey of SPECIAL_CASES) { - if (specialKey.includes(phase) && specialKey in attributeData && !confirmedFields.includes(specialKey)) { - confirmedFields.push(specialKey); - } - } - - // Add mielipiteet field if present - addMielipiteetField(confirmedFields, seenPhases, phase, attributeData); - } + confirmedFields.push( + ...getGroupSuffixFields(phase, attributeData), + ...getVaiheFields(phase, attributeData), + ...getSpecialCaseFields(phase, attributeData), + ...getMielipiteetFields(phase, attributeData, seenPhases) + ); + }); - // Also add any other special cases you see in your data - for (const specialKey of SPECIAL_CASES) { - if (specialKey in attributeData && !confirmedFields.includes(specialKey)) { - confirmedFields.push(specialKey); - } - } + // Add global special cases if present + confirmedFields.push( + ...SPECIAL_CASES.filter( + key => key in attributeData && !confirmedFields.includes(key) + ) + ); // Add all *_fieldset fields if present - addFieldsetFields(confirmedFields, attributeData); + confirmedFields.push( + ...getFieldsetFields(attributeData, confirmedFields) + ); return [...new Set(confirmedFields)]; } \ No newline at end of file From 86c01aaf9fbad9afff11b7798fca71a56eb83024 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Fri, 10 Oct 2025 13:04:39 +0300 Subject: [PATCH 08/31] KAAV-3388 solve SonarCloud issues --- src/utils/generateConfirmedFields.js | 62 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 348af257e..7f8b70804 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -18,7 +18,11 @@ function getVaiheFields(phase, attributeData) { `${phase}_vaihe_alkaa_pvm`, `${phase}_vaihe_paattyy_pvm` ]; - return variants.filter(key => key in attributeData); + const result = []; + for (const key of variants) { + if (key in attributeData) result.push(key); + } + return result; } // Get all timetable fields for a phase/group/suffix combination @@ -26,7 +30,7 @@ function getGroupSuffixFields(phase, attributeData) { const fields = []; for (const group of GROUPS) { for (const suffix of SUFFIXES) { - [ + const possibleKeys = [ `${phase}_${group}_aineiston_maaraaika${suffix}`, `milloin_${phase}_${group}_alkaa${suffix}`, `milloin_${phase}_${group}_paattyy${suffix}`, @@ -34,9 +38,10 @@ function getGroupSuffixFields(phase, attributeData) { `milloin_${phase}_${group}_alkaa_iso`, `${phase}_${group}_aineiston_maaraaika_pieni`, `${phase}_${group}_aineiston_maaraaika_iso` - ].forEach(key => { + ]; + for (const key of possibleKeys) { if (key in attributeData) fields.push(key); - }); + } } } return fields; @@ -44,9 +49,13 @@ function getGroupSuffixFields(phase, attributeData) { // Get special case fields for a phase function getSpecialCaseFields(phase, attributeData) { - return SPECIAL_CASES.filter( - key => key.includes(phase) && key in attributeData - ); + const result = []; + for (const key of SPECIAL_CASES) { + if (key.includes(phase) && key in attributeData) { + result.push(key); + } + } + return result; } // Get all mielipiteet fields for a phase (with sta/ista/numeric suffixes) @@ -67,41 +76,46 @@ function getMielipiteetFields(phase, attributeData, seenPhases) { // Get all *_fieldset fields present in attributeData function getFieldsetFields(attributeData, confirmedFields) { - return Object.keys(attributeData).filter( - key => key.endsWith('_fieldset') && !confirmedFields.includes(key) - ); + const result = []; + for (const key of Object.keys(attributeData)) { + if (key.endsWith('_fieldset') && !confirmedFields.includes(key)) { + result.push(key); + } + } + return result; } // Collect all fields that should be confirmed/locked for the backend export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - const confirmedFields = []; + let confirmedFields = []; const seenPhases = new Set(); - confirmationAttributeNames.forEach(confirmationKey => { - if (!confirmationKey.startsWith('vahvista_')) return; + for (const confirmationKey of confirmationAttributeNames) { + if (!confirmationKey.startsWith('vahvista_')) continue; const rawKey = confirmationKey.replace(/^vahvista_/, ''); const phase = phaseNames.find(p => rawKey === p || rawKey.startsWith(p + '_')); - if (!phase) return; + if (!phase) continue; - confirmedFields.push( - ...getGroupSuffixFields(phase, attributeData), - ...getVaiheFields(phase, attributeData), - ...getSpecialCaseFields(phase, attributeData), - ...getMielipiteetFields(phase, attributeData, seenPhases) + confirmedFields = confirmedFields.concat( + getGroupSuffixFields(phase, attributeData), + getVaiheFields(phase, attributeData), + getSpecialCaseFields(phase, attributeData), + getMielipiteetFields(phase, attributeData, seenPhases) ); - }); + } // Add global special cases if present - confirmedFields.push( - ...SPECIAL_CASES.filter( + confirmedFields = confirmedFields.concat( + SPECIAL_CASES.filter( key => key in attributeData && !confirmedFields.includes(key) ) ); // Add all *_fieldset fields if present - confirmedFields.push( - ...getFieldsetFields(attributeData, confirmedFields) + confirmedFields = confirmedFields.concat( + getFieldsetFields(attributeData, confirmedFields) ); + // Remove duplicates return [...new Set(confirmedFields)]; } \ No newline at end of file From 8563b01422cc20b87d3bbd8c0adfd567df486a76 Mon Sep 17 00:00:00 2001 From: Eemeli Kukkonen Date: Thu, 16 Oct 2025 17:00:13 +0300 Subject: [PATCH 09/31] KAAV-3388 add unit test to help with generateConfirmedFields --- .../utils/generateConfirmedFields.test.js | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index 3ab06a42b..3478535b4 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -8,11 +8,40 @@ const phaseNames = [ 'oas', 'luonnos', 'ehdotus', + 'kaavaluonnos', + 'kaavaehdotus', 'tarkistettu_ehdotus' ]; -describe.skip('generateConfirmedFields utility function', () => { - test('a field is found for all known confirmation attributes', () => { +const all_deadline_attribute_keys = [ + "luonnosaineiston_maaraaika", "milloin_oas_esillaolo_alkaa", "viimeistaan_mielipiteet_oas", + "luonnosaineiston_maaraaika_2", "luonnosaineiston_maaraaika_3", "milloin_oas_esillaolo_alkaa_2", + "milloin_oas_esillaolo_alkaa_3","milloin_oas_esillaolo_paattyy", "viimeistaan_mielipiteet_oas_2", + "viimeistaan_mielipiteet_oas_3", "milloin_luonnos_esillaolo_alkaa", "milloin_oas_esillaolo_paattyy_2", + "milloin_oas_esillaolo_paattyy_3", "viimeistaan_mielipiteet_luonnos", "ehdotus_kylk_aineiston_maaraaika", + "milloin_luonnos_esillaolo_alkaa_2", "milloin_luonnos_esillaolo_alkaa_3", "milloin_luonnos_esillaolo_paattyy", + "milloin_periaatteet_lautakunnassa", "oas_esillaolo_aineiston_maaraaika", "viimeistaan_mielipiteet_luonnos_2", + "viimeistaan_mielipiteet_luonnos_3", "milloin_kaavaehdotus_lautakunnassa", "milloin_kaavaluonnos_lautakunnassa", + "tarkistettu_ehdotus_kylk_maaraaika", "viimeistaan_lausunnot_ehdotuksesta","milloin_luonnos_esillaolo_paattyy_2", + "milloin_luonnos_esillaolo_paattyy_3", "milloin_periaatteet_esillaolo_alkaa","milloin_periaatteet_lautakunnassa_2", + "milloin_periaatteet_lautakunnassa_3", "milloin_periaatteet_lautakunnassa_4","oas_esillaolo_aineiston_maaraaika_2", + "oas_esillaolo_aineiston_maaraaika_3", "milloin_kaavaehdotus_lautakunnassa_2","milloin_kaavaehdotus_lautakunnassa_3", + "milloin_kaavaehdotus_lautakunnassa_4", "milloin_kaavaluonnos_lautakunnassa_2","milloin_kaavaluonnos_lautakunnassa_3", + "milloin_kaavaluonnos_lautakunnassa_4", "viimeistaan_lausunnot_ehdotuksesta_2","viimeistaan_lausunnot_ehdotuksesta_3", + "viimeistaan_lausunnot_ehdotuksesta_4", "kaavaluonnos_kylk_aineiston_maaraaika","milloin_ehdotuksen_nahtavilla_paattyy", + "milloin_periaatteet_esillaolo_alkaa_2","milloin_periaatteet_esillaolo_alkaa_3","milloin_periaatteet_esillaolo_paattyy", + "viimeistaan_mielipiteet_periaatteista","milloin_ehdotuksen_nahtavilla_alkaa_iso","milloin_ehdotuksen_nahtavilla_paattyy_2", + "milloin_ehdotuksen_nahtavilla_paattyy_3","milloin_ehdotuksen_nahtavilla_paattyy_4","milloin_periaatteet_esillaolo_paattyy_2", + "milloin_periaatteet_esillaolo_paattyy_3","viimeistaan_mielipiteet_periaatteista_2","viimeistaan_mielipiteet_periaatteista_3", + "milloin_ehdotuksen_nahtavilla_alkaa_iso_2","milloin_ehdotuksen_nahtavilla_alkaa_iso_3","milloin_ehdotuksen_nahtavilla_alkaa_iso_4", + "milloin_tarkistettu_ehdotus_lautakunnassa","periaatteet_esillaolo_aineiston_maaraaika","periaatteet_lautakunta_aineiston_maaraaika", + "milloin_tarkistettu_ehdotus_lautakunnassa_2","milloin_tarkistettu_ehdotus_lautakunnassa_3","milloin_tarkistettu_ehdotus_lautakunnassa_4", + "periaatteet_esillaolo_aineiston_maaraaika_2","periaatteet_esillaolo_aineiston_maaraaika_3", "milloin_ehdotuksen_nahtavilla_alkaa_pieni", + "milloin_ehdotuksen_nahtavilla_alkaa_pieni_2","milloin_ehdotuksen_nahtavilla_alkaa_pieni_3","milloin_ehdotuksen_nahtavilla_alkaa_pieni_4", +]; + +describe('generateConfirmedFields utility function', () => { + test('at least one field is found for all known confirmation attributes', () => { for(const confirm_attribute of confirmationAttributeNames ) { if (confirm_attribute === 'vahvista_ehdotus_esillaolo_alkaa_pieni') { // Not present in XL test data @@ -24,4 +53,32 @@ describe.skip('generateConfirmedFields utility function', () => { ).not.empty; } }); + + test("generateConfirmedFields returns all relevant attributes when all fields are confirmed", () => { + const test_data = {...test_attribute_data_XL}; + for (const confirm_attribute of confirmationAttributeNames) { + test_data[confirm_attribute] = true; + } + const result = generateConfirmedFields({...test_attribute_data_XL}, confirmationAttributeNames, phaseNames); + for (const r of all_deadline_attribute_keys) { + expect.soft(result, `Confirmed fields should include ${r}`).toContain(r); + } + }); + + test("When no fields are confirmed, generateConfirmedFields returns an empty array", () => { + const test_data = {...test_attribute_data_XL}; + for (const confirm_attribute of confirmationAttributeNames) { + test_data[confirm_attribute] = false; + } + expect(generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames).length).toBe(0); + }); + + test("generateConfirmedFields does not use outdated confirmation attributes", () => { + const outdated_attributes = [...confirmationAttributeNames].filter(attr => attr.includes('paattyy')); + const test_data = {...test_attribute_data_XL}; + for (const confirm_attribute of confirmationAttributeNames) { + test_data[confirm_attribute] = outdated_attributes.includes(confirm_attribute); + } + expect(generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames).length).toBe(0); + }); }); \ No newline at end of file From 8b1ea05e1ee49874efff279f0fd0f7505eab20be Mon Sep 17 00:00:00 2001 From: Eemeli Kukkonen Date: Thu, 16 Oct 2025 17:05:41 +0300 Subject: [PATCH 10/31] Fix highly important sonarcloud issue in unit test --- src/__tests__/utils/generateConfirmedFields.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index 3478535b4..b6612312b 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -74,10 +74,10 @@ describe('generateConfirmedFields utility function', () => { }); test("generateConfirmedFields does not use outdated confirmation attributes", () => { - const outdated_attributes = [...confirmationAttributeNames].filter(attr => attr.includes('paattyy')); + const outdated_attributes = new Set([...confirmationAttributeNames].filter(attr => attr.includes('paattyy'))); const test_data = {...test_attribute_data_XL}; for (const confirm_attribute of confirmationAttributeNames) { - test_data[confirm_attribute] = outdated_attributes.includes(confirm_attribute); + test_data[confirm_attribute] = outdated_attributes.has(confirm_attribute); } expect(generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames).length).toBe(0); }); From c5c40881af07654c4e6ff67d15c626aea9aaf20c Mon Sep 17 00:00:00 2001 From: Eemeli Kukkonen Date: Thu, 16 Oct 2025 17:19:18 +0300 Subject: [PATCH 11/31] Update first generateConfirmedFields test --- src/__tests__/utils/generateConfirmedFields.test.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index b6612312b..4e7543752 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -42,13 +42,14 @@ const all_deadline_attribute_keys = [ describe('generateConfirmedFields utility function', () => { test('at least one field is found for all known confirmation attributes', () => { - for(const confirm_attribute of confirmationAttributeNames ) { - if (confirm_attribute === 'vahvista_ehdotus_esillaolo_alkaa_pieni') { - // Not present in XL test data - continue; - } + const full_test_data = {}; + for (const attr of all_deadline_attribute_keys) { + full_test_data[attr] = "1970-01-01"; + } + for(const confirm_attribute of confirmationAttributeNames.filter(attr => !attr.includes('paattyy'))) { + let test_data = {... full_test_data, [confirm_attribute]: true} expect(generateConfirmedFields( - {...test_attribute_data_XL, [confirm_attribute]: true}, confirmationAttributeNames, phaseNames), + test_data, confirmationAttributeNames, phaseNames), `${confirm_attribute} should have related confirm field(s)` ).not.empty; } From 3ca770f6a15942751560d77d2a7ebac41e7560c7 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 13 Jan 2026 13:45:28 +0200 Subject: [PATCH 12/31] KAAV-3388 Add missing ehdotuksen_nahtavilla_alkaa_pieni fields to test data --- src/__tests__/utils/test_attribute_data.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/__tests__/utils/test_attribute_data.js b/src/__tests__/utils/test_attribute_data.js index f82f1e1f2..376c843d2 100644 --- a/src/__tests__/utils/test_attribute_data.js +++ b/src/__tests__/utils/test_attribute_data.js @@ -105,6 +105,10 @@ export const test_attribute_data_XL = { "milloin_periaatteet_esillaolo_alkaa_3": "2026-06-24", "milloin_periaatteet_esillaolo_paattyy": "2026-04-08", "viimeistaan_mielipiteet_periaatteista": "2026-04-08", + "milloin_ehdotuksen_nahtavilla_alkaa_pieni": "2027-12-28", + "milloin_ehdotuksen_nahtavilla_alkaa_pieni_2": "2028-02-24", + "milloin_ehdotuksen_nahtavilla_alkaa_pieni_3": "2028-04-25", + "milloin_ehdotuksen_nahtavilla_alkaa_pieni_4": "2028-06-26", "milloin_ehdotuksen_nahtavilla_alkaa_iso": "2027-12-28", "milloin_ehdotuksen_nahtavilla_paattyy_2": "2028-03-24", "milloin_ehdotuksen_nahtavilla_paattyy_3": "2028-05-26", From 23df35f27138e40d73253d61ea54aeb76dac929c Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 13 Jan 2026 13:45:57 +0200 Subject: [PATCH 13/31] KAAV-3388 Ensure tests cover all deadline attribute keys --- src/__tests__/utils/generateConfirmedFields.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index 4e7543752..7f0cc97b3 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -60,7 +60,7 @@ describe('generateConfirmedFields utility function', () => { for (const confirm_attribute of confirmationAttributeNames) { test_data[confirm_attribute] = true; } - const result = generateConfirmedFields({...test_attribute_data_XL}, confirmationAttributeNames, phaseNames); + const result = generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames); for (const r of all_deadline_attribute_keys) { expect.soft(result, `Confirmed fields should include ${r}`).toContain(r); } From 8996a60f4b8891dfd386ff3c858ea4a453217e79 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 13 Jan 2026 13:48:03 +0200 Subject: [PATCH 14/31] KAAV-3388 Refactor generateConfirmedFields and ensure all expected fields are included when confirmed --- src/utils/generateConfirmedFields.js | 182 +++++++++++---------------- 1 file changed, 76 insertions(+), 106 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 7f8b70804..9e0e2f72b 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,121 +1,91 @@ -const SUFFIXES = ['', '_2', '_3', '_4', '_pieni', '_iso']; -const GROUPS = ['esillaolo', 'nahtaville', 'lautakunta', 'kylk']; -const MIELIPITEET_SUFFIXES = ['', 'sta', 'ista']; -const SPECIAL_CASES = [ - 'ehdotus_nahtaville_aineiston_maaraaika', - 'milloin_ehdotuksen_nahtavilla_paattyy', - 'viimeistaan_lausunnot_ehdotuksesta', - 'luonnosaineiston_maaraaika', - 'kaavaluonnos_kylk_aineiston_maaraaika', - 'tarkistettu_ehdotus_kylk_maaraaika' -]; + const PHASE_ALIASES = { + periaatteet: ['periaatteet'], + oas: ['oas'], + luonnos: ['luonnos', 'kaavaluonnos'], + ehdotus: ['ehdotus', 'kaavaehdotus'], + tarkistettu_ehdotus: ['tarkistettu_ehdotus'] + }; -// Get all possible vaihe fields for a phase (with and without underscore) -function getVaiheFields(phase, attributeData) { - const variants = [ - `${phase}vaihe_alkaa_pvm`, - `${phase}vaihe_paattyy_pvm`, - `${phase}_vaihe_alkaa_pvm`, - `${phase}_vaihe_paattyy_pvm` + const SPECIAL_CASES = [ + // Ehdotus phase + 'viimeistaan_lausunnot_ehdotuksesta', + 'viimeistaan_lausunnot_ehdotuksesta_2', + 'viimeistaan_lausunnot_ehdotuksesta_3', + 'viimeistaan_lausunnot_ehdotuksesta_4', + 'milloin_ehdotuksen_nahtavilla_paattyy', + 'milloin_ehdotuksen_nahtavilla_paattyy_2', + 'milloin_ehdotuksen_nahtavilla_paattyy_3', + 'milloin_ehdotuksen_nahtavilla_paattyy_4', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso_2', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso_3', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso_4', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_2', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_3', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_4', + // Periaatteet phase + 'viimeistaan_mielipiteet_periaatteista', + 'viimeistaan_mielipiteet_periaatteista_2', + 'viimeistaan_mielipiteet_periaatteista_3' ]; - const result = []; - for (const key of variants) { - if (key in attributeData) result.push(key); - } - return result; -} -// Get all timetable fields for a phase/group/suffix combination -function getGroupSuffixFields(phase, attributeData) { - const fields = []; - for (const group of GROUPS) { - for (const suffix of SUFFIXES) { - const possibleKeys = [ - `${phase}_${group}_aineiston_maaraaika${suffix}`, - `milloin_${phase}_${group}_alkaa${suffix}`, - `milloin_${phase}_${group}_paattyy${suffix}`, - `milloin_${phase}_${group}_alkaa_pieni`, - `milloin_${phase}_${group}_alkaa_iso`, - `${phase}_${group}_aineiston_maaraaika_pieni`, - `${phase}_${group}_aineiston_maaraaika_iso` - ]; - for (const key of possibleKeys) { - if (key in attributeData) fields.push(key); - } - } + function isConfirmationKey(key) { + return key.startsWith('vahvista_'); } - return fields; -} -// Get special case fields for a phase -function getSpecialCaseFields(phase, attributeData) { - const result = []; - for (const key of SPECIAL_CASES) { - if (key.includes(phase) && key in attributeData) { - result.push(key); + function isSpecialCaseForPhase(key, phase) { + if (phase === 'ehdotus') { + return key.includes('ehdotuksen_nahtavilla') || key.includes('lausunnot_ehdotuksesta'); } - } - return result; -} - -// Get all mielipiteet fields for a phase (with sta/ista/numeric suffixes) -function getMielipiteetFields(phase, attributeData, seenPhases) { - const fields = []; - const nums = ['', '_2', '_3', '_4']; - for (const suffix of MIELIPITEET_SUFFIXES) { - for (const num of nums) { - const key = `viimeistaan_mielipiteet_${phase}${suffix}${num}`; - if (!seenPhases.has(key) && key in attributeData) { - fields.push(key); - seenPhases.add(key); - } + if (phase === 'periaatteet') { + return key.includes('periaatteista'); } + return false; } - return fields; -} -// Get all *_fieldset fields present in attributeData -function getFieldsetFields(attributeData, confirmedFields) { - const result = []; - for (const key of Object.keys(attributeData)) { - if (key.endsWith('_fieldset') && !confirmedFields.includes(key)) { - result.push(key); - } - } - return result; -} + export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { + const confirmedFields = new Set(); + const attributeKeys = Object.keys(attributeData); -// Collect all fields that should be confirmed/locked for the backend -export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - let confirmedFields = []; - const seenPhases = new Set(); + for (const confirmationKey of confirmationAttributeNames) { + if (confirmationKey.includes('paattyy')) continue; // skip outdated + if (!attributeData[confirmationKey]) continue; + const match = confirmationKey.match(/^vahvista_([a-z0-9_]+)$/); + if (!match) continue; + const phasePart = match[1]; + const phase = phaseNames.find(p => phasePart === p || phasePart.startsWith(p + '_')); + if (!phase) continue; + const aliases = PHASE_ALIASES[phase] || [phase]; - for (const confirmationKey of confirmationAttributeNames) { - if (!confirmationKey.startsWith('vahvista_')) continue; - const rawKey = confirmationKey.replace(/^vahvista_/, ''); - const phase = phaseNames.find(p => rawKey === p || rawKey.startsWith(p + '_')); - if (!phase) continue; + // Add all fields containing any alias substring + for (const alias of aliases) { + for (const key of attributeKeys) { + if (key.includes(alias)) { + confirmedFields.add(key); + } + } + } - confirmedFields = confirmedFields.concat( - getGroupSuffixFields(phase, attributeData), - getVaiheFields(phase, attributeData), - getSpecialCaseFields(phase, attributeData), - getMielipiteetFields(phase, attributeData, seenPhases) - ); - } + // Add special-case fields for this phase + for (const key of SPECIAL_CASES) { + if (key in attributeData && isSpecialCaseForPhase(key, phase)) { + confirmedFields.add(key); + } + } + } - // Add global special cases if present - confirmedFields = confirmedFields.concat( - SPECIAL_CASES.filter( - key => key in attributeData && !confirmedFields.includes(key) - ) - ); + // Add *_fieldset fields if present + for (const key of attributeKeys) { + if (key.endsWith('_fieldset')) { + confirmedFields.add(key); + } + } - // Add all *_fieldset fields if present - confirmedFields = confirmedFields.concat( - getFieldsetFields(attributeData, confirmedFields) - ); + // Remove confirmation attributes from the result + const filteredFields = Array.from(confirmedFields).filter( + key => !isConfirmationKey(key) + ); - // Remove duplicates - return [...new Set(confirmedFields)]; -} \ No newline at end of file + return filteredFields.sort(); + } \ No newline at end of file From 1f87c860b1bd51a2c56a1f99fd470866715d2dc6 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 13 Jan 2026 14:03:46 +0200 Subject: [PATCH 15/31] KAAV-3388 Add fix for SonarCloud issue --- src/utils/generateConfirmedFields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 9e0e2f72b..2cadae5cd 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -87,5 +87,5 @@ key => !isConfirmationKey(key) ); - return filteredFields.sort(); + return filteredFields.sort((a, b) => a.localeCompare(b)); } \ No newline at end of file From 5620c8d7936f5ca8b96eeb64fae8332a41614d74 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 13 Jan 2026 15:36:29 +0200 Subject: [PATCH 16/31] KAAV-3388 Refactor generateConfirmedFields to reduce cognitive complexity --- src/utils/generateConfirmedFields.js | 64 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 2cadae5cd..0de796d2b 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -44,45 +44,57 @@ return false; } - export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - const confirmedFields = new Set(); - const attributeKeys = Object.keys(attributeData); - - for (const confirmationKey of confirmationAttributeNames) { - if (confirmationKey.includes('paattyy')) continue; // skip outdated - if (!attributeData[confirmationKey]) continue; - const match = confirmationKey.match(/^vahvista_([a-z0-9_]+)$/); - if (!match) continue; - const phasePart = match[1]; - const phase = phaseNames.find(p => phasePart === p || phasePart.startsWith(p + '_')); - if (!phase) continue; - const aliases = PHASE_ALIASES[phase] || [phase]; + function getPhaseFromConfirmationKey(confirmationKey, phaseNames) { + const match = confirmationKey.match(/^vahvista_([a-z0-9_]+)$/); + if (!match) return null; + const phasePart = match[1]; + return phaseNames.find(p => phasePart === p || phasePart.startsWith(p + '_')); + } - // Add all fields containing any alias substring - for (const alias of aliases) { - for (const key of attributeKeys) { - if (key.includes(alias)) { - confirmedFields.add(key); - } + function addAliasFields(confirmedFields, attributeKeys, aliases) { + for (const alias of aliases) { + for (const key of attributeKeys) { + if (key.includes(alias)) { + confirmedFields.add(key); } } + } + } - // Add special-case fields for this phase - for (const key of SPECIAL_CASES) { - if (key in attributeData && isSpecialCaseForPhase(key, phase)) { - confirmedFields.add(key); - } + function addSpecialCaseFields(confirmedFields, attributeData, phase) { + for (const key of SPECIAL_CASES) { + if (key in attributeData && isSpecialCaseForPhase(key, phase)) { + confirmedFields.add(key); } } + } - // Add *_fieldset fields if present + function addFieldsetFields(confirmedFields, attributeKeys) { for (const key of attributeKeys) { if (key.endsWith('_fieldset')) { confirmedFields.add(key); } } + } + + export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { + const confirmedFields = new Set(); + const attributeKeys = Object.keys(attributeData); + + confirmationAttributeNames.forEach(confirmationKey => { + if (confirmationKey.includes('paattyy')) return; + if (!attributeData[confirmationKey]) return; + + const phase = getPhaseFromConfirmationKey(confirmationKey, phaseNames); + if (!phase) return; + + const aliases = PHASE_ALIASES[phase] || [phase]; + addAliasFields(confirmedFields, attributeKeys, aliases); + addSpecialCaseFields(confirmedFields, attributeData, phase); + }); + + addFieldsetFields(confirmedFields, attributeKeys); - // Remove confirmation attributes from the result const filteredFields = Array.from(confirmedFields).filter( key => !isConfirmationKey(key) ); From b5329cf28edb8edd0177461b7ac88b4deaeb7b0a Mon Sep 17 00:00:00 2001 From: Eemeli Kukkonen Date: Thu, 22 Jan 2026 16:04:43 +0200 Subject: [PATCH 17/31] Add test for generateConfirmedFields to validate confirmed fields output --- .../utils/generateConfirmedFields.test.js | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index 7f0cc97b3..b10da4a8b 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -82,4 +82,67 @@ describe('generateConfirmedFields utility function', () => { } expect(generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames).length).toBe(0); }); + + test("generateConfirmedFields returns correct fields when some are confirmed", () => { + const test_data = {...test_attribute_data_XL}; + for (const confirm_attribute of confirmationAttributeNames) { + test_data[confirm_attribute] = false; + } + const confirmed = [ + "vahvista_periaatteet_esillaolo_alkaa", "vahvista_periaatteet_esillaolo_alkaa_2", + "vahvista_periaatteet_lautakunnassa", "vahvista_periaatteet_lautakunnassa_2", + "vahvista_oas_esillaolo_alkaa", + "vahvista_luonnos_esillaolo_alkaa", "vahvista_luonnos_esillaolo_alkaa_2", "vahvista_luonnos_esillaolo_alkaa_3", + "vahvista_kaavaluonnos_lautakunnassa", "vahvista_kaavaluonnos_lautakunnassa_2", "vahvista_kaavaluonnos_lautakunnassa_3", + "vahvista_ehdotus_esillaolo", + "vahvista_tarkistettu_ehdotus_lautakunnassa", "vahvista_tarkistettu_ehdotus_lautakunnassa_2" + ]; + for (const attr of confirmed) { + test_data[attr] = true; + } + const result = generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames); + console.log(result) + expect.soft(result).toContain("periaatteet_esillaolo_aineiston_maaraaika"); + expect.soft(result).toContain("milloin_periaatteet_esillaolo_alkaa"); + expect.soft(result).toContain("milloin_periaatteet_esillaolo_paattyy"); + expect.soft(result).toContain("viimeistaan_mielipiteet_periaatteista"); + expect.soft(result).toContain("milloin_periaatteet_esillaolo_alkaa_2"); + expect.soft(result).not.toContain("milloin_periaatteet_esillaolo_alkaa_3"); + expect.soft(result).not.toContain("milloin_periaatteet_esillaolo_alkaa_4"); + + expect.soft(result).toContain("milloin_periaatteet_lautakunnassa"); + expect.soft(result).toContain("periaatteet_lautakunta_aineiston_maaraaika"); + + expect.soft(result).toContain("milloin_periaatteet_lautakunnassa_2"); + expect.soft(result).not.toContain("milloin_periaatteet_lautakunnassa_3"); + expect.soft(result).not.toContain("milloin_periaatteet_lautakunnassa_4"); + + expect.soft(result).toContain("oas_esillaolo_aineiston_maaraaika"); + expect.soft(result).toContain("milloin_oas_esillaolo_alkaa"); + expect.soft(result).toContain("milloin_oas_esillaolo_paattyy"); + expect.soft(result).not.toContain("milloin_oas_esillaolo_alkaa_2"); + expect.soft(result).not.toContain("milloin_oas_esillaolo_alkaa_3"); + + expect.soft(result).toContain("milloin_luonnos_esillaolo_alkaa"); + expect.soft(result).toContain("milloin_luonnos_esillaolo_alkaa_2"); + expect.soft(result).toContain("milloin_luonnos_esillaolo_alkaa_3"); + expect.soft(result).not.toContain("milloin_luonnos_esillaolo_alkaa_4"); + expect.soft(result).toContain("milloin_kaavaluonnos_lautakunnassa"); + expect.soft(result).toContain("milloin_kaavaluonnos_lautakunnassa_2"); + expect.soft(result).toContain("milloin_kaavaluonnos_lautakunnassa_3"); + expect.soft(result).not.toContain("milloin_kaavaluonnos_lautakunnassa_4"); + + expect.soft(result).not.toContain("milloin_kaavaehdotus_lautakunnassa"); + expect.soft(result).not.toContain("milloin_kaavaehdotus_lautakunnassa_2"); + expect.soft(result).toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso"); + expect.soft(result).not.toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso_2"); + expect.soft(result).not.toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso_3"); + expect.soft(result).not.toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso_4"); + + expect.soft(result).toContain("milloin_tarkistettu_ehdotus_lautakunnassa"); + expect.soft(result).toContain("milloin_tarkistettu_ehdotus_lautakunnassa_2"); + expect.soft(result).not.toContain("milloin_tarkistettu_ehdotus_lautakunnassa_3"); + expect.soft(result).not.toContain("milloin_tarkistettu_ehdotus_lautakunnassa_4"); + + }); }); \ No newline at end of file From dd814660c1813d87bce951550c450f22bc20e510 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 10:42:52 +0200 Subject: [PATCH 18/31] KAAV-3388 Add tests for index-specific confirmations, metadata filtering, and special case handling --- .../utils/generateConfirmedFields.test.js | 132 ++++++++++++++++-- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index b10da4a8b..9c24f8171 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -3,16 +3,6 @@ import { confirmationAttributeNames } from '../../utils/constants'; import { generateConfirmedFields } from "../../utils/generateConfirmedFields"; import { test_attribute_data_XL } from './test_attribute_data'; -const phaseNames = [ - 'periaatteet', - 'oas', - 'luonnos', - 'ehdotus', - 'kaavaluonnos', - 'kaavaehdotus', - 'tarkistettu_ehdotus' -]; - const all_deadline_attribute_keys = [ "luonnosaineiston_maaraaika", "milloin_oas_esillaolo_alkaa", "viimeistaan_mielipiteet_oas", "luonnosaineiston_maaraaika_2", "luonnosaineiston_maaraaika_3", "milloin_oas_esillaolo_alkaa_2", @@ -49,7 +39,7 @@ describe('generateConfirmedFields utility function', () => { for(const confirm_attribute of confirmationAttributeNames.filter(attr => !attr.includes('paattyy'))) { let test_data = {... full_test_data, [confirm_attribute]: true} expect(generateConfirmedFields( - test_data, confirmationAttributeNames, phaseNames), + test_data, confirmationAttributeNames), `${confirm_attribute} should have related confirm field(s)` ).not.empty; } @@ -60,7 +50,7 @@ describe('generateConfirmedFields utility function', () => { for (const confirm_attribute of confirmationAttributeNames) { test_data[confirm_attribute] = true; } - const result = generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames); + const result = generateConfirmedFields(test_data, confirmationAttributeNames); for (const r of all_deadline_attribute_keys) { expect.soft(result, `Confirmed fields should include ${r}`).toContain(r); } @@ -71,7 +61,7 @@ describe('generateConfirmedFields utility function', () => { for (const confirm_attribute of confirmationAttributeNames) { test_data[confirm_attribute] = false; } - expect(generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames).length).toBe(0); + expect(generateConfirmedFields(test_data, confirmationAttributeNames).length).toBe(0); }); test("generateConfirmedFields does not use outdated confirmation attributes", () => { @@ -80,7 +70,119 @@ describe('generateConfirmedFields utility function', () => { for (const confirm_attribute of confirmationAttributeNames) { test_data[confirm_attribute] = outdated_attributes.has(confirm_attribute); } - expect(generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames).length).toBe(0); + expect(generateConfirmedFields(test_data, confirmationAttributeNames).length).toBe(0); + }); + + test("Ehdotus esillaolo confirmation works with vahvista_ehdotus_esillaolo (no _iso/_pieni)", () => { + // Test ehdotus confirmation - should work for all project sizes with same key + const test_data = { + kaavaprosessin_kokoluokka: "XL", + milloin_ehdotuksen_nahtavilla_alkaa_iso: "2024-01-01", + milloin_ehdotuksen_nahtavilla_alkaa_iso_2: "2024-02-01", + milloin_ehdotuksen_nahtavilla_paattyy: "2024-01-15", + milloin_ehdotuksen_nahtavilla_paattyy_2: "2024-02-15", + vahvista_ehdotus_esillaolo: true, + vahvista_ehdotus_esillaolo_2: true + }; + const result = generateConfirmedFields(test_data, confirmationAttributeNames); + expect(result).toContain('milloin_ehdotuksen_nahtavilla_alkaa_iso'); + expect(result).toContain('milloin_ehdotuksen_nahtavilla_alkaa_iso_2'); + expect(result).toContain('milloin_ehdotuksen_nahtavilla_paattyy'); + expect(result).toContain('milloin_ehdotuksen_nahtavilla_paattyy_2'); + }); + + test("Only confirmed index is returned for esillaolo (not all _2, _3, _4)", () => { + const test_data = { + milloin_luonnos_esillaolo_alkaa: "2024-01-01", + milloin_luonnos_esillaolo_alkaa_2: "2024-02-01", + milloin_luonnos_esillaolo_alkaa_3: "2024-03-01", + milloin_luonnos_esillaolo_paattyy: "2024-01-15", + milloin_luonnos_esillaolo_paattyy_2: "2024-02-15", + vahvista_luonnos_esillaolo_alkaa: true, // Only first confirmed + vahvista_luonnos_esillaolo_alkaa_2: false, + vahvista_luonnos_esillaolo_alkaa_3: false + }; + const result = generateConfirmedFields(test_data, confirmationAttributeNames); + + // Should include only first occurrence + expect(result).toContain('milloin_luonnos_esillaolo_alkaa'); + expect(result).toContain('milloin_luonnos_esillaolo_paattyy'); + + // Should NOT include _2 and _3 (not confirmed) + expect(result).not.toContain('milloin_luonnos_esillaolo_alkaa_2'); + expect(result).not.toContain('milloin_luonnos_esillaolo_alkaa_3'); + expect(result).not.toContain('milloin_luonnos_esillaolo_paattyy_2'); + }); + + test("Each lautakunta index must be confirmed separately", () => { + const test_data = { + milloin_kaavaluonnos_lautakunnassa: "2024-01-01", + milloin_kaavaluonnos_lautakunnassa_2: "2024-02-01", + milloin_kaavaluonnos_lautakunnassa_3: "2024-03-01", + milloin_kaavaluonnos_lautakunnassa_4: "2024-04-01", + vahvista_kaavaluonnos_lautakunnassa: true, // Only first confirmed + vahvista_kaavaluonnos_lautakunnassa_3: true // And third confirmed + }; + const result = generateConfirmedFields(test_data, confirmationAttributeNames); + + // Should include only confirmed indices (_1 and _3) + expect(result).toContain('milloin_kaavaluonnos_lautakunnassa'); + expect(result).toContain('milloin_kaavaluonnos_lautakunnassa_3'); + + // Should NOT include unconfirmed indices (_2 and _4) + expect(result).not.toContain('milloin_kaavaluonnos_lautakunnassa_2'); + expect(result).not.toContain('milloin_kaavaluonnos_lautakunnassa_4'); + }); + + test("Phase dates and visibility booleans are filtered out", () => { + const test_data = { + luonnosvaihe_alkaa_pvm: "2024-01-01", + luonnosvaihe_paattyy_pvm: "2024-12-31", + jarjestetaan_luonnos_esillaolo_1: true, + kaavaluonnos_lautakuntaan_1: true, + luonnos_luotu: true, + lautakunta_paatti_luonnos: "hyvaksytty", + onko_luonnos_a_asiana: true, + milloin_luonnos_esillaolo_alkaa: "2024-01-01", + vahvista_luonnos_esillaolo_alkaa: true + }; + const result = generateConfirmedFields(test_data, confirmationAttributeNames); + + // Should include actual deadline + expect(result).toContain('milloin_luonnos_esillaolo_alkaa'); + + // Should NOT include phase dates, visibility booleans, or metadata + expect(result).not.toContain('luonnosvaihe_alkaa_pvm'); + expect(result).not.toContain('luonnosvaihe_paattyy_pvm'); + expect(result).not.toContain('jarjestetaan_luonnos_esillaolo_1'); + expect(result).not.toContain('kaavaluonnos_lautakuntaan_1'); + expect(result).not.toContain('luonnos_luotu'); + expect(result).not.toContain('lautakunta_paatti_luonnos'); + expect(result).not.toContain('onko_luonnos_a_asiana'); + }); + + test("Special cases for ehdotus and periaatteet are handled correctly", () => { + const test_data = { + viimeistaan_lausunnot_ehdotuksesta: "2024-01-01", + viimeistaan_lausunnot_ehdotuksesta_2: "2024-02-01", + milloin_ehdotuksen_nahtavilla_alkaa_iso: "2024-01-01", + viimeistaan_mielipiteet_periaatteista: "2024-03-01", + milloin_periaatteet_esillaolo_alkaa: "2024-03-01", + vahvista_ehdotus_esillaolo: true, + vahvista_periaatteet_esillaolo_alkaa: true + }; + const result = generateConfirmedFields(test_data, confirmationAttributeNames); + + // Ehdotus special cases + expect(result).toContain('viimeistaan_lausunnot_ehdotuksesta'); + expect(result).toContain('milloin_ehdotuksen_nahtavilla_alkaa_iso'); + + // Periaatteet special cases + expect(result).toContain('viimeistaan_mielipiteet_periaatteista'); + expect(result).toContain('milloin_periaatteet_esillaolo_alkaa'); + + // Should NOT include _2 (different index) + expect(result).not.toContain('viimeistaan_lausunnot_ehdotuksesta_2'); }); test("generateConfirmedFields returns correct fields when some are confirmed", () => { @@ -100,7 +202,7 @@ describe('generateConfirmedFields utility function', () => { for (const attr of confirmed) { test_data[attr] = true; } - const result = generateConfirmedFields(test_data, confirmationAttributeNames, phaseNames); + const result = generateConfirmedFields(test_data, confirmationAttributeNames); console.log(result) expect.soft(result).toContain("periaatteet_esillaolo_aineiston_maaraaika"); expect.soft(result).toContain("milloin_periaatteet_esillaolo_alkaa"); From 554b2bc042c4f3e8f4334540cbce6f80fa009a11 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 10:46:27 +0200 Subject: [PATCH 19/31] KAAV-3388 Add missing lautakunta confirmation keys and simplify ehdotus keys --- src/utils/constants.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/utils/constants.js b/src/utils/constants.js index 82817f547..555693859 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -5,18 +5,20 @@ export const confirmationAttributeNames = [ 'vahvista_periaatteet_esillaolo_alkaa', 'vahvista_periaatteet_esillaolo_paattyy', 'vahvista_periaatteet_esillaolo_alkaa_2', 'vahvista_periaatteet_esillaolo_paattyy_2', 'vahvista_periaatteet_esillaolo_alkaa_3', 'vahvista_periaatteet_esillaolo_paattyy_3', - 'vahvista_periaatteet_lautakunnassa', + 'vahvista_periaatteet_lautakunnassa', 'vahvista_periaatteet_lautakunnassa_2', + 'vahvista_periaatteet_lautakunnassa_3', 'vahvista_periaatteet_lautakunnassa_4', 'vahvista_luonnos_esillaolo_alkaa', 'vahvista_luonnos_esillaolo_paattyy', 'vahvista_luonnos_esillaolo_alkaa_2', 'vahvista_luonnos_esillaolo_paattyy_2', 'vahvista_luonnos_esillaolo_alkaa_3', 'vahvista_luonnos_esillaolo_paattyy_3', - 'vahvista_ehdotus_esillaolo_alkaa_pieni', 'vahvista_ehdotus_esillaolo_paattyy', - 'vahvista_kaavaluonnos_lautakunnassa', - 'vahvista_ehdotus_esillaolo_alkaa_iso', 'vahvista_ehdotus_esillaolo_alkaa_pieni_2', - 'vahvista_ehdotus_esillaolo_alkaa_iso_2', 'vahvista_ehdotus_esillaolo_paattyy_2', - 'vahvista_ehdotus_esillaolo_alkaa_pieni_3', 'vahvista_ehdotus_esillaolo_alkaa_iso_3', - 'vahvista_ehdotus_esillaolo_paattyy_3', 'vahvista_ehdotus_esillaolo_alkaa_pieni_4', - 'vahvista_ehdotus_esillaolo_alkaa_iso_4', 'vahvista_ehdotus_esillaolo_paattyy_4', - 'vahvista_kaavaehdotus_lautakunnassa', - 'vahvista_tarkistettu_ehdotus_lautakunnassa', + 'vahvista_ehdotus_esillaolo', 'vahvista_ehdotus_esillaolo_paattyy', + 'vahvista_kaavaluonnos_lautakunnassa', 'vahvista_kaavaluonnos_lautakunnassa_2', + 'vahvista_kaavaluonnos_lautakunnassa_3', 'vahvista_kaavaluonnos_lautakunnassa_4', + 'vahvista_ehdotus_esillaolo_2', 'vahvista_ehdotus_esillaolo_paattyy_2', + 'vahvista_ehdotus_esillaolo_3', 'vahvista_ehdotus_esillaolo_paattyy_3', + 'vahvista_ehdotus_esillaolo_4', 'vahvista_ehdotus_esillaolo_paattyy_4', + 'vahvista_kaavaehdotus_lautakunnassa', 'vahvista_kaavaehdotus_lautakunnassa_2', + 'vahvista_kaavaehdotus_lautakunnassa_3', 'vahvista_kaavaehdotus_lautakunnassa_4', + 'vahvista_tarkistettu_ehdotus_lautakunnassa', 'vahvista_tarkistettu_ehdotus_lautakunnassa_2', + 'vahvista_tarkistettu_ehdotus_lautakunnassa_3', 'vahvista_tarkistettu_ehdotus_lautakunnassa_4', ]; \ No newline at end of file From da51b500bd76e5be16bc261e992734721d21e6b4 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 13:29:17 +0200 Subject: [PATCH 20/31] KAAV-3388 Remove unused projectSize parameter from getConfirmationKeyForEsillaoloKey and refactor ehdotus confirmation check --- src/components/ProjectTimeline/VisTimelineGroup.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ProjectTimeline/VisTimelineGroup.jsx b/src/components/ProjectTimeline/VisTimelineGroup.jsx index d8bd6b73d..f9590187c 100644 --- a/src/components/ProjectTimeline/VisTimelineGroup.jsx +++ b/src/components/ProjectTimeline/VisTimelineGroup.jsx @@ -293,7 +293,7 @@ const VisTimelineGroup = forwardRef(({ groups, items, deadlines, visValues, dead if (normalizedPhase === "periaatteet") normalizedPhase = "periaatteet"; if (normalizedPhase === "oas") normalizedPhase = "oas"; - // Special case for ehdotus-phase: no _alkaa in the key! + // Special case for ehdotus-phase: uses vahvista_ehdotus_esillaolo (no _alkaa suffix) if (normalizedPhase === "ehdotus") { if (idx === "1") { return `vahvista_ehdotus_esillaolo`; @@ -347,12 +347,12 @@ const VisTimelineGroup = forwardRef(({ groups, items, deadlines, visValues, dead const getLautakuntaConfirmed = (visValRef, phase, lautakuntaCount) => { const projectSize = visValRef?.kaavaprosessin_kokoluokka; //L AND XL has phase order reversed on ehdotus phase and it is not allowed for lautakunta to be added after nahtavillaolo - if ( - phase === "ehdotus" && - (projectSize === "XL" || projectSize === "L") && - visValRef?.vahvista_ehdotus_esillaolo === true - ) { + if (phase === "ehdotus" && (projectSize === "XL" || projectSize === "L")) { + // Check the confirmation key + const confirmKey = `vahvista_ehdotus_esillaolo`; + if (visValRef?.[confirmKey] === true) { return false; + } } if (phase === "luonnos") { From e4660cd65cf566b9f6b7f26d4880b7f3628c25ae Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 13:32:33 +0200 Subject: [PATCH 21/31] KAAV-3388 Fix indentation in projectSaga error handling blocks --- src/sagas/projectSaga.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sagas/projectSaga.js b/src/sagas/projectSaga.js index 26c1103fb..7b53f116d 100644 --- a/src/sagas/projectSaga.js +++ b/src/sagas/projectSaga.js @@ -674,7 +674,7 @@ function* saveProjectPayload({ payload }) { const isNetworkErr = e?.code === 'ERR_NETWORK' const statusCode = e?.response?.status if (isNetworkErr || !statusCode || statusCode >= 500) { - yield put({ type: 'Set network status', payload: { status: 'error', errorMessage: i18.t('messages.general-save-error') } }) + yield put({ type: 'Set network status', payload: { status: 'error', errorMessage: i18.t('messages.general-save-error') } }) } } } @@ -1183,7 +1183,7 @@ function* saveProject(data) { const isNetworkErr = e?.code === 'ERR_NETWORK' const statusCode = e?.response?.status if (isNetworkErr || !statusCode || statusCode >= 500) { - yield put({ type: 'Set network status', payload: { status: 'error', errorMessage: i18.t('messages.general-save-error') } }) + yield put({ type: 'Set network status', payload: { status: 'error', errorMessage: i18.t('messages.general-save-error') } }) } } } From c50660d9f063c9c81c5349d155f3521383b76616 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 14:37:15 +0200 Subject: [PATCH 22/31] KAAV-3388 Fix generateConfirmedFields index handling, metadata filtering, and ehdotus confirmation keys --- src/utils/generateConfirmedFields.js | 277 +++++++++++++++++++-------- 1 file changed, 195 insertions(+), 82 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 0de796d2b..f7a22bac7 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,103 +1,216 @@ - const PHASE_ALIASES = { - periaatteet: ['periaatteet'], - oas: ['oas'], - luonnos: ['luonnos', 'kaavaluonnos'], - ehdotus: ['ehdotus', 'kaavaehdotus'], - tarkistettu_ehdotus: ['tarkistettu_ehdotus'] - }; - - const SPECIAL_CASES = [ - // Ehdotus phase - 'viimeistaan_lausunnot_ehdotuksesta', - 'viimeistaan_lausunnot_ehdotuksesta_2', - 'viimeistaan_lausunnot_ehdotuksesta_3', - 'viimeistaan_lausunnot_ehdotuksesta_4', - 'milloin_ehdotuksen_nahtavilla_paattyy', - 'milloin_ehdotuksen_nahtavilla_paattyy_2', - 'milloin_ehdotuksen_nahtavilla_paattyy_3', - 'milloin_ehdotuksen_nahtavilla_paattyy_4', - 'milloin_ehdotuksen_nahtavilla_alkaa_iso', - 'milloin_ehdotuksen_nahtavilla_alkaa_iso_2', - 'milloin_ehdotuksen_nahtavilla_alkaa_iso_3', - 'milloin_ehdotuksen_nahtavilla_alkaa_iso_4', - 'milloin_ehdotuksen_nahtavilla_alkaa_pieni', - 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_2', - 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_3', - 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_4', - // Periaatteet phase - 'viimeistaan_mielipiteet_periaatteista', - 'viimeistaan_mielipiteet_periaatteista_2', - 'viimeistaan_mielipiteet_periaatteista_3' - ]; - - function isConfirmationKey(key) { - return key.startsWith('vahvista_'); - } +const PHASE_ALIASES = { + periaatteet: ['periaatteet'], + oas: ['oas'], + luonnos: ['luonnos', 'kaavaluonnos'], + ehdotus: ['ehdotus', 'kaavaehdotus'], + tarkistettu_ehdotus: ['tarkistettu_ehdotus'] +}; - function isSpecialCaseForPhase(key, phase) { - if (phase === 'ehdotus') { - return key.includes('ehdotuksen_nahtavilla') || key.includes('lausunnot_ehdotuksesta'); - } - if (phase === 'periaatteet') { - return key.includes('periaatteista'); - } - return false; - } +const SPECIAL_CASES = [ + // Ehdotus phase + 'viimeistaan_lausunnot_ehdotuksesta', + 'viimeistaan_lausunnot_ehdotuksesta_2', + 'viimeistaan_lausunnot_ehdotuksesta_3', + 'viimeistaan_lausunnot_ehdotuksesta_4', + 'milloin_ehdotuksen_nahtavilla_paattyy', + 'milloin_ehdotuksen_nahtavilla_paattyy_2', + 'milloin_ehdotuksen_nahtavilla_paattyy_3', + 'milloin_ehdotuksen_nahtavilla_paattyy_4', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso_2', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso_3', + 'milloin_ehdotuksen_nahtavilla_alkaa_iso_4', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_2', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_3', + 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_4', + // Periaatteet phase + 'viimeistaan_mielipiteet_periaatteista', + 'viimeistaan_mielipiteet_periaatteista_2', + 'viimeistaan_mielipiteet_periaatteista_3' +]; + +function isConfirmationKey(key) { + return key.startsWith('vahvista_'); +} - function getPhaseFromConfirmationKey(confirmationKey, phaseNames) { - const match = confirmationKey.match(/^vahvista_([a-z0-9_]+)$/); - if (!match) return null; - const phasePart = match[1]; - return phaseNames.find(p => phasePart === p || phasePart.startsWith(p + '_')); +function isPhaseDate(key) { + // Phase start/end dates that shouldn't be in confirmed fields + return key.endsWith('vaihe_alkaa_pvm') || + key.endsWith('vaihe_paattyy_pvm') || + key.endsWith('_paattyy_pvm') || + key === 'projektin_kaynnistys_pvm'; +} + +function isVisibilityBoolean(key) { + // Visibility booleans like jarjestetaan_oas_esillaolo_1 + return key.startsWith('jarjestetaan_') || + key.includes('_nahtaville_') || + key.includes('_lautakuntaan_') || + key.endsWith('_luotu'); +} + +function isMetadataField(key) { + // Metadata fields that shouldn't be in confirmed fields + return key.startsWith('lautakunta_paatti_') || // Decision fields + key.startsWith('onko_') || // Question booleans + key.includes('_fieldset'); // Fieldset fields (handled separately) +} + +function isSpecialCaseForPhase(key, phase) { + if (phase === 'ehdotus') { + return key.includes('ehdotuksen_nahtavilla') || key.includes('lausunnot_ehdotuksesta'); + } + if (phase === 'periaatteet') { + return key.includes('periaatteista'); } + return false; +} + +function getPhaseAndIndexFromConfirmationKey(confirmationKey) { + // Extract phase name and index from confirmation key + // Examples: + // vahvista_oas_esillaolo_alkaa -> {phase: 'oas', type: 'esillaolo', index: '1'} + // vahvista_luonnos_esillaolo_alkaa_2 -> {phase: 'luonnos', type: 'esillaolo', index: '2'} + // vahvista_kaavaluonnos_lautakunnassa -> {phase: 'luonnos', type: 'lautakunta', index: '1'} + // vahvista_ehdotus_esillaolo -> {phase: 'ehdotus', type: 'esillaolo', index: '1'} + // vahvista_ehdotus_esillaolo_3 -> {phase: 'ehdotus', type: 'esillaolo', index: '3'} + + const match = confirmationKey.match(/^vahvista_(.+)$/); + if (!match) return null; + + const remaining = match[1]; + + // Extract index from end + const indexMatch = remaining.match(/_(\d+)$/); + const index = indexMatch ? indexMatch[1] : '1'; + + // Determine type + const type = remaining.includes('lautakunnassa') ? 'lautakunta' : 'esillaolo'; + + // Extract phase + let phase = remaining + .replace(/_esillaolo.*$/, '') + .replace(/_lautakunnassa.*$/, '') + .replace(/^kaava/, ''); // Remove 'kaava' prefix + + return { phase, type, index }; +} + +function addAliasFields(confirmedFields, attributeData, aliases, confirmationInfo) { + // Only add fields that match the phase AND index from the confirmation key + const { type, index } = confirmationInfo; - function addAliasFields(confirmedFields, attributeKeys, aliases) { - for (const alias of aliases) { - for (const key of attributeKeys) { - if (key.includes(alias)) { - confirmedFields.add(key); + for (const alias of aliases) { + for (const key in attributeData) { + if (!Object.prototype.hasOwnProperty.call(attributeData, key)) continue; + if (isConfirmationKey(key)) continue; + if (isPhaseDate(key)) continue; + if (isVisibilityBoolean(key)) continue; + if (isMetadataField(key)) continue; + + // Check if key matches the phase + // Alias must appear at the beginning of the field name (after milloin_ if present) + // OR at the end as _alias (for viimeistaan_mielipiteet_oas pattern) + // This prevents "ehdotus" from matching "tarkistettu_ehdotus_kylk_maaraaika" + if (key.startsWith('milloin_')) { + // For milloin_ keys, alias must be first component after milloin_ + if (!key.startsWith(`milloin_${alias}_`)) continue; + } else { + // For other keys, alias must be at the very start (including compound words) + // OR at the end as _alias or _alias_number + // Examples that MATCH "oas": + // - "oas_esillaolo_aineiston_maaraaika" (starts with oas) + // - "viimeistaan_mielipiteet_oas" (ends with _oas) + // - "viimeistaan_mielipiteet_oas_2" (ends with _oas_2) + // Examples that DO NOT MATCH "ehdotus": + // - "tarkistettu_ehdotus_kylk_maaraaika" (ehdotus not at start or end) + const startsWithAlias = key.startsWith(alias); + const endsWithAlias = key.endsWith(`_${alias}`) || key.match(new RegExp(`_${alias}(_\\d+)?$`)); + if (!startsWithAlias && !endsWithAlias) continue; + } + + // Extract index from the attribute key + const keyIndexMatch = key.match(/_(\d+)$/); + const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; + + // Match exact index for both esillaolo and lautakunta + // Each index has its own confirmation key + if (keyIndex === index) { + // Additional filtering for esillaolo/lautakunta specific keys + if (type === 'esillaolo' && (key.includes('lautakunta') || key.includes('lautakunnassa'))) { + continue; + } + if (type === 'lautakunta' && (key.includes('esillaolo') || key.includes('nahtavilla'))) { + continue; } + + confirmedFields.add(key); } } } +} - function addSpecialCaseFields(confirmedFields, attributeData, phase) { - for (const key of SPECIAL_CASES) { - if (key in attributeData && isSpecialCaseForPhase(key, phase)) { - confirmedFields.add(key); - } +function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) { + const { phase, type, index } = confirmationInfo; + + for (const key of SPECIAL_CASES) { + if (!(key in attributeData)) continue; + if (!isSpecialCaseForPhase(key, phase)) continue; + + // For ehdotus phase: special cases belong to esillaolo (nahtavilla), not lautakunta + // Only add ehdotus special cases if type is 'esillaolo' + if (phase === 'ehdotus' && type !== 'esillaolo') { + continue; + } + + // Extract index from special case key + // For ehdotus: milloin_ehdotuksen_nahtavilla_alkaa_iso -> index '1' + // milloin_ehdotuksen_nahtavilla_alkaa_iso_2 -> index '2' + // For periaatteet: viimeistaan_mielipiteet_periaatteista -> index '1' + // viimeistaan_mielipiteet_periaatteista_2 -> index '2' + + // First remove size suffix if present (_iso or _pieni) + const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, '$2'); + const keyIndexMatch = keyWithoutSize.match(/_(\d+)$/); + const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; + + // Only add if the index matches + if (keyIndex === index) { + confirmedFields.add(key); } } +} - function addFieldsetFields(confirmedFields, attributeKeys) { - for (const key of attributeKeys) { - if (key.endsWith('_fieldset')) { - confirmedFields.add(key); - } +function addFieldsetFields(confirmedFields, attributeKeys) { + for (const key of attributeKeys) { + if (key.endsWith('_fieldset')) { + confirmedFields.add(key); } } +} - export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - const confirmedFields = new Set(); - const attributeKeys = Object.keys(attributeData); +export function generateConfirmedFields(attributeData, confirmationAttributeNames) { + const confirmedFields = new Set(); - confirmationAttributeNames.forEach(confirmationKey => { - if (confirmationKey.includes('paattyy')) return; - if (!attributeData[confirmationKey]) return; + confirmationAttributeNames.forEach(confirmationKey => { + // Skip outdated paattyy confirmation attributes + if (confirmationKey.includes('paattyy')) return; + if (!attributeData[confirmationKey]) return; - const phase = getPhaseFromConfirmationKey(confirmationKey, phaseNames); - if (!phase) return; + const confirmationInfo = getPhaseAndIndexFromConfirmationKey(confirmationKey); + if (!confirmationInfo) return; - const aliases = PHASE_ALIASES[phase] || [phase]; - addAliasFields(confirmedFields, attributeKeys, aliases); - addSpecialCaseFields(confirmedFields, attributeData, phase); - }); + const aliases = PHASE_ALIASES[confirmationInfo.phase] || [confirmationInfo.phase]; + addAliasFields(confirmedFields, attributeData, aliases, confirmationInfo); + addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo); + }); - addFieldsetFields(confirmedFields, attributeKeys); + addFieldsetFields(confirmedFields, Object.keys(attributeData)); - const filteredFields = Array.from(confirmedFields).filter( - key => !isConfirmationKey(key) - ); + const filteredFields = Array.from(confirmedFields).filter( + key => !isConfirmationKey(key) && !isPhaseDate(key) && !isVisibilityBoolean(key) && !isMetadataField(key) + ); - return filteredFields.sort((a, b) => a.localeCompare(b)); - } \ No newline at end of file + return filteredFields.sort((a, b) => a.localeCompare(b)); +} \ No newline at end of file From 07ff5a8d73ff850e1e422b1a01946279e000c3ed Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 14:47:12 +0200 Subject: [PATCH 23/31] KAAV-3388 Remove unused phaseNames parameter from generateConfirmedFields calls --- src/sagas/projectSaga.js | 24 ++---------------------- src/utils/objectUtil.js | 4 +--- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/sagas/projectSaga.js b/src/sagas/projectSaga.js index 7b53f116d..5af978394 100644 --- a/src/sagas/projectSaga.js +++ b/src/sagas/projectSaga.js @@ -810,20 +810,10 @@ function* validateProjectTimetable() { // Add confirmed field locking from vahvista_* flags // leave 'kaynnistys','hyvaksyminen','voimaantulo' out because no vahvista flags there - const phaseNames = [ - 'periaatteet', - 'oas', - 'luonnos', - 'ehdotus', - 'kaavaluonnos', - 'kaavaehdotus', - 'tarkistettu_ehdotus' - ]; //Find confirmed fields from attribute_data so backend knows not to edit them const confirmed_fields = generateConfirmedFields( attribute_data, - confirmationAttributeNames, - phaseNames + confirmationAttributeNames ); try { @@ -910,21 +900,11 @@ function* saveProjectTimetable(action,retryCount = 0) { // Add confirmed field locking from vahvista_* flags // leave 'kaynnistys','hyvaksyminen','voimaantulo' out because no vahvista flags there - const phaseNames = [ - 'periaatteet', - 'oas', - 'luonnos', - 'ehdotus', - 'kaavaluonnos', - 'kaavaehdotus', - 'tarkistettu_ehdotus' - ]; //Find confirmed fields from attribute_data so backend knows not to edit them const confirmed_fields = generateConfirmedFields( attribute_data, - confirmationAttributeNames, - phaseNames + confirmationAttributeNames ); const maxRetries = 5; diff --git a/src/utils/objectUtil.js b/src/utils/objectUtil.js index 40d73ddea..6e81bed69 100644 --- a/src/utils/objectUtil.js +++ b/src/utils/objectUtil.js @@ -253,9 +253,7 @@ const getHighestNumberedObject = (obj1) => { // Lazy load to avoid circular deps (generateConfirmedFields depends on constants only) const { confirmationAttributeNames } = require('./constants'); const { generateConfirmedFields } = require('./generateConfirmedFields'); - // Phase names that have confirmation flags (exclude kaynnistys, hyvaksyminen, voimaantulo as per saga usage) - const phaseNames = ['periaatteet','oas','luonnos','ehdotus','tarkistettu_ehdotus']; - confirmedFieldSet = new Set(generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames)); + confirmedFieldSet = new Set(generateConfirmedFields(attributeData, confirmationAttributeNames)); } catch(e){ // Fail silently – if generation fails we simply don't lock by confirmation (past locking still applies) From 23f931ccebda1c1cb9dab5db25db12756078c3fb Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 15:00:36 +0200 Subject: [PATCH 24/31] KAAV-3388 Remove duplicate test to reduce code duplication --- .../utils/generateConfirmedFields.test.js | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index 9c24f8171..c8bd50014 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -184,67 +184,4 @@ describe('generateConfirmedFields utility function', () => { // Should NOT include _2 (different index) expect(result).not.toContain('viimeistaan_lausunnot_ehdotuksesta_2'); }); - - test("generateConfirmedFields returns correct fields when some are confirmed", () => { - const test_data = {...test_attribute_data_XL}; - for (const confirm_attribute of confirmationAttributeNames) { - test_data[confirm_attribute] = false; - } - const confirmed = [ - "vahvista_periaatteet_esillaolo_alkaa", "vahvista_periaatteet_esillaolo_alkaa_2", - "vahvista_periaatteet_lautakunnassa", "vahvista_periaatteet_lautakunnassa_2", - "vahvista_oas_esillaolo_alkaa", - "vahvista_luonnos_esillaolo_alkaa", "vahvista_luonnos_esillaolo_alkaa_2", "vahvista_luonnos_esillaolo_alkaa_3", - "vahvista_kaavaluonnos_lautakunnassa", "vahvista_kaavaluonnos_lautakunnassa_2", "vahvista_kaavaluonnos_lautakunnassa_3", - "vahvista_ehdotus_esillaolo", - "vahvista_tarkistettu_ehdotus_lautakunnassa", "vahvista_tarkistettu_ehdotus_lautakunnassa_2" - ]; - for (const attr of confirmed) { - test_data[attr] = true; - } - const result = generateConfirmedFields(test_data, confirmationAttributeNames); - console.log(result) - expect.soft(result).toContain("periaatteet_esillaolo_aineiston_maaraaika"); - expect.soft(result).toContain("milloin_periaatteet_esillaolo_alkaa"); - expect.soft(result).toContain("milloin_periaatteet_esillaolo_paattyy"); - expect.soft(result).toContain("viimeistaan_mielipiteet_periaatteista"); - expect.soft(result).toContain("milloin_periaatteet_esillaolo_alkaa_2"); - expect.soft(result).not.toContain("milloin_periaatteet_esillaolo_alkaa_3"); - expect.soft(result).not.toContain("milloin_periaatteet_esillaolo_alkaa_4"); - - expect.soft(result).toContain("milloin_periaatteet_lautakunnassa"); - expect.soft(result).toContain("periaatteet_lautakunta_aineiston_maaraaika"); - - expect.soft(result).toContain("milloin_periaatteet_lautakunnassa_2"); - expect.soft(result).not.toContain("milloin_periaatteet_lautakunnassa_3"); - expect.soft(result).not.toContain("milloin_periaatteet_lautakunnassa_4"); - - expect.soft(result).toContain("oas_esillaolo_aineiston_maaraaika"); - expect.soft(result).toContain("milloin_oas_esillaolo_alkaa"); - expect.soft(result).toContain("milloin_oas_esillaolo_paattyy"); - expect.soft(result).not.toContain("milloin_oas_esillaolo_alkaa_2"); - expect.soft(result).not.toContain("milloin_oas_esillaolo_alkaa_3"); - - expect.soft(result).toContain("milloin_luonnos_esillaolo_alkaa"); - expect.soft(result).toContain("milloin_luonnos_esillaolo_alkaa_2"); - expect.soft(result).toContain("milloin_luonnos_esillaolo_alkaa_3"); - expect.soft(result).not.toContain("milloin_luonnos_esillaolo_alkaa_4"); - expect.soft(result).toContain("milloin_kaavaluonnos_lautakunnassa"); - expect.soft(result).toContain("milloin_kaavaluonnos_lautakunnassa_2"); - expect.soft(result).toContain("milloin_kaavaluonnos_lautakunnassa_3"); - expect.soft(result).not.toContain("milloin_kaavaluonnos_lautakunnassa_4"); - - expect.soft(result).not.toContain("milloin_kaavaehdotus_lautakunnassa"); - expect.soft(result).not.toContain("milloin_kaavaehdotus_lautakunnassa_2"); - expect.soft(result).toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso"); - expect.soft(result).not.toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso_2"); - expect.soft(result).not.toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso_3"); - expect.soft(result).not.toContain("milloin_ehdotuksen_nahtavilla_alkaa_iso_4"); - - expect.soft(result).toContain("milloin_tarkistettu_ehdotus_lautakunnassa"); - expect.soft(result).toContain("milloin_tarkistettu_ehdotus_lautakunnassa_2"); - expect.soft(result).not.toContain("milloin_tarkistettu_ehdotus_lautakunnassa_3"); - expect.soft(result).not.toContain("milloin_tarkistettu_ehdotus_lautakunnassa_4"); - - }); }); \ No newline at end of file From 70594b94ee330daf3cff77c6d82530c7915718c8 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 15:14:32 +0200 Subject: [PATCH 25/31] KAAV-3388 Fix SonarCloud code quality issues in generateConfirmedFields --- src/utils/generateConfirmedFields.js | 82 ++++++++++++++-------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index f7a22bac7..b67ce44a4 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -97,54 +97,56 @@ function getPhaseAndIndexFromConfirmationKey(confirmationKey) { return { phase, type, index }; } +// Helper: Check if key should be filtered out +function shouldFilterKey(key) { + return isConfirmationKey(key) || isPhaseDate(key) || isVisibilityBoolean(key) || isMetadataField(key); +} + +// Helper: Check if alias matches the key (at start or end) +function aliasMatchesKey(key, alias) { + if (key.startsWith('milloin_')) { + // For milloin_ keys, alias must be first component after milloin_ + return key.startsWith(`milloin_${alias}_`); + } + + // For other keys, alias must be at the very start OR at the end as _alias + // This prevents "ehdotus" from matching "tarkistettu_ehdotus_kylk_maaraaika" + const startsWithAlias = key.startsWith(alias); + const endsWithAlias = key.endsWith(`_${alias}`) || new RegExp(String.raw`_${alias}(_\d+)?$`).exec(key); + return startsWithAlias || endsWithAlias; +} + +// Helper: Extract index from attribute key (defaults to '1') +function extractKeyIndex(key) { + const keyIndexMatch = /_(\d+)$/.exec(key); + return keyIndexMatch ? keyIndexMatch[1] : '1'; +} + +// Helper: Check if key matches the type (esillaolo vs lautakunta) +function keyMatchesType(key, type) { + if (type === 'esillaolo' && (key.includes('lautakunta') || key.includes('lautakunnassa'))) { + return false; + } + if (type === 'lautakunta' && (key.includes('esillaolo') || key.includes('nahtavilla'))) { + return false; + } + return true; +} + function addAliasFields(confirmedFields, attributeData, aliases, confirmationInfo) { // Only add fields that match the phase AND index from the confirmation key const { type, index } = confirmationInfo; for (const alias of aliases) { for (const key in attributeData) { - if (!Object.prototype.hasOwnProperty.call(attributeData, key)) continue; - if (isConfirmationKey(key)) continue; - if (isPhaseDate(key)) continue; - if (isVisibilityBoolean(key)) continue; - if (isMetadataField(key)) continue; - - // Check if key matches the phase - // Alias must appear at the beginning of the field name (after milloin_ if present) - // OR at the end as _alias (for viimeistaan_mielipiteet_oas pattern) - // This prevents "ehdotus" from matching "tarkistettu_ehdotus_kylk_maaraaika" - if (key.startsWith('milloin_')) { - // For milloin_ keys, alias must be first component after milloin_ - if (!key.startsWith(`milloin_${alias}_`)) continue; - } else { - // For other keys, alias must be at the very start (including compound words) - // OR at the end as _alias or _alias_number - // Examples that MATCH "oas": - // - "oas_esillaolo_aineiston_maaraaika" (starts with oas) - // - "viimeistaan_mielipiteet_oas" (ends with _oas) - // - "viimeistaan_mielipiteet_oas_2" (ends with _oas_2) - // Examples that DO NOT MATCH "ehdotus": - // - "tarkistettu_ehdotus_kylk_maaraaika" (ehdotus not at start or end) - const startsWithAlias = key.startsWith(alias); - const endsWithAlias = key.endsWith(`_${alias}`) || key.match(new RegExp(`_${alias}(_\\d+)?$`)); - if (!startsWithAlias && !endsWithAlias) continue; - } + if (!Object.hasOwn(attributeData, key)) continue; + if (shouldFilterKey(key)) continue; + if (!aliasMatchesKey(key, alias)) continue; - // Extract index from the attribute key - const keyIndexMatch = key.match(/_(\d+)$/); - const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; + const keyIndex = extractKeyIndex(key); // Match exact index for both esillaolo and lautakunta - // Each index has its own confirmation key - if (keyIndex === index) { - // Additional filtering for esillaolo/lautakunta specific keys - if (type === 'esillaolo' && (key.includes('lautakunta') || key.includes('lautakunnassa'))) { - continue; - } - if (type === 'lautakunta' && (key.includes('esillaolo') || key.includes('nahtavilla'))) { - continue; - } - + if (keyIndex === index && keyMatchesType(key, type)) { confirmedFields.add(key); } } @@ -172,7 +174,7 @@ function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) // First remove size suffix if present (_iso or _pieni) const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, '$2'); - const keyIndexMatch = keyWithoutSize.match(/_(\d+)$/); + const keyIndexMatch = /_(\d+)$/.exec(keyWithoutSize); const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; // Only add if the index matches From 27e3fbbb8330da1830f91fcb00d726526a9f24aa Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 15:18:52 +0200 Subject: [PATCH 26/31] KAAV-3388 Refactor addAliasFields to reduce cognitive complexity from 16 to 15 --- src/utils/generateConfirmedFields.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index b67ce44a4..9eb6b3e50 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -146,8 +146,10 @@ function addAliasFields(confirmedFields, attributeData, aliases, confirmationInf const keyIndex = extractKeyIndex(key); // Match exact index for both esillaolo and lautakunta - if (keyIndex === index && keyMatchesType(key, type)) { - confirmedFields.add(key); + if (keyIndex === index) { + if (keyMatchesType(key, type)) { + confirmedFields.add(key); + } } } } From 62d09fd6c0a1058fb339f28aad9163324db116b8 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Tue, 27 Jan 2026 15:23:42 +0200 Subject: [PATCH 27/31] KAAV-3388 Further reduce cognitive complexity by extracting validation logic to helper function --- src/utils/generateConfirmedFields.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 9eb6b3e50..6802798e1 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -133,23 +133,26 @@ function keyMatchesType(key, type) { return true; } +// Helper: Check if field should be added based on all criteria +function shouldAddField(key, alias, index, type, attributeData) { + if (!Object.hasOwn(attributeData, key)) return false; + if (shouldFilterKey(key)) return false; + if (!aliasMatchesKey(key, alias)) return false; + + const keyIndex = extractKeyIndex(key); + if (keyIndex !== index) return false; + + return keyMatchesType(key, type); +} + function addAliasFields(confirmedFields, attributeData, aliases, confirmationInfo) { // Only add fields that match the phase AND index from the confirmation key const { type, index } = confirmationInfo; for (const alias of aliases) { for (const key in attributeData) { - if (!Object.hasOwn(attributeData, key)) continue; - if (shouldFilterKey(key)) continue; - if (!aliasMatchesKey(key, alias)) continue; - - const keyIndex = extractKeyIndex(key); - - // Match exact index for both esillaolo and lautakunta - if (keyIndex === index) { - if (keyMatchesType(key, type)) { - confirmedFields.add(key); - } + if (shouldAddField(key, alias, index, type, attributeData)) { + confirmedFields.add(key); } } } From d1dec01395a43a51012ecfdbd11c7b06bdfe1389 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Wed, 4 Feb 2026 13:06:53 +0200 Subject: [PATCH 28/31] KAAV-3388 Fix field confirmation logic for KYLK deadlines, aineiston_maaraaika filtering, and ehdotus phase special cases --- src/sagas/projectSaga.js | 2 + src/utils/generateConfirmedFields.js | 156 ++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 29 deletions(-) diff --git a/src/sagas/projectSaga.js b/src/sagas/projectSaga.js index 5af978394..5e0d8a047 100644 --- a/src/sagas/projectSaga.js +++ b/src/sagas/projectSaga.js @@ -602,6 +602,8 @@ const adjustDeadlineData = (attributeData, allAttributeData) => { key.includes("milloin_ehdotuksen_nahtavilla_paattyy") || key.includes("viimeistaan_lausunnot_ehdotuksesta") || key.includes("milloin_tarkistettu_ehdotus_lautakunnassa") || + key.includes("kylk_maaraaika") || + key.includes("kylk_aineiston_maaraaika") || key.includes("kaavaehdotus_nahtaville") || key.includes("kaavaehdotus_uudelleen_nahtaville") || key.includes("vahvista")) { diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 6802798e1..a41fc2255 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -24,6 +24,10 @@ const SPECIAL_CASES = [ 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_2', 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_3', 'milloin_ehdotuksen_nahtavilla_alkaa_pieni_4', + 'kaavaehdotus_nahtaville_1', + 'kaavaehdotus_uudelleen_nahtaville_2', + 'kaavaehdotus_uudelleen_nahtaville_3', + 'kaavaehdotus_uudelleen_nahtaville_4', // Periaatteet phase 'viimeistaan_mielipiteet_periaatteista', 'viimeistaan_mielipiteet_periaatteista_2', @@ -44,6 +48,9 @@ function isPhaseDate(key) { function isVisibilityBoolean(key) { // Visibility booleans like jarjestetaan_oas_esillaolo_1 + // BUT NOT aineiston_maaraaika fields (e.g., ehdotus_nahtaville_aineiston_maaraaika) + if (key.includes('aineiston_maaraaika')) return false; + return key.startsWith('jarjestetaan_') || key.includes('_nahtaville_') || key.includes('_lautakuntaan_') || @@ -59,7 +66,10 @@ function isMetadataField(key) { function isSpecialCaseForPhase(key, phase) { if (phase === 'ehdotus') { - return key.includes('ehdotuksen_nahtavilla') || key.includes('lausunnot_ehdotuksesta'); + return key.includes('ehdotuksen_nahtavilla') || + key.includes('lausunnot_ehdotuksesta') || + key.includes('kaavaehdotus_nahtaville') || + key.includes('kaavaehdotus_uudelleen_nahtaville'); } if (phase === 'periaatteet') { return key.includes('periaatteista'); @@ -108,12 +118,44 @@ function aliasMatchesKey(key, alias) { // For milloin_ keys, alias must be first component after milloin_ return key.startsWith(`milloin_${alias}_`); } + + if (key.startsWith('viimeistaan_')) { + // For viimeistaan_ keys, check after viimeistaan_ prefix + // viimeistaan_mielipiteet_periaatteista -> matches 'periaatteet' alias + // viimeistaan_lausunnot_ehdotuksesta -> matches 'ehdotus' alias + const afterPrefix = key.substring('viimeistaan_'.length); + return afterPrefix.includes(alias); + } + + // Special case: aineiston_maaraaika and kylk_maaraaika fields can start with phase alias without underscore + // Examples: + // luonnosaineiston_maaraaika + // oas_esillaolo_aineiston_maaraaika + // periaatteet_lautakunta_aineiston_maaraaika + // ehdotus_kylk_aineiston_maaraaika / ehdotus_kylk_maaraaika + // kaavaluonnos_kylk_aineiston_maaraaika / kaavaluonnos_kylk_maaraaika + // tarkistettu_ehdotus_kylk_maaraaika + if (key.includes('aineiston_maaraaika') || key.includes('kylk_maaraaika')) { + // Match if key starts with alias (with or without underscore) + if (key.startsWith(alias)) return true; + if (key.startsWith(`${alias}_`)) return true; + // OR if key contains _alias_ pattern + if (key.includes(`_${alias}_`)) return true; + // OR for compound patterns like ehdotus_kylk, kaavaluonnos_kylk, tarkistettu_ehdotus_kylk + // Check if the key matches the pattern {alias}_kylk or {alias}_lautakunta + const maaraaikaPattern = new RegExp(`^${alias.replace('_', '_?')}_(kylk|lautakunta|esillaolo)_`); + if (maaraaikaPattern.test(key)) return true; + } - // For other keys, alias must be at the very start OR at the end as _alias - // This prevents "ehdotus" from matching "tarkistettu_ehdotus_kylk_maaraaika" - const startsWithAlias = key.startsWith(alias); - const endsWithAlias = key.endsWith(`_${alias}`) || new RegExp(String.raw`_${alias}(_\d+)?$`).exec(key); - return startsWithAlias || endsWithAlias; + // For other keys, alias must be at the very start followed by underscore + // OR at the end as _alias (with optional numeric suffix) + // This ensures "ehdotus" matches "ehdotus_kylk_..." but not "tarkistettu_ehdotus_..." + if (key.startsWith(`${alias}_`)) return true; + if (key.endsWith(`_${alias}`)) return true; + // Check for numeric suffix: _alias_1, _alias_2, etc. + const indexMatch = /_\d+$/.exec(key); + if (indexMatch && key.substring(0, indexMatch.index).endsWith(`_${alias}`)) return true; + return false; } // Helper: Extract index from attribute key (defaults to '1') @@ -124,12 +166,58 @@ function extractKeyIndex(key) { // Helper: Check if key matches the type (esillaolo vs lautakunta) function keyMatchesType(key, type) { - if (type === 'esillaolo' && (key.includes('lautakunta') || key.includes('lautakunnassa'))) { - return false; + // Aineiston määräajat ja KYLK määräajat: + // Jos kentässä on _esillaolo_, se kuuluu vain esilläoloon + // Jos kentässä on _lautakunta_, se kuuluu vain lautakuntaan + // Jos kentässä on nahtavilla JA aineiston_maaraaika, se kuuluu esilläoloon (esim. ehdotus_nahtaville_aineiston_maaraaika) + // Jos kentässä ei ole mitään näistä markereista (esim. luonnosaineiston_maaraaika), se kuuluu VAIN lautakuntaan + if (key.includes('aineiston_maaraaika') || key.includes('kylk_maaraaika')) { + // Check which type the field explicitly belongs to + const hasEsillaolo = key.includes('_esillaolo_'); + const hasLautakunta = key.includes('_lautakunta_'); + const hasNahtaville = key.includes('nahtaville'); // ehdotus_nahtaville_aineiston_maaraaika + const hasNahtavilla = key.includes('nahtavilla'); // milloin_ehdotuksen_nahtavilla_paattyy + + if (hasEsillaolo && type !== 'esillaolo') return false; + if (hasLautakunta && type !== 'lautakunta') return false; + + // If field contains nahtaville/nahtavilla + aineiston_maaraaika, it belongs to esillaolo (nähtävilläolo = display phase) + if ((hasNahtaville || hasNahtavilla) && key.includes('aineiston_maaraaika')) { + return type === 'esillaolo'; + } + + // If no explicit marker, it belongs to lautakunta only + if (!hasEsillaolo && !hasLautakunta && !hasNahtaville && !hasNahtavilla) { + return type === 'lautakunta'; + } + + return true; } - if (type === 'lautakunta' && (key.includes('esillaolo') || key.includes('nahtavilla'))) { - return false; + + // Mielipiteet ja lausunnot kuuluvat AINA esilläoloon + if (key.includes('mielipiteet') || key.includes('lausunnot')) { + return type === 'esillaolo'; } + + // Lautakunta-kentät + if (type === 'lautakunta') { + // Lautakunta-vahvistus: vain milloin_*_lautakunnassa ja aineiston_maaraaika + if (key.includes('esillaolo') || key.includes('nahtavilla')) { + return false; + } + return key.includes('lautakunta') || key.includes('lautakunnassa'); + } + + // Esilläolo-kentät + if (type === 'esillaolo') { + // Esilläolo-vahvistus: milloin_*_esillaolo_alkaa/paattyy, nahtavilla, mielipiteet, lausunnot + // EI lautakunta-kenttiä (paitsi aineiston_maaraaika joka hyväksytään ylhäällä) + if (key.includes('lautakunta') || key.includes('lautakunnassa')) { + return false; + } + return true; + } + return true; } @@ -162,8 +250,12 @@ function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) const { phase, type, index } = confirmationInfo; for (const key of SPECIAL_CASES) { - if (!(key in attributeData)) continue; - if (!isSpecialCaseForPhase(key, phase)) continue; + if (!(key in attributeData)) { + continue; + } + if (!isSpecialCaseForPhase(key, phase)) { + continue; + } // For ehdotus phase: special cases belong to esillaolo (nahtavilla), not lautakunta // Only add ehdotus special cases if type is 'esillaolo' @@ -171,6 +263,11 @@ function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) continue; } + // For periaatteet phase: special cases belong to esillaolo, not lautakunta + if (phase === 'periaatteet' && type !== 'esillaolo') { + continue; + } + // Extract index from special case key // For ehdotus: milloin_ehdotuksen_nahtavilla_alkaa_iso -> index '1' // milloin_ehdotuksen_nahtavilla_alkaa_iso_2 -> index '2' @@ -178,7 +275,7 @@ function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) // viimeistaan_mielipiteet_periaatteista_2 -> index '2' // First remove size suffix if present (_iso or _pieni) - const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, '$2'); + const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, '$2' || ''); const keyIndexMatch = /_(\d+)$/.exec(keyWithoutSize); const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; @@ -189,35 +286,36 @@ function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) } } -function addFieldsetFields(confirmedFields, attributeKeys) { - for (const key of attributeKeys) { - if (key.endsWith('_fieldset')) { - confirmedFields.add(key); - } - } -} - export function generateConfirmedFields(attributeData, confirmationAttributeNames) { const confirmedFields = new Set(); confirmationAttributeNames.forEach(confirmationKey => { // Skip outdated paattyy confirmation attributes - if (confirmationKey.includes('paattyy')) return; - if (!attributeData[confirmationKey]) return; + if (confirmationKey.includes('paattyy')) { + return; + } + // Only process if confirmation field exists AND is set to true + if (!attributeData[confirmationKey] || attributeData[confirmationKey] !== true) { + return; + } const confirmationInfo = getPhaseAndIndexFromConfirmationKey(confirmationKey); if (!confirmationInfo) return; - + const aliases = PHASE_ALIASES[confirmationInfo.phase] || [confirmationInfo.phase]; addAliasFields(confirmedFields, attributeData, aliases, confirmationInfo); addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo); }); - addFieldsetFields(confirmedFields, Object.keys(attributeData)); - - const filteredFields = Array.from(confirmedFields).filter( - key => !isConfirmationKey(key) && !isPhaseDate(key) && !isVisibilityBoolean(key) && !isMetadataField(key) - ); + // Filter out unwanted keys and fieldset fields + const filteredFields = Array.from(confirmedFields).filter(key => { + if (isConfirmationKey(key)) return false; + if (isPhaseDate(key)) return false; + if (isVisibilityBoolean(key)) return false; + if (isMetadataField(key)) return false; + if (key.includes('_fieldset')) return false; // Remove fieldset fields + return true; + }); return filteredFields.sort((a, b) => a.localeCompare(b)); } \ No newline at end of file From 64caca385deb351c0a6e53dfe4f507fa02ee1bf9 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Wed, 4 Feb 2026 14:11:41 +0200 Subject: [PATCH 29/31] KAAV-3388 Refactor generateConfirmedFields to reduce cognitive complexity and fix SonarCloud issues --- src/utils/generateConfirmedFields.js | 61 ++++++++++++---------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index a41fc2255..95277c373 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -202,20 +202,15 @@ function keyMatchesType(key, type) { // Lautakunta-kentät if (type === 'lautakunta') { // Lautakunta-vahvistus: vain milloin_*_lautakunnassa ja aineiston_maaraaika - if (key.includes('esillaolo') || key.includes('nahtavilla')) { - return false; - } - return key.includes('lautakunta') || key.includes('lautakunnassa'); + return !(key.includes('esillaolo') || key.includes('nahtavilla')) && + (key.includes('lautakunta') || key.includes('lautakunnassa')); } // Esilläolo-kentät if (type === 'esillaolo') { // Esilläolo-vahvistus: milloin_*_esillaolo_alkaa/paattyy, nahtavilla, mielipiteet, lausunnot // EI lautakunta-kenttiä (paitsi aineiston_maaraaika joka hyväksytään ylhäällä) - if (key.includes('lautakunta') || key.includes('lautakunnassa')) { - return false; - } - return true; + return !(key.includes('lautakunta') || key.includes('lautakunnassa')); } return true; @@ -246,40 +241,36 @@ function addAliasFields(confirmedFields, attributeData, aliases, confirmationInf } } +// Helper: Check if special case key should be included based on phase and type +function shouldIncludeSpecialCase(key, phase, type, attributeData) { + if (!(key in attributeData)) return false; + if (!isSpecialCaseForPhase(key, phase)) return false; + + // For ehdotus and periaatteet phases: special cases belong to esillaolo, not lautakunta + if ((phase === 'ehdotus' || phase === 'periaatteet') && type !== 'esillaolo') { + return false; + } + + return true; +} + +// Helper: Extract index from special case key (handling _iso/_pieni suffixes) +function extractSpecialCaseIndex(key) { + // First remove size suffix if present (_iso or _pieni) + const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, (match, p1, p2) => p2 || ''); + const keyIndexMatch = /_(\d+)$/.exec(keyWithoutSize); + return keyIndexMatch ? keyIndexMatch[1] : '1'; +} + function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) { const { phase, type, index } = confirmationInfo; for (const key of SPECIAL_CASES) { - if (!(key in attributeData)) { - continue; - } - if (!isSpecialCaseForPhase(key, phase)) { - continue; - } - - // For ehdotus phase: special cases belong to esillaolo (nahtavilla), not lautakunta - // Only add ehdotus special cases if type is 'esillaolo' - if (phase === 'ehdotus' && type !== 'esillaolo') { + if (!shouldIncludeSpecialCase(key, phase, type, attributeData)) { continue; } - // For periaatteet phase: special cases belong to esillaolo, not lautakunta - if (phase === 'periaatteet' && type !== 'esillaolo') { - continue; - } - - // Extract index from special case key - // For ehdotus: milloin_ehdotuksen_nahtavilla_alkaa_iso -> index '1' - // milloin_ehdotuksen_nahtavilla_alkaa_iso_2 -> index '2' - // For periaatteet: viimeistaan_mielipiteet_periaatteista -> index '1' - // viimeistaan_mielipiteet_periaatteista_2 -> index '2' - - // First remove size suffix if present (_iso or _pieni) - const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, '$2' || ''); - const keyIndexMatch = /_(\d+)$/.exec(keyWithoutSize); - const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; - - // Only add if the index matches + const keyIndex = extractSpecialCaseIndex(key); if (keyIndex === index) { confirmedFields.add(key); } From 76c09916d5b12b383f95979f1ddeab392aa9c38b Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Wed, 4 Feb 2026 14:53:15 +0200 Subject: [PATCH 30/31] KAAV-3388 Extract helper functions matchesDeadlineFieldType and shouldAddSpecialCase to reduce cognitive complexity --- src/utils/generateConfirmedFields.js | 87 +++++++++++++--------------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index 95277c373..9d87375b2 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -164,52 +164,50 @@ function extractKeyIndex(key) { return keyIndexMatch ? keyIndexMatch[1] : '1'; } +// Helper: Check aineiston_maaraaika and kylk_maaraaika type matching +function matchesDeadlineFieldType(key, type) { + const hasEsillaolo = key.includes('_esillaolo_'); + const hasLautakunta = key.includes('_lautakunta_'); + const hasNahtaville = key.includes('nahtaville'); + const hasNahtavilla = key.includes('nahtavilla'); + + // Field explicitly marked for wrong type + if (hasEsillaolo && type !== 'esillaolo') return false; + if (hasLautakunta && type !== 'lautakunta') return false; + + // nahtaville/nahtavilla + aineiston_maaraaika belongs to esillaolo + if ((hasNahtaville || hasNahtavilla) && key.includes('aineiston_maaraaika')) { + return type === 'esillaolo'; + } + + // No explicit marker means lautakunta only + if (!hasEsillaolo && !hasLautakunta && !hasNahtaville && !hasNahtavilla) { + return type === 'lautakunta'; + } + + return true; +} + // Helper: Check if key matches the type (esillaolo vs lautakunta) function keyMatchesType(key, type) { - // Aineiston määräajat ja KYLK määräajat: - // Jos kentässä on _esillaolo_, se kuuluu vain esilläoloon - // Jos kentässä on _lautakunta_, se kuuluu vain lautakuntaan - // Jos kentässä on nahtavilla JA aineiston_maaraaika, se kuuluu esilläoloon (esim. ehdotus_nahtaville_aineiston_maaraaika) - // Jos kentässä ei ole mitään näistä markereista (esim. luonnosaineiston_maaraaika), se kuuluu VAIN lautakuntaan + // Aineiston määräajat ja KYLK määräajat have special rules if (key.includes('aineiston_maaraaika') || key.includes('kylk_maaraaika')) { - // Check which type the field explicitly belongs to - const hasEsillaolo = key.includes('_esillaolo_'); - const hasLautakunta = key.includes('_lautakunta_'); - const hasNahtaville = key.includes('nahtaville'); // ehdotus_nahtaville_aineiston_maaraaika - const hasNahtavilla = key.includes('nahtavilla'); // milloin_ehdotuksen_nahtavilla_paattyy - - if (hasEsillaolo && type !== 'esillaolo') return false; - if (hasLautakunta && type !== 'lautakunta') return false; - - // If field contains nahtaville/nahtavilla + aineiston_maaraaika, it belongs to esillaolo (nähtävilläolo = display phase) - if ((hasNahtaville || hasNahtavilla) && key.includes('aineiston_maaraaika')) { - return type === 'esillaolo'; - } - - // If no explicit marker, it belongs to lautakunta only - if (!hasEsillaolo && !hasLautakunta && !hasNahtaville && !hasNahtavilla) { - return type === 'lautakunta'; - } - - return true; + return matchesDeadlineFieldType(key, type); } - // Mielipiteet ja lausunnot kuuluvat AINA esilläoloon + // Mielipiteet ja lausunnot always belong to esillaolo if (key.includes('mielipiteet') || key.includes('lausunnot')) { return type === 'esillaolo'; } - // Lautakunta-kentät + // Lautakunta fields if (type === 'lautakunta') { - // Lautakunta-vahvistus: vain milloin_*_lautakunnassa ja aineiston_maaraaika return !(key.includes('esillaolo') || key.includes('nahtavilla')) && (key.includes('lautakunta') || key.includes('lautakunnassa')); } - // Esilläolo-kentät + // Esillaolo fields if (type === 'esillaolo') { - // Esilläolo-vahvistus: milloin_*_esillaolo_alkaa/paattyy, nahtavilla, mielipiteet, lausunnot - // EI lautakunta-kenttiä (paitsi aineiston_maaraaika joka hyväksytään ylhäällä) return !(key.includes('lautakunta') || key.includes('lautakunnassa')); } @@ -241,37 +239,32 @@ function addAliasFields(confirmedFields, attributeData, aliases, confirmationInf } } -// Helper: Check if special case key should be included based on phase and type -function shouldIncludeSpecialCase(key, phase, type, attributeData) { +// Helper: Check if special case should be added +function shouldAddSpecialCase(key, phase, type, index, attributeData) { + // Key must exist in data if (!(key in attributeData)) return false; + + // Key must be relevant for this phase if (!isSpecialCaseForPhase(key, phase)) return false; - // For ehdotus and periaatteet phases: special cases belong to esillaolo, not lautakunta + // For ehdotus and periaatteet: special cases belong to esillaolo only if ((phase === 'ehdotus' || phase === 'periaatteet') && type !== 'esillaolo') { return false; } - return true; -} - -// Helper: Extract index from special case key (handling _iso/_pieni suffixes) -function extractSpecialCaseIndex(key) { - // First remove size suffix if present (_iso or _pieni) + // Extract and match index const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, (match, p1, p2) => p2 || ''); const keyIndexMatch = /_(\d+)$/.exec(keyWithoutSize); - return keyIndexMatch ? keyIndexMatch[1] : '1'; + const keyIndex = keyIndexMatch ? keyIndexMatch[1] : '1'; + + return keyIndex === index; } function addSpecialCaseFields(confirmedFields, attributeData, confirmationInfo) { const { phase, type, index } = confirmationInfo; for (const key of SPECIAL_CASES) { - if (!shouldIncludeSpecialCase(key, phase, type, attributeData)) { - continue; - } - - const keyIndex = extractSpecialCaseIndex(key); - if (keyIndex === index) { + if (shouldAddSpecialCase(key, phase, type, index, attributeData)) { confirmedFields.add(key); } } From 229ace18aa7f012494659824707c8e5e52c22f73 Mon Sep 17 00:00:00 2001 From: Minna Honkanen Date: Wed, 4 Feb 2026 14:59:06 +0200 Subject: [PATCH 31/31] KAAV-3388 Fix SonarCloud issues in VisTimelineGroup: use Number.parseInt and globalThis --- src/components/ProjectTimeline/VisTimelineGroup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProjectTimeline/VisTimelineGroup.jsx b/src/components/ProjectTimeline/VisTimelineGroup.jsx index f9590187c..5badd1d19 100644 --- a/src/components/ProjectTimeline/VisTimelineGroup.jsx +++ b/src/components/ProjectTimeline/VisTimelineGroup.jsx @@ -1300,7 +1300,7 @@ const VisTimelineGroup = forwardRef(({ groups, items, deadlines, visValues, dead mouseY >= (itemBounds.top - verticalBuffer) && mouseY <= (itemBounds.bottom + verticalBuffer) ) { - const zIndex = parseInt(window.getComputedStyle(itemDom).zIndex, 10); + const zIndex = Number.parseInt(globalThis.getComputedStyle(itemDom).zIndex, 10); if (zIndex > highestZIndex) { highestZIndex = zIndex; topmostItem = item;