jobber 1.1.6-alpha

Minimalistic console work time tracker
use crate::{
    database::{Database, Error, Result},
    entities::{DateTime, Duration, RoundMode, RunningWork, Work, WorkTime},
    jobber::*,
};

impl JobberWork for Database {
    fn start_work(
        &mut self,
        job_id: usize,
        start: DateTime,
        subject: Option<impl ToString>,
        tags: impl IntoIterator<Item = impl ToString>,
    ) -> Result<RunningWork> {
        let job = self.get_job_mut(job_id)?;
        let scheme = &job.scheme(job_id)?;
        let start = scheme.round(&start, RoundMode::Past);
        if let Some(current) = &job.running {
            return Err(Error::AlreadyWorking {
                start: scheme.format_date_time(current.start),
                desc: current.subject.as_ref().unwrap_or(&job.subject).clone(),
                client: job.client.clone(),
            });
        }
        let running_work = RunningWork {
            start,
            subject: subject.map(|d| d.to_string()),
            tags: tags.into_iter().map(|t| t.to_string()).collect(),
        };
        job.running = Some(running_work.clone());
        Ok(running_work)
    }

    fn end_work(
        &mut self,
        job_id: usize,
        end: DateTime,
        subject: Mod<impl ToString>,
        tags: impl IntoIterator<Item = impl ToString>,
    ) -> Result<Work> {
        let job = self.get_job_mut(job_id)?;
        let end = job.scheme(job_id)?.round(&end, RoundMode::Future);
        let scheme = &job.scheme(job_id)?;
        if let Some(current) = &job.running {
            let subject = match (&current.subject, subject) {
                (Some(s), Mod::None) => s.clone(),
                (None, Mod::None) => {
                    return Err(Error::DescriptionMissing);
                }
                (None, Mod::Set(s)) | (None, Mod::Replace(s)) | (None, Mod::Append(s)) => {
                    s.to_string()
                }
                (Some(old), Mod::Set(_)) => {
                    return Err(Error::DescriptionExists(old.to_string()));
                }
                (Some(_), Mod::Replace(new)) => new.to_string(),
                (Some(old), Mod::Append(new)) => format!("{}\n{}", old, new.to_string()),
            };
            let tags = current
                .tags
                .iter()
                .cloned()
                .chain(tags.into_iter().map(|s| s.to_string()))
                .collect();
            if current.start > end {
                return Err(Error::EndsBeforeStart(
                    scheme.format_date_time(current.start),
                    scheme.format_date_time(end),
                ));
            }
            let work = Work {
                work_time: WorkTime::new(current.start, end),
                subject,
                tags,
                ..Default::default()
            };

            job.work.push(work.clone());
            job.running = None;
            Ok(work)
        } else {
            Err(Error::NotWorking)
        }
    }

    fn add_work(
        &mut self,
        job_id: usize,
        start: DateTime,
        end: DateTime,
        subject: impl ToString,
        tags: impl IntoIterator<Item = impl ToString>,
    ) -> Result<Work> {
        let scheme = &self.get_job(job_id)?.scheme(job_id)?;
        let start = scheme.round(&start, RoundMode::Past);
        let end = scheme.round(&end, RoundMode::Future);
        if start > end {
            Err(Error::EndsBeforeStart(
                scheme.format_date_time(start),
                scheme.format_date_time(end),
            ))?;
        }
        let running = self.cancel_work(job_id).ok();
        self.start_work(job_id, start, Some(subject), tags)?;
        let work = self.end_work(job_id, end, Mod::<String>::None, Vec::<String>::new())?;
        if let Some(running) = running {
            self.start_work(job_id, running.start, running.subject, running.tags)?;
        }
        Ok(work)
    }

    fn cancel_work(&mut self, job: usize) -> Result<RunningWork> {
        let job = self.get_job_mut(job)?;
        if let Some(current) = job.running.take() {
            Ok(current)
        } else {
            Err(Error::NotWorking)
        }
    }

    fn sort(&mut self, job_id: usize) -> Result<()> {
        let job = self.get_job_mut(job_id)?;
        job.work.sort();
        Ok(())
    }

    fn duration_by_client(&self, client_id: impl ToString) -> Duration {
        let id = client_id.to_string();
        self.jobs
            .iter()
            .filter(|job| job.client == id && !job.deleted)
            .flat_map(|job| job.work.iter())
            .map(|work| work.duration())
            .sum()
    }

    fn duration_by_worker(&self, worker_id: impl ToString) -> Duration {
        let id = worker_id.to_string();
        self.jobs
            .iter()
            .filter(|job| job.worker == id && !job.deleted)
            .flat_map(|job| job.work.iter())
            .map(|work| work.duration())
            .sum()
    }

    fn duration_by_day(&self, job_id: Option<usize>, day: DateTime) -> Duration {
        let year = day.year();
        let month = day.month();
        let day = day.day();
        let day_start = DateTime::from_ymd(year, month, day).expect("internal date/time error");
        let day_end = day_start + Duration::days(1);
        if let Some(work) = self
            .jobs
            .iter()
            .enumerate()
            .filter(|(_, job)| !job.deleted)
            .find_map(|(id, job)| job_id.is_none_or(|job_id| job_id == id).then_some(job))
            .map(|job| job.work.iter())
        {
            work.map(|w| w.work_time.duration_intersect(day_start, day_end))
                .sum()
        } else {
            Duration::zero()
        }
    }

    fn work_range(&self, job_id: &Option<usize>, deleted: Deleted) -> (DateTime, DateTime) {
        self.iter_jobs(deleted)
            .filter(|(id, _)| job_id.is_none_or(|job_id| job_id == *id))
            .flat_map(|(_, job)| job.work.iter())
            .filter(|work| deleted.check(work.deleted))
            .fold((DateTime::max(), DateTime::min()), |(min, max), work| {
                (min.min(work.start()), max.max(work.end()))
            })
    }
}