1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use super::{event_type::EventType, location::Location};
use crate::db::pool::DbPool;
use chrono::{Local, NaiveDate, NaiveTime};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Event {
pub id: i32,
pub date: NaiveDate, // ⇔ events.date (TEXT "YYYY-MM-DD")
pub time: NaiveTime, // ⇔ events.time (TEXT "HH:MM")
pub kind: EventType, // ⇔ events.kind ('in' | 'out')
pub location: Location, // ⇔ events.position ('O','R','H','C','M')
pub lunch: Option<i32>, // ⇔ events.lunch_break (INT, default 0)
pub work_gap: bool, // ⇔ events.meta/work_gap logica futura
pub pair: i32, // ⇔ events.pair (INT NOT NULL DEFAULT 0)
pub source: String, // ⇔ events.source (TEXT, default 'cli')
pub meta: Option<String>, // ⇔ events.meta (TEXT, default '')
pub created_at: String, // ⇔ events.created_at (TEXT, ISO8601)
}
#[derive(Debug, Clone, Default)]
pub struct EventExtras {
pub lunch: Option<i32>,
pub work_gap: bool,
pub meta: Option<String>,
pub source: Option<String>,
pub pair: Option<i32>,
pub created_at: Option<String>,
}
impl Event {
/// Costruttore "di alto livello" per eventi creati dalla CLI.
/// - Imposta `pair = 0` (sarà ricalcolato da recalc_all_pairs)
/// - Imposta `created_at = now() in ISO8601`
pub fn new(
id: i32,
date: NaiveDate,
time: NaiveTime,
kind: EventType,
location: Location,
extras: EventExtras,
) -> Self {
Self {
id,
date,
time,
kind,
location,
lunch: extras.lunch,
work_gap: extras.work_gap,
pair: extras.pair.unwrap_or(0),
source: extras.source.unwrap_or_else(|| "cli".to_string()),
meta: extras.meta,
created_at: extras
.created_at
.unwrap_or_else(|| Local::now().to_rfc3339()),
}
}
pub fn date_str(&self) -> String {
self.date.format("%Y-%m-%d").to_string()
}
pub fn time_str(&self) -> String {
self.time.format("%H:%M").to_string()
}
pub fn timestamp(&self) -> chrono::DateTime<Local> {
let dt = self.date.and_time(self.time);
// convert naive to Local
dt.and_local_timezone(Local).unwrap()
}
pub fn get_date_time(&self) -> String {
self.date
.and_time(self.time)
.format("%Y-%m-%d %H:%M")
.to_string()
}
pub fn has_events_for_dates(pool: &mut DbPool, dates: &[NaiveDate]) -> rusqlite::Result<bool> {
if dates.is_empty() {
return Ok(false);
}
// Converti le date in stringhe "YYYY-MM-DD"
let date_strings: Vec<String> = dates
.iter()
.map(|d| d.format("%Y-%m-%d").to_string())
.collect();
// Crea una lista di placeholder: ?, ?, ?, ...
let placeholders = vec!["?"; date_strings.len()].join(",");
// Query con IN (...)
let sql = format!(
"SELECT 1 FROM events WHERE date IN ({}) LIMIT 1",
placeholders
);
// Converti in una lista di &dyn ToSql per rusqlite
let params: Vec<&dyn rusqlite::ToSql> = date_strings
.iter()
.map(|s| s as &dyn rusqlite::ToSql)
.collect();
let exists = {
let conn = &mut pool.conn;
let mut stmt = conn.prepare(&sql)?;
stmt.exists(rusqlite::params_from_iter(params))?
};
Ok(exists)
}
#[cfg(test)]
pub fn test_with_meta(meta: Option<&str>) -> Self {
Self {
id: 0,
date: Default::default(),
time: Default::default(),
kind: EventType::In,
location: Location::Office,
lunch: None,
work_gap: false,
pair: 0,
source: "".to_string(),
meta: meta.map(|s| s.to_string()),
// Inizializza qui TUTTI gli altri campi con valori “dummy” validi.
// Esempi tipici:
// id: 0,
// date: chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
// kind: EventKind::In,
// location: Location::Office,
// lunch: None,
// source: "test".into(),
// pair: 0,
// work_gap: false,
// ...
created_at: "".to_string(),
}
}
}