adding the queries for the scheduler
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL=postgres://postgres:password@localhost/rust_manager_employees
|
||||||
+14
@@ -1 +1,15 @@
|
|||||||
|
### Rust ###
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
/target
|
/target
|
||||||
|
# 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
|
||||||
|
|||||||
Generated
+2567
File diff suppressed because it is too large
Load Diff
@@ -4,3 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = "0.4.42"
|
||||||
|
crossterm = "0.29.0"
|
||||||
|
ratatui = "0.30.2"
|
||||||
|
runtime-tokio = "0.0.0"
|
||||||
|
sqlx = { version = "0.9.0", features = ["chrono", "derive", "postgres", "runtime-tokio"] }
|
||||||
|
tokio = "1.53.1"
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::storage::{Company, CompanyUser};
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod company_queries;
|
||||||
|
pub mod storage;
|
||||||
|
pub mod user_queries;
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
|
||||||
|
#[sqlx(type_name = "work_time", rename_all = "snake_case")]
|
||||||
|
pub enum WorkTime {
|
||||||
|
Morning,
|
||||||
|
Evening,
|
||||||
|
Night,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type)]
|
||||||
|
#[sqlx(type_name = "day_of_week", rename_all = "snake_case")]
|
||||||
|
pub enum DayOfWeek {
|
||||||
|
Sunday,
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
pub preference: WorkTime,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct CompanyUser {
|
||||||
|
pub id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub company_id: i32,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
pub struct Company {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub date_created: NaiveDateTime,
|
||||||
|
pub date_updated: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const CREATE_WORK_TIME_ENUM: &str = r#"
|
||||||
|
CREATE TYPE work_time AS ENUM ('morning', 'evening', 'night');
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_DAY_OF_WEEK_ENUM: &str = r#"
|
||||||
|
CREATE TYPE day_of_week AS ENUM (
|
||||||
|
'sunday',
|
||||||
|
'monday',
|
||||||
|
'tuesday',
|
||||||
|
'wednesday',
|
||||||
|
'thursday',
|
||||||
|
'friday',
|
||||||
|
'saturday'
|
||||||
|
);
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub const CREATE_COMPANIES_TABLE: &str = r#"
|
||||||
|
CREATE TABLE 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 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 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 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_SCHEMA_QUERIES: [&str; 6] = [
|
||||||
|
CREATE_WORK_TIME_ENUM,
|
||||||
|
CREATE_DAY_OF_WEEK_ENUM,
|
||||||
|
CREATE_COMPANIES_TABLE,
|
||||||
|
CREATE_USERS_TABLE,
|
||||||
|
CREATE_COMPANY_USERS_TABLE,
|
||||||
|
CREATE_SCHEDULES_TABLE,
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::database::storage::{DayOfWeek, Schedule, User, 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 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: NaiveDateTime,
|
||||||
|
) -> Result<Schedule, sqlx::Error> {
|
||||||
|
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 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, out_time, date_created, date_updated
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(day_of_week)
|
||||||
|
.bind(out_time)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
+143
-2
@@ -1,3 +1,144 @@
|
|||||||
fn main() {
|
use std::io;
|
||||||
println!("Hello, world!");
|
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use ratatui::{
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::Rect,
|
||||||
|
style::Stylize,
|
||||||
|
symbols::border,
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::{Block, Paragraph, Widget},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
ratatui::run(|terminal| App::default().run(terminal))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct App {
|
||||||
|
counter: u8,
|
||||||
|
exit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// runs the application's main loop until the user quits
|
||||||
|
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||||
|
while !self.exit {
|
||||||
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
|
frame.render_widget(self, frame.area());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// updates the application's state based on user input
|
||||||
|
fn handle_events(&mut self) -> io::Result<()> {
|
||||||
|
match event::read()? {
|
||||||
|
// it's important to check that the event is a key press event as
|
||||||
|
// crossterm also emits key release and repeat events on Windows.
|
||||||
|
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||||
|
self.handle_key_event(key_event)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Char('q') => self.exit(),
|
||||||
|
KeyCode::Left => self.decrement_counter(),
|
||||||
|
KeyCode::Right => self.increment_counter(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&mut self) {
|
||||||
|
self.exit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_counter(&mut self) {
|
||||||
|
self.counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_counter(&mut self) {
|
||||||
|
self.counter -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let title = Line::from(" Counter App Tutorial ".bold());
|
||||||
|
let instructions = Line::from(vec![
|
||||||
|
" Decrement ".into(),
|
||||||
|
"<Left>".blue().bold(),
|
||||||
|
" Increment ".into(),
|
||||||
|
"<Right>".blue().bold(),
|
||||||
|
" Quit ".into(),
|
||||||
|
"<Q> ".blue().bold(),
|
||||||
|
]);
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title(title.centered())
|
||||||
|
.title_bottom(instructions.centered())
|
||||||
|
.border_set(border::THICK);
|
||||||
|
|
||||||
|
let counter_text = Text::from(vec![Line::from(vec![
|
||||||
|
"Value: ".into(),
|
||||||
|
self.counter.to_string().yellow(),
|
||||||
|
])]);
|
||||||
|
|
||||||
|
Paragraph::new(counter_text)
|
||||||
|
.centered()
|
||||||
|
.block(block)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use ratatui::style::Style;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render() {
|
||||||
|
let app = App::default();
|
||||||
|
let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4));
|
||||||
|
|
||||||
|
app.render(buf.area, &mut buf);
|
||||||
|
|
||||||
|
let mut expected = Buffer::with_lines(vec![
|
||||||
|
"┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓",
|
||||||
|
"┃ Value: 0 ┃",
|
||||||
|
"┃ ┃",
|
||||||
|
"┗━ Decrement <Left> Increment <Right> Quit <Q> ━━┛",
|
||||||
|
]);
|
||||||
|
let title_style = Style::new().bold();
|
||||||
|
let counter_style = Style::new().yellow();
|
||||||
|
let key_style = Style::new().blue().bold();
|
||||||
|
expected.set_style(Rect::new(14, 0, 22, 1), title_style);
|
||||||
|
expected.set_style(Rect::new(28, 1, 1, 1), counter_style);
|
||||||
|
expected.set_style(Rect::new(13, 3, 6, 1), key_style);
|
||||||
|
expected.set_style(Rect::new(30, 3, 7, 1), key_style);
|
||||||
|
expected.set_style(Rect::new(43, 3, 4, 1), key_style);
|
||||||
|
|
||||||
|
assert_eq!(buf, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handle_key_event() {
|
||||||
|
let mut app = App::default();
|
||||||
|
app.handle_key_event(KeyCode::Right.into());
|
||||||
|
assert_eq!(app.counter, 1);
|
||||||
|
|
||||||
|
app.handle_key_event(KeyCode::Left.into());
|
||||||
|
assert_eq!(app.counter, 0);
|
||||||
|
|
||||||
|
let mut app = App::default();
|
||||||
|
app.handle_key_event(KeyCode::Char('q').into());
|
||||||
|
assert!(app.exit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub conflicts;
|
||||||
|
pub scheduler;
|
||||||
|
pub validator;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub employee;
|
||||||
|
pub schedule;
|
||||||
|
pub shift;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub event;
|
||||||
|
pub ui;
|
||||||
|
pub screens;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub employees;
|
||||||
|
pub menu;
|
||||||
|
pub preferences;
|
||||||
|
pub schedule;
|
||||||
|
|||||||
Reference in New Issue
Block a user