From 8a1bacafb71d59e0606535ab4425ae6d21e369c8 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:33:23 -0700 Subject: [PATCH 1/6] add initial draft --- public/officer/new/index.html | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 public/officer/new/index.html diff --git a/public/officer/new/index.html b/public/officer/new/index.html new file mode 100644 index 0000000..0ae2158 --- /dev/null +++ b/public/officer/new/index.html @@ -0,0 +1,57 @@ + + + + + + Input Officer Info + + + + + + + + + + + + + + + + + +
+ TODO: this +
+ + + From 163ef72d33fe3803fd9c6ee90af6ae4b66975544 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Thu, 22 Aug 2024 22:14:26 -0700 Subject: [PATCH 2/6] switch to being a profile page instead --- public/officer/new/index.html | 57 ----------- public/profile/index.html | 187 ++++++++++++++++++++++++++++++++++ public/profile/style.css | 103 +++++++++++++++++++ 3 files changed, 290 insertions(+), 57 deletions(-) delete mode 100644 public/officer/new/index.html create mode 100644 public/profile/index.html create mode 100644 public/profile/style.css diff --git a/public/officer/new/index.html b/public/officer/new/index.html deleted file mode 100644 index 0ae2158..0000000 --- a/public/officer/new/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - Input Officer Info - - - - - - - - - - - - - - - - - -
- TODO: this -
- - - diff --git a/public/profile/index.html b/public/profile/index.html new file mode 100644 index 0000000..c732f4e --- /dev/null +++ b/public/profile/index.html @@ -0,0 +1,187 @@ + + + + + + CSSS User Profile + + + + + + + + + + + + + + + + + +
+
+
+

Officer Info

+ +
+

Welcome new CSSS Officer!

+

+ Please input your info here as soon as possible, so that we can give you access + to our services, such as the CSSS Github, + or the CSSS Google Drive. +

+
+ +
+ +
+
+
Officer Info
+
Not yet complete
+
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ + + +
+ + + +
+ + + +
+ + +
+ + +
+
+
Term Info
+
Not yet complete
+
+ + +
+
+
+ +
+
+

Metadata

+
+
+
+
User Info
+
+
+ + + + + + + + + + + + + + +
SFU computing id
Most recent login
First login
+
+
+
+
+ + diff --git a/public/profile/style.css b/public/profile/style.css new file mode 100644 index 0000000..2ee8cbf --- /dev/null +++ b/public/profile/style.css @@ -0,0 +1,103 @@ +body { + margin: 0; + + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + + background-color: #24242e; +} + +a { + color: #2fbdda; + font-weight: bold; +} +a:hover { + color: #28c074; + font-weight: bold; +} + +h3 { + margin-top: 0; + margin-bottom: 0; +} + +input[type=text], +input[type=date], +select { + padding: 0.25rem; + + border: #eee solid 3px; + border-radius: 0.25rem; + outline: none; + + font-family: monospace; + + background-color: #fff; +} + +input[type=text]:focus, +input[type=date]:focus, +select:focus { + border: #aaa solid 3px; +} + + +table { + border-collapse: collapse; +} +td { + padding: 0.5rem; +} + +#header { + display: flex; + flex-direction: column; + justify-content: center; + + margin: auto; + padding: 3rem 3rem 1rem 3rem; + max-width: calc(980px - 6rem); + + background-color: #fff; +} + +#content { + display: flex; + flex-direction: column; + + margin: auto; + padding: 1rem 3rem; + max-width: calc(980px - 6rem); + min-height: 600px; + + background-color: #fff; +} + +.widget { + display: flex; + flex-direction: column; + align-items: start; + + margin: auto; + margin-bottom: 1rem; + max-width: 600px; + padding: 1rem; + border-radius: 1rem; + + background-color: #eee; +} + +.clear-widget { + display: flex; + flex-direction: column; + align-items: start; + + margin: auto; + margin-bottom: 1rem; + max-width: 600px; + padding: 1rem; + border-radius: 1rem; +} + +.title { + font-family: Poppins, sans-serif; +} \ No newline at end of file From d9de9174682388a2aecde98fb044b8fe9138e92f Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sat, 24 Aug 2024 00:34:14 -0700 Subject: [PATCH 3/6] call endpoint get officer info if the officer info exists & style input --- public/profile/index.html | 147 ++++++++++++++++++++------- public/profile/script.js | 12 +++ public/profile/style.css | 61 ++++++++++- public/static/icons/chevron-down.svg | 1 + public/static/icons/minus.svg | 1 + 5 files changed, 182 insertions(+), 40 deletions(-) create mode 100644 public/profile/script.js create mode 100644 public/static/icons/chevron-down.svg create mode 100644 public/static/icons/minus.svg diff --git a/public/profile/index.html b/public/profile/index.html index c732f4e..40e9fc2 100644 --- a/public/profile/index.html +++ b/public/profile/index.html @@ -54,6 +54,39 @@ }); } }); + + fetch("/api/officers/my_info").then(response => { + if (response.status == 401) { + // redirect the user to the sfu login api if they are not yet logged in + const CAS_LOGIN_URL = "https://cas.sfu.ca/cas/login?service="; + const API_LOGIN_URL = window.location.protocol + "//" + window.location.host + "/api/auth/login"; + let loginURL = CAS_LOGIN_URL + API_LOGIN_URL + "?next_url=" + encodeURIComponent(window.location.href); + hasPermission = false; + window.location.replace(loginURL); + } else if (response.status == 404) { + // do nothing, but happily + console.log("user is not an officer"); + } else if (!response.ok) { + // assume that failed + console.log("unexpected:"); + console.log(response); + } else { + // check if user has same computing_id as query param + document.getElementById("officer-info-container").removeAttribute("hidden"); + + response.json().then(json => { + console.log(json); + document.getElementById("legal-name").value = json.legal_name; + document.getElementById("db-legal-name").innerHTML = json.legal_name; + + document.getElementById("phone-number").value = json.phone_number; + document.getElementById("db-phone-number").innerHTML = json.phone_number; + + // TODO: update the data fields with this info + + }); + } + }); } function logOut() { @@ -79,7 +112,7 @@
-
+
+ -
+
-
Officer Info
+
+
Officer Info
+ +
Not yet complete
+
+
+
+ Personal +
+
+
-
- - -
- -
+
+
+ + +
+ +
-
- - -
+
+
+ + +
+
+
-
- - -
+
+
+ Accounts +
+
+
-
- - -
+
+ + +
-
+
+ + +
- - -
+
+ + +
- - -
+
- - -
+
+ + +
- - -
+
+ + +
+ +
- - + +
+
-
+
Term Info
Not yet complete
- +
@@ -184,4 +253,6 @@

Metadata

+ + diff --git a/public/profile/script.js b/public/profile/script.js new file mode 100644 index 0000000..01d6f21 --- /dev/null +++ b/public/profile/script.js @@ -0,0 +1,12 @@ +function toggleHidden(elementID, collapsibleID) { + let element = document.getElementById(elementID); + let collapsible = document.getElementById(collapsibleID); + + if (element.getAttribute("hidden") !== null) { + element.removeAttribute("hidden"); + collapsible.src = "/static/icons/minus.svg"; + } else { + element.setAttribute("hidden", ""); + collapsible.src = "/static/icons/chevron-down.svg"; + } +} \ No newline at end of file diff --git a/public/profile/style.css b/public/profile/style.css index 2ee8cbf..656daef 100644 --- a/public/profile/style.css +++ b/public/profile/style.css @@ -20,12 +20,17 @@ h3 { margin-bottom: 0; } +label { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + input[type=text], input[type=date], select { - padding: 0.25rem; + padding: 0.5rem; - border: #eee solid 3px; + border: #fff solid 3px; border-radius: 0.25rem; outline: none; @@ -72,6 +77,10 @@ td { background-color: #fff; } +#officer-info-form-contents { + width: 100%; +} + .widget { display: flex; flex-direction: column; @@ -100,4 +109,52 @@ td { .title { font-family: Poppins, sans-serif; +} + +.officer-info-input-container { + display: flex; + flex-direction: row; +} +.officer-info-input { + display: flex; + flex-direction: column; + + width: 50%; +} +.officer-info-input .smol-text { + margin-left: auto; + font-weight: 400; +} + +.db-result { + text-align: left; + display: flex; + align-items: center; + + color: #777; + border-bottom: 1px solid #bbb; + + width: calc(50% - 2 * 1rem); + margin: 2rem 1rem 0.5rem 1rem; +} + +.unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.collapsible { + display: inline-block; +} + +.grey { + color: #aaa; +} + +.smol-text { + font-size: 0.9rem; } \ No newline at end of file diff --git a/public/static/icons/chevron-down.svg b/public/static/icons/chevron-down.svg new file mode 100644 index 0000000..e99c51b --- /dev/null +++ b/public/static/icons/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/static/icons/minus.svg b/public/static/icons/minus.svg new file mode 100644 index 0000000..eb8b3f4 --- /dev/null +++ b/public/static/icons/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file From 1b6fd8f2bedefa80cfda5e905285d705ca767d76 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sun, 25 Aug 2024 00:46:52 -0700 Subject: [PATCH 4/6] add support for more fields & using the new /update_info endpoint --- public/profile/index.html | 164 +++++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 46 deletions(-) diff --git a/public/profile/index.html b/public/profile/index.html index 40e9fc2..4844c3c 100644 --- a/public/profile/index.html +++ b/public/profile/index.html @@ -20,6 +20,7 @@ @@ -117,9 +174,9 @@

Officer Info

-

Welcome new CSSS Officer!

+

Welcome CSSS Officer!

- Please input your info here as soon as possible, so that we can give you access + Please ensure your info is up to date as soon as possible, so that we can give you access to our services, such as the CSSS Github, or the CSSS Google Drive.

@@ -155,7 +212,7 @@

Welcome new CSSS Officer!

- +
@@ -167,48 +224,63 @@

Welcome new CSSS Officer!


-
- - +
+
+ + +
+
-
- - +
+
+ + +
+
-
- - +
+
+ + +
+

-
- - +
+
+ + +
+
-
- - +
+
+ + +
+

- +
From 07821aa9e1f1176c75bc070eac964d3e282f8af6 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Sun, 1 Sep 2024 17:19:16 -0700 Subject: [PATCH 5/6] add support for officer info section --- public/profile/index.html | 51 ++++++++++++++++++++++++++++++++------- public/profile/style.css | 2 +- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/public/profile/index.html b/public/profile/index.html index 4844c3c..6748306 100644 --- a/public/profile/index.html +++ b/public/profile/index.html @@ -87,6 +87,9 @@ document.getElementById("discord-name").value = json.discord_name; document.getElementById("db-discord-name").innerHTML = json.discord_name; + document.getElementById("discord-nickname").value = json.discord_nickname; + document.getElementById("db-discord-nickname").innerHTML = json.discord_nickname; + document.getElementById("github-username").value = json.github_username; document.getElementById("db-github-username").innerHTML = json.github_username; @@ -118,7 +121,7 @@ return; } - fetch(`/api/officers/{computingID}/update_info`, { + fetch(`/api/officers/${computingID}/update_info`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -126,7 +129,7 @@ "legal_name": document.getElementById("legal-name").value, "phone_number": document.getElementById("phone-number").value, "discord_name": document.getElementById("discord-name").value, - "github_username": document.getElementById("discord-username").value, + "github_username": document.getElementById("github-username").value, "google_drive_email": document.getElementById("google-email").value, }) }).then(response => { @@ -141,12 +144,37 @@ document.getElementById("officer-info-form-status").innerHTML = json.is_filled_in ? "Complete" : "Not yet complete"; // update the data fields with this info - document.getElementById("db-legal-name").innerHTML = json.legal_name; - document.getElementById("db-phone-number").innerHTML = json.phone_number; - document.getElementById("db-discord-id").innerHTML = json.discord_id; - document.getElementById("db-discord-name").innerHTML = json.discord_name; - document.getElementById("db-github-username").innerHTML = json.github_username; - document.getElementById("db-google-email").innerHTML = json.google_drive_email; + document.getElementById("db-legal-name").innerHTML = json.updated_officer_info.legal_name; + document.getElementById("db-phone-number").innerHTML = json.updated_officer_info.phone_number; + + document.getElementById("db-discord-name").innerHTML = json.updated_officer_info.discord_name; + document.getElementById("db-discord-nickname").innerHTML = json.updated_officer_info.discord_nickname; + document.getElementById("db-discord-id").innerHTML = json.updated_officer_info.discord_id; + + document.getElementById("discord-name").value = json.updated_officer_info.discord_name; + document.getElementById("discord-nickname").value = json.updated_officer_info.discord_nickname; + document.getElementById("discord-id").value = json.updated_officer_info.discord_id; + + document.getElementById("db-github-username").innerHTML = json.updated_officer_info.github_username; + document.getElementById("db-google-email").innerHTML = json.updated_officer_info.google_drive_email; + + let validationErrorsElement = document.getElementById("officer-info-validation-failures"); + if (json.validation_failures.length == 0) { + validationErrorsElement.innerHTML = ""; + } else { + validationErrorsElement.innerHTML = "Validation Errors:"; + for (let i = 0; i < json.validation_failures.length; i++) { + const desc = json.validation_failures[i]; + const colour = (i % 2 == 0) ? "fff" : "ddd"; + validationErrorsElement.innerHTML += ` +
+ ${i} + ${desc} +
+ `; + } + } + }); } }); @@ -228,7 +256,10 @@

Welcome CSSS Officer!

@@ -272,6 +303,7 @@

Welcome CSSS Officer!

+
@@ -281,6 +313,7 @@

Welcome CSSS Officer!


+

diff --git a/public/profile/style.css b/public/profile/style.css index 656daef..839728b 100644 --- a/public/profile/style.css +++ b/public/profile/style.css @@ -156,5 +156,5 @@ td { } .smol-text { - font-size: 0.9rem; + font-size: 0.6rem; } \ No newline at end of file From 543969100bcaa080bdd6bf220d4ddb446ef2dc00 Mon Sep 17 00:00:00 2001 From: Gabe WSL Debian <24978329+EarthenSky@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:44:50 -0700 Subject: [PATCH 6/6] add officer term functionality --- public/profile/index.html | 306 +++++++++++++++++++++++++++++++++----- public/profile/style.css | 16 +- 2 files changed, 278 insertions(+), 44 deletions(-) diff --git a/public/profile/index.html b/public/profile/index.html index 6748306..8de06f5 100644 --- a/public/profile/index.html +++ b/public/profile/index.html @@ -51,11 +51,15 @@ document.getElementById("user-info-computing-id").innerHTML = computingID; document.getElementById("user-info-last-log-in").innerHTML = dateTimeFormat.format(new Date(json["last_logged_in"])) document.getElementById("user-info-first-log-in").innerHTML = dateTimeFormat.format(new Date(json["first_logged_in"])) + + initOfficerInfo(computingID); }); } }); + } - fetch("/api/officers/my_info").then(response => { + function initOfficerInfo(computingID) { + fetch(`/api/officers/info/${computingID}`).then(response => { if (response.status == 401) { loginRedirect(); } else if (response.status == 404) { @@ -72,7 +76,7 @@ response.json().then(json => { console.log(json); - document.getElementById("officer-info-form-status").innerHTML = json.is_filled_in ? "Complete" : "Not yet complete"; + document.getElementById("officer-info-form-status").innerHTML = json.is_filled_in ? "Complete" : "Not yet complete"; // update the data fields with this info document.getElementById("legal-name").value = json.legal_name; @@ -96,6 +100,183 @@ document.getElementById("google-email").value = json.google_drive_email; document.getElementById("db-google-email").innerHTML = json.google_drive_email; }); + + // once officer info has been found, also fetch officer terms + initOfficerTerms(computingID); + } + }); + } + + function initOfficerTerms(computingID) { + if (document.getElementById("officer-info-container").getAttribute("hidden")) + return; + + fetch(`/api/officers/terms/${computingID}`).then(response => { + if (response.status == 401) { + loginRedirect(); + } else if (!response.ok) { + // assume that failed + console.log("unexpected:"); + console.log(response); + } else { + // response.json is a list of serializble dict + response.json().then(json => { + console.log(json); + + for(let termData of json) { + let i = termData.id; + let element = document.createElement("div"); + element.classList.add("widget"); + element.id = `officer-term-form-${i}`; + element.innerHTML = ` +
+
+
Officer Term ${i}
+ +
+
Not yet complete
+
+
+
+
+ Admin +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ Personal +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+
+ +
+ + +

+
+ `; + + document.getElementById("officer-widgets").appendChild(element); + + document.getElementById(`officer-term-form-${i}-status`).innerHTML = termData.is_filled_in ? "Complete" : "Not yet complete"; + + // update the data fields with this info + document.getElementById(`position-${i}`).value = termData.position; + document.getElementById(`db-position-${i}`).innerHTML = termData.position; + + document.getElementById(`start_date-${i}`).value = termData.start_date; + document.getElementById(`db-start_date-${i}`).innerHTML = termData.start_date; + + document.getElementById(`nickname-${i}`).value = termData.nickname; + document.getElementById(`db-nickname-${i}`).innerHTML = termData.nickname; + + document.getElementById(`favourite_course_0-${i}`).value = termData.favourite_course_0; + document.getElementById(`db-favourite_course_0-${i}`).innerHTML = termData.favourite_course_0; + + document.getElementById(`favourite_course_1-${i}`).value = termData.favourite_course_1; + document.getElementById(`db-favourite_course_1-${i}`).innerHTML = termData.favourite_course_1; + + document.getElementById(`favourite_pl_0-${i}`).value = termData.favourite_pl_0; + document.getElementById(`db-favourite_pl_0-${i}`).innerHTML = termData.favourite_pl_0; + + document.getElementById(`favourite_pl_1-${i}`).value = termData.favourite_pl_1; + document.getElementById(`db-favourite_pl_1-${i}`).innerHTML = termData.favourite_pl_1; + + document.getElementById(`biography-${i}`).value = termData.biography; + document.getElementById(`db-biography-${i}`).innerHTML = termData.biography; + + document.getElementById(`photo_url-${i}`).value = termData.photo_url; + document.getElementById(`db-photo_url-${i}`).innerHTML = termData.photo_url; + } + }); } }); } @@ -114,15 +295,15 @@ window.location.replace("/"); } - // TODO: take inspiration from google settings update -> have a button to edit, then press a button to leave edit mode. + // TODO: (GUI) take inspiration from google settings update -> have a button to edit, then press a button to leave edit mode. function updateInfo() { if (computingID === null) { console.log("computing_id is null"); return; } - fetch(`/api/officers/${computingID}/update_info`, { - method: "POST", + fetch(`/api/officers/info/${computingID}`, { + method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ // TODO: stop legal_name from being None @@ -141,7 +322,7 @@ response.json().then(json => { console.log(json); - document.getElementById("officer-info-form-status").innerHTML = json.is_filled_in ? "Complete" : "Not yet complete"; + document.getElementById("officer-info-form-status").innerHTML = json.is_filled_in ? "Complete" : "Not yet complete"; // update the data fields with this info document.getElementById("db-legal-name").innerHTML = json.updated_officer_info.legal_name; @@ -157,7 +338,7 @@ document.getElementById("db-github-username").innerHTML = json.updated_officer_info.github_username; document.getElementById("db-google-email").innerHTML = json.updated_officer_info.google_drive_email; - + let validationErrorsElement = document.getElementById("officer-info-validation-failures"); if (json.validation_failures.length == 0) { validationErrorsElement.innerHTML = ""; @@ -174,7 +355,74 @@ `; } } + }); + } + }); + } + + function updateTerm(termID) { + if (computingID === null) { + console.log("computing_id is null"); + return; + } + + // TODO: update term by element & pass term id to backend + fetch(`/api/officers/term/${termID}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + "position": document.getElementById(`position-${termID}`).value, + "start_date": document.getElementById(`start_date-${termID}`).value, + "end_date": document.getElementById(`end_date-${termID}`).value == "" ? null : document.getElementById(`end_date-${termID}`).value, + "nickname": document.getElementById(`nickname-${termID}`).value, + "favourite_course_0": document.getElementById(`favourite_course_0-${termID}`).value, + "favourite_course_1": document.getElementById(`favourite_course_1-${termID}`).value, + "favourite_pl_0": document.getElementById(`favourite_pl_0-${termID}`).value, + "favourite_pl_1": document.getElementById(`favourite_pl_1-${termID}`).value, + "biography": document.getElementById(`biography-${termID}`).value, + "photo_url": document.getElementById(`photo_url-${termID}`).value, + }) + }).then(response => { + if (!response.ok) { + // assume that failed + console.log("unexpected:"); + console.log(response); + } else { + response.json().then(json => { + console.log(json); + document.getElementById(`officer-term-form-${termID}-status`).innerHTML = json.is_filled_in ? "Complete" : "Not yet complete"; + + // update the data fields with this info + document.getElementById(`db-position-${termID}`).innerHTML = json.updated_officer_term.position; + document.getElementById(`db-start_date-${termID}`).innerHTML = json.updated_officer_term.start_date; + document.getElementById(`db-end_date-${termID}`).innerHTML = json.updated_officer_term.end_date; + + document.getElementById(`db-nickname-${termID}`).innerHTML = json.updated_officer_term.nickname; + document.getElementById(`db-favourite_course_0-${termID}`).innerHTML = json.updated_officer_term.favourite_course_0; + document.getElementById(`db-favourite_course_1-${termID}`).innerHTML = json.updated_officer_term.favourite_course_1; + document.getElementById(`db-favourite_pl_0-${termID}`).innerHTML = json.updated_officer_term.favourite_pl_0; + document.getElementById(`db-favourite_pl_1-${termID}`).innerHTML = json.updated_officer_term.favourite_pl_1; + document.getElementById(`db-biography-${termID}`).innerHTML = json.updated_officer_term.biography; + document.getElementById(`db-photo_url-${termID}`).innerHTML = json.updated_officer_term.photo_url; + + // TODO: into a function + let validationErrorsElement = document.getElementById(`officer-term-${termID}-validation-failures`); + if (json.validation_failures.length == 0) { + validationErrorsElement.innerHTML = ""; + } else { + validationErrorsElement.innerHTML = "Validation Errors:"; + for (let i = 0; i < json.validation_failures.length; i++) { + const desc = json.validation_failures[i]; + const colour = (i % 2 == 0) ? "fff" : "ddd"; + validationErrorsElement.innerHTML += ` +
+ ${i} + ${desc} +
+ `; + } + } }); } }); @@ -210,9 +458,7 @@

Welcome CSSS Officer!

-
- - +
@@ -221,7 +467,7 @@

Welcome CSSS Officer!

Not yet complete
-
+

Personal @@ -229,16 +475,16 @@

Welcome CSSS Officer!


-
-
+
+
-
-
+
+
@@ -252,8 +498,8 @@

Welcome CSSS Officer!


-
-
+
+
-
-
+
+
-
-
+
+