adding endpoints
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
|||||||
|
### Rust ###
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "manager_employees_rust"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8.4"
|
||||||
|
chrono = { version = "0.4.42", features = ["serde"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
sqlx = { version = "0.9.0", features = ["chrono", "derive", "postgres", "runtime-tokio"] }
|
||||||
|
tokio = { version = "1.53.1", features = ["full"] }
|
||||||
|
tower-http = { version = "0.6.6", features = ["cors"] }
|
||||||
|
utoipa = { version = "5", features = ["axum_extras", "chrono"] }
|
||||||
|
utoipa-swagger-ui = { version = "9", features = ["axum"] }
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 CarGDev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiError(sqlx::Error);
|
||||||
|
|
||||||
|
pub type ApiResult<T> = Result<T, ApiError>;
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for ApiError {
|
||||||
|
fn from(error: sqlx::Error) -> Self {
|
||||||
|
Self(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let status = match self.0 {
|
||||||
|
sqlx::Error::RowNotFound => StatusCode::NOT_FOUND,
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
Json(ErrorResponse {
|
||||||
|
error: self.0.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||||
|
|
||||||
|
use crate::database::schema;
|
||||||
|
|
||||||
|
pub async fn connect(database_url: &str) -> Result<PgPool, sqlx::Error> {
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(database_url)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_database(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||||
|
for query in schema::CREATE_SCHEMA_QUERIES {
|
||||||
|
sqlx::query(query).execute(pool).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct Company {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct CompanyUser {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub company_id: i32,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod company;
|
||||||
|
pub mod company_user;
|
||||||
|
pub mod schedule;
|
||||||
|
pub mod user;
|
||||||
|
pub mod weekly_preference;
|
||||||
|
|
||||||
|
pub use company::Company;
|
||||||
|
pub use company_user::CompanyUser;
|
||||||
|
pub use schedule::Schedule;
|
||||||
|
pub use user::User;
|
||||||
|
pub use weekly_preference::WeeklyPreference;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::database::types::DayOfWeek;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct Schedule {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub enter_time: NaiveDateTime,
|
||||||
|
pub day_of_week: DayOfWeek,
|
||||||
|
pub out_time: NaiveDateTime,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::database::types::WorkTime;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub preference: WorkTime,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::database::types::{DayOfWeek, WorkTime};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct WeeklyPreference {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub day_of_week: DayOfWeek,
|
||||||
|
pub first_preference: WorkTime,
|
||||||
|
pub second_preference: WorkTime,
|
||||||
|
pub third_preference: WorkTime,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod connection;
|
||||||
|
pub mod entities;
|
||||||
|
pub mod queries;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod types;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::entities::{Company, CompanyUser, User};
|
||||||
|
|
||||||
|
pub async fn add_company(pool: &PgPool, name: &str) -> Result<Company, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Company>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO companies (name)
|
||||||
|
VALUES ($1)
|
||||||
|
RETURNING id, name, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(name)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_company_by_id(pool: &PgPool, id: i32) -> Result<Company, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Company>(
|
||||||
|
r#"
|
||||||
|
SELECT id, name, date_created, date_updated
|
||||||
|
FROM companies
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_companies(pool: &PgPool) -> Result<Vec<Company>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Company>(
|
||||||
|
r#"
|
||||||
|
SELECT id, name, date_created, date_updated
|
||||||
|
FROM companies
|
||||||
|
ORDER BY name
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_company(pool: &PgPool, id: i32) -> Result<Company, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Company>(
|
||||||
|
r#"
|
||||||
|
DELETE FROM companies
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_company_user(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_id: i32,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<CompanyUser, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, CompanyUser>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO company_users (user_id, company_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, user_id, company_id, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_company_by_name(pool: &PgPool, name: &str) -> Result<Company, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Company>(
|
||||||
|
r#"
|
||||||
|
SELECT id, name, date_created, date_updated
|
||||||
|
FROM companies
|
||||||
|
WHERE name = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(name)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_id_by_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_id: i32,
|
||||||
|
) -> Result<Vec<i32>, sqlx::Error> {
|
||||||
|
sqlx::query_scalar::<_, i32>(
|
||||||
|
r#"
|
||||||
|
SELECT user_id
|
||||||
|
FROM company_users
|
||||||
|
WHERE company_id = $1
|
||||||
|
ORDER BY user_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users_by_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_id: i32,
|
||||||
|
) -> Result<Vec<User>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, User>(
|
||||||
|
r#"
|
||||||
|
SELECT users.id, users.name, users.last_name, users.preference,
|
||||||
|
users.date_created, users.date_updated
|
||||||
|
FROM users
|
||||||
|
INNER JOIN company_users ON company_users.user_id = users.id
|
||||||
|
WHERE company_users.company_id = $1
|
||||||
|
ORDER BY users.name, users.last_name
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::{entities::User, types::WorkTime};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, sqlx::FromRow, utoipa::ToSchema)]
|
||||||
|
pub struct EmployeeRow {
|
||||||
|
pub id: i32,
|
||||||
|
pub company_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub preference: WorkTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_employees(pool: &PgPool) -> Result<Vec<EmployeeRow>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, EmployeeRow>(
|
||||||
|
r#"
|
||||||
|
SELECT users.id, company_users.company_id, users.name, users.last_name, users.preference
|
||||||
|
FROM users
|
||||||
|
INNER JOIN company_users ON company_users.user_id = users.id
|
||||||
|
ORDER BY users.name, users.last_name
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_employee(pool: &PgPool, id: i32) -> Result<EmployeeRow, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, EmployeeRow>(
|
||||||
|
r#"
|
||||||
|
SELECT users.id, company_users.company_id, users.name, users.last_name, users.preference
|
||||||
|
FROM users
|
||||||
|
INNER JOIN company_users ON company_users.user_id = users.id
|
||||||
|
WHERE users.id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_employee_user(pool: &PgPool, id: i32) -> Result<User, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, User>(
|
||||||
|
r#"
|
||||||
|
SELECT id, name, last_name, preference, date_created, date_updated
|
||||||
|
FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_employee_names_by_company_name(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_name: &str,
|
||||||
|
) -> Result<Vec<String>, sqlx::Error> {
|
||||||
|
sqlx::query_scalar::<_, String>(
|
||||||
|
r#"
|
||||||
|
SELECT users.name || ' ' || users.last_name
|
||||||
|
FROM users
|
||||||
|
INNER JOIN company_users ON company_users.user_id = users.id
|
||||||
|
INNER JOIN companies ON companies.id = company_users.company_id
|
||||||
|
WHERE companies.name = $1
|
||||||
|
ORDER BY users.name, users.last_name
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(company_name)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod company;
|
||||||
|
pub mod employee;
|
||||||
|
pub mod preference;
|
||||||
|
pub mod schedule;
|
||||||
|
pub mod user;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::{
|
||||||
|
entities::WeeklyPreference,
|
||||||
|
types::{DayOfWeek, WorkTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn upsert_weekly_preference(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
first_preference: WorkTime,
|
||||||
|
second_preference: WorkTime,
|
||||||
|
third_preference: WorkTime,
|
||||||
|
) -> Result<WeeklyPreference, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, WeeklyPreference>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO employee_weekly_preferences (
|
||||||
|
user_id, day_of_week, first_preference, second_preference, third_preference
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
ON CONFLICT (user_id, day_of_week)
|
||||||
|
DO UPDATE SET
|
||||||
|
first_preference = EXCLUDED.first_preference,
|
||||||
|
second_preference = EXCLUDED.second_preference,
|
||||||
|
third_preference = EXCLUDED.third_preference,
|
||||||
|
date_updated = NOW()
|
||||||
|
RETURNING id, user_id, day_of_week, first_preference, second_preference,
|
||||||
|
third_preference, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(first_preference)
|
||||||
|
.bind(second_preference)
|
||||||
|
.bind(third_preference)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_preferences_for_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_id: i32,
|
||||||
|
) -> Result<Vec<WeeklyPreference>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, WeeklyPreference>(
|
||||||
|
r#"
|
||||||
|
SELECT employee_weekly_preferences.id,
|
||||||
|
employee_weekly_preferences.user_id,
|
||||||
|
employee_weekly_preferences.day_of_week,
|
||||||
|
employee_weekly_preferences.first_preference,
|
||||||
|
employee_weekly_preferences.second_preference,
|
||||||
|
employee_weekly_preferences.third_preference,
|
||||||
|
employee_weekly_preferences.date_created,
|
||||||
|
employee_weekly_preferences.date_updated
|
||||||
|
FROM employee_weekly_preferences
|
||||||
|
INNER JOIN company_users ON company_users.user_id = employee_weekly_preferences.user_id
|
||||||
|
WHERE company_users.company_id = $1
|
||||||
|
ORDER BY employee_weekly_preferences.day_of_week, employee_weekly_preferences.user_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::{entities::Schedule, types::DayOfWeek};
|
||||||
|
|
||||||
|
pub async fn add_schedule(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
enter_time: NaiveDateTime,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: NaiveDateTime,
|
||||||
|
) -> Result<Schedule, sqlx::Error> {
|
||||||
|
// Keep compatible with legacy DBs that have an extra `shift` column.
|
||||||
|
// Try insert with `shift` first, fall back to 4-column insert if the column was dropped/never created.
|
||||||
|
let with_shift = sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO schedules (user_id, enter_time, day_of_week, shift, out_time)
|
||||||
|
VALUES ($1, $2, $3, 'morning', $4)
|
||||||
|
RETURNING id, user_id, enter_time, day_of_week, out_time, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(enter_time)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(out_time)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match with_shift {
|
||||||
|
Ok(schedule) => Ok(schedule),
|
||||||
|
Err(_) => {
|
||||||
|
sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO schedules (user_id, enter_time, day_of_week, out_time)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, user_id, enter_time, day_of_week, out_time, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(enter_time)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(out_time)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_schedules(pool: &PgPool) -> Result<Vec<Schedule>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
SELECT id, user_id, enter_time, day_of_week, out_time, date_created, date_updated
|
||||||
|
FROM schedules
|
||||||
|
ORDER BY day_of_week, user_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_schedules_for_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_id: i32,
|
||||||
|
) -> Result<Vec<Schedule>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
SELECT schedules.id, schedules.user_id, schedules.enter_time, schedules.day_of_week,
|
||||||
|
schedules.out_time, schedules.date_created, schedules.date_updated
|
||||||
|
FROM schedules
|
||||||
|
INNER JOIN company_users ON company_users.user_id = schedules.user_id
|
||||||
|
WHERE company_users.company_id = $1
|
||||||
|
ORDER BY schedules.day_of_week, schedules.user_id
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_schedules_for_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_id: i32,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
|
DELETE FROM schedules
|
||||||
|
USING company_users
|
||||||
|
WHERE company_users.user_id = schedules.user_id
|
||||||
|
AND company_users.company_id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(company_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clock_in(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
now: NaiveDateTime,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
) -> Result<Schedule, sqlx::Error> {
|
||||||
|
// out_time is required in reference API, initialize with enter_time and let clock-out update it
|
||||||
|
add_schedule(pool, user_id, now, day_of_week, now).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clock_out(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: NaiveDateTime,
|
||||||
|
) -> Result<Schedule, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
UPDATE schedules
|
||||||
|
SET out_time = $3, date_updated = NOW()
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id
|
||||||
|
FROM schedules
|
||||||
|
WHERE user_id = $1 AND day_of_week = $2
|
||||||
|
ORDER BY enter_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
RETURNING id, user_id, enter_time, day_of_week, out_time, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(out_time)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::{
|
||||||
|
entities::{Schedule, User},
|
||||||
|
types::{DayOfWeek, WorkTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn add_user(
|
||||||
|
pool: &PgPool,
|
||||||
|
name: &str,
|
||||||
|
last_name: &str,
|
||||||
|
preference: WorkTime,
|
||||||
|
) -> Result<User, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, User>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO users (name, last_name, preference)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, name, last_name, preference, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(name)
|
||||||
|
.bind(last_name)
|
||||||
|
.bind(preference)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_preference(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
preference: WorkTime,
|
||||||
|
) -> Result<User, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, User>(
|
||||||
|
r#"
|
||||||
|
UPDATE users
|
||||||
|
SET preference = $2, date_updated = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, last_name, preference, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(preference)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_user(pool: &PgPool, id: i32) -> Result<User, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, User>(
|
||||||
|
r#"
|
||||||
|
DELETE FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, name, last_name, preference, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_schedule(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
enter_time: NaiveDateTime,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: Option<NaiveDateTime>,
|
||||||
|
) -> Result<Schedule, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
INSERT INTO schedules (user_id, enter_time, day_of_week, shift, out_time)
|
||||||
|
VALUES ($1, $2, $3, 'morning', $4)
|
||||||
|
RETURNING id, user_id, enter_time, day_of_week, shift, out_time, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(enter_time)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(out_time)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_days_worked_by_user(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<Vec<DayOfWeek>, sqlx::Error> {
|
||||||
|
sqlx::query_scalar::<_, DayOfWeek>(
|
||||||
|
r#"
|
||||||
|
SELECT DISTINCT day_of_week
|
||||||
|
FROM schedules
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY day_of_week
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_enter_time_by_day(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
) -> Result<Option<NaiveDateTime>, sqlx::Error> {
|
||||||
|
sqlx::query_scalar::<_, NaiveDateTime>(
|
||||||
|
r#"
|
||||||
|
SELECT enter_time
|
||||||
|
FROM schedules
|
||||||
|
WHERE user_id = $1 AND day_of_week = $2
|
||||||
|
ORDER BY date_created DESC
|
||||||
|
LIMIT 1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_day_end_by_user_and_day(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: NaiveDateTime,
|
||||||
|
) -> Result<Schedule, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, Schedule>(
|
||||||
|
r#"
|
||||||
|
UPDATE schedules
|
||||||
|
SET out_time = $3, date_updated = NOW()
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id
|
||||||
|
FROM schedules
|
||||||
|
WHERE user_id = $1 AND day_of_week = $2
|
||||||
|
ORDER BY date_created DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
RETURNING id, user_id, enter_time, day_of_week, shift, out_time, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(out_time)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
pub const CREATE_WORK_TIME_ENUM: &str = r#"
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE TYPE work_time AS ENUM ('morning', 'afternoon', 'evening');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_DAY_OF_WEEK_ENUM: &str = r#"
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
CREATE TYPE day_of_week AS ENUM (
|
||||||
|
'sunday',
|
||||||
|
'monday',
|
||||||
|
'tuesday',
|
||||||
|
'wednesday',
|
||||||
|
'thursday',
|
||||||
|
'friday',
|
||||||
|
'saturday'
|
||||||
|
);
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN NULL;
|
||||||
|
END $$;
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_COMPANIES_TABLE: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
date_updated TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_USERS_TABLE: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
last_name TEXT NOT NULL,
|
||||||
|
preference work_time NOT NULL,
|
||||||
|
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
date_updated TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_COMPANY_USERS_TABLE: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS company_users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
date_updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (user_id, company_id)
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_SCHEDULES_TABLE: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS schedules (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
enter_time TIMESTAMP NOT NULL,
|
||||||
|
day_of_week day_of_week NOT NULL,
|
||||||
|
out_time TIMESTAMP NOT NULL,
|
||||||
|
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
date_updated TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_EMPLOYEE_WEEKLY_PREFERENCES_TABLE: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_weekly_preferences (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
day_of_week day_of_week NOT NULL,
|
||||||
|
first_preference work_time NOT NULL,
|
||||||
|
second_preference work_time NOT NULL,
|
||||||
|
third_preference work_time NOT NULL,
|
||||||
|
date_created TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
date_updated TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (user_id, day_of_week)
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const ALTER_SCHEDULES_ADD_SHIFT: &str = r#"
|
||||||
|
ALTER TABLE schedules
|
||||||
|
ADD COLUMN IF NOT EXISTS shift work_time NOT NULL DEFAULT 'morning';
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const ALTER_SCHEDULES_DROP_OUT_TIME_NOT_NULL: &str = r#"
|
||||||
|
ALTER TABLE schedules
|
||||||
|
ALTER COLUMN out_time DROP NOT NULL;
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_SCHEMA_QUERIES: [&str; 9] = [
|
||||||
|
CREATE_WORK_TIME_ENUM,
|
||||||
|
CREATE_DAY_OF_WEEK_ENUM,
|
||||||
|
CREATE_COMPANIES_TABLE,
|
||||||
|
CREATE_USERS_TABLE,
|
||||||
|
CREATE_COMPANY_USERS_TABLE,
|
||||||
|
CREATE_SCHEDULES_TABLE,
|
||||||
|
CREATE_EMPLOYEE_WEEKLY_PREFERENCES_TABLE,
|
||||||
|
ALTER_SCHEDULES_ADD_SHIFT,
|
||||||
|
ALTER_SCHEDULES_DROP_OUT_TIME_NOT_NULL,
|
||||||
|
];
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
std::hash::Hash,
|
||||||
|
Deserialize,
|
||||||
|
Serialize,
|
||||||
|
sqlx::Type,
|
||||||
|
utoipa::ToSchema,
|
||||||
|
)]
|
||||||
|
#[sqlx(type_name = "work_time", rename_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[schema(rename_all = "snake_case")]
|
||||||
|
pub enum WorkTime {
|
||||||
|
Morning,
|
||||||
|
Afternoon,
|
||||||
|
Evening,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
std::hash::Hash,
|
||||||
|
Deserialize,
|
||||||
|
Serialize,
|
||||||
|
sqlx::Type,
|
||||||
|
utoipa::ToSchema,
|
||||||
|
)]
|
||||||
|
#[sqlx(type_name = "day_of_week", rename_all = "snake_case")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[schema(rename_all = "snake_case")]
|
||||||
|
pub enum DayOfWeek {
|
||||||
|
Sunday,
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
mod app;
|
||||||
|
mod database;
|
||||||
|
mod manager;
|
||||||
|
mod models;
|
||||||
|
mod router;
|
||||||
|
|
||||||
|
use std::{env, net::SocketAddr};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
let database_url = env::var("DATABASE_URL")?;
|
||||||
|
let pool = database::connection::connect(&database_url).await?;
|
||||||
|
database::connection::init_database(&pool).await?;
|
||||||
|
|
||||||
|
let router = router::build_router(pool);
|
||||||
|
|
||||||
|
let address = env::var("APP_ADDRESS")
|
||||||
|
.unwrap_or_else(|_| "127.0.0.1:5005".to_string())
|
||||||
|
.parse::<SocketAddr>()?;
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(address).await?;
|
||||||
|
axum::serve(listener, router).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assignment_rules() {
|
||||||
|
// collect employee names and preferred shifts for each day of the week
|
||||||
|
// store employees, companies, preferences, and schedules in database tables
|
||||||
|
// use conditionals to reject invalid schedules
|
||||||
|
// use loops to assign shifts for every day and every turn
|
||||||
|
// use branching to resolve conflicts when a preferred shift is unavailable
|
||||||
|
// ensure each employee works at most one shift per day
|
||||||
|
// ensure each employee works at most five days per week
|
||||||
|
// ensure every company shift has at least two employees
|
||||||
|
// if fewer than two employees are available, choose another valid employee
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// import validator functions before moving an employee to another shift or day
|
||||||
|
// import database queries to read preferences and existing schedules
|
||||||
|
|
||||||
|
pub fn detect_preferred_shift_conflict() {
|
||||||
|
// check if the employee's preferred shift is already full for the selected day
|
||||||
|
// check if assigning this employee would break one-shift-per-day rule
|
||||||
|
// check if assigning this employee would break five-days-per-week rule
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_conflict_same_day() {
|
||||||
|
// loop over the other shifts on the same day
|
||||||
|
// choose the first shift where the employee can legally work
|
||||||
|
// assign the employee only if validation passes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_conflict_next_day() {
|
||||||
|
// if same-day alternatives fail, move to the next day
|
||||||
|
// loop through next-day shifts by preference priority
|
||||||
|
// assign the employee only if validation passes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_missing_second_employee() {
|
||||||
|
// find shifts with only one employee assigned
|
||||||
|
// loop over available employees in the company
|
||||||
|
// skip employees who already work that day
|
||||||
|
// skip employees who already worked five days this week
|
||||||
|
// assign a valid extra employee to satisfy the two-employees-per-shift rule
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod conflict;
|
||||||
|
pub mod scheduler;
|
||||||
|
pub mod validator;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// import database::company_queries to register users into companies when needed
|
||||||
|
// import database::user_queries to add schedules and update enter/out times
|
||||||
|
// import validator module to enforce assignment rules before saving a schedule
|
||||||
|
// import conflict module to move employees when their preferred shift is not available
|
||||||
|
|
||||||
|
pub fn generate_weekly_schedule() {
|
||||||
|
// load all employees for the selected company
|
||||||
|
// load all employee preferences for all seven days
|
||||||
|
// loop through each day of the week
|
||||||
|
// loop through morning, afternoon, and evening shifts
|
||||||
|
// try to assign employees based on preferences first
|
||||||
|
// validate one-shift-per-day before each assignment
|
||||||
|
// validate five-days-per-week before each assignment
|
||||||
|
// validate at least two employees per shift after assignments
|
||||||
|
// resolve conflicts by assigning another shift on the same day or next day
|
||||||
|
// save the final schedule to the schedules table
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assign_preferred_shift() {
|
||||||
|
// receive employee_id, day_of_week, and preferred shift
|
||||||
|
// call validator before inserting a schedule
|
||||||
|
// call user_queries::add_schedule when the assignment is valid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_missing_shift_coverage() {
|
||||||
|
// find each company shift with fewer than two employees
|
||||||
|
// if only one employee is assigned, search for another valid employee
|
||||||
|
// randomly choose or rotate through employees who have not worked five days
|
||||||
|
// add the selected employee to the schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_clock_in() {
|
||||||
|
// receive user_id and current day/time from the TUI
|
||||||
|
// reject if the user already worked the same day
|
||||||
|
// save or update the schedule enter_time
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_clock_out() {
|
||||||
|
// receive user_id, day_of_week, and current time from the TUI
|
||||||
|
// call user_queries::update_day_end_by_user_and_day
|
||||||
|
// keep the existing schedule row and update out_time only
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_final_schedule() {
|
||||||
|
// query final schedules with employee names
|
||||||
|
// group output by day and shift
|
||||||
|
// show every employee assigned to each shift
|
||||||
|
// show warnings for shifts that still do not have two employees
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// import database::user_queries to read existing schedules for one user
|
||||||
|
// import database::storage::DayOfWeek to validate one day at a time
|
||||||
|
// import database::storage::WorkTime to validate the selected shift
|
||||||
|
|
||||||
|
pub fn validate_user_has_not_worked_same_day() {
|
||||||
|
// query schedules for the user and selected day
|
||||||
|
// if a schedule already exists for that user and day, reject the new shift
|
||||||
|
// otherwise allow the scheduler to continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_user_has_not_worked_more_than_five_days() {
|
||||||
|
// query distinct days worked by the user for the current week
|
||||||
|
// count the worked days using a loop
|
||||||
|
// if the count is five or more, reject the new shift
|
||||||
|
// otherwise allow the user to be assigned
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_shift_has_minimum_two_employees() {
|
||||||
|
// query schedules for company_id, day_of_week, and shift
|
||||||
|
// count assigned employees for that shift
|
||||||
|
// if count is less than two, ask scheduler to add another available employee
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_employee_can_join_shift() {
|
||||||
|
// check same-day rule first
|
||||||
|
// check five-days-per-week rule second
|
||||||
|
// check shift capacity and company coverage rules last
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_company_has_enough_employees() {
|
||||||
|
// query employee names based on company_id
|
||||||
|
// if the company has fewer than two employees, ask the TUI to register another employee
|
||||||
|
// if enough employees exist, continue schedule generation
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod requests;
|
||||||
|
pub mod service;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct CreateCompanyRequest {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
database::{entities::Company, queries::company as company_queries},
|
||||||
|
models::company::requests::CreateCompanyRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn create_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
request: CreateCompanyRequest,
|
||||||
|
) -> Result<Company, sqlx::Error> {
|
||||||
|
company_queries::add_company(pool, request.name.trim()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_company(pool: &PgPool, id: i32) -> Result<Company, sqlx::Error> {
|
||||||
|
company_queries::get_company_by_id(pool, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_companies(pool: &PgPool) -> Result<Vec<Company>, sqlx::Error> {
|
||||||
|
company_queries::list_companies(pool).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_company(pool: &PgPool, id: i32) -> Result<Company, sqlx::Error> {
|
||||||
|
company_queries::remove_company(pool, id).await
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::{
|
||||||
|
company_queries,
|
||||||
|
storage::{User, WorkTime},
|
||||||
|
user_queries,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateEmployeeRequest {
|
||||||
|
pub company_id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub preference: WorkTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EmployeeModelGuide;
|
||||||
|
|
||||||
|
pub fn create_employee(
|
||||||
|
pool: &PgPool,
|
||||||
|
Json(payload): Json<CreateEmployeeRequest>,
|
||||||
|
) -> ApiResult<Json<crate::database::storage::User>> {
|
||||||
|
let user = user_queries::add_user(
|
||||||
|
&state.pool,
|
||||||
|
&payload.name,
|
||||||
|
&payload.last_name,
|
||||||
|
payload.preference,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
company_queries::add_company_user(pool, payload.company_id, user.id).await?;
|
||||||
|
Ok(Json(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register_employee_preference(
|
||||||
|
pool: &PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
preference: WorkTime,
|
||||||
|
) -> Result<User, sqlx::Error> {
|
||||||
|
user_queries::update_preference(pool, user_id, preference).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_employee_names_by_company(
|
||||||
|
pool: &PgPool,
|
||||||
|
company_name: &str,
|
||||||
|
) -> Result<Vec<String>, sqlx::Error> {
|
||||||
|
let company = company_queries::get_company_by_name(pool, company_name).await?;
|
||||||
|
let users = company_queries::get_users_by_company(pool, company.id).await?;
|
||||||
|
|
||||||
|
Ok(users
|
||||||
|
.into_iter()
|
||||||
|
.map(|user| format!("{} {}", user.name, user.last_name))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_user_into_company(user_id: i32, company: &string) {
|
||||||
|
let company = company_queries::get_company_by_name(pool, company_name).await?;
|
||||||
|
company_queries::add_company_user(pool, company.id, user_id);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod company;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// import database::storage::Schedule for saved schedule rows
|
||||||
|
// import database::storage::DayOfWeek and WorkTime for schedule grouping
|
||||||
|
// import user_queries when reading and writing schedule rows
|
||||||
|
|
||||||
|
pub struct ScheduleModelGuide;
|
||||||
|
|
||||||
|
pub fn define_schedule_fields() {
|
||||||
|
// add user_id to identify the employee
|
||||||
|
// add company_id if schedules need to be filtered by company
|
||||||
|
// add day_of_week for the assigned day
|
||||||
|
// add shift value for morning, afternoon, or evening
|
||||||
|
// add enter_time and out_time for clock-in and clock-out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_user_enter_and_out() {
|
||||||
|
// check if the employee already worked on the same day
|
||||||
|
// reject the entry if the employee already has a schedule for that day
|
||||||
|
// check if the employee has already worked five days this week
|
||||||
|
// save enter_time when the employee starts working
|
||||||
|
// update out_time when the employee leaves
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output_weekly_schedule() {
|
||||||
|
// group saved schedules by day of week
|
||||||
|
// inside each day, group schedules by shift
|
||||||
|
// list employee names for each shift
|
||||||
|
// mark missing coverage when a shift has fewer than two employees
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// import database::types::WorkTime for morning, afternoon, and evening shifts
|
||||||
|
|
||||||
|
pub struct ShiftModelGuide;
|
||||||
|
|
||||||
|
pub fn define_shift_values() {
|
||||||
|
// use one value for morning shift
|
||||||
|
// use one value for afternoon shift
|
||||||
|
// use one value for evening shift
|
||||||
|
// keep the names consistent with the database enum values
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn define_day_values() {
|
||||||
|
// use Sunday through Saturday from database::storage::DayOfWeek
|
||||||
|
// loop over all seven days when generating the weekly schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_shift_capacity() {
|
||||||
|
// count how many employees are assigned to one shift on one day
|
||||||
|
// reject or fill the shift if fewer than two employees are assigned
|
||||||
|
// detect conflicts when a preferred shift is already full
|
||||||
|
}
|
||||||
+660
@@ -0,0 +1,660 @@
|
|||||||
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
extract::{Path, State},
|
||||||
|
routing::{get, post, put},
|
||||||
|
};
|
||||||
|
use chrono::{Datelike, NaiveDateTime, Utc, Weekday};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{ApiResult, AppState},
|
||||||
|
database::{
|
||||||
|
entities::{Company, CompanyUser, Schedule, User, WeeklyPreference},
|
||||||
|
queries::{
|
||||||
|
company as company_queries, employee as employee_queries,
|
||||||
|
preference as preference_queries, schedule as schedule_queries, user as user_queries,
|
||||||
|
},
|
||||||
|
types::{DayOfWeek, WorkTime},
|
||||||
|
},
|
||||||
|
models::company::{requests::CreateCompanyRequest, service as company_service},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
struct HealthResponse {
|
||||||
|
status: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct CreateEmployeeRequest {
|
||||||
|
company_id: i32,
|
||||||
|
name: String,
|
||||||
|
last_name: String,
|
||||||
|
preference: WorkTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct RegisterPreferenceRequest {
|
||||||
|
preference: WorkTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct WeeklyPreferenceRequest {
|
||||||
|
preferences: Vec<DayPreferenceRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct DayPreferenceRequest {
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
first: WorkTime,
|
||||||
|
second: WorkTime,
|
||||||
|
third: WorkTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct CreateScheduleRequest {
|
||||||
|
user_id: i32,
|
||||||
|
enter_time: NaiveDateTime,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct ClockOutRequest {
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, utoipa::ToSchema)]
|
||||||
|
struct GenerateWeeklyScheduleRequest {
|
||||||
|
company_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
struct EmployeeResponse {
|
||||||
|
id: i32,
|
||||||
|
company_id: i32,
|
||||||
|
name: String,
|
||||||
|
last_name: String,
|
||||||
|
preference: WorkTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
struct ScheduleResponse {
|
||||||
|
id: i32,
|
||||||
|
user_id: i32,
|
||||||
|
enter_time: NaiveDateTime,
|
||||||
|
day_of_week: DayOfWeek,
|
||||||
|
out_time: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
struct WeeklyScheduleResponse {
|
||||||
|
schedules: Vec<ScheduleResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
struct EmployeeNamesResponse {
|
||||||
|
employees: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
health_check,
|
||||||
|
create_company,
|
||||||
|
list_companies,
|
||||||
|
get_company,
|
||||||
|
delete_company,
|
||||||
|
create_employee,
|
||||||
|
list_employees,
|
||||||
|
assign_employee_to_company,
|
||||||
|
delete_employee,
|
||||||
|
register_employee_preference,
|
||||||
|
register_weekly_employee_preferences,
|
||||||
|
get_employee_names_by_company,
|
||||||
|
create_schedule,
|
||||||
|
get_weekly_schedule,
|
||||||
|
generate_weekly_schedule,
|
||||||
|
clock_in,
|
||||||
|
clock_out
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(
|
||||||
|
Company,
|
||||||
|
CompanyUser,
|
||||||
|
CreateCompanyRequest,
|
||||||
|
HealthResponse,
|
||||||
|
CreateEmployeeRequest,
|
||||||
|
RegisterPreferenceRequest,
|
||||||
|
WeeklyPreferenceRequest,
|
||||||
|
DayPreferenceRequest,
|
||||||
|
CreateScheduleRequest,
|
||||||
|
ClockOutRequest,
|
||||||
|
GenerateWeeklyScheduleRequest,
|
||||||
|
EmployeeResponse,
|
||||||
|
EmployeeNamesResponse,
|
||||||
|
ScheduleResponse,
|
||||||
|
WeeklyScheduleResponse,
|
||||||
|
WorkTime,
|
||||||
|
DayOfWeek
|
||||||
|
)
|
||||||
|
),
|
||||||
|
tags(
|
||||||
|
(name = "health", description = "Application health endpoints"),
|
||||||
|
(name = "companies", description = "Company management endpoints"),
|
||||||
|
(name = "employees", description = "Employee endpoints to implement"),
|
||||||
|
(name = "schedules", description = "Schedule endpoints to implement")
|
||||||
|
),
|
||||||
|
servers(
|
||||||
|
(url = "{{rust_url}}", description = "Rust API base URL")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
struct ApiDoc;
|
||||||
|
|
||||||
|
pub fn build_router(pool: PgPool) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/health", get(health_check))
|
||||||
|
.route("/companies", post(create_company).get(list_companies))
|
||||||
|
.route("/companies/{id}", get(get_company).delete(delete_company))
|
||||||
|
.route("/employees", post(create_employee).get(list_employees))
|
||||||
|
.route(
|
||||||
|
"/companies/{company_id}/employees/{employee_id}",
|
||||||
|
post(assign_employee_to_company),
|
||||||
|
)
|
||||||
|
.route("/employees/{id}", axum::routing::delete(delete_employee))
|
||||||
|
.route(
|
||||||
|
"/employees/{id}/preference",
|
||||||
|
put(register_employee_preference),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/employees/{id}/weekly-preferences",
|
||||||
|
put(register_weekly_employee_preferences),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/companies/{company_name}/employees/names",
|
||||||
|
get(get_employee_names_by_company),
|
||||||
|
)
|
||||||
|
.route("/schedules", post(create_schedule))
|
||||||
|
.route("/schedules/weekly", get(get_weekly_schedule))
|
||||||
|
.route("/schedules/weekly/generate", post(generate_weekly_schedule))
|
||||||
|
.route("/employees/{id}/clock-in", post(clock_in))
|
||||||
|
.route("/employees/{id}/clock-out", put(clock_out))
|
||||||
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
|
.with_state(AppState { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn day_of_week_from_weekday(weekday: Weekday) -> DayOfWeek {
|
||||||
|
match weekday {
|
||||||
|
Weekday::Mon => DayOfWeek::Monday,
|
||||||
|
Weekday::Tue => DayOfWeek::Tuesday,
|
||||||
|
Weekday::Wed => DayOfWeek::Wednesday,
|
||||||
|
Weekday::Thu => DayOfWeek::Thursday,
|
||||||
|
Weekday::Fri => DayOfWeek::Friday,
|
||||||
|
Weekday::Sat => DayOfWeek::Saturday,
|
||||||
|
Weekday::Sun => DayOfWeek::Sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_response(schedule: Schedule) -> ScheduleResponse {
|
||||||
|
ScheduleResponse {
|
||||||
|
id: schedule.id,
|
||||||
|
user_id: schedule.user_id,
|
||||||
|
enter_time: schedule.enter_time,
|
||||||
|
day_of_week: schedule.day_of_week,
|
||||||
|
out_time: schedule.out_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn employee_response(employee: employee_queries::EmployeeRow) -> EmployeeResponse {
|
||||||
|
EmployeeResponse {
|
||||||
|
id: employee.id,
|
||||||
|
company_id: employee.company_id,
|
||||||
|
name: employee.name,
|
||||||
|
last_name: employee.last_name,
|
||||||
|
preference: employee.preference,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_days() -> [DayOfWeek; 7] {
|
||||||
|
[
|
||||||
|
DayOfWeek::Monday,
|
||||||
|
DayOfWeek::Tuesday,
|
||||||
|
DayOfWeek::Wednesday,
|
||||||
|
DayOfWeek::Thursday,
|
||||||
|
DayOfWeek::Friday,
|
||||||
|
DayOfWeek::Saturday,
|
||||||
|
DayOfWeek::Sunday,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_shifts() -> [WorkTime; 3] {
|
||||||
|
[WorkTime::Morning, WorkTime::Afternoon, WorkTime::Evening]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preference_rank(
|
||||||
|
preference: Option<&WeeklyPreference>,
|
||||||
|
shift: WorkTime,
|
||||||
|
fallback: WorkTime,
|
||||||
|
) -> i32 {
|
||||||
|
match preference {
|
||||||
|
Some(preference) if preference.first_preference == shift => 0,
|
||||||
|
Some(preference) if preference.second_preference == shift => 1,
|
||||||
|
Some(preference) if preference.third_preference == shift => 2,
|
||||||
|
_ if fallback == shift => 3,
|
||||||
|
_ => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/health",
|
||||||
|
tag = "health",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Application is healthy", body = HealthResponse)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn health_check() -> Json<HealthResponse> {
|
||||||
|
Json(HealthResponse { status: "ok" })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/companies",
|
||||||
|
tag = "companies",
|
||||||
|
request_body = CreateCompanyRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Company created", body = Company),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn create_company(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<CreateCompanyRequest>,
|
||||||
|
) -> ApiResult<Json<Company>> {
|
||||||
|
let company = company_service::create_company(&state.pool, payload).await?;
|
||||||
|
Ok(Json(company))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/companies",
|
||||||
|
tag = "companies",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Companies found", body = [Company]),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn list_companies(State(state): State<AppState>) -> ApiResult<Json<Vec<Company>>> {
|
||||||
|
let companies = company_service::list_companies(&state.pool).await?;
|
||||||
|
Ok(Json(companies))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/companies/{id}",
|
||||||
|
tag = "companies",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Company id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Company found", body = Company),
|
||||||
|
(status = 404, description = "Company not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_company(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
) -> ApiResult<Json<Company>> {
|
||||||
|
let company = company_service::get_company(&state.pool, id).await?;
|
||||||
|
Ok(Json(company))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/companies/{id}",
|
||||||
|
tag = "companies",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Company id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Company deleted", body = Company),
|
||||||
|
(status = 404, description = "Company not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn delete_company(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
) -> ApiResult<Json<Company>> {
|
||||||
|
let company = company_service::delete_company(&state.pool, id).await?;
|
||||||
|
Ok(Json(company))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/employees",
|
||||||
|
tag = "employees",
|
||||||
|
request_body = CreateEmployeeRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Employee created", body = EmployeeResponse),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn create_employee(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<CreateEmployeeRequest>,
|
||||||
|
) -> ApiResult<Json<EmployeeResponse>> {
|
||||||
|
let user = user_queries::add_user(
|
||||||
|
&state.pool,
|
||||||
|
payload.name.trim(),
|
||||||
|
payload.last_name.trim(),
|
||||||
|
payload.preference,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
company_queries::add_company_user(&state.pool, payload.company_id, user.id).await?;
|
||||||
|
let employee = employee_queries::get_employee(&state.pool, user.id).await?;
|
||||||
|
Ok(Json(employee_response(employee)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/employees",
|
||||||
|
tag = "employees",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Employees found", body = [EmployeeResponse]),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn list_employees(State(state): State<AppState>) -> ApiResult<Json<Vec<EmployeeResponse>>> {
|
||||||
|
let employees = employee_queries::list_employees(&state.pool).await?;
|
||||||
|
Ok(Json(employees.into_iter().map(employee_response).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/companies/{company_id}/employees/{employee_id}",
|
||||||
|
tag = "employees",
|
||||||
|
params(
|
||||||
|
("company_id" = i32, Path, description = "Company id"),
|
||||||
|
("employee_id" = i32, Path, description = "Employee id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Employee assigned to company", body = CompanyUser),
|
||||||
|
(status = 404, description = "Company or employee not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn assign_employee_to_company(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path((company_id, employee_id)): Path<(i32, i32)>,
|
||||||
|
) -> ApiResult<Json<CompanyUser>> {
|
||||||
|
company_queries::get_company_by_id(&state.pool, company_id).await?;
|
||||||
|
employee_queries::get_employee_user(&state.pool, employee_id).await?;
|
||||||
|
let company_user =
|
||||||
|
company_queries::add_company_user(&state.pool, company_id, employee_id).await?;
|
||||||
|
Ok(Json(company_user))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/employees/{id}",
|
||||||
|
tag = "employees",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Employee id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Employee deleted", body = EmployeeResponse),
|
||||||
|
(status = 404, description = "Employee not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn delete_employee(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
) -> ApiResult<Json<EmployeeResponse>> {
|
||||||
|
let employee = employee_queries::get_employee(&state.pool, id).await?;
|
||||||
|
user_queries::remove_user(&state.pool, id).await?;
|
||||||
|
Ok(Json(employee_response(employee)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/employees/{id}/preference",
|
||||||
|
tag = "employees",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Employee id")
|
||||||
|
),
|
||||||
|
request_body = RegisterPreferenceRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Employee preference saved", body = EmployeeResponse),
|
||||||
|
(status = 404, description = "Employee not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn register_employee_preference(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Json(payload): Json<RegisterPreferenceRequest>,
|
||||||
|
) -> ApiResult<Json<EmployeeResponse>> {
|
||||||
|
user_queries::update_preference(&state.pool, id, payload.preference).await?;
|
||||||
|
let employee = employee_queries::get_employee(&state.pool, id).await?;
|
||||||
|
Ok(Json(employee_response(employee)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/employees/{id}/weekly-preferences",
|
||||||
|
tag = "employees",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Employee id")
|
||||||
|
),
|
||||||
|
request_body = WeeklyPreferenceRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Weekly preferences saved", body = EmployeeResponse),
|
||||||
|
(status = 404, description = "Employee not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn register_weekly_employee_preferences(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Json(payload): Json<WeeklyPreferenceRequest>,
|
||||||
|
) -> ApiResult<Json<EmployeeResponse>> {
|
||||||
|
employee_queries::get_employee_user(&state.pool, id).await?;
|
||||||
|
for preference in payload.preferences {
|
||||||
|
preference_queries::upsert_weekly_preference(
|
||||||
|
&state.pool,
|
||||||
|
id,
|
||||||
|
preference.day_of_week,
|
||||||
|
preference.first,
|
||||||
|
preference.second,
|
||||||
|
preference.third,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let employee = employee_queries::get_employee(&state.pool, id).await?;
|
||||||
|
Ok(Json(employee_response(employee)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/companies/{company_name}/employees/names",
|
||||||
|
tag = "employees",
|
||||||
|
params(
|
||||||
|
("company_name" = String, Path, description = "Company name")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Employee names for company", body = EmployeeNamesResponse),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_employee_names_by_company(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(company_name): Path<String>,
|
||||||
|
) -> ApiResult<Json<EmployeeNamesResponse>> {
|
||||||
|
let employees =
|
||||||
|
employee_queries::get_employee_names_by_company_name(&state.pool, &company_name).await?;
|
||||||
|
Ok(Json(EmployeeNamesResponse { employees }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/schedules",
|
||||||
|
tag = "schedules",
|
||||||
|
request_body = CreateScheduleRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Schedule created", body = ScheduleResponse),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn create_schedule(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<CreateScheduleRequest>,
|
||||||
|
) -> ApiResult<Json<ScheduleResponse>> {
|
||||||
|
let schedule = schedule_queries::add_schedule(
|
||||||
|
&state.pool,
|
||||||
|
payload.user_id,
|
||||||
|
payload.enter_time,
|
||||||
|
payload.day_of_week,
|
||||||
|
payload.out_time,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(schedule_response(schedule)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/schedules/weekly",
|
||||||
|
tag = "schedules",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Weekly schedule found", body = WeeklyScheduleResponse),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_weekly_schedule(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> ApiResult<Json<WeeklyScheduleResponse>> {
|
||||||
|
let schedules = schedule_queries::list_schedules(&state.pool).await?;
|
||||||
|
Ok(Json(WeeklyScheduleResponse {
|
||||||
|
schedules: schedules.into_iter().map(schedule_response).collect(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/schedules/weekly/generate",
|
||||||
|
tag = "schedules",
|
||||||
|
request_body = GenerateWeeklyScheduleRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Weekly schedule generated", body = WeeklyScheduleResponse),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn generate_weekly_schedule(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<GenerateWeeklyScheduleRequest>,
|
||||||
|
) -> ApiResult<Json<WeeklyScheduleResponse>> {
|
||||||
|
let employees = company_queries::get_users_by_company(&state.pool, payload.company_id).await?;
|
||||||
|
let preferences =
|
||||||
|
preference_queries::get_preferences_for_company(&state.pool, payload.company_id).await?;
|
||||||
|
let mut preferences_by_user_day = HashMap::new();
|
||||||
|
|
||||||
|
for preference in &preferences {
|
||||||
|
preferences_by_user_day.insert((preference.user_id, preference.day_of_week), preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule_queries::delete_schedules_for_company(&state.pool, payload.company_id).await?;
|
||||||
|
|
||||||
|
let mut assigned_days: HashMap<(i32, DayOfWeek), bool> = HashMap::new();
|
||||||
|
let mut day_count_by_user: HashMap<i32, i32> = HashMap::new();
|
||||||
|
let mut generated = Vec::new();
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
for day in all_days() {
|
||||||
|
for shift in all_shifts() {
|
||||||
|
let mut candidates: Vec<&User> = employees
|
||||||
|
.iter()
|
||||||
|
.filter(|employee| !assigned_days.contains_key(&(employee.id, day)))
|
||||||
|
.filter(|employee| day_count_by_user.get(&employee.id).copied().unwrap_or(0) < 5)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
candidates.sort_by_key(|employee| {
|
||||||
|
preference_rank(
|
||||||
|
preferences_by_user_day.get(&(employee.id, day)).copied(),
|
||||||
|
shift,
|
||||||
|
employee.preference,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
for employee in candidates.into_iter().take(2) {
|
||||||
|
let shift_end = now + chrono::Duration::hours(8);
|
||||||
|
let schedule =
|
||||||
|
schedule_queries::add_schedule(&state.pool, employee.id, now, day, shift_end)
|
||||||
|
.await?;
|
||||||
|
assigned_days.insert((employee.id, day), true);
|
||||||
|
*day_count_by_user.entry(employee.id).or_insert(0) += 1;
|
||||||
|
generated.push(schedule_response(schedule));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Json(WeeklyScheduleResponse {
|
||||||
|
schedules: generated,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/employees/{id}/clock-in",
|
||||||
|
tag = "schedules",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Employee id")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Clock-in registered", body = ScheduleResponse),
|
||||||
|
(status = 404, description = "Employee not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn clock_in(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
) -> ApiResult<Json<ScheduleResponse>> {
|
||||||
|
let now_utc = Utc::now();
|
||||||
|
let day_of_week = day_of_week_from_weekday(now_utc.weekday());
|
||||||
|
let schedule =
|
||||||
|
schedule_queries::clock_in(&state.pool, id, now_utc.naive_utc(), day_of_week).await?;
|
||||||
|
Ok(Json(schedule_response(schedule)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/employees/{id}/clock-out",
|
||||||
|
tag = "schedules",
|
||||||
|
params(
|
||||||
|
("id" = i32, Path, description = "Employee id")
|
||||||
|
),
|
||||||
|
request_body = ClockOutRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Clock-out registered", body = ScheduleResponse),
|
||||||
|
(status = 404, description = "Open clock-in record not found"),
|
||||||
|
(status = 500, description = "Database error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn clock_out(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
Json(payload): Json<ClockOutRequest>,
|
||||||
|
) -> ApiResult<Json<ScheduleResponse>> {
|
||||||
|
let schedule =
|
||||||
|
schedule_queries::clock_out(&state.pool, id, payload.day_of_week, payload.out_time).await?;
|
||||||
|
Ok(Json(schedule_response(schedule)))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user