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
This commit is contained in:
Tống Thành Đạt
2026-04-22 03:48:02 +07:00
parent 5d0fae6d51
commit f26b072954
3 changed files with 112 additions and 19 deletions
+45 -1
View File
@@ -66,6 +66,50 @@ function normalizeCalculator(calculator) {
return nextCalculator; 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) { function normalizeAdmissionsPayload(rawPayload) {
const payload = JSON.parse(JSON.stringify(rawPayload || {})); const payload = JSON.parse(JSON.stringify(rawPayload || {}));
@@ -78,7 +122,7 @@ function normalizeAdmissionsPayload(rawPayload) {
id: "eligibility", id: "eligibility",
}; };
payload.tuition = { payload.tuition = {
...(payload.tuition || {}), ...normalizeTuition(payload.tuition),
id: "tuition-breakdown", id: "tuition-breakdown",
}; };
payload.keyDates = { payload.keyDates = {
+43 -12
View File
@@ -72,22 +72,53 @@
{ {
"label": "Traditional University", "label": "Traditional University",
"color": "#850f0f", "color": "#850f0f",
"values": [ "points": [
25000, {
49998, "time": "Year 1",
30000, "value": 25000
100000, },
120000 {
"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", "label": "LAMS",
"color": "#a700b3", "color": "#a700b3",
"values": [ "points": [
3588, {
7176, "time": "Year 1",
10764, "value": 3588
10000 },
{
"time": "Year 2",
"value": 7176
},
{
"time": "Year 3",
"value": 10764
},
{
"time": "Year 4",
"value": 10000
}
] ]
} }
] ]
@@ -195,7 +226,7 @@
"items": [ "items": [
{ {
"title": "Working Adult GrantWorking Adult GrantWorking Adul", "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" "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"
}, },
{ {
+24 -6
View File
@@ -114,12 +114,30 @@ module.exports = {
[ [
text("label", "Series label", { maxLength: 40 }), text("label", "Series label", { maxLength: 40 }),
{ key: "color", label: "Series color", type: "color" }, { key: "color", label: "Series color", type: "color" },
stringList("values", "Data points", { objectList(
itemLabel: "Point", "points",
fieldType: "number", "Data points",
min: 0, [
sortable: true, 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", itemLabel: "Series",