From f26b072954acf88db3ecb8603cfc2cba1fba82e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=E1=BB=91ng=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= <84076965+tongthanhdat009@users.noreply.github.com> Date: Wed, 22 Apr 2026 03:48:02 +0700 Subject: [PATCH] feat(cms): implement structured tuition data normalization and editor Refactor tuition data from a simple numeric array to a structured object containing time labels and values. This allows for more flexible charting and precise data entry in the CMS. - Add `normalizeTuition`, `normalizeTuitionSeries`, and `normalizeTuitionPoint` to `admissionsController.js` - Update `admissionsConfig.js` to use `objectList` for tuition data points instead of `stringList` - Migrate existing tuition data in `admissions.json` to the new structured format - Fix duplicate text in `admissions.json` grant amounts and descriptions --- controllers/admissionsController.js | 46 +++++++++++++++++++- data/admissions.json | 55 ++++++++++++++++++------ utils/contentEditors/admissionsConfig.js | 30 ++++++++++--- 3 files changed, 112 insertions(+), 19 deletions(-) diff --git a/controllers/admissionsController.js b/controllers/admissionsController.js index 1bc6c42..d88c1d0 100644 --- a/controllers/admissionsController.js +++ b/controllers/admissionsController.js @@ -66,6 +66,50 @@ function normalizeCalculator(calculator) { return nextCalculator; } +function normalizeTuitionPoint(point, index) { + if (point && typeof point === "object" && !Array.isArray(point)) { + const numericValue = Number(point.value); + return { + time: String(point.time || point.label || `Year ${index + 1}`), + value: Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : 0, + }; + } + + const numericValue = Number(point); + return { + time: `Year ${index + 1}`, + value: Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : 0, + }; +} + +function normalizeTuitionSeries(series, index) { + const source = typeof series === "string" ? { label: series } : { ...(series || {}) }; + const rawPoints = Array.isArray(source.points) && source.points.length + ? source.points + : Array.isArray(source.values) + ? source.values + : []; + + return { + label: String(source.label || `Series ${index + 1}`).slice(0, 40), + color: String(source.color || "#0F172A"), + points: rawPoints.map(normalizeTuitionPoint), + }; +} + +function normalizeTuition(tuition) { + const nextTuition = { ...(tuition || {}) }; + + nextTuition.title = String(nextTuition.title || ""); + nextTuition.chartTitle = String(nextTuition.chartTitle || ""); + nextTuition.chartDescription = String(nextTuition.chartDescription || ""); + nextTuition.series = Array.isArray(nextTuition.series) + ? nextTuition.series.map(normalizeTuitionSeries) + : []; + + return nextTuition; +} + function normalizeAdmissionsPayload(rawPayload) { const payload = JSON.parse(JSON.stringify(rawPayload || {})); @@ -78,7 +122,7 @@ function normalizeAdmissionsPayload(rawPayload) { id: "eligibility", }; payload.tuition = { - ...(payload.tuition || {}), + ...normalizeTuition(payload.tuition), id: "tuition-breakdown", }; payload.keyDates = { diff --git a/data/admissions.json b/data/admissions.json index 30dd3d5..5d6155f 100644 --- a/data/admissions.json +++ b/data/admissions.json @@ -72,22 +72,53 @@ { "label": "Traditional University", "color": "#850f0f", - "values": [ - 25000, - 49998, - 30000, - 100000, - 120000 + "points": [ + { + "time": "Year 1", + "value": 25000 + }, + { + "time": "Year 2", + "value": 49998 + }, + { + "time": "Year 3", + "value": 30000 + }, + { + "time": "Year 4", + "value": 100000 + }, + { + "time": "Year 5", + "value": 120000 + }, + { + "time": "Year 6", + "value": 500000 + } ] }, { "label": "LAMS", "color": "#a700b3", - "values": [ - 3588, - 7176, - 10764, - 10000 + "points": [ + { + "time": "Year 1", + "value": 3588 + }, + { + "time": "Year 2", + "value": 7176 + }, + { + "time": "Year 3", + "value": 10764 + }, + { + "time": "Year 4", + "value": 10000 + } ] } ] @@ -195,7 +226,7 @@ "items": [ { "title": "Working Adult GrantWorking Adult GrantWorking Adul", - "amount": "Up to $1,500Up to $1,500", + "amount": "Up to $1,500", "description": "For students employed full-time while studying.For students employed full-time while studying.For students employed full-time while studying.For students employ" }, { diff --git a/utils/contentEditors/admissionsConfig.js b/utils/contentEditors/admissionsConfig.js index 4482452..afed38f 100644 --- a/utils/contentEditors/admissionsConfig.js +++ b/utils/contentEditors/admissionsConfig.js @@ -114,12 +114,30 @@ module.exports = { [ text("label", "Series label", { maxLength: 40 }), { key: "color", label: "Series color", type: "color" }, - stringList("values", "Data points", { - itemLabel: "Point", - fieldType: "number", - min: 0, - sortable: true, - }), + objectList( + "points", + "Data points", + [ + text("time", "Time label", { + maxLength: 30, + placeholder: "Year 1", + }), + { + key: "value", + label: "Value", + type: "number", + min: 0, + }, + ], + { + itemLabel: "Point", + sortable: true, + itemTitleKey: "time", + itemSubtitleKey: "value", + helpText: "Each chart point requires both a time label and a numeric value.", + emptyText: "No data points yet.", + }, + ), ], { itemLabel: "Series",