diff --git a/src/__tests__/utils/generateConfirmedFields.test.js b/src/__tests__/utils/generateConfirmedFields.test.js index 3ab06a42b..c8bd50014 100644 --- a/src/__tests__/utils/generateConfirmedFields.test.js +++ b/src/__tests__/utils/generateConfirmedFields.test.js @@ -3,25 +3,185 @@ 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', - '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", + "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.skip('generateConfirmedFields utility function', () => { - test('a 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; - } +describe('generateConfirmedFields utility function', () => { + test('at least one field is found for all known confirmation attributes', () => { + 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), `${confirm_attribute} should have related confirm field(s)` ).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_data, confirmationAttributeNames); + 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).length).toBe(0); + }); + + test("generateConfirmedFields does not use outdated confirmation attributes", () => { + 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.has(confirm_attribute); + } + 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'); + }); }); \ No newline at end of file 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", diff --git a/src/components/ProjectTimeline/VisTimelineGroup.jsx b/src/components/ProjectTimeline/VisTimelineGroup.jsx index 8194382e7..ec2401575 100644 --- a/src/components/ProjectTimeline/VisTimelineGroup.jsx +++ b/src/components/ProjectTimeline/VisTimelineGroup.jsx @@ -300,7 +300,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`; @@ -354,12 +354,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") { diff --git a/src/sagas/projectSaga.js b/src/sagas/projectSaga.js index 6fdb8c55b..05a5df88f 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' @@ -601,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")) { @@ -673,7 +676,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') } }) } } } @@ -809,18 +812,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', - '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 { @@ -907,19 +902,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', - '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; @@ -1178,7 +1165,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') } }) } } } 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 diff --git a/src/utils/generateConfirmedFields.js b/src/utils/generateConfirmedFields.js index da3aae6d5..9d87375b2 100644 --- a/src/utils/generateConfirmedFields.js +++ b/src/utils/generateConfirmedFields.js @@ -1,53 +1,305 @@ -export function generateConfirmedFields(attributeData, confirmationAttributeNames, phaseNames) { - const confirmedFields = []; - const seenPhases = new Set(); +const PHASE_ALIASES = { + periaatteet: ['periaatteet'], + oas: ['oas'], + luonnos: ['luonnos', 'kaavaluonnos'], + ehdotus: ['ehdotus', 'kaavaehdotus'], + tarkistettu_ehdotus: ['tarkistettu_ehdotus'] +}; - confirmationAttributeNames.forEach((confirmationKey) => { - if (!attributeData[confirmationKey]) return; +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', + 'kaavaehdotus_nahtaville_1', + 'kaavaehdotus_uudelleen_nahtaville_2', + 'kaavaehdotus_uudelleen_nahtaville_3', + 'kaavaehdotus_uudelleen_nahtaville_4', + // Periaatteet phase + 'viimeistaan_mielipiteet_periaatteista', + 'viimeistaan_mielipiteet_periaatteista_2', + 'viimeistaan_mielipiteet_periaatteista_3' +]; - const rawKey = confirmationKey.replace(/^vahvista_/, ''); - const phase = phaseNames.find((p) => rawKey === p || rawKey.startsWith(p + '_')); - if (!phase) return; +function isConfirmationKey(key) { + return key.startsWith('vahvista_'); +} - const suffixMatch = rawKey.match(/(_\d+)$/); - const suffix = suffixMatch ? suffixMatch[1] : ''; - const finalSuffix = suffix === '_1' ? '' : suffix; +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'; +} - const keyWithoutSuffix = suffix ? rawKey.slice(0, -suffix.length) : rawKey; - const base = keyWithoutSuffix.replace(`${phase}_`, ''); +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; - const parts = base.split('_'); - let group = parts[0]; - let type = parts[1]; + return key.startsWith('jarjestetaan_') || + key.includes('_nahtaville_') || + key.includes('_lautakuntaan_') || + key.endsWith('_luotu'); +} - if (parts.length === 1) { - // Special case like vahvista_periaatteet_lautakunnassa - const field1 = `milloin_${phase}_${base}${finalSuffix}`; - const field2 = `${phase}_lautakunta_aineiston_maaraaika${finalSuffix}`; +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) +} - confirmedFields.push(field1); - confirmedFields.push(field2); +function isSpecialCaseForPhase(key, phase) { + if (phase === 'ehdotus') { + 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'); + } + return false; +} - return; - } +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 }; +} + +// 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}_`); + } + + 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 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') +function extractKeyIndex(key) { + const keyIndexMatch = /_(\d+)$/.exec(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 have special rules + if (key.includes('aineiston_maaraaika') || key.includes('kylk_maaraaika')) { + return matchesDeadlineFieldType(key, type); + } + + // Mielipiteet ja lausunnot always belong to esillaolo + if (key.includes('mielipiteet') || key.includes('lausunnot')) { + return type === 'esillaolo'; + } + + // Lautakunta fields + if (type === 'lautakunta') { + return !(key.includes('esillaolo') || key.includes('nahtavilla')) && + (key.includes('lautakunta') || key.includes('lautakunnassa')); + } + + // Esillaolo fields + if (type === 'esillaolo') { + return !(key.includes('lautakunta') || key.includes('lautakunnassa')); + } + + return true; +} - // 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}`; +// 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; - [aineisto, alkaa, paattyy].forEach((key) => { - if (key in attributeData) { - confirmedFields.push(key); + 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 (shouldAddField(key, alias, index, type, attributeData)) { + confirmedFields.add(key); } - }); + } + } +} - if (!seenPhases.has(phase) && mielipiteet in attributeData) { - confirmedFields.push(mielipiteet); - seenPhases.add(phase); +// 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: special cases belong to esillaolo only + if ((phase === 'ehdotus' || phase === 'periaatteet') && type !== 'esillaolo') { + return false; + } + + // Extract and match index + const keyWithoutSize = key.replace(/_(iso|pieni)(_\d+)?$/, (match, p1, p2) => p2 || ''); + const keyIndexMatch = /_(\d+)$/.exec(keyWithoutSize); + 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 (shouldAddSpecialCase(key, phase, type, index, attributeData)) { + 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; + } + // 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); + }); + + // 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 [...new Set(confirmedFields)]; + return filteredFields.sort((a, b) => a.localeCompare(b)); } \ No newline at end of file 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)