Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e7507a3
initial nextjs setup
KavikaPalletenne Nov 13, 2025
fff376f
rename "Resume Application" -> "Continue Application"
KavikaPalletenne Nov 23, 2025
3d2ee4e
update frontend-nextjs/.gitignore
KavikaPalletenne Nov 23, 2025
b4d51de
Update bun.lock
KavikaPalletenne Nov 23, 2025
d5557fc
setup server-side auth functions
KavikaPalletenne Nov 23, 2025
eb9a248
basic sidebar
KavikaPalletenne Nov 23, 2025
7728269
setup i18n
KavikaPalletenne Nov 23, 2025
55abec0
add common terms to dictionaries
KavikaPalletenne Nov 23, 2025
f1114d7
remove unused backend imports
KavikaPalletenne Nov 23, 2025
00a9cf7
run rust build workflow on pulls to `CHAOS-571-integrate-be-fe`
KavikaPalletenne Nov 23, 2025
ee2a68e
react query auth setup
KavikaPalletenne Nov 29, 2025
0c30315
fetch user organisations for sidebar
KavikaPalletenne Nov 29, 2025
b3dddd2
handle organisation change on dashboard
KavikaPalletenne Nov 29, 2025
87a759b
setup basic dashboard campaigns page
KavikaPalletenne Nov 29, 2025
4fcaef6
add data table to display campaigns
KavikaPalletenne Nov 29, 2025
48c785a
dashboard campaign details page
KavikaPalletenne Nov 29, 2025
f73b94c
application review setup
KavikaPalletenne Nov 29, 2025
40220fa
campaign details rearrange
KavikaPalletenne Nov 29, 2025
5997262
application review scaffold
KavikaPalletenne Nov 29, 2025
d1335ad
move application models
KavikaPalletenne Nov 29, 2025
5623d9b
application review component basics
KavikaPalletenne Nov 29, 2025
b203872
get, set and update application rating
KavikaPalletenne Dec 1, 2025
c774811
fix setting & updating application rating
KavikaPalletenne Dec 2, 2025
472783f
add `chaos.png`
KavikaPalletenne Dec 2, 2025
01e998b
add buttons for publish & edit campaign questions
KavikaPalletenne Dec 2, 2025
693a6f9
Email templates UI (#608)
KavikaPalletenne Dec 3, 2025
8a7ae69
Update .gitignore
KavikaPalletenne Dec 3, 2025
e3226d1
Update .gitignore
KavikaPalletenne Dec 3, 2025
b1f6875
chopped drag and drop
Plebbaroni Dec 8, 2025
f978bf9
drag and drop less chopped
Plebbaroni Dec 8, 2025
f00faa9
less chopped drag and drop
Plebbaroni Dec 8, 2025
3134069
role tabs
Plebbaroni Dec 9, 2025
dc714fc
textbox
Plebbaroni Dec 9, 2025
2fe8892
refactor roleselector and switch to globals
Plebbaroni Dec 11, 2025
c954c65
save role selection and fix frontend 404
Plebbaroni Dec 11, 2025
53f6140
cleanups
Plebbaroni Dec 15, 2025
a92d665
fix ranking(bit of vibe)
Plebbaroni Dec 17, 2025
309b702
fix role ordering
Plebbaroni Dec 17, 2025
b42db64
run migration
Plebbaroni Dec 18, 2025
96a7262
fix: make applicationanswer keep track of qanda states
Plebbaroni Dec 20, 2025
bd29b0a
feat: refactor to prefetch answers
Plebbaroni Dec 21, 2025
164ac39
feat: display questions inside card
Plebbaroni Dec 22, 2025
922ac52
card submit
Plebbaroni Dec 22, 2025
e289c20
fix: asterisk on required
Plebbaroni Dec 22, 2025
7118a3a
fix: get card to deactivate
Plebbaroni Dec 22, 2025
00984f1
fix: shared answers now update in card
Plebbaroni Dec 23, 2025
26de84b
feat: guard submitted applications from accessing the apply screen
Plebbaroni Dec 24, 2025
1a3e8df
fix: do a pass to clean up some code
Plebbaroni Dec 25, 2025
7fa1b7b
feat: use dict across component
Plebbaroni Dec 27, 2025
6912a9d
fix: dropwdown clear works without bricking the backend on fetch, now…
Plebbaroni Jan 1, 2026
c0a04b6
style: fix no answer message
Plebbaroni Jan 5, 2026
8e2a1a3
feat: refactor empty question types to use delete answer
Plebbaroni Jan 5, 2026
fbd47cc
fix: fix a few breaking changes caused by swapping over and in general
Plebbaroni Jan 5, 2026
38b28b0
fix: fix question type logic which broke when swapping over to the de…
Plebbaroni Jan 5, 2026
486aed8
fix: bug fixes
Plebbaroni Jan 6, 2026
4dbf473
fix: role reordering behaviour
Plebbaroni Jan 7, 2026
f8a6bfa
fix: fix inconsistent redirect behaviour
Plebbaroni Jan 7, 2026
2f1e134
merge: with chaos-598-nextjs
Plebbaroni Jan 7, 2026
38c8dd5
chore: regenerate bun thing
Plebbaroni Jan 7, 2026
f717603
merge: more merges + bug fixes
Plebbaroni Jan 7, 2026
5959ae3
feat: link answerpage component to campaign info component
Plebbaroni Jan 7, 2026
7265d0e
fix: wait for queries to be ready before repopulating qamap, (hopeful…
Plebbaroni Jan 7, 2026
afacdf7
chore: remove node_modules
Plebbaroni Jan 8, 2026
e85cc15
fix: cleanup file
Plebbaroni Jan 8, 2026
905edf1
fix: stringify answers in application review page
Plebbaroni Jan 8, 2026
d967cd4
Merge branch 'CHAOS-598-nextjs' into CHAOS-615-nextjsapplicationanswe…
Plebbaroni Jan 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/migrations/20251218085416_max_roles_to_apply_for.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE campaigns
ADD COLUMN max_roles_per_application integer;
48 changes: 48 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added backend/package.json
Empty file.
23 changes: 23 additions & 0 deletions backend/server/src/handler/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,29 @@ impl ApplicationHandler {
) -> Result<impl IntoResponse, ChaosError> {
let application = Application::get(application_id, admin.user_id, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok((StatusCode::OK, Json(application)))
}

/// Retrieves the details of a specific application regardless of submission status.
///
/// This handler allows regular applicants to view application details in the answer screen.
///
/// # Arguments
///
/// * `application_id` - The ID of the application to retrieve
/// * `_admin` - The authenticated user (must be an application admin)
/// * `transaction` - Database transaction
///
/// # Returns
///
/// * `Result<impl IntoResponse, ChaosError>` - Application details or error
pub async fn get_in_progress(
Path(application_id): Path<i64>,
user: AuthUser,
mut transaction: DBTransaction<'_>,
) -> Result<impl IntoResponse, ChaosError> {
let application = Application::get_in_progress(application_id, user.user_id, &mut transaction.tx).await?;
transaction.tx.commit().await?;
Ok(Json(application))
}

Expand Down
45 changes: 43 additions & 2 deletions backend/server/src/models/answer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ pub struct Answer {
updated_at: DateTime<Utc>,
}

/// A view type which collects an answer in the system along with it's
/// associated role.
#[derive(Deserialize, Serialize)]
pub struct AnswerWithRole {
#[serde(serialize_with = "crate::models::serde_string::serialize")]
id: i64,
/// ID of the question this answer is for
#[serde(serialize_with = "crate::models::serde_string::serialize")]
question_id: i64,

/// The actual answer data, flattened in serialization
#[serde(flatten)]
data: AnswerData,

// role ID
#[serde(serialize_with = "crate::models::serde_string::serialize")]
role_id: i64
}
/// Data structure for creating a new answer.
///
/// Contains the question ID and the answer data.
Expand Down Expand Up @@ -118,6 +136,16 @@ impl Answer {
) -> Result<i64, ChaosError> {
data.validate()?;

sqlx::query!(
"
DELETE FROM answers
WHERE application_id = $1 AND question_id = $2
",
application_id,
question_id
)
.execute(transaction.deref_mut())
.await?;
let id = snowflake_generator.real_time_generate();

sqlx::query!(
Expand Down Expand Up @@ -378,7 +406,9 @@ impl Answer {
data: AnswerData,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<(), ChaosError> {
data.validate()?;
if !data.is_empty() {
data.validate()?;
}

let answer = sqlx::query_as!(
AnswerTypeApplicationId,
Expand All @@ -396,7 +426,9 @@ impl Answer {
let old_data = AnswerData::from_question_type(&answer.question_type);
old_data.delete_from_db(id, transaction).await?;

data.insert_into_db(id, transaction).await?;
if !data.is_empty() {
data.insert_into_db(id, transaction).await?;
}

sqlx::query!(
"UPDATE applications SET updated_at = $1 WHERE id = $2",
Expand Down Expand Up @@ -520,6 +552,15 @@ impl AnswerData {
}
}

pub fn is_empty(&self) -> bool {
match self {
AnswerData::ShortAnswer(text) => text.is_empty(),
AnswerData::MultiSelect(options) | AnswerData::Ranking(options) => options.is_empty(),
AnswerData::MultiChoice(option_id) => false,
AnswerData::DropDown(option_id) => *option_id == 0
}
}

/// Validates the answer data.
///
/// # Returns
Expand Down
28 changes: 14 additions & 14 deletions backend/server/src/models/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,20 +277,6 @@ pub async fn app() -> Result<Router, ChaosError> {
.delete(RatingHandler::delete)
.put(RatingHandler::update),
)
.route(
"/api/v1/application/:application_id/rating",
get(ApplicationHandler::get_rating_by_current_user)
.post(ApplicationHandler::create_rating)
.put(ApplicationHandler::update_rating),
)
.route(
"/api/v1/application/:application_id/ratings",
get(ApplicationHandler::get_ratings),
)
.route(
"/api/v1/campaign/:campaign_id/avg_ratings",
get(ApplicationHandler::get_application_ratings_summary),
)
.route(
"/api/v1/campaign/:campaign_id/role",
post(CampaignHandler::create_role),
Expand Down Expand Up @@ -381,6 +367,20 @@ pub async fn app() -> Result<Router, ChaosError> {
"/api/v1/application/:application_id",
get(ApplicationHandler::get),
)
.route(
"/api/v1/application/:application_id/inprogress",
get(ApplicationHandler::get_in_progress),
)
.route(
"/api/v1/application/:application_id/rating",
get(ApplicationHandler::get_rating_by_current_user)
.post(ApplicationHandler::create_rating)
.put(ApplicationHandler::update_rating),
)
.route(
"/api/v1/application/:application_id/ratings",
get(ApplicationHandler::get_ratings),
)
.route(
"/api/v1/application/:application_id/status",
patch(ApplicationHandler::set_status),
Expand Down
98 changes: 95 additions & 3 deletions backend/server/src/models/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ pub struct ApplicationRole {
pub preference: i32,
}

/// Data structure for creating a new application.
///
/// Contains a list of role ids and preferences
#[derive(Deserialize)]
pub struct UpdateRoleEntry {
/// ID of the campaign role being applied for
#[serde(serialize_with = "crate::models::serde_string::serialize")]
#[serde(deserialize_with = "crate::models::serde_string::deserialize")]
pub campaign_role_id: i64,
/// User's preference ranking for this role (lower number = higher preference)
pub preference: i32,
}

/// Data structure for creating a new application.
///
/// Contains the list of roles the user is applying for, with their preferences.
Expand Down Expand Up @@ -159,7 +172,8 @@ pub struct ApplicationAppliedRoleDetails {
#[derive(Deserialize)]
pub struct ApplicationRoleUpdate {
/// Updated list of role preferences
pub roles: Vec<ApplicationRole>,
//pub roles: Vec<ApplicationRole>,
pub roles: Vec<UpdateRoleEntry>,
}

/// Possible statuses for an application.
Expand Down Expand Up @@ -402,6 +416,84 @@ impl Application {
})
}

/// Retrieves an application by its ID regardless of submission status.
/// This handler allows regular applicants to view application details in the answer screen.
/// # Arguments
///
/// * `id` - ID of the application to retrieve
/// * `transaction` - Database transaction to use
///
/// # Returns
///
/// * `Result<ApplicationDetails, ChaosError>` - Application details or error
pub async fn get_in_progress(
id: i64,
current_user: i64,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<ApplicationDetails, ChaosError> {
let application_data = match sqlx::query_as!(
ApplicationData,
"
SELECT a.id AS id, campaign_id, user_id, status AS \"status: ApplicationStatus\",
private_status AS \"private_status: ApplicationStatus\", u.email AS user_email,
u.zid AS user_zid, u.name AS user_name, u.gender AS user_gender,
u.pronouns AS user_pronouns, u.degree_name AS user_degree_name,
u.degree_starting_year AS user_degree_starting_year,
(ar.id IS NOT NULL) AS \"current_user_rated!: bool\"
FROM applications a
JOIN users u ON u.id = a.user_id
JOIN campaigns c ON c.id = a.campaign_id
LEFT JOIN application_ratings ar ON ar.application_id = a.id AND ar.application_id = $2
WHERE a.id = $1 AND a.submitted = false
",
id,
current_user
)
.fetch_one(transaction.deref_mut())
.await {
Ok(application) => application,
Err(sqlx::Error::RowNotFound) => {
return Err(ChaosError::BadRequest);
},
Err(e) => return Err(e.into()),
};

let applied_roles = sqlx::query_as!(
ApplicationAppliedRoleDetails,
"
SELECT application_roles.campaign_role_id,
application_roles.preference, campaign_roles.name AS role_name
FROM application_roles
JOIN campaign_roles
ON application_roles.campaign_role_id = campaign_roles.id
WHERE application_id = $1
ORDER BY campaign_roles.id
",
id
)
.fetch_all(transaction.deref_mut())
.await?;

Ok(ApplicationDetails {
id: application_data.id,
campaign_id: application_data.campaign_id,
status: application_data.status,
private_status: application_data.private_status,
applied_roles,
current_user_rated: application_data.current_user_rated,
user: UserDetails {
id: application_data.user_id,
email: application_data.user_email,
zid: application_data.user_zid,
name: application_data.user_name,
pronouns: application_data.user_pronouns,
gender: application_data.user_gender,
degree_name: application_data.user_degree_name,
degree_starting_year: application_data.user_degree_starting_year,
},
})
}

/// Retrieves all applications for a specific role.
///
/// # Arguments
Expand Down Expand Up @@ -786,7 +878,7 @@ impl Application {
SELECT id, application_id, campaign_role_id, preference
FROM application_roles
WHERE application_id = $1
ORDER BY campaign_role_id
ORDER BY preference
",
id
)
Expand All @@ -809,7 +901,7 @@ impl Application {
/// * `Result<(), ChaosError>` - Success or error
pub async fn update_roles(
id: i64,
roles: Vec<ApplicationRole>,
roles: Vec<UpdateRoleEntry>,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<(), ChaosError> {
sqlx::query!(
Expand Down
8 changes: 6 additions & 2 deletions backend/server/src/models/campaign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pub struct Campaign {
pub application_requirements: Option<String>,
/// Whether the campaign is published
pub published: bool,
/// Max amount of roles an applicant can apply for
pub max_roles_per_application: Option<i32>
}

/// Detailed view of a campaign.
Expand Down Expand Up @@ -109,6 +111,8 @@ pub struct CampaignDetails {
pub application_requirements: Option<String>,
/// Whether the campaign is published
pub published: bool,
/// Max amount of roles an applicant can apply for
pub max_roles_per_application: Option<i32>
}

/// Simplified view of a campaign for organization listings.
Expand Down Expand Up @@ -322,7 +326,7 @@ impl Campaign {
o.slug AS organisation_slug, o.name as organisation_name,
o.contact_email, o.website_url, c.cover_image,
c.description, c.starts_at, c.ends_at, c.published, c.interview_period_starts_at,
c.interview_period_ends_at, c.interview_format, c.outcomes_released_at,
c.interview_period_ends_at, c.interview_format, c.outcomes_released_at, c.max_roles_per_application,
c.application_requirements
FROM campaigns c
JOIN organisations o on c.organisation_id = o.id
Expand Down Expand Up @@ -399,7 +403,7 @@ impl Campaign {
SELECT c.id, c.slug AS campaign_slug, c.name, c.organisation_id,
o.slug AS organisation_slug, o.name as organisation_name,
o.contact_email, o.website_url, c.cover_image,
c.description, c.starts_at, c.ends_at, c.published,
c.description, c.starts_at, c.ends_at, c.published, c.max_roles_per_application,
c.interview_period_starts_at, c.interview_period_ends_at, c.interview_format,
c.outcomes_released_at, c.application_requirements
FROM campaigns c
Expand Down
Loading
Loading