Add basic employee hour balance report

This commit is contained in:
Simon Goller 2024-06-23 18:12:54 +02:00
parent 0eb885216a
commit d4adcb182f
31 changed files with 2155 additions and 5 deletions

View file

@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "SELECT id, sales_person_id, amount, category, description, date_time, deleted FROM extra_hours WHERE sales_person_id = ? AND strftime('%Y', date_time) = ? AND strftime('%m', date_time) <= ?",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "sales_person_id",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "amount",
"ordinal": 2,
"type_info": "Float"
},
{
"name": "category",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "date_time",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "deleted",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
true,
false,
true
]
},
"hash": "0bae9db9d14012e1b614e2f63b3704cac2c8c030fd05637d5d877060f7f59912"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO working_hours (\n id,\n sales_person_id,\n expected_hours,\n from_calendar_week,\n from_year,\n to_calendar_week,\n to_year,\n created,\n update_process,\n update_version\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "0e0d06ab641ad595b8626e89d876c92bff41c677b050393eb210fe3c8eb47b7d"
}

View file

@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n sales_person.id as sales_person_id,\n sum((STRFTIME('%H', slot.time_to) + STRFTIME('%M', slot.time_to) / 60.0) - (STRFTIME('%H', slot.time_from) + STRFTIME('%M', slot.time_from))) as hours,\n booking.year\n FROM slot\n INNER JOIN booking ON (booking.slot_id = slot.id AND booking.deleted IS NULL)\n INNER JOIN sales_person ON booking.sales_person_id = sales_person.id\n WHERE booking.year = ?\n AND booking.calendar_week <= ?\n GROUP BY sales_person_id, year\n ",
"describe": {
"columns": [
{
"name": "sales_person_id",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "hours",
"ordinal": 1,
"type_info": "Float"
},
{
"name": "year",
"ordinal": 2,
"type_info": "Int64"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
true,
false
]
},
"hash": "3834c9f6fc72c8d6ab28f01f0607e830240201908048364d8c85246f22929b9b"
}

View file

@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n sales_person_id,\n expected_hours,\n from_calendar_week,\n from_year,\n to_calendar_week,\n to_year,\n created,\n deleted,\n update_version\n FROM\n working_hours\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "sales_person_id",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "expected_hours",
"ordinal": 2,
"type_info": "Float"
},
{
"name": "from_calendar_week",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "from_year",
"ordinal": 4,
"type_info": "Int64"
},
{
"name": "to_calendar_week",
"ordinal": 5,
"type_info": "Int64"
},
{
"name": "to_year",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "created",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "deleted",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "update_version",
"ordinal": 9,
"type_info": "Blob"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "53ab5252dae2a4bbbb5f098bcdb9ee7d55b61ef54b82b80310bea94276dfb7b2"
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE working_hours SET\n deleted = ?,\n update_process = ?\n WHERE\n id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "a49a3abf9187ba8d16e2e6cc0d6898a2acb26fe5639f8b2351854eb21ffa85d5"
}

View file

@ -0,0 +1,74 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n sales_person_id,\n expected_hours,\n from_calendar_week,\n from_year,\n to_calendar_week,\n to_year,\n created,\n deleted,\n update_version\n FROM\n working_hours\n WHERE\n sales_person_id = ?\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "sales_person_id",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "expected_hours",
"ordinal": 2,
"type_info": "Float"
},
{
"name": "from_calendar_week",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "from_year",
"ordinal": 4,
"type_info": "Int64"
},
{
"name": "to_calendar_week",
"ordinal": 5,
"type_info": "Int64"
},
{
"name": "to_year",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "created",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "deleted",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "update_version",
"ordinal": 9,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false
]
},
"hash": "a85db22630c8d55b5eb6be027cca9016d77615573d141348015f7c69021f1581"
}

View file

@ -0,0 +1,44 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n sales_person.id as sales_person_id,\n sum((STRFTIME('%H', slot.time_to) + STRFTIME('%M', slot.time_to) / 60.0) - (STRFTIME('%H', slot.time_from) + STRFTIME('%M', slot.time_from))) as hours,\n booking.calendar_week, booking.year, slot.day_of_week\n FROM slot\n INNER JOIN booking ON (booking.slot_id = slot.id AND booking.deleted IS NULL)\n INNER JOIN sales_person ON booking.sales_person_id = sales_person.id\n WHERE sales_person.id = ?\n AND booking.year = ?\n AND booking.calendar_week <= ?\n GROUP BY year, calendar_week, day_of_week\n ",
"describe": {
"columns": [
{
"name": "sales_person_id",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "hours",
"ordinal": 1,
"type_info": "Float"
},
{
"name": "calendar_week",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "year",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "day_of_week",
"ordinal": 4,
"type_info": "Int64"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
true,
false,
false,
false
]
},
"hash": "c4cb7c4ac52ee81fb6f61e2399b308d2ac6e11103dd2cfb0f8db60d562ff03df"
}

View file

@ -1,5 +1,9 @@
use std::sync::Arc;
use dao_impl::{
extra_hours::ExtraHoursDaoImpl, shiftplan_report::ShiftplanReportDaoImpl,
working_hours::WorkingHoursDaoImpl,
};
use sqlx::SqlitePool;
#[cfg(feature = "mock_auth")]
@ -30,6 +34,21 @@ type BookingService = service_impl::booking::BookingServiceImpl<
SalesPersonService,
SlotService,
>;
type ReportingService = service_impl::reporting::ReportingServiceImpl<
dao_impl::extra_hours::ExtraHoursDaoImpl,
dao_impl::shiftplan_report::ShiftplanReportDaoImpl,
dao_impl::working_hours::WorkingHoursDaoImpl,
SalesPersonService,
PermissionService,
ClockService,
UuidService,
>;
type WorkingHoursService = service_impl::working_hours::WorkingHoursServiceImpl<
dao_impl::working_hours::WorkingHoursDaoImpl,
PermissionService,
ClockService,
UuidService,
>;
#[derive(Clone)]
pub struct RestStateImpl {
@ -38,6 +57,8 @@ pub struct RestStateImpl {
slot_service: Arc<SlotService>,
sales_person_service: Arc<SalesPersonService>,
booking_service: Arc<BookingService>,
reporting_service: Arc<ReportingService>,
working_hours_service: Arc<WorkingHoursService>,
}
impl rest::RestStateDef for RestStateImpl {
type UserService = UserService;
@ -45,6 +66,8 @@ impl rest::RestStateDef for RestStateImpl {
type SlotService = SlotService;
type SalesPersonService = SalesPersonService;
type BookingService = BookingService;
type ReportingService = ReportingService;
type WorkingHoursService = WorkingHoursService;
fn user_service(&self) -> Arc<Self::UserService> {
self.user_service.clone()
@ -61,13 +84,22 @@ impl rest::RestStateDef for RestStateImpl {
fn booking_service(&self) -> Arc<Self::BookingService> {
self.booking_service.clone()
}
fn reporting_service(&self) -> Arc<Self::ReportingService> {
self.reporting_service.clone()
}
fn working_hours_service(&self) -> Arc<Self::WorkingHoursService> {
self.working_hours_service.clone()
}
}
impl RestStateImpl {
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
let permission_dao = dao_impl::PermissionDaoImpl::new(pool.clone());
let slot_dao = dao_impl::slot::SlotDaoImpl::new(pool.clone());
let sales_person_dao = dao_impl::sales_person::SalesPersonDaoImpl::new(pool.clone());
let booking_dao = dao_impl::booking::BookingDaoImpl::new(pool);
let booking_dao = dao_impl::booking::BookingDaoImpl::new(pool.clone());
let extra_hours_dao = Arc::new(ExtraHoursDaoImpl::new(pool.clone()));
let shiftplan_report_dao = Arc::new(ShiftplanReportDaoImpl::new(pool.clone()));
let working_hours_dao = Arc::new(WorkingHoursDaoImpl::new(pool.clone()));
// Always authenticate with DEVUSER during development.
// This is used to test the permission service locally without a login service.
@ -102,17 +134,35 @@ impl RestStateImpl {
let booking_service = Arc::new(service_impl::booking::BookingServiceImpl::new(
booking_dao.into(),
permission_service.clone(),
clock_service,
uuid_service,
clock_service.clone(),
uuid_service.clone(),
sales_person_service.clone(),
slot_service.clone(),
));
let reporting_service = Arc::new(service_impl::reporting::ReportingServiceImpl::new(
extra_hours_dao,
shiftplan_report_dao,
working_hours_dao.clone(),
sales_person_service.clone(),
permission_service.clone(),
clock_service.clone(),
uuid_service.clone(),
));
let working_hours_service =
Arc::new(service_impl::working_hours::WorkingHoursServiceImpl::new(
working_hours_dao,
permission_service.clone(),
clock_service,
uuid_service,
));
Self {
user_service,
permission_service,
slot_service,
sales_person_service,
booking_service,
reporting_service,
working_hours_service,
}
}
}

40
dao/src/extra_hours.rs Normal file
View file

@ -0,0 +1,40 @@
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExtraHoursCategoryEntity {
ExtraWork,
Vacation,
SickLeave,
Holiday,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtraHoursEntity {
pub id: Uuid,
pub sales_person_id: Uuid,
pub amount: f32,
pub category: ExtraHoursCategoryEntity,
pub description: Arc<str>,
pub date_time: time::PrimitiveDateTime,
pub deleted: Option<time::PrimitiveDateTime>,
}
#[automock]
#[async_trait]
pub trait ExtraHoursDao {
async fn find_by_sales_person_id_and_year(
&self,
sales_person_id: Uuid,
year: u32,
until_week: u8,
) -> Result<Arc<[ExtraHoursEntity]>, crate::DaoError>;
async fn create(&self, entity: &ExtraHoursEntity, process: &str)
-> Result<(), crate::DaoError>;
async fn update(&self, entity: &ExtraHoursEntity, process: &str)
-> Result<(), crate::DaoError>;
async fn delete(&self, id: Uuid, process: &str) -> Result<(), crate::DaoError>;
}

View file

@ -5,9 +5,12 @@ use mockall::automock;
use thiserror::Error;
pub mod booking;
pub mod extra_hours;
pub mod permission;
pub mod sales_person;
pub mod shiftplan_report;
pub mod slot;
pub mod working_hours;
pub use permission::MockPermissionDao;
pub use permission::PermissionDao;
@ -28,6 +31,12 @@ pub enum DaoError {
#[error("Date/Time parse error: {0}")]
DateTimeParseError(#[from] time::error::Parse),
#[error("Date/Time format error: {0}")]
DateTimeFormatError(#[from] time::error::Format),
#[error("Enum value not found: {0}")]
EnumValueNotFound(Arc<str>),
}
#[automock]

View file

@ -0,0 +1,41 @@
use async_trait::async_trait;
use mockall::automock;
use std::sync::Arc;
use uuid::Uuid;
use crate::slot::DayOfWeek;
use crate::DaoError;
#[derive(Debug, PartialEq)]
pub struct ShiftplanReportEntity {
pub sales_person_id: Uuid,
pub hours: f32,
pub year: u32,
pub calendar_week: u8,
pub day_of_week: DayOfWeek,
}
pub struct ShiftplanQuickOverviewEntity {
pub sales_person_id: Uuid,
pub hours: f32,
pub year: u32,
}
#[automock]
#[async_trait]
pub trait ShiftplanReportDao {
/// A report which contains the worked hours of a sales person for each day.
async fn extract_shiftplan_report(
&self,
sales_person_id: Uuid,
year: u32,
until_week: u8,
) -> Result<Arc<[ShiftplanReportEntity]>, DaoError>;
/// A report which shows the summed up yearly work hours of all sales persons.
async fn extract_quick_shiftplan_report(
&self,
year: u32,
until_week: u8,
) -> Result<Arc<[ShiftplanQuickOverviewEntity]>, DaoError>;
}

32
dao/src/working_hours.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::DaoError;
use async_trait::async_trait;
use mockall::automock;
use std::sync::Arc;
use time::PrimitiveDateTime;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq)]
pub struct WorkingHoursEntity {
pub id: Uuid,
pub sales_person_id: Uuid,
pub expected_hours: f32,
pub from_calendar_week: u8,
pub from_year: u32,
pub to_calendar_week: u8,
pub to_year: u32,
pub created: PrimitiveDateTime,
pub deleted: Option<PrimitiveDateTime>,
pub version: Uuid,
}
#[automock]
#[async_trait]
pub trait WorkingHoursDao {
async fn all(&self) -> Result<Arc<[WorkingHoursEntity]>, DaoError>;
async fn find_by_sales_person_id(
&self,
sales_person_id: Uuid,
) -> Result<Arc<[WorkingHoursEntity]>, DaoError>;
async fn create(&self, entity: &WorkingHoursEntity, process: &str) -> Result<(), DaoError>;
async fn update(&self, entity: &WorkingHoursEntity, process: &str) -> Result<(), DaoError>;
}

108
dao_impl/src/extra_hours.rs Normal file
View file

@ -0,0 +1,108 @@
use std::sync::Arc;
use crate::ResultDbErrorExt;
use async_trait::async_trait;
use dao::{
extra_hours::{ExtraHoursCategoryEntity, ExtraHoursDao, ExtraHoursEntity},
DaoError,
};
use sqlx::query_as;
use time::{format_description::well_known::Iso8601, PrimitiveDateTime};
use uuid::Uuid;
struct ExtraHoursDb {
id: Vec<u8>,
sales_person_id: Vec<u8>,
amount: f64,
category: String,
description: Option<String>,
date_time: String,
deleted: Option<String>,
}
impl TryFrom<&ExtraHoursDb> for ExtraHoursEntity {
type Error = DaoError;
fn try_from(extra_hours: &ExtraHoursDb) -> Result<Self, DaoError> {
Ok(Self {
id: Uuid::from_slice(extra_hours.id.as_ref()).unwrap(),
sales_person_id: Uuid::from_slice(extra_hours.sales_person_id.as_ref()).unwrap(),
amount: extra_hours.amount as f32,
category: match extra_hours.category.as_str() {
"ExtraWork" => ExtraHoursCategoryEntity::ExtraWork,
"Vacation" => ExtraHoursCategoryEntity::Vacation,
"SickLeave" => ExtraHoursCategoryEntity::SickLeave,
"Holiday" => ExtraHoursCategoryEntity::Holiday,
value @ _ => return Err(DaoError::EnumValueNotFound(value.into())),
},
description: extra_hours
.description
.clone()
.unwrap_or_else(|| String::new())
.as_str()
.into(),
date_time: PrimitiveDateTime::parse(
extra_hours.date_time.as_str(),
&Iso8601::DATE_TIME,
)
.unwrap(),
deleted: extra_hours
.deleted
.as_ref()
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE_TIME))
.transpose()
.unwrap(),
})
}
}
pub struct ExtraHoursDaoImpl {
pub pool: Arc<sqlx::SqlitePool>,
}
impl ExtraHoursDaoImpl {
pub fn new(pool: Arc<sqlx::SqlitePool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl ExtraHoursDao for ExtraHoursDaoImpl {
async fn find_by_sales_person_id_and_year(
&self,
sales_person_id: Uuid,
year: u32,
until_week: u8,
) -> Result<Arc<[ExtraHoursEntity]>, crate::DaoError> {
let id_vec = sales_person_id.as_bytes().to_vec();
Ok(query_as!(
ExtraHoursDb,
"SELECT id, sales_person_id, amount, category, description, date_time, deleted FROM extra_hours WHERE sales_person_id = ? AND strftime('%Y', date_time) = ? AND strftime('%m', date_time) <= ?",
id_vec,
year,
until_week,
).fetch_all(self.pool.as_ref())
.await
.map_db_error()?
.iter()
.map(ExtraHoursEntity::try_from)
.collect::<Result<Arc<[_]>, _>>()?
.into())
}
async fn create(
&self,
_entity: &ExtraHoursEntity,
_process: &str,
) -> Result<(), crate::DaoError> {
unimplemented!()
}
async fn update(
&self,
_entity: &ExtraHoursEntity,
_process: &str,
) -> Result<(), crate::DaoError> {
unimplemented!()
}
async fn delete(&self, _id: Uuid, _process: &str) -> Result<(), crate::DaoError> {
unimplemented!()
}
}

View file

@ -5,8 +5,11 @@ use dao::{DaoError, PrivilegeEntity};
use sqlx::{query, query_as, SqlitePool};
pub mod booking;
pub mod extra_hours;
pub mod sales_person;
pub mod shiftplan_report;
pub mod slot;
pub mod working_hours;
pub trait ResultDbErrorExt<T, E> {
fn map_db_error(self) -> Result<T, DaoError>;

View file

@ -0,0 +1,123 @@
use std::sync::Arc;
use crate::ResultDbErrorExt;
use async_trait::async_trait;
use dao::{
shiftplan_report::{ShiftplanQuickOverviewEntity, ShiftplanReportDao, ShiftplanReportEntity},
slot::DayOfWeek,
DaoError,
};
use sqlx::query_as;
use uuid::Uuid;
pub struct ShiftplanReportDb {
pub sales_person_id: Vec<u8>,
pub hours: Option<f64>,
pub year: i64,
pub calendar_week: i64,
pub day_of_week: i64,
}
impl TryFrom<&ShiftplanReportDb> for ShiftplanReportEntity {
type Error = DaoError;
fn try_from(entity: &ShiftplanReportDb) -> Result<Self, DaoError> {
Ok(Self {
sales_person_id: Uuid::from_slice(entity.sales_person_id.as_ref())?,
hours: entity.hours.unwrap_or(0.0) as f32,
year: entity.year as u32,
calendar_week: entity.calendar_week as u8,
day_of_week: DayOfWeek::from_number(entity.day_of_week as u8)
.ok_or_else(|| DaoError::InvalidDayOfWeek(entity.day_of_week as u8))?,
})
}
}
pub struct ShiftplanQuickOverviewDb {
pub sales_person_id: Vec<u8>,
pub hours: Option<f64>,
pub year: i64,
}
impl From<&ShiftplanQuickOverviewDb> for ShiftplanQuickOverviewEntity {
fn from(entity: &ShiftplanQuickOverviewDb) -> Self {
Self {
sales_person_id: Uuid::from_slice(entity.sales_person_id.as_ref()).unwrap(),
hours: entity.hours.unwrap_or(0.0) as f32,
year: entity.year as u32,
}
}
}
pub struct ShiftplanReportDaoImpl {
pub pool: Arc<sqlx::SqlitePool>,
}
impl ShiftplanReportDaoImpl {
pub fn new(pool: Arc<sqlx::SqlitePool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl ShiftplanReportDao for ShiftplanReportDaoImpl {
async fn extract_shiftplan_report(
&self,
sales_person_id: Uuid,
year: u32,
until_week: u8,
) -> Result<Arc<[ShiftplanReportEntity]>, DaoError> {
let sales_person_id_vec = sales_person_id.as_bytes().to_vec();
Ok(query_as!(
ShiftplanReportDb,
r#"
SELECT
sales_person.id as sales_person_id,
sum((STRFTIME('%H', slot.time_to) + STRFTIME('%M', slot.time_to) / 60.0) - (STRFTIME('%H', slot.time_from) + STRFTIME('%M', slot.time_from))) as hours,
booking.calendar_week, booking.year, slot.day_of_week
FROM slot
INNER JOIN booking ON (booking.slot_id = slot.id AND booking.deleted IS NULL)
INNER JOIN sales_person ON booking.sales_person_id = sales_person.id
WHERE sales_person.id = ?
AND booking.year = ?
AND booking.calendar_week <= ?
GROUP BY year, calendar_week, day_of_week
"#,
sales_person_id_vec,
year,
until_week
).fetch_all(self.pool.as_ref())
.await
.map_db_error()?
.iter()
.map(ShiftplanReportEntity::try_from)
.collect::<Result<Arc<[_]>, _>>()?
)
}
async fn extract_quick_shiftplan_report(
&self,
year: u32,
until_week: u8,
) -> Result<Arc<[ShiftplanQuickOverviewEntity]>, DaoError> {
Ok(query_as!(
ShiftplanQuickOverviewDb,
r#"
SELECT
sales_person.id as sales_person_id,
sum((STRFTIME('%H', slot.time_to) + STRFTIME('%M', slot.time_to) / 60.0) - (STRFTIME('%H', slot.time_from) + STRFTIME('%M', slot.time_from))) as hours,
booking.year
FROM slot
INNER JOIN booking ON (booking.slot_id = slot.id AND booking.deleted IS NULL)
INNER JOIN sales_person ON booking.sales_person_id = sales_person.id
WHERE booking.year = ?
AND booking.calendar_week <= ?
GROUP BY sales_person_id, year
"#,
year,
until_week
).fetch_all(self.pool.as_ref())
.await
.map_db_error()?
.iter()
.map(ShiftplanQuickOverviewEntity::from)
.collect::<Arc<[_]>>()
)
}
}

View file

@ -0,0 +1,183 @@
use std::sync::Arc;
use crate::ResultDbErrorExt;
use async_trait::async_trait;
use dao::{
working_hours::{WorkingHoursDao, WorkingHoursEntity},
DaoError,
};
use sqlx::{query, query_as};
use time::{format_description::well_known::Iso8601, PrimitiveDateTime};
use uuid::Uuid;
pub struct WorkingHoursDb {
pub id: Vec<u8>,
pub sales_person_id: Vec<u8>,
pub expected_hours: f64,
pub from_calendar_week: i64,
pub from_year: i64,
pub to_calendar_week: i64,
pub to_year: i64,
pub created: String,
pub deleted: Option<String>,
update_version: Vec<u8>,
}
impl TryFrom<&WorkingHoursDb> for WorkingHoursEntity {
type Error = DaoError;
fn try_from(working_hours: &WorkingHoursDb) -> Result<Self, DaoError> {
Ok(Self {
id: Uuid::from_slice(working_hours.id.as_ref())?,
sales_person_id: Uuid::from_slice(working_hours.sales_person_id.as_ref()).unwrap(),
expected_hours: working_hours.expected_hours as f32,
from_calendar_week: working_hours.from_calendar_week as u8,
from_year: working_hours.from_year as u32,
to_calendar_week: working_hours.to_calendar_week as u8,
to_year: working_hours.to_year as u32,
created: PrimitiveDateTime::parse(working_hours.created.as_str(), &Iso8601::DATE_TIME)?,
deleted: working_hours
.deleted
.as_ref()
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE_TIME))
.transpose()?,
version: Uuid::from_slice(&working_hours.update_version)?,
})
}
}
pub struct WorkingHoursDaoImpl {
pub pool: Arc<sqlx::SqlitePool>,
}
impl WorkingHoursDaoImpl {
pub fn new(pool: Arc<sqlx::SqlitePool>) -> Self {
Self { pool }
}
}
#[async_trait]
impl WorkingHoursDao for WorkingHoursDaoImpl {
async fn all(&self) -> Result<Arc<[WorkingHoursEntity]>, DaoError> {
query_as!(
WorkingHoursDb,
r#"
SELECT
id,
sales_person_id,
expected_hours,
from_calendar_week,
from_year,
to_calendar_week,
to_year,
created,
deleted,
update_version
FROM
working_hours
"#
)
.fetch_all(self.pool.as_ref())
.await
.map_db_error()?
.iter()
.map(WorkingHoursEntity::try_from)
.collect::<Result<_, _>>()
}
async fn find_by_sales_person_id(
&self,
sales_person_id: Uuid,
) -> Result<Arc<[WorkingHoursEntity]>, DaoError> {
let id_vec = sales_person_id.as_bytes().to_vec();
query_as!(
WorkingHoursDb,
r#"
SELECT
id,
sales_person_id,
expected_hours,
from_calendar_week,
from_year,
to_calendar_week,
to_year,
created,
deleted,
update_version
FROM
working_hours
WHERE
sales_person_id = ?
"#,
id_vec
)
.fetch_all(self.pool.as_ref())
.await
.map_db_error()?
.iter()
.map(WorkingHoursEntity::try_from)
.collect::<Result<_, _>>()
}
async fn create(&self, entity: &WorkingHoursEntity, process: &str) -> Result<(), DaoError> {
let id = entity.id.as_bytes().to_vec();
let sales_person_id = entity.sales_person_id.as_bytes().to_vec();
let expected_hours = entity.expected_hours as f64;
let from_calendar_week = entity.from_calendar_week as i64;
let from_year = entity.from_year as i64;
let to_calendar_week = entity.to_calendar_week as i64;
let to_year = entity.to_year as i64;
let created = entity.created.format(&Iso8601::DATE_TIME)?;
let version = entity.id.as_bytes().to_vec();
query!(
r#"
INSERT INTO working_hours (
id,
sales_person_id,
expected_hours,
from_calendar_week,
from_year,
to_calendar_week,
to_year,
created,
update_process,
update_version
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
id,
sales_person_id,
expected_hours,
from_calendar_week,
from_year,
to_calendar_week,
to_year,
created,
process,
version,
)
.execute(self.pool.as_ref())
.await
.map_db_error()?;
Ok(())
}
async fn update(&self, entity: &WorkingHoursEntity, process: &str) -> Result<(), DaoError> {
let id = entity.id.as_bytes().to_vec();
let deleted = entity.deleted.as_ref().map(|deleted| deleted.to_string());
query!(
r#"
UPDATE working_hours SET
deleted = ?,
update_process = ?
WHERE
id = ?
"#,
deleted,
process,
id
)
.execute(self.pool.as_ref())
.await
.map_db_error()?;
Ok(())
}
}

View file

@ -6,6 +6,7 @@ ADD COLUMN is_paid BOOLEAN DEFAULT 0 NOT NULL;
CREATE TABLE working_hours (
id blob(16) NOT NULL PRIMARY KEY,
sales_person_id blob(16) NOT NULL,
expected_hours FLOAT NOT NULL,
from_calendar_week INTEGER NOT NULL,
from_year INTEGER NOT NULL,
to_calendar_week INTEGER NOT NULL,
@ -16,15 +17,16 @@ CREATE TABLE working_hours (
update_timestamp TEXT,
update_process TEXT NOT NULL,
update_version blob(16) NOT NULL,
FOREIGN KEY (sales_person_id) REFERENCES sales_person(id)
);
CREATE TABLE extra_hours (
id blob(16) NOT NULL PRIMARY KEY,
sales_person_id blob(16) NOT NULL,
amount INTEGER NOT NULL,
amount FLOAT NOT NULL,
category TEXT NOT NULL,
description TEXT,
date_time TEXT NOT NULL,
created TEXT NOT NULL,
deleted TEXT,

View file

@ -0,0 +1,64 @@
-- Add migration script here
INSERT INTO slot VALUES(X'08ab25881f9e4d03b70fd6ecd9747e53',1,'09:00:00.0','10:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'5a4be30de0f743789a2da07d11b4f14d');
INSERT INTO slot VALUES(X'46fe075a4e9b49d28d138a3ce368ba3e',1,'10:00:00.0','11:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'52163187643344519943bac153a5d33f');
INSERT INTO slot VALUES(X'c34ad85a7bfb4a35b98ecd858eb84ea9',1,'11:00:00.0','12:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'9b3ee57e71eb46eea33eb57ae6abd212');
INSERT INTO slot VALUES(X'4b6808fd1955436aaee6afaad87832f8',1,'12:00:00.0','13:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'3f719589b0474c5bb575af52290ac24f');
INSERT INTO slot VALUES(X'58ee28a028b44debae3cb909e14bbc82',1,'13:00:00.0','14:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'3e7351fdfc9049649b69c43dfb51d860');
INSERT INTO slot VALUES(X'8ef857dea2a7429aad765a898e01948b',1,'14:00:00.0','15:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'b3dc7f132a354ad2b719366b507691ac');
INSERT INTO slot VALUES(X'0824b95ae1a54ef98150cc7c18172c9f',1,'15:00:00.0','16:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'023a5ad0fe0b4e55bca61bb6634ac1db');
INSERT INTO slot VALUES(X'ae4fa09ca6b94234b2fad2daa993fee0',1,'16:00:00.0','17:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'2109af124c5742e6b1076559a252e6fe');
INSERT INTO slot VALUES(X'70695390f5ec4f7b83ca1b4a68bb8a14',1,'17:00:00.0','18:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'1644962cc6fe455f8e6df37126ce67c1');
INSERT INTO slot VALUES(X'8b538e961f214935ac4c4c045c989473',1,'18:00:00.0','19:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'726b7c1c741246efa7e36114af3c8db5');
INSERT INTO slot VALUES(X'f0037047d29e4579ae0d1d743818bddc',1,'19:00:00.0','19:30:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'ea91542af18a4a75bc5d1c04ab53e596');
INSERT INTO slot VALUES(X'96465be6f1664ba68c3630858a7e4207',2,'09:00:00.0','10:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'6a6a7e107738466aacdb7ca4e4f1ab8a');
INSERT INTO slot VALUES(X'6851abd6b9134ee7b311fbb9502a1d05',3,'09:00:00.0','10:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'0d379974b71d42029ad4f174461d70c4');
INSERT INTO slot VALUES(X'54d84cb4622b424ba1f6cd3b09910db3',4,'09:00:00.0','10:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'64eb8bcbd88648dc888132e3e5486967');
INSERT INTO slot VALUES(X'012f7b1eb86a479497ad6d12bf502e1a',5,'09:00:00.0','10:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'72a49fcb3ce9496f85a6be39e067a5ab');
INSERT INTO slot VALUES(X'a847e67a1a9a45339710987a022dac4b',6,'09:00:00.0','10:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'a3bf1b1fecd04294991ef3f432425991');
INSERT INTO slot VALUES(X'7cf59fc6d6fe4aba83a6a70044583eeb',6,'11:00:00.0','12:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'db82bcb229c74cec828a2f9c2fef761b');
INSERT INTO slot VALUES(X'd53ec0272aef4b41823bf500932d6283',6,'12:00:00.0','13:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'76dc52e48cd4429c8c8f48ad9e3643bb');
INSERT INTO slot VALUES(X'70fb58cf2f674f099ac3dddb8ee5cbc2',6,'13:00:00.0','14:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'197021b8f291497282d9c1f597b5ffeb');
INSERT INTO slot VALUES(X'c9ff96df42464a18beac02ae7790fb37',6,'14:00:00.0','15:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'd5f2bc5d014a4387afc79085a6bd3b01');
INSERT INTO slot VALUES(X'6a2537e618f94658abea65504da38325',6,'15:00:00.0','16:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'60b24f0a3dc64634bfaf0e86912abe6b');
INSERT INTO slot VALUES(X'c4cfc6f8f2c14b7596392bd878c3fe2a',5,'10:00:00.0','11:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'a58e50f7996a47dbbb52d345ce77b02a');
INSERT INTO slot VALUES(X'194f4a66275f4529a65db8995f225afb',5,'11:00:00.0','12:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'321cb7911dab4d189d371317d853e5ab');
INSERT INTO slot VALUES(X'd59417fe72244e189a21f28c4e7d52cc',5,'12:00:00.0','13:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'b531a3180b8948f8b2d134b2ceacf6ac');
INSERT INTO slot VALUES(X'08d74382d32c404ca5f796c109f00787',5,'13:00:00.0','14:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'578a705492c04327afbde317de6624a0');
INSERT INTO slot VALUES(X'bbea533beaab463bafe80e40a69c8393',5,'14:00:00.0','15:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'c38361f42db9491ca62858ea0beddb50');
INSERT INTO slot VALUES(X'35f629542e3942b081bd0b1b214d5b59',5,'15:00:00.0','16:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'76edd2f0214d4ffeb0e0d32aa668f248');
INSERT INTO slot VALUES(X'8720cc55e3954470be7b977c1e31719f',5,'16:00:00.0','17:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'cc5f682b46b84c839bd5b62269422637');
INSERT INTO slot VALUES(X'345968cebe6c477f9650d81bd28e226a',5,'17:00:00.0','18:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'95feede9d4ab4d0ca5c14a564b4ee4f4');
INSERT INTO slot VALUES(X'3afe8ab64c034c71bfe8b95ea1872d2b',5,'18:00:00.0','19:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'60bf3c7046334ce7b6ccde20f2768c25');
INSERT INTO slot VALUES(X'68f62933dead423d8e11c19911a3bc57',5,'19:00:00.0','19:30:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'c9f4bf0f0c07416391f7893da531009b');
INSERT INTO slot VALUES(X'e38a96be82fc4e8890609bf1326ed377',4,'10:00:00.0','11:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'ed81821119be4dffb1266e29f5dcf5d6');
INSERT INTO slot VALUES(X'96d0aad4dc334ceb87c4ac233e0c7c88',6,'10:00:00.0','11:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'7b5c1d9ec707472abf25a9f224ee41d1');
INSERT INTO slot VALUES(X'b07a606213204cee89109c3140f676b8',4,'11:00:00.0','12:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'bded643784b444118ea427821be72f0e');
INSERT INTO slot VALUES(X'de904c71fc3f4ec2a5e8104e99754fdc',4,'12:00:00.0','13:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'3a0375b7f97c47198db697de983de9fc');
INSERT INTO slot VALUES(X'87f3e28c0da1443593835f3aed078c1c',4,'13:00:00.0','14:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'bac09f2e9e174c4284585f72fd09f343');
INSERT INTO slot VALUES(X'81df50ac841e46f18fc7b929c4a83d58',4,'14:00:00.0','15:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'53a09581911446a5b2a75a10db9e34e2');
INSERT INTO slot VALUES(X'c90300ba0f3446099eba97244267cc2c',4,'15:00:00.0','16:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'0e554ef38f3a4306b2318bd8237ef595');
INSERT INTO slot VALUES(X'96aad0153a794bc4a90195b7ac8b67d5',4,'16:00:00.0','17:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'bb314bf0d8b64960b8a91f72717de972');
INSERT INTO slot VALUES(X'844d9ca424c0410798a67518540b1bb7',4,'17:00:00.0','18:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'6040220400a644ef8fbe867e92363a86');
INSERT INTO slot VALUES(X'62d008388f274da2a6ba0cbf9b2da9f1',4,'18:00:00.0','19:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'46bc35a8fa45423cb8f5dc44d639f95e');
INSERT INTO slot VALUES(X'06525073a2e1474faf43d013654f1219',4,'19:00:00.0','19:30:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'63affcfa76f8496ba3e93441d085f607');
INSERT INTO slot VALUES(X'0141437ecfed44dab8e921fd24b3f960',3,'10:00:00.0','11:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'c5cee643e99e421493eda930b33a9e12');
INSERT INTO slot VALUES(X'fd190d07a228449d96778454c4b7d4e7',3,'11:00:00.0','12:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'60b766ff2229400d95eebc8c101b509d');
INSERT INTO slot VALUES(X'c254812651734677b7fedca933405920',3,'12:00:00.0','13:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'32a5c06431a046b09fc3a6b6f701dc7f');
INSERT INTO slot VALUES(X'68efe36f94084de0b409655239a6488e',3,'13:00:00.0','14:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'2781e182e09e49bd88fa1531fbe5ff3a');
INSERT INTO slot VALUES(X'4ea95b27fe1c402aa5a6e45141fc4a9b',3,'14:00:00.0','15:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'ac7b30f3379d4d7696731f55da78df11');
INSERT INTO slot VALUES(X'a001bb86efab4f2fbe817cbb065be1da',3,'15:00:00.0','16:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'b912efd3b92c4535962b99d95ab6fee2');
INSERT INTO slot VALUES(X'af02ed2b3fee48928db7b58c11df0d75',3,'16:00:00.0','17:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'5d42c8a871c54cd79cc56172d7c5deb2');
INSERT INTO slot VALUES(X'69f19afd18e044d0b75776f2f8704df1',3,'17:00:00.0','18:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'5bc60267cc5149ae9cbda0f89cd43b51');
INSERT INTO slot VALUES(X'af973478b5c54362863385b39967c8e0',3,'18:00:00.0','19:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'5c4d18709a9b4f8097e16511f96f7415');
INSERT INTO slot VALUES(X'72b3086cb7db4a7cbe21c0a2b87be249',3,'19:00:00.0','19:30:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'b8e08ba8c6a047db8954c6e63ef33d35');
INSERT INTO slot VALUES(X'16b777bec4434e69a255665adcdd2977',2,'10:00:00.0','11:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'1d8947cf33f246159fe61c2472ecf20a');
INSERT INTO slot VALUES(X'47fd87b94aab45848ca8fe3ace7e1469',2,'11:00:00.0','12:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'4c675d9d0ea642f6b75d102a2dfccf2b');
INSERT INTO slot VALUES(X'81d4f730bc6941338788a078d4f3fc33',2,'12:00:00.0','13:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'446fd4ecfd354f498037ad7a014245ca');
INSERT INTO slot VALUES(X'ea2ec8d628184d50b820de050f291efc',2,'13:00:00.0','14:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'55ce7330b09545f3b640a163552cd421');
INSERT INTO slot VALUES(X'1cabdbf2c66a413895fa24c157e68cff',2,'14:00:00.0','15:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'f330de0dcb2e4fccb57919aaa5b2e8af');
INSERT INTO slot VALUES(X'197f64b7ff294b3ea9fcea6aa3c2335d',2,'15:00:00.0','16:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'484f6a08b70042debab12ae44cca3bc5');
INSERT INTO slot VALUES(X'959a15a2e77f4f8085f84016befdf588',2,'16:00:00.0','17:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'0e35c7310b734d508cfee3774f71716e');
INSERT INTO slot VALUES(X'4a9b6cdccdd94e14a221af4f07149e03',2,'17:00:00.0','18:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'0f36127f96e94dd0918edc11d3aa3fe2');
INSERT INTO slot VALUES(X'a90a970739b94f439a0a46f9134b3aa3',2,'18:00:00.0','19:00:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'bc9fe5894e3b4327afd2831e0842c9c7');
INSERT INTO slot VALUES(X'bf5245e03bef43b580c1fe245de4a8a9',2,'19:00:00.0','19:30:00.0','2020-01-01',NULL,NULL,NULL,'slot-service',X'b08db53d35844c9ea36f7e63389dd727');

View file

@ -221,3 +221,191 @@ impl From<&SlotTO> for service::slot::Slot {
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ShortEmployeeReportTO {
pub sales_person: SalesPersonTO,
pub balance_hours: f32,
}
impl From<&service::reporting::ShortEmployeeReport> for ShortEmployeeReportTO {
fn from(report: &service::reporting::ShortEmployeeReport) -> Self {
Self {
sales_person: SalesPersonTO::from(report.sales_person.as_ref()),
balance_hours: report.balance_hours,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum ExtraHoursCategoryTO {
Shiftplan,
ExtraWork,
Vacation,
SickLeave,
Holiday,
}
impl From<&service::reporting::ExtraHoursCategory> for ExtraHoursCategoryTO {
fn from(category: &service::reporting::ExtraHoursCategory) -> Self {
match category {
service::reporting::ExtraHoursCategory::Shiftplan => Self::Shiftplan,
service::reporting::ExtraHoursCategory::ExtraWork => Self::ExtraWork,
service::reporting::ExtraHoursCategory::Vacation => Self::Vacation,
service::reporting::ExtraHoursCategory::SickLeave => Self::SickLeave,
service::reporting::ExtraHoursCategory::Holiday => Self::Holiday,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkingHoursDayTO {
pub date: time::Date,
pub hours: f32,
pub category: ExtraHoursCategoryTO,
}
impl From<&service::reporting::WorkingHoursDay> for WorkingHoursDayTO {
fn from(day: &service::reporting::WorkingHoursDay) -> Self {
Self {
date: day.date,
hours: day.hours,
category: (&day.category).into(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkingHoursReportTO {
pub from: time::Date,
pub to: time::Date,
pub expected_hours: f32,
pub overall_hours: f32,
pub balance: f32,
pub shiftplan_hours: f32,
pub extra_work_hours: f32,
pub vacation_hours: f32,
pub sick_leave_hours: f32,
pub holiday_hours: f32,
pub days: Arc<[WorkingHoursDayTO]>,
}
impl From<&service::reporting::WorkingHours> for WorkingHoursReportTO {
fn from(hours: &service::reporting::WorkingHours) -> Self {
Self {
from: hours.from,
to: hours.to,
expected_hours: hours.expected_hours,
overall_hours: hours.overall_hours,
balance: hours.balance,
shiftplan_hours: hours.shiftplan_hours,
extra_work_hours: hours.extra_work_hours,
vacation_hours: hours.vacation_hours,
sick_leave_hours: hours.sick_leave_hours,
holiday_hours: hours.holiday_hours,
days: hours
.days
.iter()
.map(|day| WorkingHoursDayTO::from(day))
.collect::<Vec<_>>()
.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EmployeeReportTO {
pub sales_person: Arc<SalesPersonTO>,
pub balance_hours: f32,
pub overall_hours: f32,
pub expected_hours: f32,
pub shiftplan_hours: f32,
pub extra_work_hours: f32,
pub vacation_hours: f32,
pub sick_leave_hours: f32,
pub holiday_hours: f32,
pub by_week: Arc<[WorkingHoursReportTO]>,
pub by_month: Arc<[WorkingHoursReportTO]>,
}
impl From<&service::reporting::EmployeeReport> for EmployeeReportTO {
fn from(report: &service::reporting::EmployeeReport) -> Self {
Self {
sales_person: Arc::new(SalesPersonTO::from(report.sales_person.as_ref())),
balance_hours: report.balance_hours,
overall_hours: report.overall_hours,
expected_hours: report.expected_hours,
shiftplan_hours: report.shiftplan_hours,
extra_work_hours: report.extra_work_hours,
vacation_hours: report.vacation_hours,
sick_leave_hours: report.sick_leave_hours,
holiday_hours: report.holiday_hours,
by_week: report
.by_week
.iter()
.map(|hours| WorkingHoursReportTO::from(hours))
.collect::<Vec<_>>()
.into(),
by_month: report
.by_month
.iter()
.map(|hours| WorkingHoursReportTO::from(hours))
.collect::<Vec<_>>()
.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkingHoursTO {
#[serde(default)]
pub id: Uuid,
pub sales_person_id: Uuid,
pub expected_hours: f32,
pub from_calendar_week: u8,
pub from_year: u32,
pub to_calendar_week: u8,
pub to_year: u32,
#[serde(default)]
pub created: Option<time::PrimitiveDateTime>,
#[serde(default)]
pub deleted: Option<time::PrimitiveDateTime>,
#[serde(rename = "$version")]
#[serde(default)]
pub version: Uuid,
}
impl From<&service::working_hours::WorkingHours> for WorkingHoursTO {
fn from(working_hours: &service::working_hours::WorkingHours) -> Self {
Self {
id: working_hours.id,
sales_person_id: working_hours.sales_person_id,
expected_hours: working_hours.expected_hours,
from_calendar_week: working_hours.from_calendar_week,
from_year: working_hours.from_year,
to_calendar_week: working_hours.to_calendar_week,
to_year: working_hours.to_year,
created: working_hours.created,
deleted: working_hours.deleted,
version: working_hours.version,
}
}
}
impl From<&WorkingHoursTO> for service::working_hours::WorkingHours {
fn from(working_hours: &WorkingHoursTO) -> Self {
Self {
id: working_hours.id,
sales_person_id: working_hours.sales_person_id,
expected_hours: working_hours.expected_hours,
from_calendar_week: working_hours.from_calendar_week,
from_year: working_hours.from_year,
to_calendar_week: working_hours.to_calendar_week,
to_year: working_hours.to_year,
created: working_hours.created,
deleted: working_hours.deleted,
version: working_hours.version,
}
}
}

View file

@ -2,8 +2,10 @@ use std::{convert::Infallible, sync::Arc};
mod booking;
mod permission;
mod report;
mod sales_person;
mod slot;
mod working_hours;
#[cfg(feature = "oidc")]
use axum::error_handling::HandleErrorLayer;
@ -199,6 +201,12 @@ fn error_handler(result: Result<Response, RestError>) -> Response {
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(err @ service::ServiceError::TimeComponentRangeError(_))) => {
Response::builder()
.status(500)
.body(Body::new(err.to_string()))
.unwrap()
}
Err(RestError::ServiceError(ServiceError::InternalError)) => Response::builder()
.status(500)
.body(Body::new("Internal server error".to_string()))
@ -215,12 +223,22 @@ pub trait RestStateDef: Clone + Send + Sync + 'static {
+ Sync
+ 'static;
type BookingService: service::booking::BookingService<Context = Context> + Send + Sync + 'static;
type ReportingService: service::reporting::ReportingService<Context = Context>
+ Send
+ Sync
+ 'static;
type WorkingHoursService: service::working_hours::WorkingHoursService<Context = Context>
+ Send
+ Sync
+ 'static;
fn user_service(&self) -> Arc<Self::UserService>;
fn permission_service(&self) -> Arc<Self::PermissionService>;
fn slot_service(&self) -> Arc<Self::SlotService>;
fn sales_person_service(&self) -> Arc<Self::SalesPersonService>;
fn booking_service(&self) -> Arc<Self::BookingService>;
fn reporting_service(&self) -> Arc<Self::ReportingService>;
fn working_hours_service(&self) -> Arc<Self::WorkingHoursService>;
}
pub struct OidcConfig {
@ -324,6 +342,8 @@ pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
.nest("/slot", slot::generate_route())
.nest("/sales-person", sales_person::generate_route())
.nest("/booking", booking::generate_route())
.nest("/report", report::generate_route())
.nest("/working-hours", working_hours::generate_route())
.with_state(rest_state)
.layer(middleware::from_fn(context_extractor));

77
rest/src/report.rs Normal file
View file

@ -0,0 +1,77 @@
use std::sync::Arc;
use axum::{
body::Body,
extract::{Path, Query, State},
response::Response,
routing::get,
Extension, Router,
};
use rest_types::{EmployeeReportTO, ShortEmployeeReportTO};
use serde::Deserialize;
use service::reporting::ReportingService;
use uuid::Uuid;
use crate::{error_handler, Context, RestStateDef};
pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
Router::new()
.route("/", get(get_short_report_for_all::<RestState>))
.route("/:id", get(get_report::<RestState>))
}
#[derive(Clone, Debug, Deserialize)]
pub struct ReportRequest {
year: u32,
until_week: u8,
}
pub async fn get_short_report_for_all<RestState: RestStateDef>(
rest_state: State<RestState>,
query: Query<ReportRequest>,
Extension(context): Extension<Context>,
) -> Response {
error_handler(
(async {
let short_report: Arc<[ShortEmployeeReportTO]> = rest_state
.reporting_service()
.get_reports_for_all_employees(query.year, query.until_week, context.into())
.await?
.iter()
.map(ShortEmployeeReportTO::from)
.collect();
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&short_report).unwrap()))
.unwrap())
})
.await,
)
}
pub async fn get_report<RestState: RestStateDef>(
rest_state: State<RestState>,
query: Query<ReportRequest>,
Path(sales_person_id): Path<Uuid>,
Extension(context): Extension<Context>,
) -> Response {
error_handler(
(async {
let report: EmployeeReportTO = (&rest_state
.reporting_service()
.get_report_for_employee(
&sales_person_id,
query.year,
query.until_week,
context.into(),
)
.await?)
.into();
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&report).unwrap()))
.unwrap())
})
.await,
)
}

34
rest/src/working_hours.rs Normal file
View file

@ -0,0 +1,34 @@
use axum::{
body::Body, extract::State, response::Response, routing::post, Extension, Json, Router,
};
use rest_types::WorkingHoursTO;
use service::working_hours::WorkingHoursService;
use crate::{error_handler, Context, RestStateDef};
pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
Router::new().route("/", post(create_working_hours::<RestState>))
}
pub async fn create_working_hours<RestState: RestStateDef>(
rest_state: State<RestState>,
Extension(context): Extension<Context>,
Json(working_hours): Json<WorkingHoursTO>,
) -> Response {
error_handler(
(async {
let working_hours = WorkingHoursTO::from(
&rest_state
.working_hours_service()
.create(&(&working_hours).into(), context.into())
.await?,
);
Ok(Response::builder()
.status(200)
.body(Body::new(serde_json::to_string(&working_hours).unwrap()))
.unwrap())
})
.await,
)
}

View file

@ -0,0 +1,68 @@
use std::{collections::BTreeMap, sync::Arc};
use time::{error::ComponentRange, Date};
use crate::slot::DayOfWeek;
pub fn calenar_week_to_date(
year: i32,
week: u8,
day_of_week: DayOfWeek,
) -> Result<Date, ComponentRange> {
Date::from_iso_week_date(year, week, day_of_week.into())
}
pub fn date_to_calendar_week(date: Date) -> (i32, u8, DayOfWeek) {
let year = date.year();
let week = date.iso_week();
let day_of_week = DayOfWeek::from(date.weekday());
(year, week, day_of_week)
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
pub struct CalendarWeek {
pub year: i32,
pub week: u8,
}
pub trait AsCalendarWeek {
fn as_date(&self) -> CalendarWeek;
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
pub struct Month {
pub year: i32,
pub month: u8,
}
pub trait AsMonth {
fn as_date(&self) -> Month;
}
pub fn group_by_calendar_week<T: AsCalendarWeek + Clone>(
items: &[T],
) -> BTreeMap<CalendarWeek, Arc<[T]>> {
let mut map = BTreeMap::new();
for item in items {
let calendar_week = item.as_date();
map.entry(calendar_week)
.or_insert_with(Vec::new)
.push(item.to_owned());
}
map.into_iter()
.map(|(calendar_week, items)| (calendar_week, items.into()))
.collect()
}
pub fn group_by_month<T: AsMonth + Clone>(items: &[T]) -> BTreeMap<Month, Arc<[T]>> {
let mut map = BTreeMap::new();
for item in items {
let month = item.as_date();
map.entry(month)
.or_insert_with(Vec::new)
.push(item.to_owned());
}
map.into_iter()
.map(|(month, items)| (month, items.into()))
.collect()
}

View file

@ -0,0 +1,86 @@
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
use dao::DaoError;
#[derive(Clone, Debug, PartialEq)]
pub enum ExtraHoursCategory {
ExtraWork,
Vacation,
SickLeave,
Holiday,
}
impl From<&dao::extra_hours::ExtraHoursCategoryEntity> for ExtraHoursCategory {
fn from(category: &dao::extra_hours::ExtraHoursCategoryEntity) -> Self {
match category {
dao::extra_hours::ExtraHoursCategoryEntity::ExtraWork => Self::ExtraWork,
dao::extra_hours::ExtraHoursCategoryEntity::Vacation => Self::Vacation,
dao::extra_hours::ExtraHoursCategoryEntity::SickLeave => Self::SickLeave,
dao::extra_hours::ExtraHoursCategoryEntity::Holiday => Self::Holiday,
}
}
}
impl From<&ExtraHoursCategory> for dao::extra_hours::ExtraHoursCategoryEntity {
fn from(category: &ExtraHoursCategory) -> Self {
match category {
ExtraHoursCategory::ExtraWork => Self::ExtraWork,
ExtraHoursCategory::Vacation => Self::Vacation,
ExtraHoursCategory::SickLeave => Self::SickLeave,
ExtraHoursCategory::Holiday => Self::Holiday,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ExtraHours {
pub id: Uuid,
pub sales_person_id: Uuid,
pub amount: f32,
pub category: ExtraHoursCategory,
pub description: Arc<str>,
pub date_time: time::PrimitiveDateTime,
pub deleted: Option<time::PrimitiveDateTime>,
}
impl From<&dao::extra_hours::ExtraHoursEntity> for ExtraHours {
fn from(extra_hours: &dao::extra_hours::ExtraHoursEntity) -> Self {
Self {
id: extra_hours.id,
sales_person_id: extra_hours.sales_person_id,
amount: extra_hours.amount,
category: (&extra_hours.category).into(),
description: extra_hours.description.clone(),
date_time: extra_hours.date_time,
deleted: extra_hours.deleted,
}
}
}
impl From<&ExtraHours> for dao::extra_hours::ExtraHoursEntity {
fn from(extra_hours: &ExtraHours) -> Self {
Self {
id: extra_hours.id,
sales_person_id: extra_hours.sales_person_id,
amount: extra_hours.amount,
category: (&extra_hours.category).into(),
description: extra_hours.description.clone(),
date_time: extra_hours.date_time,
deleted: extra_hours.deleted,
}
}
}
#[automock]
#[async_trait]
pub trait ExtraHoursService {
fn find_by_sales_person_id_and_year(
&self,
sales_person_id: Uuid,
year: u32,
until_week: u8,
) -> Result<Arc<[ExtraHours]>, DaoError>;
fn create(&self, entity: &ExtraHours, process: &str) -> Result<(), DaoError>;
fn update(&self, entity: &ExtraHours, process: &str) -> Result<(), DaoError>;
fn delete(&self, id: Uuid, process: &str) -> Result<(), DaoError>;
}

View file

@ -6,11 +6,15 @@ use uuid::Uuid;
pub mod booking;
pub mod clock;
pub mod datetime_utils;
pub mod extra_hours;
pub mod permission;
pub mod reporting;
pub mod sales_person;
pub mod slot;
pub mod user_service;
pub mod uuid_service;
pub mod working_hours;
pub use permission::MockPermissionService;
pub use permission::PermissionService;
@ -64,6 +68,9 @@ pub enum ServiceError {
#[error("Date order wrong. {0} must is not smaller or equal to {1}")]
DateOrderWrong(Date, Date),
#[error("Time component range error: {0}")]
TimeComponentRangeError(#[from] time::error::ComponentRange),
#[error("Internal error")]
InternalError,
}

99
service/src/reporting.rs Normal file
View file

@ -0,0 +1,99 @@
use std::fmt::Debug;
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use uuid::Uuid;
use crate::permission::Authentication;
use crate::sales_person::SalesPerson;
use crate::ServiceError;
#[derive(Clone, Debug, PartialEq)]
pub enum ExtraHoursCategory {
Shiftplan,
ExtraWork,
Vacation,
SickLeave,
Holiday,
}
impl From<&dao::extra_hours::ExtraHoursCategoryEntity> for ExtraHoursCategory {
fn from(category: &dao::extra_hours::ExtraHoursCategoryEntity) -> Self {
match category {
dao::extra_hours::ExtraHoursCategoryEntity::ExtraWork => Self::ExtraWork,
dao::extra_hours::ExtraHoursCategoryEntity::Vacation => Self::Vacation,
dao::extra_hours::ExtraHoursCategoryEntity::SickLeave => Self::SickLeave,
dao::extra_hours::ExtraHoursCategoryEntity::Holiday => Self::Holiday,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct WorkingHoursDay {
pub date: time::Date,
pub hours: f32,
pub category: ExtraHoursCategory,
}
#[derive(Clone, Debug, PartialEq)]
pub struct WorkingHours {
pub from: time::Date,
pub to: time::Date,
pub expected_hours: f32,
pub overall_hours: f32,
pub balance: f32,
pub shiftplan_hours: f32,
pub extra_work_hours: f32,
pub vacation_hours: f32,
pub sick_leave_hours: f32,
pub holiday_hours: f32,
pub days: Arc<[WorkingHoursDay]>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShortEmployeeReport {
pub sales_person: Arc<SalesPerson>,
pub balance_hours: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EmployeeReport {
pub sales_person: Arc<SalesPerson>,
pub balance_hours: f32,
pub overall_hours: f32,
pub expected_hours: f32,
pub shiftplan_hours: f32,
pub extra_work_hours: f32,
pub vacation_hours: f32,
pub sick_leave_hours: f32,
pub holiday_hours: f32,
pub by_week: Arc<[WorkingHours]>,
pub by_month: Arc<[WorkingHours]>,
}
#[automock(type Context=();)]
#[async_trait]
pub trait ReportingService {
type Context: Clone + Debug + PartialEq + Eq + Send + Sync + 'static;
async fn get_reports_for_all_employees(
&self,
years: u32,
until_week: u8,
context: Authentication<Self::Context>,
) -> Result<Arc<[ShortEmployeeReport]>, ServiceError>;
async fn get_report_for_employee(
&self,
sales_person_id: &Uuid,
years: u32,
until_week: u8,
context: Authentication<Self::Context>,
) -> Result<EmployeeReport, ServiceError>;
}

View file

@ -2,6 +2,7 @@ use async_trait::async_trait;
use mockall::automock;
use std::fmt::Debug;
use std::sync::Arc;
use time::Weekday;
use uuid::Uuid;
use crate::permission::Authentication;
@ -43,6 +44,32 @@ impl From<DayOfWeek> for dao::slot::DayOfWeek {
}
}
}
impl From<Weekday> for DayOfWeek {
fn from(weekday: Weekday) -> Self {
match weekday {
Weekday::Monday => Self::Monday,
Weekday::Tuesday => Self::Tuesday,
Weekday::Wednesday => Self::Wednesday,
Weekday::Thursday => Self::Thursday,
Weekday::Friday => Self::Friday,
Weekday::Saturday => Self::Saturday,
Weekday::Sunday => Self::Sunday,
}
}
}
impl From<DayOfWeek> for Weekday {
fn from(day_of_week: DayOfWeek) -> Self {
match day_of_week {
DayOfWeek::Monday => Self::Monday,
DayOfWeek::Tuesday => Self::Tuesday,
DayOfWeek::Wednesday => Self::Wednesday,
DayOfWeek::Thursday => Self::Thursday,
DayOfWeek::Friday => Self::Friday,
DayOfWeek::Saturday => Self::Saturday,
DayOfWeek::Sunday => Self::Sunday,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Slot {

View file

@ -0,0 +1,85 @@
use std::fmt::Debug;
use std::sync::Arc;
use async_trait::async_trait;
use mockall::automock;
use time::PrimitiveDateTime;
use uuid::Uuid;
use crate::permission::Authentication;
use crate::ServiceError;
#[derive(Clone, Debug, PartialEq)]
pub struct WorkingHours {
pub id: Uuid,
pub sales_person_id: Uuid,
pub expected_hours: f32,
pub from_calendar_week: u8,
pub from_year: u32,
pub to_calendar_week: u8,
pub to_year: u32,
pub created: Option<PrimitiveDateTime>,
pub deleted: Option<PrimitiveDateTime>,
pub version: Uuid,
}
impl From<&dao::working_hours::WorkingHoursEntity> for WorkingHours {
fn from(working_hours: &dao::working_hours::WorkingHoursEntity) -> Self {
Self {
id: working_hours.id,
sales_person_id: working_hours.sales_person_id,
expected_hours: working_hours.expected_hours,
from_calendar_week: working_hours.from_calendar_week,
from_year: working_hours.from_year,
to_calendar_week: working_hours.to_calendar_week,
to_year: working_hours.to_year,
created: Some(working_hours.created),
deleted: working_hours.deleted,
version: working_hours.version,
}
}
}
impl TryFrom<&WorkingHours> for dao::working_hours::WorkingHoursEntity {
type Error = ServiceError;
fn try_from(working_hours: &WorkingHours) -> Result<Self, Self::Error> {
Ok(Self {
id: working_hours.id,
sales_person_id: working_hours.sales_person_id,
expected_hours: working_hours.expected_hours,
from_calendar_week: working_hours.from_calendar_week,
from_year: working_hours.from_year,
to_calendar_week: working_hours.to_calendar_week,
to_year: working_hours.to_year,
created: working_hours
.created
.ok_or_else(|| ServiceError::InternalError)?,
deleted: working_hours.deleted,
version: working_hours.version,
})
}
}
#[automock(type Context=();)]
#[async_trait]
pub trait WorkingHoursService {
type Context: Clone + Debug + PartialEq + Eq + Send + Sync + 'static;
async fn all(
&self,
context: Authentication<Self::Context>,
) -> Result<Arc<[WorkingHours]>, ServiceError>;
async fn find_by_sales_person_id(
&self,
sales_person_id: Uuid,
context: Authentication<Self::Context>,
) -> Result<Arc<[WorkingHours]>, ServiceError>;
async fn create(
&self,
entity: &WorkingHours,
context: Authentication<Self::Context>,
) -> Result<WorkingHours, ServiceError>;
async fn update(
&self,
entity: &WorkingHours,
context: Authentication<Self::Context>,
) -> Result<WorkingHours, ServiceError>;
}

View file

@ -5,10 +5,12 @@ use async_trait::async_trait;
pub mod booking;
pub mod clock;
pub mod permission;
pub mod reporting;
pub mod sales_person;
pub mod slot;
mod test;
pub mod uuid_service;
pub mod working_hours;
pub use permission::PermissionServiceImpl;
use service::permission::MockContext;

View file

@ -0,0 +1,389 @@
use std::sync::Arc;
use async_trait::async_trait;
use dao::{
extra_hours::{ExtraHoursCategoryEntity, ExtraHoursEntity},
shiftplan_report::ShiftplanReportEntity,
working_hours::WorkingHoursEntity,
};
use service::{
permission::{Authentication, HR_PRIVILEGE},
reporting::{
EmployeeReport, ExtraHoursCategory, ShortEmployeeReport, WorkingHours, WorkingHoursDay,
},
ServiceError,
};
use tokio::join;
use uuid::Uuid;
pub struct ReportingServiceImpl<
ExtraHoursDao,
ShiftplanReportDao,
WorkingHoursDao,
SalesPersonService,
PermissionService,
ClockService,
UuidService,
> where
ExtraHoursDao: dao::extra_hours::ExtraHoursDao + Send + Sync,
ShiftplanReportDao: dao::shiftplan_report::ShiftplanReportDao + Send + Sync,
WorkingHoursDao: dao::working_hours::WorkingHoursDao + Send + Sync,
SalesPersonService: service::sales_person::SalesPersonService + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
pub extra_hours_dao: Arc<ExtraHoursDao>,
pub shiftplan_report_dao: Arc<ShiftplanReportDao>,
pub working_hours_dao: Arc<WorkingHoursDao>,
pub sales_person_service: Arc<SalesPersonService>,
pub permission_service: Arc<PermissionService>,
pub clock_service: Arc<ClockService>,
pub uuid_service: Arc<UuidService>,
}
impl<
ExtraHoursDao,
ShiftplanReportDao,
WorkingHoursDao,
SalesPersonService,
PermissionService,
ClockService,
UuidService,
>
ReportingServiceImpl<
ExtraHoursDao,
ShiftplanReportDao,
WorkingHoursDao,
SalesPersonService,
PermissionService,
ClockService,
UuidService,
>
where
ExtraHoursDao: dao::extra_hours::ExtraHoursDao + Send + Sync,
ShiftplanReportDao: dao::shiftplan_report::ShiftplanReportDao + Send + Sync,
WorkingHoursDao: dao::working_hours::WorkingHoursDao + Send + Sync,
SalesPersonService: service::sales_person::SalesPersonService + Send + Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
pub fn new(
extra_hours_dao: Arc<ExtraHoursDao>,
shiftplan_report_dao: Arc<ShiftplanReportDao>,
working_hours_dao: Arc<WorkingHoursDao>,
sales_person_service: Arc<SalesPersonService>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
) -> Self {
Self {
extra_hours_dao,
shiftplan_report_dao,
working_hours_dao,
sales_person_service,
permission_service,
clock_service,
uuid_service,
}
}
pub async fn check_user_is_sales_person<AuthContext>(
&self,
sales_person_id: Uuid,
context: AuthContext,
) -> Result<(), ServiceError>
where
AuthContext: Clone
+ Send
+ Sync
+ std::fmt::Debug
+ Eq
+ 'static
+ Into<Authentication<SalesPersonService::Context>>,
{
if let Some(sales_person) = self
.sales_person_service
.get_sales_person_current_user(context.into())
.await?
{
if sales_person.id == sales_person_id {
Ok(())
} else {
Err(ServiceError::Forbidden)
}
} else {
Err(ServiceError::Forbidden)
}
}
}
pub fn find_working_hours_for_calendar_week(
working_hours: &[WorkingHoursEntity],
year: u32,
week: u8,
) -> Option<&WorkingHoursEntity> {
dbg!((year, week));
working_hours.iter().find(|wh| {
(year, week) >= (wh.from_year, wh.from_calendar_week)
&& (year, week) <= (wh.to_year, wh.to_calendar_week)
})
}
#[async_trait]
impl<
ExtraHoursDao,
ShiftplanReportDao,
WorkingHoursDao,
SalesPersonService,
PermissionService,
ClockService,
UuidService,
> service::reporting::ReportingService
for ReportingServiceImpl<
ExtraHoursDao,
ShiftplanReportDao,
WorkingHoursDao,
SalesPersonService,
PermissionService,
ClockService,
UuidService,
>
where
ExtraHoursDao: dao::extra_hours::ExtraHoursDao + Send + Sync,
ShiftplanReportDao: dao::shiftplan_report::ShiftplanReportDao + Send + Sync,
WorkingHoursDao: dao::working_hours::WorkingHoursDao + Send + Sync,
SalesPersonService: service::sales_person::SalesPersonService<Context = PermissionService::Context>
+ Send
+ Sync,
PermissionService: service::permission::PermissionService + Send + Sync,
ClockService: service::clock::ClockService + Send + Sync,
UuidService: service::uuid_service::UuidService + Send + Sync,
{
type Context = PermissionService::Context;
async fn get_reports_for_all_employees(
&self,
year: u32,
until_week: u8,
context: Authentication<Self::Context>,
) -> Result<Arc<[ShortEmployeeReport]>, ServiceError> {
self.permission_service
.check_permission(HR_PRIVILEGE, context)
.await?;
let shiftplan_report = self
.shiftplan_report_dao
.extract_quick_shiftplan_report(year, until_week)
.await?;
let working_hours = self.working_hours_dao.all().await?;
let employees = self
.sales_person_service
.get_all(Authentication::Full)
.await?;
let mut short_employee_report: Vec<ShortEmployeeReport> = Vec::new();
for paid_employee in employees
.iter()
.filter(|employee| employee.is_paid.unwrap_or(false))
{
let shiftplan_hours = shiftplan_report
.iter()
.filter(|r| r.sales_person_id == paid_employee.id)
.map(|r| r.hours)
.sum::<f32>();
let working_hours: Arc<[WorkingHoursEntity]> = working_hours
.iter()
.filter(|wh| wh.sales_person_id == paid_employee.id)
.cloned()
.collect();
let planned_hours: f32 = (1..=until_week)
.map(|week| {
find_working_hours_for_calendar_week(&working_hours, year, week)
.map(|wh| wh.expected_hours)
.unwrap_or(0.0)
})
.sum();
let extra_hours = self
.extra_hours_dao
.find_by_sales_person_id_and_year(paid_employee.id, year, until_week)
.await?
.iter()
.map(|eh| eh.amount)
.sum::<f32>();
let balance_hours = shiftplan_hours + extra_hours - planned_hours;
short_employee_report.push(ShortEmployeeReport {
sales_person: Arc::new(paid_employee.clone()),
balance_hours,
});
}
Ok(short_employee_report.into())
}
async fn get_report_for_employee(
&self,
sales_person_id: &Uuid,
year: u32,
until_week: u8,
context: Authentication<Self::Context>,
) -> Result<EmployeeReport, ServiceError> {
let (hr_permission, user_permission) = join!(
self.permission_service
.check_permission(HR_PRIVILEGE, context.clone()),
self.check_user_is_sales_person(*sales_person_id, context.clone())
);
hr_permission.or(user_permission)?;
let sales_person = self
.sales_person_service
.get(*sales_person_id, context)
.await?;
let working_hours = self
.working_hours_dao
.find_by_sales_person_id(*sales_person_id)
.await?;
let shiftplan_report = self
.shiftplan_report_dao
.extract_shiftplan_report(*sales_person_id, year, until_week)
.await?;
let extra_hours = self
.extra_hours_dao
.find_by_sales_person_id_and_year(*sales_person_id, year, until_week)
.await?;
let planned_hours: f32 = (1..=until_week)
.map(|week| {
find_working_hours_for_calendar_week(&working_hours, year, week)
.map(|wh| wh.expected_hours)
.unwrap_or(0.0)
})
.sum();
let shiftplan_hours = shiftplan_report.iter().map(|r| r.hours).sum::<f32>() as f32;
let overall_extra_hours = extra_hours.iter().map(|eh| eh.amount).sum::<f32>();
let employee_report = EmployeeReport {
sales_person: Arc::new(sales_person),
balance_hours: shiftplan_hours + overall_extra_hours - planned_hours,
overall_hours: shiftplan_hours + overall_extra_hours,
expected_hours: planned_hours,
shiftplan_hours,
extra_work_hours: extra_hours
.iter()
.filter(|extra_hours| extra_hours.category == ExtraHoursCategoryEntity::ExtraWork)
.map(|extra_hours| extra_hours.amount)
.sum(),
vacation_hours: extra_hours
.iter()
.filter(|extra_hours| extra_hours.category == ExtraHoursCategoryEntity::Vacation)
.map(|extra_hours| extra_hours.amount)
.sum(),
sick_leave_hours: extra_hours
.iter()
.filter(|extra_hours| extra_hours.category == ExtraHoursCategoryEntity::SickLeave)
.map(|extra_hours| extra_hours.amount)
.sum(),
holiday_hours: extra_hours
.iter()
.filter(|extra_hours| extra_hours.category == ExtraHoursCategoryEntity::Holiday)
.map(|extra_hours| extra_hours.amount)
.sum(),
by_week: hours_per_week(
&shiftplan_report,
&extra_hours,
&working_hours,
year,
until_week,
)?,
by_month: Arc::new([]),
};
Ok(employee_report)
}
}
fn hours_per_week(
shiftplan_hours_list: &Arc<[ShiftplanReportEntity]>,
extra_hours_list: &Arc<[ExtraHoursEntity]>,
working_hours: &[WorkingHoursEntity],
year: u32,
week_until: u8,
) -> Result<Arc<[WorkingHours]>, ServiceError> {
let mut weeks: Vec<WorkingHours> = Vec::new();
for week in 1..=week_until {
let shiftplan_hours = shiftplan_hours_list
.iter()
.filter(|r| r.calendar_week == week)
.map(|r| r.hours)
.sum::<f32>();
let working_hours = working_hours
.iter()
.filter(|wh| wh.from_calendar_week <= week && wh.to_calendar_week >= week)
.map(|wh| wh.expected_hours)
.sum::<f32>();
let extra_hours = extra_hours_list
.iter()
.filter(|eh| eh.date_time.iso_week() == week)
.map(|eh| eh.amount)
.sum::<f32>();
let mut day_list = extra_hours_list
.iter()
.map(|eh| {
Ok(WorkingHoursDay {
date: eh.date_time.date(),
hours: eh.amount,
category: (&eh.category).into(),
})
})
.chain(shiftplan_hours_list.iter().map(|working_hours_day| {
Ok::<WorkingHoursDay, ServiceError>(WorkingHoursDay {
date: time::Date::from_iso_week_date(
year as i32,
working_hours_day.calendar_week,
time::Weekday::Sunday.nth_next(working_hours_day.day_of_week.to_number()),
)?,
hours: working_hours_day.hours,
category: ExtraHoursCategory::Shiftplan,
})
}))
.collect::<Result<Vec<WorkingHoursDay>, ServiceError>>()?;
day_list.sort_by_key(|day| day.date);
weeks.push(WorkingHours {
from: time::Date::from_iso_week_date(year as i32, week, time::Weekday::Monday).unwrap(),
to: time::Date::from_iso_week_date(year as i32, week, time::Weekday::Sunday).unwrap(),
expected_hours: working_hours,
overall_hours: shiftplan_hours + extra_hours,
balance: shiftplan_hours + extra_hours - working_hours,
shiftplan_hours,
extra_work_hours: extra_hours_list
.iter()
.filter(|eh| eh.category == ExtraHoursCategoryEntity::ExtraWork)
.map(|eh| eh.amount)
.sum(),
vacation_hours: extra_hours_list
.iter()
.filter(|eh| eh.category == ExtraHoursCategoryEntity::Vacation)
.map(|eh| eh.amount)
.sum(),
sick_leave_hours: extra_hours_list
.iter()
.filter(|eh| eh.category == ExtraHoursCategoryEntity::SickLeave)
.map(|eh| eh.amount)
.sum(),
holiday_hours: extra_hours_list
.iter()
.filter(|eh| eh.category == ExtraHoursCategoryEntity::Holiday)
.map(|eh| eh.amount)
.sum(),
days: day_list
.iter()
.filter(|day| day.date.iso_week() == week && day.date.year() == year as i32)
.cloned()
.collect(),
});
}
Ok(weeks.into())
}

View file

@ -0,0 +1,109 @@
use std::sync::Arc;
use async_trait::async_trait;
use dao::working_hours::WorkingHoursEntity;
use service::{
permission::{Authentication, HR_PRIVILEGE},
working_hours::WorkingHours,
ServiceError,
};
use uuid::Uuid;
pub struct WorkingHoursServiceImpl<
WorkingHoursDao: dao::working_hours::WorkingHoursDao,
PermissionService: service::PermissionService,
ClockService: service::clock::ClockService,
UuidService: service::uuid_service::UuidService,
> {
working_hours_dao: Arc<WorkingHoursDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
}
impl<WorkingHoursDao, PermissionService, ClockService, UuidService>
WorkingHoursServiceImpl<WorkingHoursDao, PermissionService, ClockService, UuidService>
where
WorkingHoursDao: dao::working_hours::WorkingHoursDao + Sync + Send,
PermissionService: service::PermissionService + Sync + Send,
ClockService: service::clock::ClockService + Sync + Send,
UuidService: service::uuid_service::UuidService + Sync + Send,
{
pub fn new(
working_hours_dao: Arc<WorkingHoursDao>,
permission_service: Arc<PermissionService>,
clock_service: Arc<ClockService>,
uuid_service: Arc<UuidService>,
) -> Self {
Self {
working_hours_dao,
permission_service,
clock_service,
uuid_service,
}
}
}
#[async_trait]
impl<
WorkingHoursDao: dao::working_hours::WorkingHoursDao + Sync + Send,
PermissionService: service::PermissionService + Sync + Send,
ClockService: service::clock::ClockService + Sync + Send,
UuidService: service::uuid_service::UuidService + Sync + Send,
> service::working_hours::WorkingHoursService
for WorkingHoursServiceImpl<WorkingHoursDao, PermissionService, ClockService, UuidService>
{
type Context = PermissionService::Context;
async fn all(
&self,
_context: Authentication<Self::Context>,
) -> Result<Arc<[WorkingHours]>, ServiceError> {
unimplemented!()
}
async fn find_by_sales_person_id(
&self,
_sales_person_id: Uuid,
_context: Authentication<Self::Context>,
) -> Result<Arc<[WorkingHours]>, ServiceError> {
unimplemented!()
}
async fn create(
&self,
working_hours: &WorkingHours,
context: Authentication<Self::Context>,
) -> Result<WorkingHours, ServiceError> {
let mut working_hours = working_hours.to_owned();
self.permission_service
.check_permission(HR_PRIVILEGE, context)
.await?;
working_hours.created = Some(self.clock_service.date_time_now());
let mut entity: WorkingHoursEntity = (&working_hours).try_into()?;
if !entity.id.is_nil() {
return Err(ServiceError::IdSetOnCreate);
}
if !entity.version.is_nil() {
return Err(ServiceError::VersionSetOnCreate);
}
entity.id = self
.uuid_service
.new_uuid("working-hours-service::create id");
entity.version = self
.uuid_service
.new_uuid("working-hours-service::create version");
self.working_hours_dao
.create(&entity, "working-hours-service::create")
.await?;
Ok(WorkingHours::from(&entity))
}
async fn update(
&self,
_entity: &WorkingHours,
_context: Authentication<Self::Context>,
) -> Result<WorkingHours, ServiceError> {
unimplemented!()
}
}