adding endpoints

This commit is contained in:
2026-09-18 12:45:11 -04:00
commit 29c8d0f27d
34 changed files with 1842 additions and 0 deletions
+16
View File
@@ -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
View File
@@ -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"] }
+18
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
# manager_employees_rust
+44
View File
@@ -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()
}
}
+18
View File
@@ -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(())
}
+10
View File
@@ -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,
}
+11
View File
@@ -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,
}
+11
View File
@@ -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;
+15
View File
@@ -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,
}
+14
View File
@@ -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,
}
+5
View File
@@ -0,0 +1,5 @@
pub mod connection;
pub mod entities;
pub mod queries;
pub mod schema;
pub mod types;
+121
View File
@@ -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
}
+72
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
pub mod company;
pub mod employee;
pub mod preference;
pub mod schedule;
pub mod user;
+64
View File
@@ -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
}
+134
View File
@@ -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
}
+145
View File
@@ -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
}
+104
View File
@@ -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,
];
+47
View File
@@ -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
View File
@@ -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
}
+28
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
pub mod conflict;
pub mod scheduler;
pub mod validator;
+49
View File
@@ -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
}
+34
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
pub mod requests;
pub mod service;
+6
View File
@@ -0,0 +1,6 @@
use serde::Deserialize;
#[derive(Deserialize, utoipa::ToSchema)]
pub struct CreateCompanyRequest {
pub name: String,
}
+25
View File
@@ -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
}
+58
View File
@@ -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);
}
+1
View File
@@ -0,0 +1 @@
pub mod company;
+28
View File
@@ -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
}
+21
View File
@@ -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
View File
@@ -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)))
}