This commit is contained in:
2024-07-16 08:30:02 +01:00
parent c1b8636407
commit 45f1497df2
4 changed files with 353 additions and 15 deletions
+6 -5
View File
@@ -1,5 +1,5 @@
use std::ops::Sub;
use chrono::{TimeDelta, Utc};
use chrono::NaiveDate;
use toggl::TogglApi;
mod toggl;
@@ -12,8 +12,9 @@ async fn main() {
sensitive::WORKSPACE_ID,
);
dbg!(api.get_time_user_entries_between(
Utc::now().sub(TimeDelta::days(2)),
Utc::now(),
).await);
dbg!(api.search(toggl::types::TogglReportFilters {
start_date: Some(NaiveDate::from_ymd_opt(2024, 07, 10).unwrap()),
end_date: Some(NaiveDate::from_ymd_opt(2024, 07, 16).unwrap()),
..Default::default()
}).await);
}
+187 -6
View File
@@ -95,13 +95,12 @@ impl TogglApi {
base_url = BASE_URL,
);
let mut url = Url::parse(&url).unwrap();
url.query_pairs_mut()
.append_pair("start_date", &start.to_rfc3339_opts(SecondsFormat::Secs, true))
.append_pair("end_date", &until.to_rfc3339_opts(SecondsFormat::Secs, true));
Self::parse(self.client.get(url)
.headers(self.headers.clone())
.query(&[
("start_date", start.to_rfc3339_opts(SecondsFormat::Secs, true)),
("end_date", until.to_rfc3339_opts(SecondsFormat::Secs, true)),
])
.send().await?).await
}
@@ -166,11 +165,47 @@ impl TogglApi {
.headers(self.headers.clone())
.send().await?).await
}
fn paginate_filters(original_filters: &types::TogglReportFilters, last_row_id: u64) -> types::TogglReportFilters {
let mut filters = original_filters.clone();
filters.first_row_number = Some(last_row_id + 1);
filters
}
pub async fn search(&self, filters: types::TogglReportFilters) -> Result<Vec<types::ReportEntry>, TogglError> {
let url = format!(
"{base_url}/workspace/{workspace_id}/search/time_entries",
base_url = &REPORTS_BASE_URL,
workspace_id = self.workspace_id,
);
let mut last_row_number = Some(0);
let mut results = vec![];
while let Some(last_row_number_n) = last_row_number {
// If we are not on the first page, wait a bit to avoid rate limiting
if last_row_number_n != 0 {
tokio::time::sleep(Duration::from_secs(1)).await;
}
let data: Vec<types::ReportEntry> = Self::parse(self.client.post(&url)
.headers(self.headers.clone())
.json(&Self::paginate_filters(&filters, last_row_number_n))
.send().await?).await?;
last_row_number = data.last().map(|e| e.row_number as u64);
data.into_iter().for_each(|e| results.push(e));
}
Ok(results) }
}
mod types {
pub mod types {
use std::collections::HashMap;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TimeEntry {
@@ -297,6 +332,152 @@ mod types {
deleted_at: Option<DateTime<Utc>>,
permissions: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ReportEntry {
pub user_id: u32,
pub username: String,
pub project_id: Option<u64>,
pub task_id: Option<u64>,
pub billable: bool,
pub description: String,
pub tag_ids: Vec<u64>,
pub billable_amount_in_cents: Option<u64>,
pub hourly_rate_in_cents: Option<u64>,
pub currency: String,
pub time_entries: Vec<ReportEntryTimeDetails>,
pub row_number: u32,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ReportEntryTimeDetails {
pub id: u64,
pub seconds: u32,
pub start: DateTime<Utc>,
pub stop: DateTime<Utc>,
pub at: DateTime<Utc>,
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct TogglReportFilters {
pub billable: Option<bool>,
pub client_ids: Option<Vec<u64>>,
pub description: Option<String>,
pub end_date: Option<String>,
pub first_id: Option<u64>,
pub first_row_number: Option<u64>,
pub first_timestamp: Option<u64>,
pub group_ids: Option<Vec<u64>>,
pub grouped: Option<bool>,
pub hide_amounts: Option<bool>,
pub max_duration_seconds: Option<u64>,
pub min_duration_seconds: Option<u64>,
pub order_by: Option<String>,
pub order_dir: Option<String>,
#[serde(rename = "postedFields")]
pub posted_fields: Option<Vec<String>>,
pub project_ids: Option<Vec<u64>>,
pub rounding: Option<u64>,
pub rounding_minutes: Option<u64>,
#[serde(rename = "startTime")]
pub start_time: Option<String>,
pub start_date: Option<String>,
pub tag_ids: Option<Vec<u64>>,
pub task_ids: Option<Vec<u64>>,
pub time_entry_ids: Option<Vec<u64>>,
pub user_ids: Option<Vec<u64>>,
#[serde(flatten)]
pub rest: HashMap<String, serde_json::Value>,
}
use std::fmt;
impl fmt::Debug for TogglReportFilters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ds = f.debug_struct("TogglQuery");
if let Some(billable) = &self.billable {
ds.field("billable", billable);
}
if let Some(client_ids) = &self.client_ids {
ds.field("client_ids", client_ids);
}
if let Some(description) = &self.description {
ds.field("description", description);
}
if let Some(end_date) = &self.end_date {
ds.field("end_date", end_date);
}
if let Some(first_id) = &self.first_id {
ds.field("first_id", first_id);
}
if let Some(first_row_number) = &self.first_row_number {
ds.field("first_row_number", first_row_number);
}
if let Some(first_timestamp) = &self.first_timestamp {
ds.field("first_timestamp", first_timestamp);
}
if let Some(group_ids) = &self.group_ids {
ds.field("group_ids", group_ids);
}
if let Some(grouped) = &self.grouped {
ds.field("grouped", grouped);
}
if let Some(hide_amounts) = &self.hide_amounts {
ds.field("hide_amounts", hide_amounts);
}
if let Some(max_duration_seconds) = &self.max_duration_seconds {
ds.field("max_duration_seconds", max_duration_seconds);
}
if let Some(min_duration_seconds) = &self.min_duration_seconds {
ds.field("min_duration_seconds", min_duration_seconds);
}
if let Some(order_by) = &self.order_by {
ds.field("order_by", order_by);
}
if let Some(order_dir) = &self.order_dir {
ds.field("order_dir", order_dir);
}
if let Some(posted_fields) = &self.posted_fields {
ds.field("postedFields", posted_fields);
}
if let Some(project_ids) = &self.project_ids {
ds.field("project_ids", project_ids);
}
if let Some(rounding) = &self.rounding {
ds.field("rounding", rounding);
}
if let Some(rounding_minutes) = &self.rounding_minutes {
ds.field("rounding_minutes", rounding_minutes);
}
if let Some(start_time) = &self.start_time {
ds.field("startTime", start_time);
}
if let Some(start_date) = &self.start_date {
ds.field("start_date", start_date);
}
if let Some(tag_ids) = &self.tag_ids {
ds.field("tag_ids", tag_ids);
}
if let Some(task_ids) = &self.task_ids {
ds.field("task_ids", task_ids);
}
if let Some(time_entry_ids) = &self.time_entry_ids {
ds.field("time_entry_ids", time_entry_ids);
}
if let Some(user_ids) = &self.user_ids {
ds.field("user_ids", user_ids);
}
if !self.rest.is_empty() {
ds.field("rest", &self.rest);
}
ds.finish()
}
}
}
#[derive(Debug, thiserror::Error)]