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",