Skip to content

Commit

Permalink
Merge pull request #64 from vklachkov/occupancy
Browse files Browse the repository at this point in the history
Конструкторские бюро и ко
  • Loading branch information
vklachkov authored Oct 23, 2024
2 parents 2287752 + c5a93c4 commit 8a37a71
Show file tree
Hide file tree
Showing 28 changed files with 381 additions and 152 deletions.
4 changes: 2 additions & 2 deletions backend/scripts/backup_participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
print(f"Failed to login. Status code: {login_request.status_code}")
sys.exit(1)

participants_request = requests.get(f"{host}/api/v1/org/participants?sort=id&order=desc", cookies=login_request.cookies)
participants_request = requests.get(f"{host}/api/v1/org/participants?sort=id&order=desc&deleted=true", cookies=login_request.cookies)
if participants_request.status_code != 200:
print(f"Failed to fetch participants. Status code: {participants_request.status_code}")
print(f"Failed to fetch participants. Status code: {participants_request.status_code}. Text: {participants_request.text}")
sys.exit(1)

with open('participants-backup.json', 'w') as file:
Expand Down
33 changes: 33 additions & 0 deletions backend/src/api/v1/bureau.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::{
api::{error::Result, BackendState},
domain::*,
};
use axum::{extract::State, Json};
use std::{collections::HashMap, sync::Arc};

pub async fn juries() -> Json<HashMap<&'static str, Bureau>> {
Json(Bureau::all().into_iter().map(|b| (b.jury(), b)).collect())
}

pub async fn stats(
State(state): State<Arc<BackendState>>,
) -> Result<Json<HashMap<Bureau, BureauStats>>> {
let participants = state
.datasource
.participants
.get_all(None, Sort::Id, Order::Asc, false)
.await?;

let mut bureaus = Bureau::all()
.into_iter()
.map(|b| (b, BureauStats::default()))
.collect::<HashMap<_, _>>();

for p in participants {
if let Some(bureau) = p.jury.as_ref().and_then(|j| Bureau::from_jury(&j.name)) {
bureaus.get_mut(&bureau).unwrap().participants += 1;
}
}

Ok(Json(bureaus))
}
23 changes: 23 additions & 0 deletions backend/src/api/v1/jury.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ use axum::{
};
use std::sync::Arc;

pub async fn stats(
auth_session: auth::AuthSession,
State(state): State<Arc<BackendState>>,
) -> Result<Json<BureauStats>> {
let jury_id = auth_session.user.as_ref().unwrap().0.id;

let participants = state
.datasource
.participants
.get_all(None, Sort::Id, Order::Asc, false)
.await?;

let count = participants
.into_iter()
.filter(|p| p.jury.as_ref().is_some_and(|jury| jury.id == jury_id))
.count();

Ok(Json(BureauStats {
participants: count,
max_participants: MAX_PARTICIPANTS_PER_BUREAU,
}))
}

pub async fn all_participants(
auth_session: auth::AuthSession,
State(state): State<Arc<BackendState>>,
Expand Down
4 changes: 4 additions & 0 deletions backend/src/api/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod bureau;
mod jury;
mod login;
mod org;
Expand Down Expand Up @@ -48,6 +49,8 @@ fn orgs_methods() -> Router<Arc<BackendState>> {
.route("/adults", get(org::all_adults))
.route("/adult", post(org::create_adult))
.route("/adult/:id", delete(org::delete_adult))
.route("/bureaus/juries", get(bureau::juries))
.route("/bureaus/stats", get(bureau::stats))
.route_layer(axum_login::permission_required!(
auth::Backend,
AdultRole::Org,
Expand All @@ -56,6 +59,7 @@ fn orgs_methods() -> Router<Arc<BackendState>> {

fn juries_methods() -> Router<Arc<BackendState>> {
Router::new()
.route("/stats", get(jury::stats))
.route("/participants", get(jury::all_participants))
.route("/participant/:id", get(jury::get_participant))
.route("/participant/:id/rate", post(jury::set_rate))
Expand Down
99 changes: 66 additions & 33 deletions backend/src/api/v1/report.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
api::{error::*, BackendState},
datasource::DataSource,
domain::*,
};
use anyhow::Context;
Expand All @@ -13,46 +14,35 @@ use serde::Serialize;
use std::{collections::HashMap, sync::Arc};

#[derive(Serialize)]
struct AnonymousRate {
struct Report {
pub bureaus: Vec<Bureau>,
pub bureaus_participants_count: HashMap<Bureau, usize>,
pub max_participants_per_bureau: usize,
pub salaries: Vec<ParticipantSalaries>,
}

#[derive(Serialize)]
struct ParticipantSalaries {
pub code: String,
pub rates: HashMap<&'static str, Option<i32>>,
pub bureaus: HashMap<Bureau, Option<i32>>,
}

pub async fn build_report(State(state): State<Arc<BackendState>>) -> Result<Response> {
let participants = state
.datasource
.participants
.get_all(None, Sort::Id, Order::Asc, false)
.await?;
let participants = get_participants(&state.datasource).await?;

let bureaus_participants_count = count_participants(&participants);

let adults = state.datasource.adults.get_all().await?;
let salaries = code_salaries(&participants, &adults);

let get_design_bureau_rate = |participant: &Participant, adult_name: &str| {
adults
.iter()
.find(|adult| adult.name == adult_name)
.map(|adult| adult.id)
.and_then(|adult_id| participant.rates.get(&adult_id).cloned())
.flatten()
.map(|rate| rate.salary)
let report = Report {
bureaus: Bureau::all().to_vec(),
bureaus_participants_count,
max_participants_per_bureau: MAX_PARTICIPANTS_PER_BUREAU,
salaries,
};

let data = participants
.into_iter()
.filter(|p| p.jury.is_none())
.map(|p| AnonymousRate {
rates: HashMap::from_iter([
("1D", get_design_bureau_rate(&p, "Матюхин Андрей")),
("Салют", get_design_bureau_rate(&p, "Кириевский Дмитрий")),
("Звёздное", get_design_bureau_rate(&p, "Каменева Вероника")),
("Родное", get_design_bureau_rate(&p, "Овчинников Илья")),
("Око", get_design_bureau_rate(&p, "Калинкин Александр")),
]),
code: p.code,
})
.collect();

let response = match get_report(&state.services.report_generator, data).await {
let response = match get_report(&state.services.report_generator, report).await {
Ok(pdf) => ([(axum::http::header::CONTENT_TYPE, "application/pdf")], pdf).into_response(),
Err(err) => {
tracing::error!("Failed to generate pdf report: {err:#}");
Expand All @@ -63,10 +53,53 @@ pub async fn build_report(State(state): State<Arc<BackendState>>) -> Result<Resp
Ok(response)
}

async fn get_report(url: &Url, data: Vec<AnonymousRate>) -> anyhow::Result<Vec<u8>> {
async fn get_participants(datasource: &DataSource) -> Result<Vec<Participant>> {
datasource
.participants
.get_all(None, Sort::Id, Order::Asc, false)
.await
.map_err(Into::into)
}

fn count_participants(participants: &[Participant]) -> HashMap<Bureau, usize> {
let bureau = Bureau::all().into_iter().map(|b| (b, 0)).collect();

participants.iter().fold(bureau, |mut acc, p| {
if let Some(bureau) = p.jury.as_ref().and_then(|j| Bureau::from_jury(&j.name)) {
*acc.entry(bureau).or_default() += 1;
}

acc
})
}

fn code_salaries(participants: &[Participant], adults: &[Adult]) -> Vec<ParticipantSalaries> {
let adult_ids = adults
.iter()
.map(|adult| (adult.name.as_str(), adult.id))
.collect::<HashMap<&str, AdultId>>();

participants
.iter()
.filter(|p| p.jury.is_none())
.map(|p| ParticipantSalaries {
code: p.code.clone(),
bureaus: Bureau::all()
.into_iter()
.map(|b| {
let adult_id = adult_ids[b.jury()];
let rate = p.rates.get(&adult_id).and_then(Option::as_ref);
(b, rate.map(|r| r.salary))
})
.collect(),
})
.collect()
}

async fn get_report(url: &Url, report: Report) -> anyhow::Result<Vec<u8>> {
reqwest::Client::new()
.post(url.to_owned())
.json(&data)
.json(&report)
.send()
.await
.context("sending request")?
Expand Down
54 changes: 54 additions & 0 deletions backend/src/domain/bureau.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use serde::Serialize;

#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[rustfmt::skip]
pub enum Bureau {
#[serde(rename = "1D")] OneD,
#[serde(rename = "Салют")] Salut,
#[serde(rename = "Звёздное")] Zvezdnoe,
#[serde(rename = "Родное")] Rodnoe,
#[serde(rename = "Око")] Oko,
}

impl Bureau {
pub fn all() -> [Bureau; 5] {
use Bureau::*;
[OneD, Salut, Zvezdnoe, Rodnoe, Oko]
}

pub fn from_jury(name: &str) -> Option<Self> {
match name {
"Матюхин Андрей" => Some(Self::OneD),
"Кириевский Дмитрий" => Some(Self::Salut),
"Каменева Вероника" => Some(Self::Zvezdnoe),
"Овчинников Илья" => Some(Self::Rodnoe),
"Калинкин Александр" => Some(Self::Oko),
_ => None,
}
}

pub fn jury(self) -> &'static str {
match self {
Self::OneD => "Матюхин Андрей",
Self::Salut => "Кириевский Дмитрий",
Self::Zvezdnoe => "Каменева Вероника",
Self::Rodnoe => "Овчинников Илья",
Self::Oko => "Калинкин Александр",
}
}
}

#[derive(Clone, Debug, Serialize)]
pub struct BureauStats {
pub participants: usize,
pub max_participants: usize,
}

impl Default for BureauStats {
fn default() -> Self {
Self {
participants: 0,
max_participants: super::MAX_PARTICIPANTS_PER_BUREAU,
}
}
}
4 changes: 4 additions & 0 deletions backend/src/domain/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
mod adult;
mod bureau;
mod operations;
mod participant;

pub use adult::{Adult, AdultId, AdultRole};
pub use bureau::{Bureau, BureauStats};
pub use operations::{Order, Sort};
pub use participant::{
JuryParticipant, Participant, ParticipantAnswers, ParticipantCode, ParticipantId,
ParticipantInfo, ParticipantRate,
};

pub const MAX_PARTICIPANTS_PER_BUREAU: usize = 24;
4 changes: 2 additions & 2 deletions frontend/src/app/components/answers/answers.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KeyValuePipe } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Answers } from '@models/api/participant.interface';
import { QA } from '@models/api/participant.interface';
import { decode } from 'he';
import { NzTypographyComponent } from 'ng-zorro-antd/typography';

Expand All @@ -24,7 +24,7 @@ export const QUESTIONS: string[] = [
export class AnswersComponent {
protected readonly QUESTIONS: string[] = QUESTIONS;

@Input({ required: true }) answers!: Answers;
@Input({ required: true }) answers!: QA;

protected textDecode(text: string): string {
return decode(text);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NzTypographyComponent } from 'ng-zorro-antd/typography';
import { NzButtonComponent } from 'ng-zorro-antd/button';
import { JURY_ROOT_PATHS } from '../../app.routes';
import { BaseComponent } from '@components/base/base.component';
import { AnonymousParticipant } from '@models/api/anonymous-participant.interface';
import { JuryParticipant } from '@models/api/jury-participant.interface';

@Component({
selector: 'app-applications-group',
Expand All @@ -18,5 +18,5 @@ export class ApplicationsGroupComponent extends BaseComponent {
protected readonly applicationPath: string = JURY_ROOT_PATHS.Application;

@Input({ required: true }) title!: string;
@Input({ required: true }) participants!: AnonymousParticipant[];
@Input({ required: true }) participants!: JuryParticipant[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { AnswersComponent } from '@components/answers/answers.component';
import { AnswersEditableComponent } from '@components/answers-editable/answers-editable.component';
import { OrganizerService } from '@services/organizer.service';
import { DownloadService } from '@services/download.service';
import { Answers, Participant, ParticipantInfo } from '@models/api/participant.interface';
import { QA, Participant, ParticipantInfo } from '@models/api/participant.interface';
import { ParticipantUpdateInfo } from '@models/api/participant-update-info.interface';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';

Expand All @@ -44,7 +44,7 @@ type FormGroupType = {
answers: FormGroup<AnswersFormType>
}

type FormValue = Omit<ParticipantInfo, 'photo_url'> & { answers: Answers };
type FormValue = Omit<ParticipantInfo, 'photo_url'> & { answers: QA };

@Component({
selector: 'app-participant-questionnarie-tab',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
</nz-card>
} @else {
<div class="team">
<p nz-typography class="team__title">Команда участника:</p>
<p nz-typography class="team__title">Конструкторское бюро:</p>

<nz-spin [nzSpinning]="isSettingCommandLoading">
<nz-select class="team__select" nzPlaceHolder="Не выбрано" [nzAllowClear]="true" [formControl]="teamControl">
@for (jury of juries; track jury.id) {
@for (jury of juries.juries; track jury.id) {
<nz-option
[nzValue]="jury.id"
[nzLabel]="jury.name"
[nzDisabled]="!participant.rates[jury.id] || participant.rates[jury.id]?.salary === 0">
[nzLabel]="bureauSelectorText(jury)"
[nzDisabled]="!canSelectBureau(jury)">
</nz-option>
}
</nz-select>
Expand All @@ -26,7 +26,7 @@
<nz-table #basicTable [nzData]="ratesTableData" [nzShowPagination]="false">
<thead>
<tr>
<th style="background-color: white; font-weight: 700;" nzWidth="20%">Имя жюри</th>
<th style="background-color: white; font-weight: 700;" nzWidth="20%">Конструкторское Бюро</th>
<th style="background-color: white; font-weight: 700;" nzWidth="10%">Зарплата</th>
<th style="background-color: white; font-weight: 700;">Комментарий</th>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
}

&__select {
min-width: 14em;
min-width: 20em;
}
}
Loading

0 comments on commit 8a37a71

Please sign in to comment.