1use super::common::workbook_not_found;
6use crate::store::WorkbookStore;
7use crate::types::enums::{ChartType, LegendPosition};
8use crate::types::inputs::*;
9use crate::types::responses::*;
10
11fn find_sheet(wb: &zavora_xlsx::Workbook, name: &str) -> Option<usize> {
12 wb.sheet_names().iter().position(|n| *n == name)
13}
14fn sheet_err(name: &str) -> String {
15 error(
16 ErrorCategory::NotFound,
17 &format!("Sheet '{}' not found", name),
18 "Check sheet name.",
19 )
20}
21
22pub fn set_page_setup(
25 store: &mut WorkbookStore,
26 input: SetPageSetupInput,
27) -> Result<String, anyhow::Error> {
28 let entry = match store.get_mut(&input.workbook_id) {
29 Some(e) => e,
30 None => return Ok(workbook_not_found(store, &input.workbook_id)),
31 };
32 let idx = match find_sheet(&entry.data, &input.sheet_name) {
33 Some(i) => i,
34 None => return Ok(sheet_err(&input.sheet_name)),
35 };
36 let ws = entry
37 .data
38 .worksheet(idx)
39 .map_err(|e| anyhow::anyhow!("{e}"))?;
40 if input.landscape == Some(true) {
41 ws.set_landscape();
42 } else if input.landscape == Some(false) {
43 ws.set_portrait();
44 }
45 if let Some(ps) = input.paper_size {
46 ws.set_paper_size(ps);
47 }
48 if let Some(ref m) = input.margins {
49 ws.set_margins(
50 m.top.unwrap_or(0.75),
51 m.bottom.unwrap_or(0.75),
52 m.left.unwrap_or(0.7),
53 m.right.unwrap_or(0.7),
54 );
55 }
56 if let Some(ref f) = input.fit_to_pages {
57 ws.set_fit_to_page(f.width, f.height);
58 }
59 if let Some(s) = input.print_scale {
60 ws.set_print_scale(s);
61 }
62 if let Some(ref pa) = input.print_area {
63 let (r1, c1, r2, c2) =
64 zavora_xlsx::utility::parse_range_ref(pa).map_err(|e| anyhow::anyhow!("{e}"))?;
65 ws.set_print_area(r1, c1, r2, c2);
66 }
67 if let Some(ref rr) = input.repeat_rows {
68 ws.set_repeat_rows(rr.first, rr.last);
69 }
70 if let Some(ref h) = input.header {
71 ws.set_header(h);
72 }
73 if let Some(ref f) = input.footer {
74 ws.set_footer(f);
75 }
76 if let Some(true) = input.print_gridlines {
77 ws.set_print_settings(&zavora_xlsx::PrintSettings::new().print_gridlines(true));
78 }
79 if input.center_horizontally == Some(true) {
80 ws.set_print_settings(&zavora_xlsx::PrintSettings::new().center_horizontally(true));
81 }
82 Ok(success_no_data("Page setup configured"))
83}
84
85pub fn add_comment(
86 store: &mut WorkbookStore,
87 input: AddCommentInput,
88) -> Result<String, anyhow::Error> {
89 let entry = match store.get_mut(&input.workbook_id) {
90 Some(e) => e,
91 None => return Ok(workbook_not_found(store, &input.workbook_id)),
92 };
93 let idx = match find_sheet(&entry.data, &input.sheet_name) {
94 Some(i) => i,
95 None => return Ok(sheet_err(&input.sheet_name)),
96 };
97 let (row, col) =
98 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
99 let ws = entry
100 .data
101 .worksheet(idx)
102 .map_err(|e| anyhow::anyhow!("{e}"))?;
103 if let Some(ref author) = input.author {
104 ws.add_comment_with_author(row, col, &input.text, author);
105 } else {
106 ws.add_comment(row, col, &input.text);
107 }
108 Ok(success_no_data(&format!("Comment added at {}", input.cell)))
109}
110
111pub fn add_hyperlink(
112 store: &mut WorkbookStore,
113 input: AddHyperlinkInput,
114) -> Result<String, anyhow::Error> {
115 let entry = match store.get_mut(&input.workbook_id) {
116 Some(e) => e,
117 None => return Ok(workbook_not_found(store, &input.workbook_id)),
118 };
119 let idx = match find_sheet(&entry.data, &input.sheet_name) {
120 Some(i) => i,
121 None => return Ok(sheet_err(&input.sheet_name)),
122 };
123 let (row, col) =
124 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
125 let ws = entry
126 .data
127 .worksheet(idx)
128 .map_err(|e| anyhow::anyhow!("{e}"))?;
129 let display = input.display_text.as_deref().unwrap_or(&input.url);
130 ws.write(row, col, display)
131 .map_err(|e| anyhow::anyhow!("{e}"))?;
132 ws.write_url(
133 row,
134 col,
135 &input.url,
136 input.display_text.as_deref().unwrap_or(&input.url),
137 )?;
138 Ok(success_no_data(&format!(
139 "Hyperlink added at {}",
140 input.cell
141 )))
142}
143
144pub fn add_defined_name(
145 store: &mut WorkbookStore,
146 input: AddDefinedNameInput,
147) -> Result<String, anyhow::Error> {
148 let entry = match store.get_mut(&input.workbook_id) {
149 Some(e) => e,
150 None => return Ok(workbook_not_found(store, &input.workbook_id)),
151 };
152 entry.data.define_name(&input.name, &input.formula);
153 Ok(success_no_data(&format!(
154 "Defined name '{}' added",
155 input.name
156 )))
157}
158
159pub fn list_defined_names(
160 store: &mut WorkbookStore,
161 input: ListDefinedNamesInput,
162) -> Result<String, anyhow::Error> {
163 let entry = match store.get(&input.workbook_id) {
164 Some(e) => e,
165 None => return Ok(workbook_not_found(store, &input.workbook_id)),
166 };
167 let names: Vec<serde_json::Value> = entry
168 .data
169 .defined_names()
170 .iter()
171 .map(|(n, f)| serde_json::json!({"name": n, "formula": f}))
172 .collect();
173 Ok(success("Defined names listed", names))
174}
175
176pub fn set_sheet_settings(
177 store: &mut WorkbookStore,
178 input: SetSheetSettingsInput,
179) -> Result<String, anyhow::Error> {
180 let entry = match store.get_mut(&input.workbook_id) {
181 Some(e) => e,
182 None => return Ok(workbook_not_found(store, &input.workbook_id)),
183 };
184 let idx = match find_sheet(&entry.data, &input.sheet_name) {
185 Some(i) => i,
186 None => return Ok(sheet_err(&input.sheet_name)),
187 };
188 let ws = entry
189 .data
190 .worksheet(idx)
191 .map_err(|e| anyhow::anyhow!("{e}"))?;
192 if input.hidden == Some(true) {
193 ws.set_hidden();
194 }
195 if input.very_hidden == Some(true) {
196 ws.set_very_hidden();
197 }
198 if let Some(z) = input.zoom {
199 ws.set_zoom(z);
200 }
201 if input.hide_gridlines == Some(true) {
202 ws.hide_gridlines();
203 }
204 if input.hide_headings == Some(true) {
205 ws.hide_headings();
206 }
207 if let Some(ref c) = input.tab_color {
208 ws.set_tab_color(c.as_str());
209 }
210 if input.right_to_left == Some(true) {
211 ws.set_right_to_left();
212 }
213 Ok(success_no_data("Sheet settings updated"))
214}
215
216pub fn set_active_sheet(
217 store: &mut WorkbookStore,
218 input: SetActiveSheetInput,
219) -> Result<String, anyhow::Error> {
220 let entry = match store.get_mut(&input.workbook_id) {
221 Some(e) => e,
222 None => return Ok(workbook_not_found(store, &input.workbook_id)),
223 };
224 entry.data.set_active_sheet(input.sheet_index);
225 Ok(success_no_data(&format!(
226 "Active sheet set to index {}",
227 input.sheet_index
228 )))
229}
230
231pub fn insert_rows(
234 store: &mut WorkbookStore,
235 input: InsertDeleteRowsInput,
236) -> Result<String, anyhow::Error> {
237 let entry = match store.get_mut(&input.workbook_id) {
238 Some(e) => e,
239 None => return Ok(workbook_not_found(store, &input.workbook_id)),
240 };
241 let idx = match find_sheet(&entry.data, &input.sheet_name) {
242 Some(i) => i,
243 None => return Ok(sheet_err(&input.sheet_name)),
244 };
245 entry
246 .data
247 .worksheet(idx)
248 .map_err(|e| anyhow::anyhow!("{e}"))?
249 .insert_rows(input.at_row.saturating_sub(1), input.count)
250 .map_err(|e| anyhow::anyhow!("{e}"))?;
251 Ok(success_no_data(&format!(
252 "{} rows inserted at row {}",
253 input.count, input.at_row
254 )))
255}
256
257pub fn delete_rows(
258 store: &mut WorkbookStore,
259 input: InsertDeleteRowsInput,
260) -> Result<String, anyhow::Error> {
261 let entry = match store.get_mut(&input.workbook_id) {
262 Some(e) => e,
263 None => return Ok(workbook_not_found(store, &input.workbook_id)),
264 };
265 let idx = match find_sheet(&entry.data, &input.sheet_name) {
266 Some(i) => i,
267 None => return Ok(sheet_err(&input.sheet_name)),
268 };
269 entry
270 .data
271 .worksheet(idx)
272 .map_err(|e| anyhow::anyhow!("{e}"))?
273 .remove_rows(input.at_row.saturating_sub(1), input.count)
274 .map_err(|e| anyhow::anyhow!("{e}"))?;
275 Ok(success_no_data(&format!(
276 "{} rows deleted at row {}",
277 input.count, input.at_row
278 )))
279}
280
281pub fn insert_columns(
282 store: &mut WorkbookStore,
283 input: InsertDeleteColumnsInput,
284) -> Result<String, anyhow::Error> {
285 let entry = match store.get_mut(&input.workbook_id) {
286 Some(e) => e,
287 None => return Ok(workbook_not_found(store, &input.workbook_id)),
288 };
289 let idx = match find_sheet(&entry.data, &input.sheet_name) {
290 Some(i) => i,
291 None => return Ok(sheet_err(&input.sheet_name)),
292 };
293 let col = zavora_xlsx::utility::col_from_letter(&input.at_column)
294 .map_err(|e| anyhow::anyhow!("{e}"))?;
295 entry
296 .data
297 .worksheet(idx)
298 .map_err(|e| anyhow::anyhow!("{e}"))?
299 .insert_columns(col, input.count)
300 .map_err(|e| anyhow::anyhow!("{e}"))?;
301 Ok(success_no_data(&format!(
302 "{} columns inserted at {}",
303 input.count, input.at_column
304 )))
305}
306
307pub fn delete_columns(
308 store: &mut WorkbookStore,
309 input: InsertDeleteColumnsInput,
310) -> Result<String, anyhow::Error> {
311 let entry = match store.get_mut(&input.workbook_id) {
312 Some(e) => e,
313 None => return Ok(workbook_not_found(store, &input.workbook_id)),
314 };
315 let idx = match find_sheet(&entry.data, &input.sheet_name) {
316 Some(i) => i,
317 None => return Ok(sheet_err(&input.sheet_name)),
318 };
319 let col = zavora_xlsx::utility::col_from_letter(&input.at_column)
320 .map_err(|e| anyhow::anyhow!("{e}"))?;
321 entry
322 .data
323 .worksheet(idx)
324 .map_err(|e| anyhow::anyhow!("{e}"))?
325 .remove_columns(col, input.count)
326 .map_err(|e| anyhow::anyhow!("{e}"))?;
327 Ok(success_no_data(&format!(
328 "{} columns deleted at {}",
329 input.count, input.at_column
330 )))
331}
332
333pub fn group_rows(
334 store: &mut WorkbookStore,
335 input: GroupRowsInput,
336) -> Result<String, anyhow::Error> {
337 let entry = match store.get_mut(&input.workbook_id) {
338 Some(e) => e,
339 None => return Ok(workbook_not_found(store, &input.workbook_id)),
340 };
341 let idx = match find_sheet(&entry.data, &input.sheet_name) {
342 Some(i) => i,
343 None => return Ok(sheet_err(&input.sheet_name)),
344 };
345 entry
346 .data
347 .worksheet(idx)
348 .map_err(|e| anyhow::anyhow!("{e}"))?
349 .group_rows(
350 input.start.saturating_sub(1),
351 input.end.saturating_sub(1),
352 input.level,
353 );
354 Ok(success_no_data(&format!(
355 "Rows {}-{} grouped at level {}",
356 input.start, input.end, input.level
357 )))
358}
359
360pub fn group_columns(
361 store: &mut WorkbookStore,
362 input: GroupColumnsInput,
363) -> Result<String, anyhow::Error> {
364 let entry = match store.get_mut(&input.workbook_id) {
365 Some(e) => e,
366 None => return Ok(workbook_not_found(store, &input.workbook_id)),
367 };
368 let idx = match find_sheet(&entry.data, &input.sheet_name) {
369 Some(i) => i,
370 None => return Ok(sheet_err(&input.sheet_name)),
371 };
372 let c1 =
373 zavora_xlsx::utility::col_from_letter(&input.start).map_err(|e| anyhow::anyhow!("{e}"))?;
374 let c2 =
375 zavora_xlsx::utility::col_from_letter(&input.end).map_err(|e| anyhow::anyhow!("{e}"))?;
376 entry
377 .data
378 .worksheet(idx)
379 .map_err(|e| anyhow::anyhow!("{e}"))?
380 .group_columns(c1, c2, input.level);
381 Ok(success_no_data(&format!(
382 "Columns {}-{} grouped at level {}",
383 input.start, input.end, input.level
384 )))
385}
386
387pub fn protect_sheet(
388 store: &mut WorkbookStore,
389 input: ProtectSheetInput,
390) -> Result<String, anyhow::Error> {
391 let entry = match store.get_mut(&input.workbook_id) {
392 Some(e) => e,
393 None => return Ok(workbook_not_found(store, &input.workbook_id)),
394 };
395 let idx = match find_sheet(&entry.data, &input.sheet_name) {
396 Some(i) => i,
397 None => return Ok(sheet_err(&input.sheet_name)),
398 };
399 let ws = entry
400 .data
401 .worksheet(idx)
402 .map_err(|e| anyhow::anyhow!("{e}"))?;
403 if let Some(ref pw) = input.password {
404 ws.protect_with_password(pw);
405 } else {
406 ws.protect();
407 }
408 Ok(success_no_data(&format!(
409 "Sheet '{}' protected",
410 input.sheet_name
411 )))
412}
413
414pub fn protect_workbook(
415 store: &mut WorkbookStore,
416 input: ProtectWorkbookInput,
417) -> Result<String, anyhow::Error> {
418 let entry = match store.get_mut(&input.workbook_id) {
419 Some(e) => e,
420 None => return Ok(workbook_not_found(store, &input.workbook_id)),
421 };
422 if let Some(ref pw) = input.password {
423 entry.data.protect_with_password(pw);
424 } else {
425 entry.data.protect();
426 }
427 Ok(success_no_data("Workbook protected"))
428}
429
430pub fn autofit_columns(
431 store: &mut WorkbookStore,
432 input: AutofitColumnsInput,
433) -> Result<String, anyhow::Error> {
434 let entry = match store.get_mut(&input.workbook_id) {
435 Some(e) => e,
436 None => return Ok(workbook_not_found(store, &input.workbook_id)),
437 };
438 let idx = match find_sheet(&entry.data, &input.sheet_name) {
439 Some(i) => i,
440 None => return Ok(sheet_err(&input.sheet_name)),
441 };
442 entry
443 .data
444 .worksheet(idx)
445 .map_err(|e| anyhow::anyhow!("{e}"))?
446 .autofit()
447 .map_err(|e| anyhow::anyhow!("{e}"))?;
448 Ok(success_no_data("Columns autofitted"))
449}
450
451pub fn add_chart_enhanced(
454 store: &mut WorkbookStore,
455 input: AddChartEnhancedInput,
456) -> Result<String, anyhow::Error> {
457 let entry = match store.get_mut(&input.workbook_id) {
458 Some(e) => e,
459 None => return Ok(workbook_not_found(store, &input.workbook_id)),
460 };
461 let idx = match find_sheet(&entry.data, &input.sheet_name) {
462 Some(i) => i,
463 None => return Ok(sheet_err(&input.sheet_name)),
464 };
465 let ct = match input.chart_type {
466 ChartType::Bar => zavora_xlsx::ChartType::Bar,
467 ChartType::Column => zavora_xlsx::ChartType::Column,
468 ChartType::Line => zavora_xlsx::ChartType::Line,
469 ChartType::Pie => zavora_xlsx::ChartType::Pie,
470 ChartType::Scatter => zavora_xlsx::ChartType::Scatter,
471 ChartType::Area => zavora_xlsx::ChartType::Area,
472 ChartType::Doughnut => zavora_xlsx::ChartType::Doughnut,
473 };
474 let mut chart = zavora_xlsx::Chart::new(ct);
475 if let Some(ref t) = input.title {
476 chart.set_title(t);
477 }
478 if let Some(ref x) = input.x_axis_label {
479 chart.set_x_axis_name(x);
480 }
481 if let Some(ref y) = input.y_axis_label {
482 chart.set_y_axis_name(y);
483 }
484 if let Some(ref lp) = input.legend_position {
485 chart.set_legend_position(match lp {
486 LegendPosition::Top => zavora_xlsx::LegendPosition::Top,
487 LegendPosition::Bottom => zavora_xlsx::LegendPosition::Bottom,
488 LegendPosition::Left => zavora_xlsx::LegendPosition::Left,
489 LegendPosition::Right => zavora_xlsx::LegendPosition::Right,
490 LegendPosition::None => zavora_xlsx::LegendPosition::None,
491 });
492 }
493 chart.set_width(input.width);
494 chart.set_height(input.height);
495 if let Some(ref ps) = input.pivot_source {
496 chart.set_pivot_source(&ps.pivot_table, &ps.sheet);
497 }
498 if input.show_data_table == Some(true) {
500 chart.show_data_table(true);
501 }
502 if let Some(ref v) = input.view_3d {
503 let mut view = zavora_xlsx::View3D::default();
504 if let Some(rx) = v.rot_x {
505 view.rot_x = rx;
506 }
507 if let Some(ry) = v.rot_y {
508 view.rot_y = ry;
509 }
510 if let Some(p) = v.perspective {
511 view.perspective = p;
512 }
513 chart.set_view3d(view);
514 }
515 if let Some(s) = input.style {
516 chart.set_style(s);
517 }
518 if let Some(ref at) = input.alt_text {
519 chart.set_alt_text(&at.title, &at.description);
520 }
521 if let Some(v) = input.y_axis_min {
522 chart.set_y_axis_min(v);
523 }
524 if let Some(v) = input.y_axis_max {
525 chart.set_y_axis_max(v);
526 }
527 if let Some(v) = input.y_axis_log_base {
528 chart.set_y_axis_log_base(v);
529 }
530 if input.x_axis_reverse == Some(true) {
531 chart.set_x_axis_reverse();
532 }
533 if input.y_axis_reverse == Some(true) {
534 chart.set_y_axis_reverse();
535 }
536 if let Some(ref fmt) = input.x_axis_format {
537 let af = zavora_xlsx::AxisFormat {
538 num_format: Some(fmt.clone()),
539 ..Default::default()
540 };
541 chart.set_x_axis_format(af);
542 }
543 if let Some(ref fmt) = input.y_axis_format {
544 let af = zavora_xlsx::AxisFormat {
545 num_format: Some(fmt.clone()),
546 ..Default::default()
547 };
548 chart.set_y_axis_format(af);
549 }
550 if input.drop_lines == Some(true) {
551 chart.set_drop_lines(true);
552 }
553 if input.high_low_lines == Some(true) {
554 chart.set_high_low_lines(true);
555 }
556 if let Some(ref fill) = input.plot_area_fill {
557 let rgb = parse_hex_color(fill);
558 let paf = zavora_xlsx::PlotAreaFormat {
559 fill: Some(rgb),
560 border: None,
561 gradient: None,
562 };
563 chart.set_plot_area_format(paf);
564 }
565 if !input.series.is_empty() {
567 for si in &input.series {
568 let s = chart.add_series();
569 s.set_values(&si.values);
570 if let Some(ref c) = si.categories {
571 s.set_categories(c);
572 }
573 if let Some(ref n) = si.name {
574 s.set_name(n);
575 }
576 if let Some(ref c) = si.color {
577 s.set_color(c.as_str());
578 }
579 if si.data_labels == Some(true) {
580 s.set_data_labels(true);
581 }
582 if si.secondary_axis == Some(true) {
583 s.set_secondary_axis(true);
584 }
585 if let Some(ref t) = si.trendline {
586 let tt = match t.as_str() {
587 "exponential" => zavora_xlsx::TrendlineType::Exponential,
588 "power" => zavora_xlsx::TrendlineType::Power,
589 "logarithmic" => zavora_xlsx::TrendlineType::Logarithmic,
590 _ => zavora_xlsx::TrendlineType::Linear,
591 };
592 s.set_trendline(tt);
593 }
594 if let Some(ref m) = si.marker {
595 let mt = match m.as_str() {
596 "circle" => zavora_xlsx::MarkerType::Circle,
597 "diamond" => zavora_xlsx::MarkerType::Diamond,
598 "square" => zavora_xlsx::MarkerType::Square,
599 "triangle" => zavora_xlsx::MarkerType::Triangle,
600 _ => zavora_xlsx::MarkerType::None,
601 };
602 s.set_marker(mt);
603 }
604 if let Some(lw) = si.line_width {
605 s.set_line_width(lw);
606 }
607 if let Some(ref ds) = si.dash_style {
608 let style = match ds.as_str() {
609 "dash" => zavora_xlsx::DashStyle::Dash,
610 "dot" => zavora_xlsx::DashStyle::Dot,
611 "dash_dot" => zavora_xlsx::DashStyle::DashDot,
612 "long_dash" => zavora_xlsx::DashStyle::LongDash,
613 "long_dash_dot" => zavora_xlsx::DashStyle::LongDashDot,
614 _ => zavora_xlsx::DashStyle::Solid,
615 };
616 s.set_dash_style(style);
617 }
618 if let Some(ref g) = si.gradient {
619 let stops: Vec<([u8; 3], f64)> = g
620 .iter()
621 .map(|gs| (parse_hex_color(&gs.color), gs.position))
622 .collect();
623 s.set_gradient(stops);
624 }
625 if let Some(ref bs) = si.bubble_sizes {
626 s.set_bubble_sizes(bs);
627 }
628 if let Some(ref eb) = si.error_bars {
629 let bt = match eb.bar_type.as_str() {
630 "plus" => zavora_xlsx::ErrorBarType::Plus,
631 "minus" => zavora_xlsx::ErrorBarType::Minus,
632 _ => zavora_xlsx::ErrorBarType::Both,
633 };
634 let vt = match eb.value_type.as_str() {
635 "percentage" => zavora_xlsx::ErrorBarValueType::Percentage,
636 "std_dev" => zavora_xlsx::ErrorBarValueType::StandardDeviation,
637 "std_error" => zavora_xlsx::ErrorBarValueType::StandardError,
638 _ => zavora_xlsx::ErrorBarValueType::FixedValue,
639 };
640 s.set_error_bars(zavora_xlsx::ErrorBar::new(bt, vt, eb.value));
641 }
642 }
643 } else if let Some(ref dr) = input.data_range {
644 chart.add_series().set_values(dr);
645 }
646 let (row, col) = if let Some(ref c) = input.cell {
647 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
648 } else {
649 (0, 0)
650 };
651 entry
652 .data
653 .worksheet(idx)
654 .map_err(|e| anyhow::anyhow!("{e}"))?
655 .insert_chart(row, col, &chart)?;
656 Ok(success_no_data(&format!(
657 "Chart added to '{}'",
658 input.sheet_name
659 )))
660}
661
662pub fn add_pivot_table(
663 store: &mut WorkbookStore,
664 input: AddPivotTableInput,
665) -> Result<String, anyhow::Error> {
666 let entry = match store.get_mut(&input.workbook_id) {
667 Some(e) => e,
668 None => return Ok(workbook_not_found(store, &input.workbook_id)),
669 };
670 let idx = match find_sheet(&entry.data, &input.sheet_name) {
671 Some(i) => i,
672 None => return Ok(sheet_err(&input.sheet_name)),
673 };
674 let mut pt = zavora_xlsx::PivotTable::new(&input.name, &input.source_range);
675 for f in &input.row_fields {
676 pt = pt.add_row_field(f);
677 }
678 for f in &input.column_fields {
679 pt = pt.add_column_field(f);
680 }
681 for vf in &input.value_fields {
682 let agg = match vf.aggregation.as_str() {
683 "count" => zavora_xlsx::PivotAggregation::Count,
684 "average" => zavora_xlsx::PivotAggregation::Average,
685 "max" => zavora_xlsx::PivotAggregation::Max,
686 "min" => zavora_xlsx::PivotAggregation::Min,
687 "product" => zavora_xlsx::PivotAggregation::Product,
688 _ => zavora_xlsx::PivotAggregation::Sum,
689 };
690 pt = pt.add_value_field(&vf.field, agg);
691 }
692 for f in &input.filter_fields {
693 pt = pt.add_filter_field(f);
694 }
695 if let Some(ref s) = input.style {
696 pt = pt.set_style_name(s);
697 }
698 if let Some(ref l) = input.layout {
699 let layout = match l.as_str() {
700 "outline" => zavora_xlsx::PivotLayout::Outline,
701 "tabular" => zavora_xlsx::PivotLayout::Tabular,
702 _ => zavora_xlsx::PivotLayout::Compact,
703 };
704 pt = pt.set_layout(layout);
705 }
706 for cf in &input.calculated_fields {
708 pt = pt.add_calculated_field(&cf.name, &cf.formula);
709 }
710 for dg in &input.date_groups {
711 let levels: Vec<zavora_xlsx::DateGroupLevel> = dg
712 .levels
713 .iter()
714 .map(|l| match l.as_str() {
715 "years" => zavora_xlsx::DateGroupLevel::Years,
716 "quarters" => zavora_xlsx::DateGroupLevel::Quarters,
717 "months" => zavora_xlsx::DateGroupLevel::Months,
718 "days" => zavora_xlsx::DateGroupLevel::Days,
719 "hours" => zavora_xlsx::DateGroupLevel::Hours,
720 "minutes" => zavora_xlsx::DateGroupLevel::Minutes,
721 "seconds" => zavora_xlsx::DateGroupLevel::Seconds,
722 _ => zavora_xlsx::DateGroupLevel::Months,
723 })
724 .collect();
725 pt = pt.group_by_date(&dg.field, &levels);
726 }
727 for rg in &input.range_groups {
728 pt = pt.group_by_range(&rg.field, rg.start, rg.end, rg.interval);
729 }
730 for vf in &input.value_formats {
731 pt = pt.set_value_format(&vf.field, &vf.format);
732 }
733 for st in &input.subtotals {
734 pt = pt.show_subtotals(&st.field, st.show);
735 }
736 if let (Some(rows), Some(cols)) = (input.grand_total_rows, input.grand_total_cols) {
737 pt = pt.show_grand_totals(rows, cols);
738 } else if let Some(rows) = input.grand_total_rows {
739 pt = pt.show_grand_totals(rows, true);
740 } else if let Some(cols) = input.grand_total_cols {
741 pt = pt.show_grand_totals(true, cols);
742 }
743 if let Some(v) = input.show_row_headers {
744 pt = pt.show_row_headers(v);
745 }
746 if let Some(v) = input.show_column_headers {
747 pt = pt.show_column_headers(v);
748 }
749 if let Some(v) = input.show_row_stripes {
750 pt = pt.show_row_stripes(v);
751 }
752 let (row, col) = if let Some(ref c) = input.cell {
753 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
754 } else {
755 (0, 0)
756 };
757 entry
758 .data
759 .worksheet(idx)
760 .map_err(|e| anyhow::anyhow!("{e}"))?
761 .add_pivot_table(row, col, &pt)?;
762 Ok(success_no_data(&format!(
763 "Pivot table '{}' added",
764 input.name
765 )))
766}
767
768pub fn read_comments(
771 store: &mut WorkbookStore,
772 input: ReadCommentsInput,
773) -> Result<String, anyhow::Error> {
774 let entry = match store.get_mut(&input.workbook_id) {
775 Some(e) => e,
776 None => return Ok(workbook_not_found(store, &input.workbook_id)),
777 };
778 let idx = match find_sheet(&entry.data, &input.sheet_name) {
779 Some(i) => i,
780 None => return Ok(sheet_err(&input.sheet_name)),
781 };
782 let ws = entry
783 .data
784 .worksheet(idx)
785 .map_err(|e| anyhow::anyhow!("{e}"))?;
786 let comments: Vec<serde_json::Value> = ws.comments().iter().map(|c| {
787 serde_json::json!({ "cell": zavora_xlsx::utility::to_a1(c.row, c.col), "author": c.author, "text": c.text })
788 }).collect();
789 Ok(success("Comments read", comments))
790}
791
792pub fn write_rich_text(
793 store: &mut WorkbookStore,
794 input: WriteRichTextInput,
795) -> Result<String, anyhow::Error> {
796 let entry = match store.get_mut(&input.workbook_id) {
797 Some(e) => e,
798 None => return Ok(workbook_not_found(store, &input.workbook_id)),
799 };
800 let idx = match find_sheet(&entry.data, &input.sheet_name) {
801 Some(i) => i,
802 None => return Ok(sheet_err(&input.sheet_name)),
803 };
804 let (row, col) =
805 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
806 let mut rt = zavora_xlsx::RichText::new();
807 for run in &input.runs {
808 let mut built = zavora_xlsx::RichTextRun {
809 text: run.text.clone(),
810 bold: run.bold.unwrap_or(false),
811 italic: run.italic.unwrap_or(false),
812 font_size: run.font_size,
813 font_name: None,
814 color: None,
815 superscript: false,
816 subscript: false,
817 };
818 if let Some(ref c) = run.color {
819 built.color = Some(c.clone());
820 }
821 rt.runs.push(built);
822 }
823 entry
824 .data
825 .worksheet(idx)
826 .map_err(|e| anyhow::anyhow!("{e}"))?
827 .write_rich_text(row, col, &rt)
828 .map_err(|e| anyhow::anyhow!("{e}"))?;
829 Ok(success_no_data(&format!(
830 "Rich text written to {}",
831 input.cell
832 )))
833}
834
835fn build_format(
840 bold: Option<bool>,
841 italic: Option<bool>,
842 font_size: Option<f64>,
843 font_color: Option<&str>,
844 bg: Option<&str>,
845 nf: Option<&str>,
846) -> zavora_xlsx::Format {
847 let mut f = zavora_xlsx::Format::new();
848 if bold == Some(true) {
849 f = f.bold();
850 }
851 if italic == Some(true) {
852 f = f.italic();
853 }
854 if let Some(s) = font_size {
855 f = f.font_size(s);
856 }
857 if let Some(c) = font_color {
858 f = f.font_color(c);
859 }
860 if let Some(c) = bg {
861 f = f.background_color(c);
862 }
863 if let Some(n) = nf {
864 f = f.num_format(n);
865 }
866 f
867}
868
869pub fn set_column_format(
870 store: &mut WorkbookStore,
871 input: SetColumnFormatInput,
872) -> Result<String, anyhow::Error> {
873 let entry = match store.get_mut(&input.workbook_id) {
874 Some(e) => e,
875 None => return Ok(workbook_not_found(store, &input.workbook_id)),
876 };
877 let idx = match find_sheet(&entry.data, &input.sheet_name) {
878 Some(i) => i,
879 None => return Ok(sheet_err(&input.sheet_name)),
880 };
881 let col =
882 zavora_xlsx::utility::col_from_letter(&input.column).map_err(|e| anyhow::anyhow!("{e}"))?;
883 let fmt = build_format(
884 input.bold,
885 input.italic,
886 input.font_size,
887 input.font_color.as_deref(),
888 input.background_color.as_deref(),
889 input.number_format.as_deref(),
890 );
891 entry
892 .data
893 .worksheet(idx)
894 .map_err(|e| anyhow::anyhow!("{e}"))?
895 .set_column_format(col, &fmt);
896 Ok(success_no_data(&format!(
897 "Column {} format set",
898 input.column
899 )))
900}
901
902pub fn set_row_format(
903 store: &mut WorkbookStore,
904 input: SetRowFormatInput,
905) -> Result<String, anyhow::Error> {
906 let entry = match store.get_mut(&input.workbook_id) {
907 Some(e) => e,
908 None => return Ok(workbook_not_found(store, &input.workbook_id)),
909 };
910 let idx = match find_sheet(&entry.data, &input.sheet_name) {
911 Some(i) => i,
912 None => return Ok(sheet_err(&input.sheet_name)),
913 };
914 let fmt = build_format(
915 input.bold,
916 input.italic,
917 input.font_size,
918 input.font_color.as_deref(),
919 input.background_color.as_deref(),
920 input.number_format.as_deref(),
921 );
922 entry
923 .data
924 .worksheet(idx)
925 .map_err(|e| anyhow::anyhow!("{e}"))?
926 .set_row_format(input.row.saturating_sub(1), &fmt);
927 Ok(success_no_data(&format!("Row {} format set", input.row)))
928}
929
930pub fn set_column_hidden(
931 store: &mut WorkbookStore,
932 input: SetColumnHiddenInput,
933) -> Result<String, anyhow::Error> {
934 let entry = match store.get_mut(&input.workbook_id) {
935 Some(e) => e,
936 None => return Ok(workbook_not_found(store, &input.workbook_id)),
937 };
938 let idx = match find_sheet(&entry.data, &input.sheet_name) {
939 Some(i) => i,
940 None => return Ok(sheet_err(&input.sheet_name)),
941 };
942 let col =
943 zavora_xlsx::utility::col_from_letter(&input.column).map_err(|e| anyhow::anyhow!("{e}"))?;
944 entry
945 .data
946 .worksheet(idx)
947 .map_err(|e| anyhow::anyhow!("{e}"))?
948 .set_column_hidden(col, input.hidden);
949 Ok(success_no_data(&format!(
950 "Column {} hidden={}",
951 input.column, input.hidden
952 )))
953}
954
955pub fn set_row_hidden(
956 store: &mut WorkbookStore,
957 input: SetRowHiddenInput,
958) -> Result<String, anyhow::Error> {
959 let entry = match store.get_mut(&input.workbook_id) {
960 Some(e) => e,
961 None => return Ok(workbook_not_found(store, &input.workbook_id)),
962 };
963 let idx = match find_sheet(&entry.data, &input.sheet_name) {
964 Some(i) => i,
965 None => return Ok(sheet_err(&input.sheet_name)),
966 };
967 entry
968 .data
969 .worksheet(idx)
970 .map_err(|e| anyhow::anyhow!("{e}"))?
971 .set_row_hidden(input.row.saturating_sub(1), input.hidden);
972 Ok(success_no_data(&format!(
973 "Row {} hidden={}",
974 input.row, input.hidden
975 )))
976}
977
978pub fn set_column_range_width(
979 store: &mut WorkbookStore,
980 input: SetColumnRangeWidthInput,
981) -> Result<String, anyhow::Error> {
982 let entry = match store.get_mut(&input.workbook_id) {
983 Some(e) => e,
984 None => return Ok(workbook_not_found(store, &input.workbook_id)),
985 };
986 let idx = match find_sheet(&entry.data, &input.sheet_name) {
987 Some(i) => i,
988 None => return Ok(sheet_err(&input.sheet_name)),
989 };
990 let c1 = zavora_xlsx::utility::col_from_letter(&input.first_column)
991 .map_err(|e| anyhow::anyhow!("{e}"))?;
992 let c2 = zavora_xlsx::utility::col_from_letter(&input.last_column)
993 .map_err(|e| anyhow::anyhow!("{e}"))?;
994 entry
995 .data
996 .worksheet(idx)
997 .map_err(|e| anyhow::anyhow!("{e}"))?
998 .set_column_range_width(c1, c2, input.width);
999 Ok(success_no_data(&format!(
1000 "Columns {}:{} width set to {}",
1001 input.first_column, input.last_column, input.width
1002 )))
1003}
1004
1005pub fn set_default_row_height(
1006 store: &mut WorkbookStore,
1007 input: SetDefaultRowHeightInput,
1008) -> Result<String, anyhow::Error> {
1009 let entry = match store.get_mut(&input.workbook_id) {
1010 Some(e) => e,
1011 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1012 };
1013 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1014 Some(i) => i,
1015 None => return Ok(sheet_err(&input.sheet_name)),
1016 };
1017 entry
1018 .data
1019 .worksheet(idx)
1020 .map_err(|e| anyhow::anyhow!("{e}"))?
1021 .set_default_row_height(input.height);
1022 Ok(success_no_data(&format!(
1023 "Default row height set to {}",
1024 input.height
1025 )))
1026}
1027
1028pub fn set_selection(
1029 store: &mut WorkbookStore,
1030 input: SetSelectionInput,
1031) -> Result<String, anyhow::Error> {
1032 let entry = match store.get_mut(&input.workbook_id) {
1033 Some(e) => e,
1034 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1035 };
1036 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1037 Some(i) => i,
1038 None => return Ok(sheet_err(&input.sheet_name)),
1039 };
1040 let (row, col) =
1041 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1042 entry
1043 .data
1044 .worksheet(idx)
1045 .map_err(|e| anyhow::anyhow!("{e}"))?
1046 .set_selection(row, col);
1047 Ok(success_no_data(&format!("Selection set to {}", input.cell)))
1048}
1049
1050pub fn set_autofilter(
1051 store: &mut WorkbookStore,
1052 input: SetAutofilterInput,
1053) -> Result<String, anyhow::Error> {
1054 let entry = match store.get_mut(&input.workbook_id) {
1055 Some(e) => e,
1056 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1057 };
1058 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1059 Some(i) => i,
1060 None => return Ok(sheet_err(&input.sheet_name)),
1061 };
1062 let (r1, c1, r2, c2) =
1063 zavora_xlsx::utility::parse_range_ref(&input.range).map_err(|e| anyhow::anyhow!("{e}"))?;
1064 entry
1065 .data
1066 .worksheet(idx)
1067 .map_err(|e| anyhow::anyhow!("{e}"))?
1068 .set_autofilter(r1, c1, r2, c2);
1069 Ok(success_no_data(&format!(
1070 "Autofilter set on {}",
1071 input.range
1072 )))
1073}
1074
1075pub fn filter_column(
1076 store: &mut WorkbookStore,
1077 input: FilterColumnInput,
1078) -> Result<String, anyhow::Error> {
1079 let entry = match store.get_mut(&input.workbook_id) {
1080 Some(e) => e,
1081 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1082 };
1083 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1084 Some(i) => i,
1085 None => return Ok(sheet_err(&input.sheet_name)),
1086 };
1087 let col =
1088 zavora_xlsx::utility::col_from_letter(&input.column).map_err(|e| anyhow::anyhow!("{e}"))?;
1089 let strs: Vec<&str> = input.values.iter().map(|s| s.as_str()).collect();
1090 entry
1091 .data
1092 .worksheet(idx)
1093 .map_err(|e| anyhow::anyhow!("{e}"))?
1094 .filter_column(col, &strs);
1095 Ok(success_no_data(&format!(
1096 "Filter applied to column {}",
1097 input.column
1098 )))
1099}
1100
1101pub fn ignore_error(
1102 store: &mut WorkbookStore,
1103 input: IgnoreErrorInput,
1104) -> Result<String, anyhow::Error> {
1105 let entry = match store.get_mut(&input.workbook_id) {
1106 Some(e) => e,
1107 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1108 };
1109 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1110 Some(i) => i,
1111 None => return Ok(sheet_err(&input.sheet_name)),
1112 };
1113 entry
1114 .data
1115 .worksheet(idx)
1116 .map_err(|e| anyhow::anyhow!("{e}"))?
1117 .ignore_error(&input.error_type, &input.range);
1118 Ok(success_no_data("Error ignored"))
1119}
1120
1121pub fn set_page_breaks(
1122 store: &mut WorkbookStore,
1123 input: SetPageBreaksInput,
1124) -> Result<String, anyhow::Error> {
1125 let entry = match store.get_mut(&input.workbook_id) {
1126 Some(e) => e,
1127 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1128 };
1129 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1130 Some(i) => i,
1131 None => return Ok(sheet_err(&input.sheet_name)),
1132 };
1133 entry
1134 .data
1135 .worksheet(idx)
1136 .map_err(|e| anyhow::anyhow!("{e}"))?
1137 .set_page_breaks(&input.row_breaks, &input.col_breaks);
1138 Ok(success_no_data("Page breaks set"))
1139}
1140
1141pub fn unprotect_range(
1142 store: &mut WorkbookStore,
1143 input: UnprotectRangeInput,
1144) -> Result<String, anyhow::Error> {
1145 let entry = match store.get_mut(&input.workbook_id) {
1146 Some(e) => e,
1147 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1148 };
1149 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1150 Some(i) => i,
1151 None => return Ok(sheet_err(&input.sheet_name)),
1152 };
1153 let ws = entry
1154 .data
1155 .worksheet(idx)
1156 .map_err(|e| anyhow::anyhow!("{e}"))?;
1157 if let Some(ref pw) = input.password {
1158 ws.unprotect_range_with_password(&input.range, &input.title, pw);
1159 } else {
1160 ws.unprotect_range(&input.range, &input.title);
1161 }
1162 Ok(success_no_data(&format!(
1163 "Range {} unprotected",
1164 input.range
1165 )))
1166}
1167
1168pub fn write_formula(
1169 store: &mut WorkbookStore,
1170 input: WriteFormulaInput,
1171) -> Result<String, anyhow::Error> {
1172 let entry = match store.get_mut(&input.workbook_id) {
1173 Some(e) => e,
1174 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1175 };
1176 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1177 Some(i) => i,
1178 None => return Ok(sheet_err(&input.sheet_name)),
1179 };
1180 let (row, col) =
1181 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1182 let ws = entry
1183 .data
1184 .worksheet(idx)
1185 .map_err(|e| anyhow::anyhow!("{e}"))?;
1186 if let Some(result) = input.cached_result {
1187 ws.write_formula_with_result(row, col, &input.formula, result)
1188 .map_err(|e| anyhow::anyhow!("{e}"))?;
1189 } else {
1190 ws.write_formula(row, col, &input.formula)
1191 .map_err(|e| anyhow::anyhow!("{e}"))?;
1192 }
1193 Ok(success_no_data(&format!(
1194 "Formula written to {}",
1195 input.cell
1196 )))
1197}
1198
1199pub fn write_array_formula(
1200 store: &mut WorkbookStore,
1201 input: WriteArrayFormulaInput,
1202) -> Result<String, anyhow::Error> {
1203 let entry = match store.get_mut(&input.workbook_id) {
1204 Some(e) => e,
1205 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1206 };
1207 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1208 Some(i) => i,
1209 None => return Ok(sheet_err(&input.sheet_name)),
1210 };
1211 let (r1, c1, r2, c2) =
1212 zavora_xlsx::utility::parse_range_ref(&input.range).map_err(|e| anyhow::anyhow!("{e}"))?;
1213 entry
1214 .data
1215 .worksheet(idx)
1216 .map_err(|e| anyhow::anyhow!("{e}"))?
1217 .write_array_formula(r1, c1, r2, c2, &input.formula)
1218 .map_err(|e| anyhow::anyhow!("{e}"))?;
1219 Ok(success_no_data(&format!(
1220 "Array formula written to {}",
1221 input.range
1222 )))
1223}
1224
1225pub fn write_dynamic_formula(
1226 store: &mut WorkbookStore,
1227 input: WriteDynamicFormulaInput,
1228) -> Result<String, anyhow::Error> {
1229 let entry = match store.get_mut(&input.workbook_id) {
1230 Some(e) => e,
1231 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1232 };
1233 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1234 Some(i) => i,
1235 None => return Ok(sheet_err(&input.sheet_name)),
1236 };
1237 let (row, col) =
1238 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1239 entry
1240 .data
1241 .worksheet(idx)
1242 .map_err(|e| anyhow::anyhow!("{e}"))?
1243 .write_dynamic_formula(row, col, &input.formula)
1244 .map_err(|e| anyhow::anyhow!("{e}"))?;
1245 Ok(success_no_data(&format!(
1246 "Dynamic formula written to {}",
1247 input.cell
1248 )))
1249}
1250
1251pub fn write_blank(
1252 store: &mut WorkbookStore,
1253 input: WriteBlankInput,
1254) -> Result<String, anyhow::Error> {
1255 let entry = match store.get_mut(&input.workbook_id) {
1256 Some(e) => e,
1257 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1258 };
1259 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1260 Some(i) => i,
1261 None => return Ok(sheet_err(&input.sheet_name)),
1262 };
1263 let (row, col) =
1264 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1265 let fmt = build_format(
1266 input.bold,
1267 None,
1268 None,
1269 None,
1270 input.background_color.as_deref(),
1271 input.number_format.as_deref(),
1272 );
1273 entry
1274 .data
1275 .worksheet(idx)
1276 .map_err(|e| anyhow::anyhow!("{e}"))?
1277 .write_blank(row, col, &fmt)
1278 .map_err(|e| anyhow::anyhow!("{e}"))?;
1279 Ok(success_no_data(&format!(
1280 "Blank cell written at {}",
1281 input.cell
1282 )))
1283}
1284
1285pub fn clear_cell(
1286 store: &mut WorkbookStore,
1287 input: ClearCellInput,
1288) -> Result<String, anyhow::Error> {
1289 let entry = match store.get_mut(&input.workbook_id) {
1290 Some(e) => e,
1291 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1292 };
1293 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1294 Some(i) => i,
1295 None => return Ok(sheet_err(&input.sheet_name)),
1296 };
1297 let (row, col) =
1298 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1299 entry
1300 .data
1301 .worksheet(idx)
1302 .map_err(|e| anyhow::anyhow!("{e}"))?
1303 .clear_cell(row, col);
1304 Ok(success_no_data(&format!("Cell {} cleared", input.cell)))
1305}
1306
1307pub fn set_calc_mode(
1308 store: &mut WorkbookStore,
1309 input: SetCalcModeInput,
1310) -> Result<String, anyhow::Error> {
1311 let entry = match store.get_mut(&input.workbook_id) {
1312 Some(e) => e,
1313 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1314 };
1315 let mode = match input.mode.as_str() {
1316 "manual" => zavora_xlsx::CalcMode::Manual,
1317 "auto_no_table" => zavora_xlsx::CalcMode::AutoNoTable,
1318 _ => zavora_xlsx::CalcMode::Auto,
1319 };
1320 entry.data.set_calc_mode(mode);
1321 Ok(success_no_data(&format!("Calc mode set to {}", input.mode)))
1322}
1323
1324pub fn set_properties(
1325 store: &mut WorkbookStore,
1326 input: SetPropertiesInput,
1327) -> Result<String, anyhow::Error> {
1328 let entry = match store.get_mut(&input.workbook_id) {
1329 Some(e) => e,
1330 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1331 };
1332 let mut props = entry.data.properties().clone();
1333 if let Some(ref t) = input.title {
1334 props.title = Some(t.clone());
1335 }
1336 if let Some(ref a) = input.author {
1337 props.author = Some(a.clone());
1338 }
1339 if let Some(ref s) = input.subject {
1340 props.subject = Some(s.clone());
1341 }
1342 if let Some(ref c) = input.company {
1343 props.company = Some(c.clone());
1344 }
1345 if let Some(ref d) = input.description {
1346 props.description = Some(d.clone());
1347 }
1348 entry.data.set_properties(props);
1349 Ok(success_no_data("Document properties set"))
1350}
1351
1352pub fn move_worksheet(
1353 store: &mut WorkbookStore,
1354 input: MoveWorksheetInput,
1355) -> Result<String, anyhow::Error> {
1356 let entry = match store.get_mut(&input.workbook_id) {
1357 Some(e) => e,
1358 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1359 };
1360 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1361 Some(i) => i,
1362 None => return Ok(sheet_err(&input.sheet_name)),
1363 };
1364 entry
1365 .data
1366 .move_worksheet(idx, input.to_index)
1367 .map_err(|e| anyhow::anyhow!("{e}"))?;
1368 Ok(success_no_data(&format!(
1369 "Sheet '{}' moved to position {}",
1370 input.sheet_name, input.to_index
1371 )))
1372}
1373
1374pub fn write_internal_link(
1375 store: &mut WorkbookStore,
1376 input: WriteInternalLinkInput,
1377) -> Result<String, anyhow::Error> {
1378 let entry = match store.get_mut(&input.workbook_id) {
1379 Some(e) => e,
1380 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1381 };
1382 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1383 Some(i) => i,
1384 None => return Ok(sheet_err(&input.sheet_name)),
1385 };
1386 let (row, col) =
1387 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1388 entry
1389 .data
1390 .worksheet(idx)
1391 .map_err(|e| anyhow::anyhow!("{e}"))?
1392 .write_internal_link(row, col, &input.location, &input.display_text)
1393 .map_err(|e| anyhow::anyhow!("{e}"))?;
1394 Ok(success_no_data(&format!(
1395 "Internal link written at {}",
1396 input.cell
1397 )))
1398}
1399
1400pub fn configure_workbook(
1405 store: &mut WorkbookStore,
1406 input: ConfigureWorkbookInput,
1407) -> Result<String, anyhow::Error> {
1408 let entry = match store.get_mut(&input.workbook_id) {
1409 Some(e) => e,
1410 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1411 };
1412 if let Some(ref m) = input.calc_mode {
1413 let mode = match m.as_str() {
1414 "manual" => zavora_xlsx::CalcMode::Manual,
1415 "auto_no_table" => zavora_xlsx::CalcMode::AutoNoTable,
1416 _ => zavora_xlsx::CalcMode::Auto,
1417 };
1418 entry.data.set_calc_mode(mode);
1419 }
1420 if let Some(i) = input.active_sheet {
1421 entry.data.set_active_sheet(i);
1422 }
1423 let mut props = entry.data.properties().clone();
1424 if let Some(ref v) = input.title {
1425 props.title = Some(v.clone());
1426 }
1427 if let Some(ref v) = input.author {
1428 props.author = Some(v.clone());
1429 }
1430 if let Some(ref v) = input.subject {
1431 props.subject = Some(v.clone());
1432 }
1433 if let Some(ref v) = input.company {
1434 props.company = Some(v.clone());
1435 }
1436 if let Some(ref v) = input.description {
1437 props.description = Some(v.clone());
1438 }
1439 entry.data.set_properties(props);
1440 Ok(success_no_data("Workbook configured"))
1441}
1442
1443pub fn modify_rows(
1444 store: &mut WorkbookStore,
1445 input: ModifyRowsInput,
1446) -> Result<String, anyhow::Error> {
1447 let entry = match store.get_mut(&input.workbook_id) {
1448 Some(e) => e,
1449 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1450 };
1451 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1452 Some(i) => i,
1453 None => return Ok(sheet_err(&input.sheet_name)),
1454 };
1455 let ws = entry
1456 .data
1457 .worksheet(idx)
1458 .map_err(|e| anyhow::anyhow!("{e}"))?;
1459 let row = input.at_row.saturating_sub(1);
1460 match input.action.as_str() {
1461 "delete" => {
1462 ws.remove_rows(row, input.count)
1463 .map_err(|e| anyhow::anyhow!("{e}"))?;
1464 }
1465 _ => {
1466 ws.insert_rows(row, input.count)
1467 .map_err(|e| anyhow::anyhow!("{e}"))?;
1468 }
1469 }
1470 Ok(success_no_data(&format!(
1471 "{} {} rows at row {}",
1472 input.action, input.count, input.at_row
1473 )))
1474}
1475
1476pub fn modify_columns(
1477 store: &mut WorkbookStore,
1478 input: ModifyColumnsInput,
1479) -> Result<String, anyhow::Error> {
1480 let entry = match store.get_mut(&input.workbook_id) {
1481 Some(e) => e,
1482 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1483 };
1484 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1485 Some(i) => i,
1486 None => return Ok(sheet_err(&input.sheet_name)),
1487 };
1488 let col = zavora_xlsx::utility::col_from_letter(&input.at_column)
1489 .map_err(|e| anyhow::anyhow!("{e}"))?;
1490 let ws = entry
1491 .data
1492 .worksheet(idx)
1493 .map_err(|e| anyhow::anyhow!("{e}"))?;
1494 match input.action.as_str() {
1495 "delete" => {
1496 ws.remove_columns(col, input.count)
1497 .map_err(|e| anyhow::anyhow!("{e}"))?;
1498 }
1499 _ => {
1500 ws.insert_columns(col, input.count)
1501 .map_err(|e| anyhow::anyhow!("{e}"))?;
1502 }
1503 }
1504 Ok(success_no_data(&format!(
1505 "{} {} columns at {}",
1506 input.action, input.count, input.at_column
1507 )))
1508}
1509
1510pub fn write_formula_consolidated(
1511 store: &mut WorkbookStore,
1512 input: WriteFormulaConsolidatedInput,
1513) -> Result<String, anyhow::Error> {
1514 let entry = match store.get_mut(&input.workbook_id) {
1515 Some(e) => e,
1516 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1517 };
1518 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1519 Some(i) => i,
1520 None => return Ok(sheet_err(&input.sheet_name)),
1521 };
1522 let ws = entry
1523 .data
1524 .worksheet(idx)
1525 .map_err(|e| anyhow::anyhow!("{e}"))?;
1526 match input.formula_type.as_deref().unwrap_or("regular") {
1527 "array" => {
1528 let (r1, c1, r2, c2) = zavora_xlsx::utility::parse_range_ref(&input.cell)
1529 .map_err(|e| anyhow::anyhow!("{e}"))?;
1530 ws.write_array_formula(r1, c1, r2, c2, &input.formula)
1531 .map_err(|e| anyhow::anyhow!("{e}"))?;
1532 }
1533 "dynamic" => {
1534 let (row, col) = zavora_xlsx::utility::parse_cell_ref(&input.cell)
1535 .map_err(|e| anyhow::anyhow!("{e}"))?;
1536 ws.write_dynamic_formula(row, col, &input.formula)
1537 .map_err(|e| anyhow::anyhow!("{e}"))?;
1538 }
1539 _ => {
1540 let (row, col) = zavora_xlsx::utility::parse_cell_ref(&input.cell)
1541 .map_err(|e| anyhow::anyhow!("{e}"))?;
1542 if let Some(r) = input.cached_result {
1543 ws.write_formula_with_result(row, col, &input.formula, r)
1544 .map_err(|e| anyhow::anyhow!("{e}"))?;
1545 } else {
1546 ws.write_formula(row, col, &input.formula)
1547 .map_err(|e| anyhow::anyhow!("{e}"))?;
1548 }
1549 }
1550 }
1551 Ok(success_no_data(&format!(
1552 "Formula written to {}",
1553 input.cell
1554 )))
1555}
1556
1557pub fn manage_cell(
1558 store: &mut WorkbookStore,
1559 input: ManageCellInput,
1560) -> Result<String, anyhow::Error> {
1561 let entry = match store.get_mut(&input.workbook_id) {
1562 Some(e) => e,
1563 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1564 };
1565 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1566 Some(i) => i,
1567 None => return Ok(sheet_err(&input.sheet_name)),
1568 };
1569 let (row, col) =
1570 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1571 let ws = entry
1572 .data
1573 .worksheet(idx)
1574 .map_err(|e| anyhow::anyhow!("{e}"))?;
1575 match input.action.as_str() {
1576 "clear" => {
1577 ws.clear_cell(row, col);
1578 }
1579 _ => {
1580 let fmt = build_format(
1581 None,
1582 None,
1583 None,
1584 None,
1585 input.background_color.as_deref(),
1586 input.number_format.as_deref(),
1587 );
1588 ws.write_blank(row, col, &fmt)
1589 .map_err(|e| anyhow::anyhow!("{e}"))?;
1590 }
1591 }
1592 Ok(success_no_data(&format!(
1593 "Cell {} {}",
1594 input.cell, input.action
1595 )))
1596}
1597
1598pub fn manage_comments(
1599 store: &mut WorkbookStore,
1600 input: ManageCommentsInput,
1601) -> Result<String, anyhow::Error> {
1602 let entry = match store.get_mut(&input.workbook_id) {
1603 Some(e) => e,
1604 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1605 };
1606 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1607 Some(i) => i,
1608 None => return Ok(sheet_err(&input.sheet_name)),
1609 };
1610 match input.action.as_str() {
1611 "read" => {
1612 let ws = entry
1613 .data
1614 .worksheet(idx)
1615 .map_err(|e| anyhow::anyhow!("{e}"))?;
1616 let comments: Vec<serde_json::Value> = ws.comments().iter().map(|c|
1617 serde_json::json!({"cell": zavora_xlsx::utility::to_a1(c.row, c.col), "author": c.author, "text": c.text})
1618 ).collect();
1619 Ok(success("Comments read", comments))
1620 }
1621 _ => {
1622 let cell = input.cell.as_deref().unwrap_or("A1");
1623 let text = input.text.as_deref().unwrap_or("");
1624 let (row, col) =
1625 zavora_xlsx::utility::parse_cell_ref(cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1626 let ws = entry
1627 .data
1628 .worksheet(idx)
1629 .map_err(|e| anyhow::anyhow!("{e}"))?;
1630 if let Some(ref a) = input.author {
1631 ws.add_comment_with_author(row, col, text, a);
1632 } else {
1633 ws.add_comment(row, col, text);
1634 }
1635 Ok(success_no_data(&format!("Comment added at {cell}")))
1636 }
1637 }
1638}
1639
1640pub fn manage_defined_names(
1641 store: &mut WorkbookStore,
1642 input: ManageDefinedNamesInput,
1643) -> Result<String, anyhow::Error> {
1644 let entry = match store.get_mut(&input.workbook_id) {
1645 Some(e) => e,
1646 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1647 };
1648 match input.action.as_str() {
1649 "list" => {
1650 let names: Vec<serde_json::Value> = entry
1651 .data
1652 .defined_names()
1653 .iter()
1654 .map(|(n, f)| serde_json::json!({"name": n, "formula": f}))
1655 .collect();
1656 Ok(success("Defined names", names))
1657 }
1658 _ => {
1659 let name = input.name.as_deref().unwrap_or("");
1660 let formula = input.formula.as_deref().unwrap_or("");
1661 entry.data.define_name(name, formula);
1662 Ok(success_no_data(&format!("Defined name '{name}' added")))
1663 }
1664 }
1665}
1666
1667pub fn add_link(store: &mut WorkbookStore, input: AddLinkInput) -> Result<String, anyhow::Error> {
1668 let entry = match store.get_mut(&input.workbook_id) {
1669 Some(e) => e,
1670 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1671 };
1672 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1673 Some(i) => i,
1674 None => return Ok(sheet_err(&input.sheet_name)),
1675 };
1676 let (row, col) =
1677 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1678 let ws = entry
1679 .data
1680 .worksheet(idx)
1681 .map_err(|e| anyhow::anyhow!("{e}"))?;
1682 match input.link_type.as_str() {
1683 "internal" => {
1684 ws.write_internal_link(
1685 row,
1686 col,
1687 &input.target,
1688 input.display_text.as_deref().unwrap_or(&input.target),
1689 )
1690 .map_err(|e| anyhow::anyhow!("{e}"))?;
1691 }
1692 _ => {
1693 ws.write_url(
1694 row,
1695 col,
1696 &input.target,
1697 input.display_text.as_deref().unwrap_or(&input.target),
1698 )
1699 .map_err(|e| anyhow::anyhow!("{e}"))?;
1700 }
1701 }
1702 Ok(success_no_data(&format!("Link added at {}", input.cell)))
1703}
1704
1705pub fn protect_consolidated(
1706 store: &mut WorkbookStore,
1707 input: ProtectInput,
1708) -> Result<String, anyhow::Error> {
1709 let entry = match store.get_mut(&input.workbook_id) {
1710 Some(e) => e,
1711 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1712 };
1713 match input.target.as_str() {
1714 "workbook" => {
1715 if let Some(ref pw) = input.password {
1716 entry.data.protect_with_password(pw);
1717 } else {
1718 entry.data.protect();
1719 }
1720 Ok(success_no_data("Workbook protected"))
1721 }
1722 "unprotect_range" => {
1723 let sn = input.sheet_name.as_deref().unwrap_or("Sheet1");
1724 let idx = match find_sheet(&entry.data, sn) {
1725 Some(i) => i,
1726 None => return Ok(sheet_err(sn)),
1727 };
1728 let ws = entry
1729 .data
1730 .worksheet(idx)
1731 .map_err(|e| anyhow::anyhow!("{e}"))?;
1732 let range = input.range.as_deref().unwrap_or("A1:A1");
1733 let title = input.range_title.as_deref().unwrap_or("Range");
1734 if let Some(ref pw) = input.password {
1735 ws.unprotect_range_with_password(range, title, pw);
1736 } else {
1737 ws.unprotect_range(range, title);
1738 }
1739 Ok(success_no_data(&format!("Range {range} unprotected")))
1740 }
1741 _ => {
1742 let sn = input.sheet_name.as_deref().unwrap_or("Sheet1");
1744 let idx = match find_sheet(&entry.data, sn) {
1745 Some(i) => i,
1746 None => return Ok(sheet_err(sn)),
1747 };
1748 let ws = entry
1749 .data
1750 .worksheet(idx)
1751 .map_err(|e| anyhow::anyhow!("{e}"))?;
1752 if let Some(ref pw) = input.password {
1753 ws.protect_with_password(pw);
1754 } else {
1755 ws.protect();
1756 }
1757 Ok(success_no_data(&format!("Sheet '{sn}' protected")))
1758 }
1759 }
1760}
1761
1762pub fn set_dimensions(
1763 store: &mut WorkbookStore,
1764 input: SetDimensionsInput,
1765) -> Result<String, anyhow::Error> {
1766 let entry = match store.get_mut(&input.workbook_id) {
1767 Some(e) => e,
1768 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1769 };
1770 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1771 Some(i) => i,
1772 None => return Ok(sheet_err(&input.sheet_name)),
1773 };
1774 let ws = entry
1775 .data
1776 .worksheet(idx)
1777 .map_err(|e| anyhow::anyhow!("{e}"))?;
1778 match input.target.as_str() {
1779 "row_height" => {
1780 ws.set_row_height(input.row.unwrap_or(1).saturating_sub(1), input.value)
1781 .map_err(|e| anyhow::anyhow!("{e}"))?;
1782 }
1783 "column_range_width" => {
1784 let c1 =
1785 zavora_xlsx::utility::col_from_letter(input.first_column.as_deref().unwrap_or("A"))
1786 .map_err(|e| anyhow::anyhow!("{e}"))?;
1787 let c2 =
1788 zavora_xlsx::utility::col_from_letter(input.last_column.as_deref().unwrap_or("A"))
1789 .map_err(|e| anyhow::anyhow!("{e}"))?;
1790 ws.set_column_range_width(c1, c2, input.value);
1791 }
1792 "default_row_height" => {
1793 ws.set_default_row_height(input.value);
1794 }
1795 _ => {
1796 let col = zavora_xlsx::utility::col_from_letter(input.column.as_deref().unwrap_or("A"))
1798 .map_err(|e| anyhow::anyhow!("{e}"))?;
1799 ws.set_column_width(col, input.value)
1800 .map_err(|e| anyhow::anyhow!("{e}"))?;
1801 }
1802 }
1803 Ok(success_no_data(&format!(
1804 "{} set to {}",
1805 input.target, input.value
1806 )))
1807}
1808
1809pub fn set_visibility(
1810 store: &mut WorkbookStore,
1811 input: SetVisibilityInput,
1812) -> Result<String, anyhow::Error> {
1813 let entry = match store.get_mut(&input.workbook_id) {
1814 Some(e) => e,
1815 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1816 };
1817 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1818 Some(i) => i,
1819 None => return Ok(sheet_err(&input.sheet_name)),
1820 };
1821 let ws = entry
1822 .data
1823 .worksheet(idx)
1824 .map_err(|e| anyhow::anyhow!("{e}"))?;
1825 match input.target.as_str() {
1826 "row" => {
1827 let r: u32 = input.identifier.parse().unwrap_or(1);
1828 ws.set_row_hidden(r.saturating_sub(1), input.hidden);
1829 }
1830 _ => {
1831 let col = zavora_xlsx::utility::col_from_letter(&input.identifier)
1832 .map_err(|e| anyhow::anyhow!("{e}"))?;
1833 ws.set_column_hidden(col, input.hidden);
1834 }
1835 }
1836 Ok(success_no_data(&format!(
1837 "{} {} hidden={}",
1838 input.target, input.identifier, input.hidden
1839 )))
1840}
1841
1842pub fn set_row_column_format(
1843 store: &mut WorkbookStore,
1844 input: SetRowColumnFormatInput,
1845) -> Result<String, anyhow::Error> {
1846 let entry = match store.get_mut(&input.workbook_id) {
1847 Some(e) => e,
1848 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1849 };
1850 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1851 Some(i) => i,
1852 None => return Ok(sheet_err(&input.sheet_name)),
1853 };
1854 let fmt = build_format(
1855 input.bold,
1856 input.italic,
1857 input.font_size,
1858 input.font_color.as_deref(),
1859 input.background_color.as_deref(),
1860 input.number_format.as_deref(),
1861 );
1862 let ws = entry
1863 .data
1864 .worksheet(idx)
1865 .map_err(|e| anyhow::anyhow!("{e}"))?;
1866 match input.target.as_str() {
1867 "row" => {
1868 let r: u32 = input.identifier.parse().unwrap_or(1);
1869 ws.set_row_format(r.saturating_sub(1), &fmt);
1870 }
1871 _ => {
1872 let col = zavora_xlsx::utility::col_from_letter(&input.identifier)
1873 .map_err(|e| anyhow::anyhow!("{e}"))?;
1874 ws.set_column_format(col, &fmt);
1875 }
1876 }
1877 Ok(success_no_data(&format!(
1878 "{} {} format set",
1879 input.target, input.identifier
1880 )))
1881}
1882
1883pub fn group_consolidated(
1884 store: &mut WorkbookStore,
1885 input: GroupInput,
1886) -> Result<String, anyhow::Error> {
1887 let entry = match store.get_mut(&input.workbook_id) {
1888 Some(e) => e,
1889 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1890 };
1891 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1892 Some(i) => i,
1893 None => return Ok(sheet_err(&input.sheet_name)),
1894 };
1895 let ws = entry
1896 .data
1897 .worksheet(idx)
1898 .map_err(|e| anyhow::anyhow!("{e}"))?;
1899 match input.target.as_str() {
1900 "columns" => {
1901 let c1 = zavora_xlsx::utility::col_from_letter(&input.start)
1902 .map_err(|e| anyhow::anyhow!("{e}"))?;
1903 let c2 = zavora_xlsx::utility::col_from_letter(&input.end)
1904 .map_err(|e| anyhow::anyhow!("{e}"))?;
1905 ws.group_columns(c1, c2, input.level);
1906 }
1907 _ => {
1908 let s: u32 = input.start.parse().unwrap_or(1);
1909 let e: u32 = input.end.parse().unwrap_or(1);
1910 ws.group_rows(s.saturating_sub(1), e.saturating_sub(1), input.level);
1911 }
1912 }
1913 Ok(success_no_data(&format!(
1914 "{} {}-{} grouped at level {}",
1915 input.target, input.start, input.end, input.level
1916 )))
1917}
1918
1919pub fn manage_autofilter(
1920 store: &mut WorkbookStore,
1921 input: ManageAutofilterInput,
1922) -> Result<String, anyhow::Error> {
1923 let entry = match store.get_mut(&input.workbook_id) {
1924 Some(e) => e,
1925 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1926 };
1927 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1928 Some(i) => i,
1929 None => return Ok(sheet_err(&input.sheet_name)),
1930 };
1931 let (r1, c1, r2, c2) =
1932 zavora_xlsx::utility::parse_range_ref(&input.range).map_err(|e| anyhow::anyhow!("{e}"))?;
1933 let ws = entry
1934 .data
1935 .worksheet(idx)
1936 .map_err(|e| anyhow::anyhow!("{e}"))?;
1937 ws.set_autofilter(r1, c1, r2, c2);
1938 if let (Some(ref fc), Some(ref fv)) = (input.filter_column, input.filter_values) {
1939 let col = zavora_xlsx::utility::col_from_letter(fc).map_err(|e| anyhow::anyhow!("{e}"))?;
1940 let strs: Vec<&str> = fv.iter().map(|s| s.as_str()).collect();
1941 ws.filter_column(col, &strs);
1942 }
1943 Ok(success_no_data(&format!(
1944 "Autofilter set on {}",
1945 input.range
1946 )))
1947}
1948
1949fn parse_hex_color(hex: &str) -> [u8; 3] {
1955 let s = hex.trim_start_matches('#');
1956 if s.len() == 6 {
1957 let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0);
1958 let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0);
1959 let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0);
1960 [r, g, b]
1961 } else {
1962 [0, 0, 0]
1963 }
1964}
1965
1966pub fn add_waterfall_chart(
1967 store: &mut WorkbookStore,
1968 input: AddWaterfallChartInput,
1969) -> Result<String, anyhow::Error> {
1970 let entry = match store.get_mut(&input.workbook_id) {
1971 Some(e) => e,
1972 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1973 };
1974 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1975 Some(i) => i,
1976 None => return Ok(sheet_err(&input.sheet_name)),
1977 };
1978 let mut chart = zavora_xlsx::WaterfallChart::new();
1979 if let Some(ref t) = input.title {
1980 chart.set_title(t);
1981 }
1982 if let Some(ref n) = input.series_name {
1983 chart.set_series_name(n);
1984 }
1985 chart.set_width(input.width);
1986 chart.set_height(input.height);
1987 for pt in &input.points {
1988 let point_type = match pt.point_type {
1989 crate::types::enums::WaterfallPointKind::Increase => {
1990 zavora_xlsx::WaterfallPointType::Increase
1991 }
1992 crate::types::enums::WaterfallPointKind::Decrease => {
1993 zavora_xlsx::WaterfallPointType::Decrease
1994 }
1995 crate::types::enums::WaterfallPointKind::Total => {
1996 zavora_xlsx::WaterfallPointType::Total
1997 }
1998 };
1999 chart.add_point(&pt.category, pt.value, point_type);
2000 }
2001 let (row, col) = if let Some(ref c) = input.cell {
2002 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2003 } else {
2004 (0, 0)
2005 };
2006 entry
2007 .data
2008 .worksheet(idx)
2009 .map_err(|e| anyhow::anyhow!("{e}"))?
2010 .insert_waterfall(row, col, &chart)?;
2011 Ok(success_no_data(&format!(
2012 "Waterfall chart added to '{}'",
2013 input.sheet_name
2014 )))
2015}
2016
2017pub fn add_funnel_chart(
2018 store: &mut WorkbookStore,
2019 input: AddFunnelChartInput,
2020) -> Result<String, anyhow::Error> {
2021 let entry = match store.get_mut(&input.workbook_id) {
2022 Some(e) => e,
2023 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2024 };
2025 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2026 Some(i) => i,
2027 None => return Ok(sheet_err(&input.sheet_name)),
2028 };
2029 let mut chart = zavora_xlsx::FunnelChart::new();
2030 if let Some(ref t) = input.title {
2031 chart.set_title(t);
2032 }
2033 if let Some(ref n) = input.series_name {
2034 chart.set_series_name(n);
2035 }
2036 chart.set_width(input.width);
2037 chart.set_height(input.height);
2038 for pt in &input.points {
2039 chart.add_point(&pt.category, pt.value);
2040 }
2041 let (row, col) = if let Some(ref c) = input.cell {
2042 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2043 } else {
2044 (0, 0)
2045 };
2046 entry
2047 .data
2048 .worksheet(idx)
2049 .map_err(|e| anyhow::anyhow!("{e}"))?
2050 .insert_funnel(row, col, &chart)?;
2051 Ok(success_no_data(&format!(
2052 "Funnel chart added to '{}'",
2053 input.sheet_name
2054 )))
2055}
2056
2057pub fn add_treemap_chart(
2058 store: &mut WorkbookStore,
2059 input: AddTreemapChartInput,
2060) -> Result<String, anyhow::Error> {
2061 let entry = match store.get_mut(&input.workbook_id) {
2062 Some(e) => e,
2063 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2064 };
2065 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2066 Some(i) => i,
2067 None => return Ok(sheet_err(&input.sheet_name)),
2068 };
2069 let mut chart = zavora_xlsx::TreemapChart::new();
2070 if let Some(ref t) = input.title {
2071 chart.set_title(t);
2072 }
2073 if let Some(ref n) = input.series_name {
2074 chart.set_series_name(n);
2075 }
2076 chart.set_width(input.width);
2077 chart.set_height(input.height);
2078 for pt in &input.points {
2079 if let Some(ref c) = pt.color {
2080 chart.add_point_with_color(&pt.category, pt.value, c.as_str());
2081 } else {
2082 chart.add_point(&pt.category, pt.value);
2083 }
2084 }
2085 let (row, col) = if let Some(ref c) = input.cell {
2086 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2087 } else {
2088 (0, 0)
2089 };
2090 entry
2091 .data
2092 .worksheet(idx)
2093 .map_err(|e| anyhow::anyhow!("{e}"))?
2094 .insert_treemap(row, col, &chart)?;
2095 Ok(success_no_data(&format!(
2096 "Treemap chart added to '{}'",
2097 input.sheet_name
2098 )))
2099}
2100
2101pub fn add_shape(store: &mut WorkbookStore, input: AddShapeInput) -> Result<String, anyhow::Error> {
2102 let entry = match store.get_mut(&input.workbook_id) {
2103 Some(e) => e,
2104 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2105 };
2106 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2107 Some(i) => i,
2108 None => return Ok(sheet_err(&input.sheet_name)),
2109 };
2110 let st = match input.shape_type {
2111 crate::types::enums::ShapeKind::Rectangle => zavora_xlsx::ShapeType::Rectangle,
2112 crate::types::enums::ShapeKind::RoundedRectangle => {
2113 zavora_xlsx::ShapeType::RoundedRectangle
2114 }
2115 crate::types::enums::ShapeKind::Ellipse => zavora_xlsx::ShapeType::Ellipse,
2116 crate::types::enums::ShapeKind::Triangle => zavora_xlsx::ShapeType::Triangle,
2117 crate::types::enums::ShapeKind::Diamond => zavora_xlsx::ShapeType::Diamond,
2118 crate::types::enums::ShapeKind::Arrow => zavora_xlsx::ShapeType::Arrow,
2119 crate::types::enums::ShapeKind::Callout => zavora_xlsx::ShapeType::Callout,
2120 crate::types::enums::ShapeKind::TextBox => zavora_xlsx::ShapeType::TextBox,
2121 };
2122 let mut shape = zavora_xlsx::Shape::new(st, input.width, input.height);
2123 if let Some(ref t) = input.text {
2124 shape = shape.text(t);
2125 }
2126 if let Some(ref c) = input.fill_color {
2127 shape = shape.fill_color(parse_hex_color(c));
2128 }
2129 if let Some(ref c) = input.outline_color {
2130 shape = shape.outline_color(parse_hex_color(c));
2131 }
2132 if let Some(w) = input.outline_width {
2133 shape = shape.outline_width(w);
2134 }
2135 if let Some(s) = input.font_size {
2136 shape = shape.font_size(s);
2137 }
2138 if input.bold == Some(true) {
2139 shape = shape.bold();
2140 }
2141 let (row, col) =
2142 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
2143 entry
2144 .data
2145 .worksheet(idx)
2146 .map_err(|e| anyhow::anyhow!("{e}"))?
2147 .add_shape(row, col, &shape)?;
2148 Ok(success_no_data(&format!("Shape added at {}", input.cell)))
2149}
2150
2151pub fn set_doc_properties(
2152 store: &mut WorkbookStore,
2153 input: SetDocPropertiesInput,
2154) -> Result<String, anyhow::Error> {
2155 let entry = match store.get_mut(&input.workbook_id) {
2156 Some(e) => e,
2157 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2158 };
2159 let mut props = zavora_xlsx::DocProperties::new();
2160 if let Some(ref v) = input.title {
2161 props = props.title(v);
2162 }
2163 if let Some(ref v) = input.author {
2164 props = props.author(v);
2165 }
2166 if let Some(ref v) = input.subject {
2167 props = props.subject(v);
2168 }
2169 if let Some(ref v) = input.description {
2170 props = props.description(v);
2171 }
2172 if let Some(ref v) = input.keywords {
2173 props = props.keywords(v);
2174 }
2175 if let Some(ref v) = input.category {
2176 props = props.category(v);
2177 }
2178 if let Some(ref v) = input.company {
2179 props = props.company(v);
2180 }
2181 entry.data.set_properties(props);
2182 Ok(success_no_data("Document properties set"))
2183}
2184
2185pub fn add_sunburst_chart(
2192 store: &mut WorkbookStore,
2193 input: AddSunburstChartInput,
2194) -> Result<String, anyhow::Error> {
2195 let entry = match store.get_mut(&input.workbook_id) {
2196 Some(e) => e,
2197 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2198 };
2199 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2200 Some(i) => i,
2201 None => return Ok(sheet_err(&input.sheet_name)),
2202 };
2203 let mut chart = zavora_xlsx::SunburstChart::new();
2204 if let Some(ref t) = input.title {
2205 chart.set_title(t);
2206 }
2207 if let Some(ref n) = input.series_name {
2208 chart.set_series_name(n);
2209 }
2210 chart.set_width(input.width);
2211 chart.set_height(input.height);
2212 let labels: Vec<&str> = input.points.iter().map(|p| p.category.as_str()).collect();
2214 let values: Vec<f64> = input.points.iter().map(|p| p.value).collect();
2215 chart.add_level(&labels);
2216 chart.set_values(&values);
2217 let (row, col) = if let Some(ref c) = input.cell {
2218 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2219 } else {
2220 (0, 0)
2221 };
2222 entry
2223 .data
2224 .worksheet(idx)
2225 .map_err(|e| anyhow::anyhow!("{e}"))?
2226 .insert_sunburst(row, col, &chart)?;
2227 Ok(success_no_data(&format!(
2228 "Sunburst chart added to '{}'",
2229 input.sheet_name
2230 )))
2231}
2232
2233pub fn add_histogram_chart(
2234 store: &mut WorkbookStore,
2235 input: AddHistogramChartInput,
2236) -> Result<String, anyhow::Error> {
2237 let entry = match store.get_mut(&input.workbook_id) {
2238 Some(e) => e,
2239 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2240 };
2241 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2242 Some(i) => i,
2243 None => return Ok(sheet_err(&input.sheet_name)),
2244 };
2245 let mut chart = if input.pareto == Some(true) {
2246 zavora_xlsx::HistogramChart::pareto()
2247 } else {
2248 zavora_xlsx::HistogramChart::new()
2249 };
2250 if let Some(ref t) = input.title {
2251 chart.set_title(t);
2252 }
2253 if let Some(ref n) = input.series_name {
2254 chart.set_series_name(n);
2255 }
2256 chart.set_width(input.width);
2257 chart.set_height(input.height);
2258 if let Some(bc) = input.bin_count {
2259 chart.set_bin_count(bc);
2260 }
2261 if let Some(bw) = input.bin_width {
2262 chart.set_bin_width(bw);
2263 }
2264 let values: Vec<f64> = input.points.iter().map(|p| p.value).collect();
2265 chart.set_values(&values);
2266 let (row, col) = if let Some(ref c) = input.cell {
2267 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2268 } else {
2269 (0, 0)
2270 };
2271 entry
2272 .data
2273 .worksheet(idx)
2274 .map_err(|e| anyhow::anyhow!("{e}"))?
2275 .insert_histogram(row, col, &chart)?;
2276 Ok(success_no_data(&format!(
2277 "Histogram chart added to '{}'",
2278 input.sheet_name
2279 )))
2280}
2281
2282pub fn add_box_whisker_chart(
2283 store: &mut WorkbookStore,
2284 input: AddBoxWhiskerChartInput,
2285) -> Result<String, anyhow::Error> {
2286 let entry = match store.get_mut(&input.workbook_id) {
2287 Some(e) => e,
2288 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2289 };
2290 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2291 Some(i) => i,
2292 None => return Ok(sheet_err(&input.sheet_name)),
2293 };
2294 let mut chart = zavora_xlsx::BoxWhiskerChart::new();
2295 if let Some(ref t) = input.title {
2296 chart.set_title(t);
2297 }
2298 if let Some(ref n) = input.series_name {
2299 chart.set_series_name(n);
2300 }
2301 chart.set_width(input.width);
2302 chart.set_height(input.height);
2303 if let Some(v) = input.show_outliers {
2304 chart.set_show_outliers(v);
2305 }
2306 if let Some(v) = input.show_mean {
2307 chart.set_show_mean_markers(v);
2308 }
2309 if let Some(v) = input.show_inner_points {
2310 chart.set_show_inner_points(v);
2311 }
2312 for pt in &input.points {
2314 chart.add_data_set(&pt.category, &[pt.value]);
2315 }
2316 let (row, col) = if let Some(ref c) = input.cell {
2317 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2318 } else {
2319 (0, 0)
2320 };
2321 entry
2322 .data
2323 .worksheet(idx)
2324 .map_err(|e| anyhow::anyhow!("{e}"))?
2325 .insert_box_whisker(row, col, &chart)?;
2326 Ok(success_no_data(&format!(
2327 "Box & whisker chart added to '{}'",
2328 input.sheet_name
2329 )))
2330}
2331
2332pub fn add_map_chart(
2333 store: &mut WorkbookStore,
2334 input: AddMapChartInput,
2335) -> Result<String, anyhow::Error> {
2336 let entry = match store.get_mut(&input.workbook_id) {
2337 Some(e) => e,
2338 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2339 };
2340 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2341 Some(i) => i,
2342 None => return Ok(sheet_err(&input.sheet_name)),
2343 };
2344 let mut chart = zavora_xlsx::MapChart::new();
2345 if let Some(ref t) = input.title {
2346 chart.set_title(t);
2347 }
2348 if let Some(ref n) = input.series_name {
2349 chart.set_series_name(n);
2350 }
2351 chart.set_width(input.width);
2352 chart.set_height(input.height);
2353 if let Some(ref ml) = input.map_level {
2354 let level = match ml.as_str() {
2355 "region" => zavora_xlsx::MapLevel::Region,
2356 _ => zavora_xlsx::MapLevel::Country,
2357 };
2358 chart.set_map_level(level);
2359 }
2360 for pt in &input.points {
2361 chart.add_point(&pt.category, pt.value);
2362 }
2363 let (row, col) = if let Some(ref c) = input.cell {
2364 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2365 } else {
2366 (0, 0)
2367 };
2368 entry
2369 .data
2370 .worksheet(idx)
2371 .map_err(|e| anyhow::anyhow!("{e}"))?
2372 .insert_map(row, col, &chart)?;
2373 Ok(success_no_data(&format!(
2374 "Map chart added to '{}'",
2375 input.sheet_name
2376 )))
2377}
2378
2379pub fn add_slicer(
2380 store: &mut WorkbookStore,
2381 input: AddSlicerInput,
2382) -> Result<String, anyhow::Error> {
2383 let entry = match store.get_mut(&input.workbook_id) {
2384 Some(e) => e,
2385 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2386 };
2387 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2388 Some(i) => i,
2389 None => return Ok(sheet_err(&input.sheet_name)),
2390 };
2391 let mut slicer = zavora_xlsx::Slicer::new(&input.pivot_table_name, &input.field_name);
2393 if let Some(w) = input.width {
2394 slicer = slicer.set_width(w);
2395 }
2396 if let Some(h) = input.height {
2397 slicer = slicer.set_height(h);
2398 }
2399 let (row, col) = if let Some(ref c) = input.cell {
2400 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2401 } else {
2402 (0, 0)
2403 };
2404 entry
2405 .data
2406 .worksheet(idx)
2407 .map_err(|e| anyhow::anyhow!("{e}"))?
2408 .add_slicer(row, col, &slicer)?;
2409 Ok(success_no_data(&format!(
2410 "Slicer for '{}' added to '{}'",
2411 input.field_name, input.sheet_name
2412 )))
2413}
2414
2415pub fn add_timeline(
2416 store: &mut WorkbookStore,
2417 input: AddTimelineInput,
2418) -> Result<String, anyhow::Error> {
2419 let entry = match store.get_mut(&input.workbook_id) {
2420 Some(e) => e,
2421 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2422 };
2423 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2424 Some(i) => i,
2425 None => return Ok(sheet_err(&input.sheet_name)),
2426 };
2427 let timeline = zavora_xlsx::Timeline::new(&input.pivot_table_name, &input.field_name);
2428 let (row, col) = if let Some(ref c) = input.cell {
2429 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2430 } else {
2431 (0, 0)
2432 };
2433 entry
2434 .data
2435 .worksheet(idx)
2436 .map_err(|e| anyhow::anyhow!("{e}"))?
2437 .add_timeline(row, col, &timeline)?;
2438 Ok(success_no_data(&format!(
2439 "Timeline for '{}' added to '{}'",
2440 input.field_name, input.sheet_name
2441 )))
2442}
2443
2444pub fn add_form_control(
2445 store: &mut WorkbookStore,
2446 input: AddFormControlInput,
2447) -> Result<String, anyhow::Error> {
2448 let entry = match store.get_mut(&input.workbook_id) {
2449 Some(e) => e,
2450 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2451 };
2452 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2453 Some(i) => i,
2454 None => return Ok(sheet_err(&input.sheet_name)),
2455 };
2456 let (row, col) =
2457 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
2458 let text = input.text.as_deref().unwrap_or("Control");
2459 let fc = match input.control_type.as_str() {
2460 "checkbox" => {
2461 if let Some(ref cl) = input.cell_link {
2462 zavora_xlsx::FormControl::checkbox_with_link(text, cl)
2463 } else {
2464 zavora_xlsx::FormControl::checkbox(text)
2465 }
2466 }
2467 "dropdown" => {
2468 let items = input
2469 .input_range
2470 .as_deref()
2471 .unwrap_or("")
2472 .split(',')
2473 .map(|s| s.trim().to_string())
2474 .collect();
2475 zavora_xlsx::FormControl::dropdown(items)
2476 }
2477 "spinner" => zavora_xlsx::FormControl::spinner(0, 100, 0),
2478 _ => zavora_xlsx::FormControl::button(text),
2479 };
2480 entry
2481 .data
2482 .worksheet(idx)
2483 .map_err(|e| anyhow::anyhow!("{e}"))?
2484 .add_form_control(row, col, fc);
2485 Ok(success_no_data(&format!(
2486 "Form control '{}' added at {}",
2487 input.control_type, input.cell
2488 )))
2489}
2490
2491pub fn save_workbook_advanced(
2492 store: &mut WorkbookStore,
2493 input: SaveWorkbookAdvancedInput,
2494) -> Result<String, anyhow::Error> {
2495 let entry = match store.get_mut(&input.workbook_id) {
2496 Some(e) => e,
2497 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2498 };
2499 if entry.read_only {
2500 return Ok(error(
2501 ErrorCategory::EngineUnsupported,
2502 "Read-only workbooks cannot be saved",
2503 "Reopen in edit mode.",
2504 ));
2505 }
2506 let _ = entry.data.recalculate();
2507 let path = std::path::Path::new(&input.file_path);
2508 match input.format.as_str() {
2509 "template" => entry
2510 .data
2511 .save_as_template(path)
2512 .map_err(|e| anyhow::anyhow!("{e}"))?,
2513 "encrypted" => {
2514 let pw = input.password.as_deref().unwrap_or("");
2515 entry
2516 .data
2517 .save_encrypted(path, pw)
2518 .map_err(|e| anyhow::anyhow!("{e}"))?;
2519 }
2520 "parallel" => entry
2521 .data
2522 .save_parallel(path)
2523 .map_err(|e| anyhow::anyhow!("{e}"))?,
2524 _ => entry
2525 .data
2526 .save(path)
2527 .map_err(|e| anyhow::anyhow!("{e}"))?,
2528 }
2529 Ok(success_no_data(&format!(
2530 "Workbook saved as {} to {}",
2531 input.format, input.file_path
2532 )))
2533}
2534
2535pub fn open_workbook_encrypted(
2536 store: &mut WorkbookStore,
2537 input: OpenWorkbookEncryptedInput,
2538) -> Result<String, anyhow::Error> {
2539 use crate::store::WorkbookEntry;
2540 use std::time::Instant;
2541
2542 if store.is_full() {
2543 return Ok(error(
2544 ErrorCategory::CapacityExceeded,
2545 "Workbook store is at maximum capacity",
2546 "Save and close an existing workbook first.",
2547 ));
2548 }
2549 let path = std::path::Path::new(&input.file_path);
2550 if !path.exists() {
2551 return Ok(error(
2552 ErrorCategory::NotFound,
2553 &format!("File not found: {}", input.file_path),
2554 "Check the file path.",
2555 ));
2556 }
2557 let wb = zavora_xlsx::Workbook::open_with_password(path, &input.password)
2558 .map_err(|e| anyhow::anyhow!("{e}"))?;
2559 let sheets = crate::engines::zavora::sheet_summaries(&wb);
2560 let entry = WorkbookEntry {
2561 id: String::new(),
2562 data: wb,
2563 read_only: false,
2564 last_access: Instant::now(),
2565 };
2566 let id = store.insert(entry).map_err(|e| anyhow::anyhow!("{}", e))?;
2567 Ok(success(
2568 "Encrypted workbook opened",
2569 crate::types::responses::WorkbookInfo {
2570 workbook_id: id,
2571 engine: "zavora-xlsx".to_string(),
2572 sheets,
2573 },
2574 ))
2575}
2576
2577pub fn manage_named_ranges(
2578 store: &mut WorkbookStore,
2579 input: ManageNamedRangesInput,
2580) -> Result<String, anyhow::Error> {
2581 let entry = match store.get_mut(&input.workbook_id) {
2582 Some(e) => e,
2583 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2584 };
2585 match input.action.as_str() {
2586 "add" => {
2587 let name = input.name.as_deref().unwrap_or("");
2588 let formula = input.formula.as_deref().unwrap_or("");
2589 entry.data.define_name(name, formula);
2590 Ok(success_no_data(&format!("Named range '{}' added", name)))
2591 }
2592 "add_scoped" => {
2593 let name = input.name.as_deref().unwrap_or("");
2594 let formula = input.formula.as_deref().unwrap_or("");
2595 let sheet_idx = input.sheet_index.unwrap_or(0);
2596 entry.data.define_name_scoped(name, formula, sheet_idx);
2597 Ok(success_no_data(&format!(
2598 "Scoped named range '{}' added for sheet {}",
2599 name, sheet_idx
2600 )))
2601 }
2602 "update" => {
2603 let name = input.name.as_deref().unwrap_or("");
2604 let formula = input.formula.as_deref().unwrap_or("");
2605 let _ = entry.data.update_named_range(name, formula);
2606 Ok(success_no_data(&format!("Named range '{}' updated", name)))
2607 }
2608 "remove" => {
2609 let name = input.name.as_deref().unwrap_or("");
2610 let scope = if let Some(idx) = input.sheet_index {
2611 zavora_xlsx::DefinedNameScope::Sheet(idx)
2612 } else {
2613 zavora_xlsx::DefinedNameScope::Workbook
2614 };
2615 let _ = entry.data.remove_named_range(name, &scope);
2616 Ok(success_no_data(&format!("Named range '{}' removed", name)))
2617 }
2618 _ => {
2619 let names: Vec<serde_json::Value> = entry
2621 .data
2622 .defined_names_with_scope()
2623 .iter()
2624 .map(|dn| {
2625 serde_json::json!({
2626 "name": dn.name,
2627 "formula": dn.formula,
2628 "scope": format!("{:?}", dn.scope),
2629 })
2630 })
2631 .collect();
2632 Ok(success("Named ranges listed", names))
2633 }
2634 }
2635}
2636
2637pub fn read_sheet_metadata(
2638 store: &mut WorkbookStore,
2639 input: ReadSheetMetadataInput,
2640) -> Result<String, anyhow::Error> {
2641 let entry = match store.get_mut(&input.workbook_id) {
2642 Some(e) => e,
2643 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2644 };
2645 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2646 Some(i) => i,
2647 None => return Ok(sheet_err(&input.sheet_name)),
2648 };
2649 let ws = entry
2650 .data
2651 .worksheet(idx)
2652 .map_err(|e| anyhow::anyhow!("{e}"))?;
2653
2654 let mut result = serde_json::Map::new();
2655
2656 if input.info == "used_range" || input.info == "all" {
2657 if let Some((r1, c1, r2, c2)) = ws.used_range() {
2658 let range_str = format!(
2659 "{}:{}",
2660 zavora_xlsx::utility::to_a1(r1, c1),
2661 zavora_xlsx::utility::to_a1(r2, c2)
2662 );
2663 result.insert("used_range".into(), serde_json::json!(range_str));
2664 } else {
2665 result.insert("used_range".into(), serde_json::Value::Null);
2666 }
2667 }
2668
2669 if input.info == "hyperlinks" || input.info == "all" {
2670 let links: Vec<serde_json::Value> = ws
2671 .hyperlinks()
2672 .iter()
2673 .map(|h| {
2674 serde_json::json!({
2675 "cell": zavora_xlsx::utility::to_a1(h.row, h.col),
2676 "url": h.url,
2677 "location": h.location,
2678 "tooltip": h.tooltip,
2679 })
2680 })
2681 .collect();
2682 result.insert("hyperlinks".into(), serde_json::json!(links));
2683 }
2684
2685 if input.info == "merge_ranges" || input.info == "all" {
2686 let merges: Vec<String> = ws
2687 .merge_ranges()
2688 .iter()
2689 .map(|(r1, c1, r2, c2)| {
2690 format!(
2691 "{}:{}",
2692 zavora_xlsx::utility::to_a1(*r1, *c1),
2693 zavora_xlsx::utility::to_a1(*r2, *c2)
2694 )
2695 })
2696 .collect();
2697 result.insert("merge_ranges".into(), serde_json::json!(merges));
2698 }
2699
2700 if input.info == "charts" || input.info == "all" {
2701 let charts: Vec<serde_json::Value> = ws
2702 .charts()
2703 .iter()
2704 .map(|c| {
2705 serde_json::json!({
2706 "title": c.title(),
2707 "type": format!("{:?}", c.chart_type()),
2708 "series_count": c.series().len(),
2709 })
2710 })
2711 .collect();
2712 result.insert("charts".into(), serde_json::json!(charts));
2713 }
2714
2715 Ok(success(
2716 "Sheet metadata read",
2717 serde_json::Value::Object(result),
2718 ))
2719}
2720
2721pub fn add_chart_sheet(
2722 store: &mut WorkbookStore,
2723 input: AddChartSheetInput,
2724) -> Result<String, anyhow::Error> {
2725 let entry = match store.get_mut(&input.workbook_id) {
2726 Some(e) => e,
2727 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2728 };
2729 let ct = match input.chart_type {
2730 ChartType::Bar => zavora_xlsx::ChartType::Bar,
2731 ChartType::Column => zavora_xlsx::ChartType::Column,
2732 ChartType::Line => zavora_xlsx::ChartType::Line,
2733 ChartType::Pie => zavora_xlsx::ChartType::Pie,
2734 ChartType::Scatter => zavora_xlsx::ChartType::Scatter,
2735 ChartType::Area => zavora_xlsx::ChartType::Area,
2736 ChartType::Doughnut => zavora_xlsx::ChartType::Doughnut,
2737 };
2738 let mut chart = zavora_xlsx::Chart::new(ct);
2739 if let Some(ref t) = input.title {
2740 chart.set_title(t);
2741 }
2742 if let Some(ref x) = input.x_axis_label {
2743 chart.set_x_axis_name(x);
2744 }
2745 if let Some(ref y) = input.y_axis_label {
2746 chart.set_y_axis_name(y);
2747 }
2748 if let Some(ref lp) = input.legend_position {
2749 chart.set_legend_position(match lp {
2750 LegendPosition::Top => zavora_xlsx::LegendPosition::Top,
2751 LegendPosition::Bottom => zavora_xlsx::LegendPosition::Bottom,
2752 LegendPosition::Left => zavora_xlsx::LegendPosition::Left,
2753 LegendPosition::Right => zavora_xlsx::LegendPosition::Right,
2754 LegendPosition::None => zavora_xlsx::LegendPosition::None,
2755 });
2756 }
2757 if !input.series.is_empty() {
2758 for si in &input.series {
2759 let s = chart.add_series();
2760 s.set_values(&si.values);
2761 if let Some(ref c) = si.categories {
2762 s.set_categories(c);
2763 }
2764 if let Some(ref n) = si.name {
2765 s.set_name(n);
2766 }
2767 }
2768 } else if let Some(ref dr) = input.data_range {
2769 chart.add_series().set_values(dr);
2770 }
2771 entry
2772 .data
2773 .add_chart_sheet(&input.sheet_name, chart)
2774 .map_err(|e| anyhow::anyhow!("{e}"))?;
2775 Ok(success_no_data(&format!(
2776 "Chart sheet '{}' added",
2777 input.sheet_name
2778 )))
2779}
2780
2781
2782pub fn add_threaded_comment(
2787 store: &mut WorkbookStore,
2788 input: AddThreadedCommentInput,
2789) -> Result<String, anyhow::Error> {
2790 let entry = match store.get_mut(&input.workbook_id) {
2791 Some(e) => e,
2792 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2793 };
2794 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2795 Some(i) => i,
2796 None => return Ok(sheet_err(&input.sheet_name)),
2797 };
2798 let (row, col) =
2799 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
2800 let mut tc = zavora_xlsx::ThreadedComment::new(&input.author, &input.text);
2801 if let Some(ref ts) = input.timestamp {
2802 tc = tc.timestamp(ts);
2803 }
2804 for reply in &input.replies {
2805 if let Some(ref ts) = reply.timestamp {
2806 tc.add_reply_with_timestamp(&reply.author, &reply.text, ts);
2807 } else {
2808 tc.add_reply(&reply.author, &reply.text);
2809 }
2810 }
2811 entry
2812 .data
2813 .worksheet(idx)
2814 .map_err(|e| anyhow::anyhow!("{e}"))?
2815 .add_threaded_comment(row, col, tc);
2816 Ok(success_no_data(&format!(
2817 "Threaded comment added at {}",
2818 input.cell
2819 )))
2820}
2821
2822pub fn protect_sheet_advanced(
2823 store: &mut WorkbookStore,
2824 input: ProtectSheetAdvancedInput,
2825) -> Result<String, anyhow::Error> {
2826 let entry = match store.get_mut(&input.workbook_id) {
2827 Some(e) => e,
2828 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2829 };
2830 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2831 Some(i) => i,
2832 None => return Ok(sheet_err(&input.sheet_name)),
2833 };
2834 let ws = entry
2835 .data
2836 .worksheet(idx)
2837 .map_err(|e| anyhow::anyhow!("{e}"))?;
2838
2839 if let Some(ref pw) = input.password {
2840 ws.protect_with_password(pw);
2841 }
2842
2843 let mut prot = zavora_xlsx::SheetProtection::default();
2846
2847 if let Some(v) = input.allow_insert_rows {
2849 prot.insert_rows = !v;
2850 }
2851 if let Some(v) = input.allow_delete_rows {
2852 prot.delete_rows = !v;
2853 }
2854 if let Some(v) = input.allow_insert_columns {
2855 prot.insert_columns = !v;
2856 }
2857 if let Some(v) = input.allow_delete_columns {
2858 prot.delete_columns = !v;
2859 }
2860 if let Some(v) = input.allow_format_cells {
2861 prot.format_cells = !v;
2862 }
2863 if let Some(v) = input.allow_format_columns {
2864 prot.format_columns = !v;
2865 }
2866 if let Some(v) = input.allow_format_rows {
2867 prot.format_rows = !v;
2868 }
2869 if let Some(v) = input.allow_sort {
2870 prot.sort = !v;
2871 }
2872 if let Some(v) = input.allow_insert_hyperlinks {
2873 prot.insert_hyperlinks = !v;
2874 }
2875 if let Some(v) = input.allow_select_locked_cells {
2876 prot.select_locked_cells = !v;
2877 }
2878 if let Some(v) = input.allow_select_unlocked_cells {
2879 prot.select_unlocked_cells = !v;
2880 }
2881 if let Some(v) = input.allow_pivot_tables {
2882 prot.pivot_tables = !v;
2883 }
2884
2885 ws.protect_with_options(prot);
2886
2887 Ok(success_no_data(&format!(
2888 "Sheet '{}' protected with custom options",
2889 input.sheet_name
2890 )))
2891}
2892
2893pub fn set_custom_property(
2894 store: &mut WorkbookStore,
2895 input: SetCustomPropertyInput,
2896) -> Result<String, anyhow::Error> {
2897 let entry = match store.get_mut(&input.workbook_id) {
2898 Some(e) => e,
2899 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2900 };
2901 let val = match input.value_type.as_str() {
2902 "number" => {
2903 let n: f64 = input.value.parse().unwrap_or(0.0);
2904 zavora_xlsx::CustomPropertyValue::Number(n)
2905 }
2906 "integer" => {
2907 let n: i32 = input.value.parse().unwrap_or(0);
2908 zavora_xlsx::CustomPropertyValue::Integer(n)
2909 }
2910 "bool" => {
2911 let b = input.value == "true" || input.value == "1";
2912 zavora_xlsx::CustomPropertyValue::Bool(b)
2913 }
2914 "datetime" => zavora_xlsx::CustomPropertyValue::DateTime(input.value.clone()),
2915 _ => zavora_xlsx::CustomPropertyValue::Text(input.value.clone()),
2916 };
2917 entry.data.set_custom_property(&input.name, val);
2918 Ok(success_no_data(&format!(
2919 "Custom property '{}' set",
2920 input.name
2921 )))
2922}
2923
2924
2925pub fn read_cell_comment(
2930 store: &mut WorkbookStore,
2931 input: ReadCellCommentInput,
2932) -> Result<String, anyhow::Error> {
2933 let entry = match store.get_mut(&input.workbook_id) {
2934 Some(e) => e,
2935 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2936 };
2937 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2938 Some(i) => i,
2939 None => return Ok(sheet_err(&input.sheet_name)),
2940 };
2941 let (row, col) =
2942 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
2943 let ws = entry
2944 .data
2945 .worksheet(idx)
2946 .map_err(|e| anyhow::anyhow!("{e}"))?;
2947 match ws.get_comment(row, col) {
2948 Some((author, text)) => Ok(success(
2949 "Comment found",
2950 serde_json::json!({
2951 "cell": input.cell,
2952 "author": author,
2953 "text": text,
2954 }),
2955 )),
2956 None => Ok(success_no_data(&format!(
2957 "No comment at {}",
2958 input.cell
2959 ))),
2960 }
2961}
2962
2963pub fn read_cell_format(
2964 store: &mut WorkbookStore,
2965 input: ReadCellFormatInput,
2966) -> Result<String, anyhow::Error> {
2967 let entry = match store.get_mut(&input.workbook_id) {
2968 Some(e) => e,
2969 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2970 };
2971 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2972 Some(i) => i,
2973 None => return Ok(sheet_err(&input.sheet_name)),
2974 };
2975 let (row, col) =
2976 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
2977 let ws = entry
2978 .data
2979 .worksheet(idx)
2980 .map_err(|e| anyhow::anyhow!("{e}"))?;
2981 match ws.cell_format(row, col) {
2982 Some(fmt) => Ok(success(
2983 "Cell format read",
2984 serde_json::json!({
2985 "cell": input.cell,
2986 "format": format!("{:?}", fmt),
2987 }),
2988 )),
2989 None => Ok(success_no_data(&format!(
2990 "No format at {}",
2991 input.cell
2992 ))),
2993 }
2994}
2995
2996pub fn manage_custom_xml(
2997 store: &mut WorkbookStore,
2998 input: ManageCustomXmlInput,
2999) -> Result<String, anyhow::Error> {
3000 let entry = match store.get_mut(&input.workbook_id) {
3001 Some(e) => e,
3002 None => return Ok(workbook_not_found(store, &input.workbook_id)),
3003 };
3004 match input.action.as_str() {
3005 "add" => {
3006 let content = input.content.as_deref().unwrap_or("");
3007 entry
3008 .data
3009 .add_custom_xml(&input.namespace, content.as_bytes());
3010 Ok(success_no_data(&format!(
3011 "Custom XML added for namespace '{}'",
3012 input.namespace
3013 )))
3014 }
3015 _ => {
3016 match entry.data.read_custom_xml(&input.namespace) {
3018 Some(bytes) => {
3019 let text = String::from_utf8_lossy(bytes).to_string();
3020 Ok(success(
3021 "Custom XML read",
3022 serde_json::json!({
3023 "namespace": input.namespace,
3024 "content": text,
3025 }),
3026 ))
3027 }
3028 None => Ok(success_no_data(&format!(
3029 "No custom XML for namespace '{}'",
3030 input.namespace
3031 ))),
3032 }
3033 }
3034 }
3035}
3036
3037pub fn add_connection(
3038 store: &mut WorkbookStore,
3039 input: AddConnectionInput,
3040) -> Result<String, anyhow::Error> {
3041 let entry = match store.get_mut(&input.workbook_id) {
3042 Some(e) => e,
3043 None => return Ok(workbook_not_found(store, &input.workbook_id)),
3044 };
3045 entry
3046 .data
3047 .add_connection(&input.connection_string, &input.command);
3048 Ok(success_no_data("External data connection added"))
3049}
3050
3051pub fn set_sst_threshold(
3052 store: &mut WorkbookStore,
3053 input: SetSstThresholdInput,
3054) -> Result<String, anyhow::Error> {
3055 let entry = match store.get_mut(&input.workbook_id) {
3056 Some(e) => e,
3057 None => return Ok(workbook_not_found(store, &input.workbook_id)),
3058 };
3059 entry.data.set_sst_threshold(input.threshold);
3060 Ok(success_no_data(&format!(
3061 "SST threshold set to {}",
3062 input.threshold
3063 )))
3064}
3065
3066pub fn write_json_rows(
3067 store: &mut WorkbookStore,
3068 input: WriteJsonRowsInput,
3069) -> Result<String, anyhow::Error> {
3070 let entry = match store.get_mut(&input.workbook_id) {
3071 Some(e) => e,
3072 None => return Ok(workbook_not_found(store, &input.workbook_id)),
3073 };
3074 let idx = match find_sheet(&entry.data, &input.sheet_name) {
3075 Some(i) => i,
3076 None => return Ok(sheet_err(&input.sheet_name)),
3077 };
3078 let (start_row, start_col) = if let Some(ref c) = input.start_cell {
3079 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
3080 } else {
3081 (0, 0)
3082 };
3083 let ws = entry
3084 .data
3085 .worksheet(idx)
3086 .map_err(|e| anyhow::anyhow!("{e}"))?;
3087
3088 if input.rows.is_empty() {
3089 return Ok(success_no_data("No rows to write"));
3090 }
3091
3092 let headers: Vec<String> = if let Some(obj) = input.rows[0].as_object() {
3094 obj.keys().cloned().collect()
3095 } else {
3096 return Ok(error(
3097 ErrorCategory::EngineUnsupported,
3098 "Rows must be JSON objects",
3099 "Each row should be {\"key\": value, ...}",
3100 ));
3101 };
3102
3103 let mut current_row = start_row;
3104
3105 if input.write_headers {
3107 for (ci, header) in headers.iter().enumerate() {
3108 ws.write(current_row, start_col + ci as u16, header.as_str())
3109 .map_err(|e| anyhow::anyhow!("{e}"))?;
3110 }
3111 current_row += 1;
3112 }
3113
3114 for row_val in &input.rows {
3116 if let Some(obj) = row_val.as_object() {
3117 for (ci, header) in headers.iter().enumerate() {
3118 if let Some(val) = obj.get(header) {
3119 crate::engines::zavora::write_json_value(
3120 ws,
3121 current_row,
3122 start_col + ci as u16,
3123 val,
3124 )
3125 .map_err(|e| anyhow::anyhow!("{e}"))?;
3126 }
3127 }
3128 }
3129 current_row += 1;
3130 }
3131
3132 Ok(success_no_data(&format!(
3133 "{} rows written to '{}'",
3134 input.rows.len(),
3135 input.sheet_name
3136 )))
3137}