1use crate::client::archive::ArchiveClient;
2use crate::config::Config;
3use crate::error::YukiError;
4use crate::output::{
5 ListOptions, OutputFormat, apply_pagination, format_json, format_table, is_tty,
6};
7
8pub async fn list(
9 config: &Config,
10 _admin: Option<&str>,
11 folder: Option<&str>,
12 doc_type: Option<&str>,
13 format: Option<&str>,
14 opts: ListOptions<'_>,
15) -> Result<(), YukiError> {
16 let mut client = ArchiveClient::new();
17 client.authenticate(&config.api_key).await?;
18
19 let docs = match (folder, doc_type) {
20 (Some(f), _) => {
21 let folder_id: i32 = f.parse().unwrap_or(0);
22 client
23 .documents_in_folder(folder_id, "2000-01-01", "2099-12-31")
24 .await?
25 }
26 (None, Some(t)) => {
27 let doc_type_id: i32 = t.parse().unwrap_or(0);
28 let xml = client.documents_by_type(doc_type_id).await?;
29 let headers = vec!["Raw XML".into()];
31 let rows = vec![vec![xml]];
32 let fmt = OutputFormat::from_flag(format, is_tty());
33 match fmt {
34 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
35 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
36 }
37 return Ok(());
38 }
39 (None, None) => {
40 client
41 .documents_in_folder(0, "2000-01-01", "2099-12-31")
42 .await?
43 }
44 };
45
46 let headers = vec![
47 "ID".into(),
48 "Date".into(),
49 "Amount".into(),
50 "Contact".into(),
51 "Subject".into(),
52 "File".into(),
53 ];
54 let mut rows: Vec<Vec<String>> = docs
55 .into_iter()
56 .map(|d| {
57 vec![
58 d.id,
59 d.document_date,
60 d.amount,
61 d.contact_name,
62 d.subject,
63 d.file_name,
64 ]
65 })
66 .collect();
67 apply_pagination(&mut rows, &opts);
68
69 let fmt = OutputFormat::from_flag(format, is_tty());
70 match fmt {
71 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
72 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
73 }
74 Ok(())
75}
76
77pub async fn search(
78 config: &Config,
79 _admin: Option<&str>,
80 query: &str,
81 format: Option<&str>,
82) -> Result<(), YukiError> {
83 let mut client = ArchiveClient::new();
84 client.authenticate(&config.api_key).await?;
85
86 let year = current_year();
88 let start = format!("{year}-01-01");
89 let end = format!("{year}-12-31");
90
91 let docs = client.search_documents(query, &start, &end).await?;
92
93 let headers = vec![
94 "ID".into(),
95 "Date".into(),
96 "Amount".into(),
97 "Contact".into(),
98 "Subject".into(),
99 "File".into(),
100 ];
101 let rows: Vec<Vec<String>> = docs
102 .into_iter()
103 .map(|d| {
104 vec![
105 d.id,
106 d.document_date,
107 d.amount,
108 d.contact_name,
109 d.subject,
110 d.file_name,
111 ]
112 })
113 .collect();
114
115 let fmt = OutputFormat::from_flag(format, is_tty());
116 match fmt {
117 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
118 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
119 }
120 Ok(())
121}
122
123pub async fn exists(
128 config: &Config,
129 _admin: Option<&str>,
130 amount: f64,
131 date: &str,
132 contact: Option<&str>,
133 format: Option<&str>,
134) -> Result<(), YukiError> {
135 let (search_start, search_end, filter_start, filter_end) = date_range(date)?;
136
137 let mut client = ArchiveClient::new();
138 client.authenticate(&config.api_key).await?;
139
140 let search_text = contact.unwrap_or("");
141 let docs = client
142 .search_documents(search_text, &search_start, &search_end)
143 .await?;
144
145 let matched: Vec<_> = docs
146 .into_iter()
147 .filter(|d| {
148 let amount_matches = d
149 .amount
150 .trim()
151 .parse::<f64>()
152 .map(|a| (a - amount).abs() <= 0.01)
153 .unwrap_or(false);
154 let date_matches = date_in_range(&d.document_date, &filter_start, &filter_end);
155 amount_matches && date_matches
156 })
157 .collect();
158
159 let headers = vec![
160 "ID".into(),
161 "Date".into(),
162 "Amount".into(),
163 "Contact".into(),
164 "Subject".into(),
165 "File".into(),
166 ];
167 let rows: Vec<Vec<String>> = matched
168 .iter()
169 .map(|d| {
170 vec![
171 d.id.clone(),
172 d.document_date.clone(),
173 d.amount.clone(),
174 d.contact_name.clone(),
175 d.subject.clone(),
176 d.file_name.clone(),
177 ]
178 })
179 .collect();
180
181 let fmt = OutputFormat::from_flag(format, is_tty());
182 match fmt {
183 OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
184 OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
185 }
186
187 if rows.is_empty() {
188 return Err(YukiError::NotFound(
189 "no matching document found".to_string(),
190 ));
191 }
192
193 Ok(())
194}
195
196fn current_year() -> u32 {
197 use std::time::{SystemTime, UNIX_EPOCH};
198 let secs = SystemTime::now()
199 .duration_since(UNIX_EPOCH)
200 .unwrap_or_default()
201 .as_secs();
202 1970 + (secs / 31_557_600) as u32
203}
204
205fn date_range(date: &str) -> Result<(String, String, String, String), YukiError> {
210 let parts: Vec<&str> = date.split('-').collect();
211 match parts.len() {
212 3 => {
213 let y: i32 = parts[0].parse().unwrap_or(0);
215 let m: i32 = parts[1].parse().unwrap_or(0);
216 let search_start = format!("{y:04}-{:02}-01", (m - 1).max(1));
217 let search_end = format!("{y:04}-{:02}-28", (m + 1).min(12));
218 let filter_start = shift_days(date, -7);
219 let filter_end = shift_days(date, 7);
220 Ok((search_start, search_end, filter_start, filter_end))
221 }
222 _ => {
223 let (start, end) = crate::period::parse_period(date)?;
225 Ok((start.clone(), end.clone(), start, end))
226 }
227 }
228}
229
230fn shift_days(date: &str, days: i32) -> String {
232 let to_days = |s: &str| -> i32 {
233 let p: Vec<&str> = s.split('-').collect();
234 if p.len() < 3 {
235 return 0;
236 }
237 let y: i32 = p[0].parse().unwrap_or(0);
238 let m: i32 = p[1].parse().unwrap_or(0);
239 let d: i32 = p[2].parse().unwrap_or(0);
240 y * 365 + m * 30 + d
241 };
242 let from_days = |total: i32| -> String {
243 let y = total / 365;
244 let rem = total % 365;
245 let m = (rem / 30).clamp(1, 12);
246 let d = (rem % 30).clamp(1, 28);
247 format!("{y:04}-{m:02}-{d:02}")
248 };
249 from_days(to_days(date) + days)
250}
251
252fn date_in_range(doc_date: &str, filter_start: &str, filter_end: &str) -> bool {
254 let normalized = doc_date.split('T').next().unwrap_or(doc_date);
255 normalized >= filter_start && normalized <= filter_end
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn date_range_exact_date() {
264 let (ss, se, fs, fe) = date_range("2025-03-15").unwrap();
265 assert_eq!(ss, "2025-02-01");
267 assert_eq!(se, "2025-04-28");
268 assert!(fs.as_str() <= "2025-03-08");
270 assert!(fe.as_str() >= "2025-03-22");
271 }
272
273 #[test]
274 fn date_range_month_period() {
275 let (ss, se, fs, fe) = date_range("2025-03").unwrap();
276 assert_eq!(ss, "2025-03-01");
277 assert_eq!(se, "2025-03-31");
278 assert_eq!(fs, ss);
279 assert_eq!(fe, se);
280 }
281
282 #[test]
283 fn date_range_quarter_period() {
284 let (ss, se, fs, fe) = date_range("2025-Q1").unwrap();
285 assert_eq!(ss, "2025-01-01");
286 assert_eq!(se, "2025-03-31");
287 assert_eq!(fs, ss);
288 assert_eq!(fe, se);
289 }
290
291 #[test]
292 fn date_range_year_period() {
293 let (ss, se, fs, fe) = date_range("2025").unwrap();
294 assert_eq!(ss, "2025-01-01");
295 assert_eq!(se, "2025-12-31");
296 assert_eq!(fs, ss);
297 assert_eq!(fe, se);
298 }
299
300 #[test]
301 fn date_in_range_within() {
302 assert!(date_in_range("2025-03-15", "2025-03-01", "2025-03-31"));
303 }
304
305 #[test]
306 fn date_in_range_strips_time() {
307 assert!(date_in_range(
308 "2025-03-15T10:30:00",
309 "2025-03-01",
310 "2025-03-31"
311 ));
312 }
313
314 #[test]
315 fn date_in_range_outside() {
316 assert!(!date_in_range("2025-04-01", "2025-03-01", "2025-03-31"));
317 }
318
319 #[test]
320 fn date_in_range_boundaries() {
321 assert!(date_in_range("2025-03-01", "2025-03-01", "2025-03-31"));
322 assert!(date_in_range("2025-03-31", "2025-03-01", "2025-03-31"));
323 }
324
325 #[test]
326 fn shift_days_forward() {
327 let result = shift_days("2025-03-15", 7);
328 assert!(result.as_str() > "2025-03-15");
329 }
330
331 #[test]
332 fn shift_days_backward() {
333 let result = shift_days("2025-03-15", -7);
334 assert!(result.as_str() < "2025-03-15");
335 }
336}