diff --git a/components/board/BoardTab.tsx b/components/board/BoardTab.tsx
index c8b8fa9..c1fa67c 100644
--- a/components/board/BoardTab.tsx
+++ b/components/board/BoardTab.tsx
@@ -14,7 +14,7 @@ const memberFields = (teams: readonly string[]): FieldDef[] => [
{ key: "role", label: "Role", placeholder: "Data Engineer" },
{ key: "team", label: "Team", type: "select", options: ["Executive", ...teams].map((t) => ({ value: t, label: t })), pair: "t" },
{ key: "alsoTeam", label: "Also leads (optional)", type: "select", options: [{ value: "", label: "None" }, ...teams.map((t) => ({ value: t, label: t }))], pair: "t" },
- { key: "tier", label: "Top of chart", type: "select", options: [{ value: "", label: "No" }, { value: "0", label: "President row" }, { value: "1", label: "Executive row" }] },
+ { key: "tier", label: "Top of chart", type: "select", options: [{ value: "", label: "No" }, { value: "0", label: "Top row" }, { value: "1", label: "Second row" }] },
{ key: "isLead", label: "Team lead", type: "checkbox" },
{ key: "email", label: "Email", type: "email", pair: "c" },
{ key: "phone", label: "Phone", type: "tel", pair: "c" },
@@ -27,6 +27,7 @@ export function BoardTab() {
const { chapter, round, data, user, go, setFocus, refresh } = useBoard();
const TEAM_ORDER = data.settings.teams;
const [editingTeams, setEditingTeams] = useState(false);
+ const [replacing, setReplacing] = useState(false);
const ALL_TEAMS = ["All", "Executive", ...TEAM_ORDER];
const MEMBER_FIELDS = memberFields(TEAM_ORDER);
const admin = user.role === "admin";
@@ -127,8 +128,23 @@ export function BoardTab() {
Edit teams
)}
+ {admin && (
+
+ )}
{editingTeams && setEditingTeams(false)} />}
+ {replacing && (
+ setReplacing(false) }}
+ onLoaded={(n) => {
+ setReplacing(false);
+ setLoaded(n);
+ refresh();
+ }}
+ />
+ )}
{editing === "new" && (
setEditing(null)} />
diff --git a/components/board/ImportMembers.tsx b/components/board/ImportMembers.tsx
index 8bab1a7..81fec7b 100644
--- a/components/board/ImportMembers.tsx
+++ b/components/board/ImportMembers.tsx
@@ -19,13 +19,13 @@ function readText(file: File): Promise {
}
// Posts the list. Returns an error message, or null when it saved.
-async function send(members: unknown[]): Promise {
+async function send(members: unknown[], replace: boolean): Promise {
let res: Response;
try {
res = await fetch(chapterUrl("/api/board/import-members"), {
method: "POST",
headers: { "content-type": "application/json" },
- body: JSON.stringify({ members }),
+ body: JSON.stringify({ members, replace }),
});
} catch {
return "Couldn't reach the server. Check your connection and try again.";
@@ -41,14 +41,26 @@ async function send(members: unknown[]): Promise {
// Admin-only: loads the member list from board-members.local.json. It isn't
// in the code because it has phone numbers. `onLoaded` runs after a
// successful import, so the Board tab can say so once this box is gone.
+// With `replacing`, it swaps out the members already loaded, after a confirm.
export function ImportMembers({
onLoaded,
+ replacing,
}: {
onLoaded: (count: number) => void;
+ replacing?: { count: number; onCancel: () => void };
}) {
const { chapter } = useBoard();
const fileName = chapter.id === "berkeley" ? "board-members.local.json" : `board-members-${chapter.id}.local.json`;
const [status, setStatus] = useState(null);
+ const [pending, setPending] = useState(null);
+
+ async function load(members: unknown[]) {
+ setStatus({ kind: "busy", text: `Loading ${members.length} members…` });
+ const err = await send(members, !!replacing);
+ if (err) return setStatus({ kind: "err", text: err });
+ setStatus({ kind: "ok", text: `Loaded ${members.length} members.` });
+ onLoaded(members.length);
+ }
async function onFile(e: ChangeEvent) {
const input = e.currentTarget;
@@ -75,12 +87,11 @@ export function ImportMembers({
text: `${file.name} doesn't have a member list in it. Choose ${fileName}.`,
});
}
- setStatus({ kind: "busy", text: `Loading ${members.length} members…` });
- const err = await send(members);
- if (err) return setStatus({ kind: "err", text: err });
- const count = members.length;
- setStatus({ kind: "ok", text: `Loaded ${count} members.` });
- onLoaded(count);
+ if (replacing) {
+ setStatus(null);
+ return setPending(members);
+ }
+ await load(members);
} catch {
setStatus({
kind: "err",
@@ -94,11 +105,41 @@ export function ImportMembers({
const busy = status?.kind === "busy";
return (
-
- The member list isn't loaded yet. Choose the member
- list file, {fileName}. It has everyone's names,
- roles, emails and phone numbers.
-
+ {replacing ? (
+
+ Replace the member list. Choose{" "}
+ {fileName}. It replaces all {replacing.count} members,
+ including edits made on this tab. The old list stays in the Changes
+ tab, where it can be restored.
+
+ ) : (
+
+ The member list isn't loaded yet. Choose the member
+ list file, {fileName}. It has everyone's names,
+ roles, emails and phone numbers.
+
+ )}
+ {pending ? (
+
+
+ Replace {replacing?.count} members with the {pending.length} in this file?
+
+
+
+
+ ) : (
+ )}
+ {replacing && !pending && (
+
+
+
+ )}
{status && (
= {
],
cc: [{ email: "ranadarwich05@gmail.com", name: "Rana Darwich", short: "Rana" }],
techTeams: ["App Dev", "Data"],
- techTo: [{ email: "rizwaanbana@g.ucla.edu", name: "Rizwaan Bana", short: "Rizwaan" }],
+ techTo: [{ email: "rizwaanbana@ucla.edu", name: "Rizwaan Bana", short: "Rizwaan" }],
},
},
};
diff --git a/lib/board/setup.ts b/lib/board/setup.ts
index ad92784..3e8ebc0 100644
--- a/lib/board/setup.ts
+++ b/lib/board/setup.ts
@@ -68,6 +68,23 @@ async function setup() {
await seedUcla(sql, empty);
await seedSettings(sql);
await markHubAdmins(sql);
+ await renameEvanceline(sql);
+}
+
+// UCLA's Design Lead is Evanceline Tang; the starting data called her
+// Celine. Renames her in UCLA's assignments, submissions and meeting notes,
+// once, so her tasks match her member entry.
+async function renameEvanceline(sql: ReturnType) {
+ await sql.begin(async (tx) => {
+ const [first] = await tx`INSERT INTO board_changes (name) VALUES ('2026-10-15-ucla-evanceline')
+ ON CONFLICT DO NOTHING RETURNING name`;
+ if (!first) return;
+ await tx`UPDATE board_assignments SET person = 'Evanceline' WHERE chapter = 'ucla' AND person = 'Celine'`;
+ await tx`UPDATE board_submissions SET person = 'Evanceline' WHERE chapter = 'ucla' AND person = 'Celine'`;
+ const word = String.raw`\mCeline\M`;
+ await tx`UPDATE board_meetings SET absent = regexp_replace(absent, ${word}, 'Evanceline', 'g')
+ WHERE chapter = 'ucla' AND absent ~ ${word}`;
+ });
}
// Ticks "Hub admin" for each chapter's starting admins, once.
diff --git a/lib/board/starter-ucla.json b/lib/board/starter-ucla.json
index 3907379..9ed3aa9 100644
--- a/lib/board/starter-ucla.json
+++ b/lib/board/starter-ucla.json
@@ -70,7 +70,7 @@
]
],
[
- "Celine",
+ "Evanceline",
"Design Lead",
[
"Start week one of your plan",
@@ -127,7 +127,7 @@
"Start week one, pick your team"
],
[
- "Celine",
+ "Evanceline",
"Design Lead",
"Start week one, pick your team"
],
@@ -253,11 +253,11 @@
"meetings": [
{
"date": "2026-10-11",
- "absent": "Celine, Rizwaan, Fernando, Ayat"
+ "absent": "Evanceline, Rizwaan, Fernando, Ayat"
},
{
"date": "2026-10-04",
- "absent": "Celine and Ray (both excused)"
+ "absent": "Evanceline and Ray (both excused)"
},
{
"date": "2026-09-27",