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

@ -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!()
}
}