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.series.is_empty() {
500 for si in &input.series {
501 let s = chart.add_series();
502 s.set_values(&si.values);
503 if let Some(ref c) = si.categories {
504 s.set_categories(c);
505 }
506 if let Some(ref n) = si.name {
507 s.set_name(n);
508 }
509 if let Some(ref c) = si.color {
510 s.set_color(c.as_str());
511 }
512 if si.data_labels == Some(true) {
513 s.set_data_labels(true);
514 }
515 if si.secondary_axis == Some(true) {
516 s.set_secondary_axis(true);
517 }
518 if let Some(ref t) = si.trendline {
519 let tt = match t.as_str() {
520 "exponential" => zavora_xlsx::TrendlineType::Exponential,
521 "power" => zavora_xlsx::TrendlineType::Power,
522 "logarithmic" => zavora_xlsx::TrendlineType::Logarithmic,
523 _ => zavora_xlsx::TrendlineType::Linear,
524 };
525 s.set_trendline(tt);
526 }
527 if let Some(ref m) = si.marker {
528 let mt = match m.as_str() {
529 "circle" => zavora_xlsx::MarkerType::Circle,
530 "diamond" => zavora_xlsx::MarkerType::Diamond,
531 "square" => zavora_xlsx::MarkerType::Square,
532 "triangle" => zavora_xlsx::MarkerType::Triangle,
533 _ => zavora_xlsx::MarkerType::None,
534 };
535 s.set_marker(mt);
536 }
537 }
538 } else if let Some(ref dr) = input.data_range {
539 chart.add_series().set_values(dr);
540 }
541 let (row, col) = if let Some(ref c) = input.cell {
542 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
543 } else {
544 (0, 0)
545 };
546 entry
547 .data
548 .worksheet(idx)
549 .map_err(|e| anyhow::anyhow!("{e}"))?
550 .insert_chart(row, col, &chart)?;
551 Ok(success_no_data(&format!(
552 "Chart added to '{}'",
553 input.sheet_name
554 )))
555}
556
557pub fn add_pivot_table(
558 store: &mut WorkbookStore,
559 input: AddPivotTableInput,
560) -> Result<String, anyhow::Error> {
561 let entry = match store.get_mut(&input.workbook_id) {
562 Some(e) => e,
563 None => return Ok(workbook_not_found(store, &input.workbook_id)),
564 };
565 let idx = match find_sheet(&entry.data, &input.sheet_name) {
566 Some(i) => i,
567 None => return Ok(sheet_err(&input.sheet_name)),
568 };
569 let mut pt = zavora_xlsx::PivotTable::new(&input.name, &input.source_range);
570 for f in &input.row_fields {
571 pt = pt.add_row_field(f);
572 }
573 for f in &input.column_fields {
574 pt = pt.add_column_field(f);
575 }
576 for vf in &input.value_fields {
577 let agg = match vf.aggregation.as_str() {
578 "count" => zavora_xlsx::PivotAggregation::Count,
579 "average" => zavora_xlsx::PivotAggregation::Average,
580 "max" => zavora_xlsx::PivotAggregation::Max,
581 "min" => zavora_xlsx::PivotAggregation::Min,
582 "product" => zavora_xlsx::PivotAggregation::Product,
583 _ => zavora_xlsx::PivotAggregation::Sum,
584 };
585 pt = pt.add_value_field(&vf.field, agg);
586 }
587 for f in &input.filter_fields {
588 pt = pt.add_filter_field(f);
589 }
590 if let Some(ref s) = input.style {
591 pt = pt.set_style_name(s);
592 }
593 if let Some(ref l) = input.layout {
594 let layout = match l.as_str() {
595 "outline" => zavora_xlsx::PivotLayout::Outline,
596 "tabular" => zavora_xlsx::PivotLayout::Tabular,
597 _ => zavora_xlsx::PivotLayout::Compact,
598 };
599 pt = pt.set_layout(layout);
600 }
601 let (row, col) = if let Some(ref c) = input.cell {
602 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
603 } else {
604 (0, 0)
605 };
606 entry
607 .data
608 .worksheet(idx)
609 .map_err(|e| anyhow::anyhow!("{e}"))?
610 .add_pivot_table(row, col, &pt)?;
611 Ok(success_no_data(&format!(
612 "Pivot table '{}' added",
613 input.name
614 )))
615}
616
617pub fn read_comments(
620 store: &mut WorkbookStore,
621 input: ReadCommentsInput,
622) -> Result<String, anyhow::Error> {
623 let entry = match store.get_mut(&input.workbook_id) {
624 Some(e) => e,
625 None => return Ok(workbook_not_found(store, &input.workbook_id)),
626 };
627 let idx = match find_sheet(&entry.data, &input.sheet_name) {
628 Some(i) => i,
629 None => return Ok(sheet_err(&input.sheet_name)),
630 };
631 let ws = entry
632 .data
633 .worksheet(idx)
634 .map_err(|e| anyhow::anyhow!("{e}"))?;
635 let comments: Vec<serde_json::Value> = ws.comments().iter().map(|c| {
636 serde_json::json!({ "cell": zavora_xlsx::utility::to_a1(c.row, c.col), "author": c.author, "text": c.text })
637 }).collect();
638 Ok(success("Comments read", comments))
639}
640
641pub fn write_rich_text(
642 store: &mut WorkbookStore,
643 input: WriteRichTextInput,
644) -> Result<String, anyhow::Error> {
645 let entry = match store.get_mut(&input.workbook_id) {
646 Some(e) => e,
647 None => return Ok(workbook_not_found(store, &input.workbook_id)),
648 };
649 let idx = match find_sheet(&entry.data, &input.sheet_name) {
650 Some(i) => i,
651 None => return Ok(sheet_err(&input.sheet_name)),
652 };
653 let (row, col) =
654 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
655 let mut rt = zavora_xlsx::RichText::new();
656 for run in &input.runs {
657 let mut built = zavora_xlsx::RichTextRun {
658 text: run.text.clone(),
659 bold: run.bold.unwrap_or(false),
660 italic: run.italic.unwrap_or(false),
661 font_size: run.font_size,
662 font_name: None,
663 color: None,
664 superscript: false,
665 subscript: false,
666 };
667 if let Some(ref c) = run.color {
668 built.color = Some(c.clone());
669 }
670 rt.runs.push(built);
671 }
672 entry
673 .data
674 .worksheet(idx)
675 .map_err(|e| anyhow::anyhow!("{e}"))?
676 .write_rich_text(row, col, &rt)
677 .map_err(|e| anyhow::anyhow!("{e}"))?;
678 Ok(success_no_data(&format!(
679 "Rich text written to {}",
680 input.cell
681 )))
682}
683
684fn build_format(
689 bold: Option<bool>,
690 italic: Option<bool>,
691 font_size: Option<f64>,
692 font_color: Option<&str>,
693 bg: Option<&str>,
694 nf: Option<&str>,
695) -> zavora_xlsx::Format {
696 let mut f = zavora_xlsx::Format::new();
697 if bold == Some(true) {
698 f = f.bold();
699 }
700 if italic == Some(true) {
701 f = f.italic();
702 }
703 if let Some(s) = font_size {
704 f = f.font_size(s);
705 }
706 if let Some(c) = font_color {
707 f = f.font_color(c);
708 }
709 if let Some(c) = bg {
710 f = f.background_color(c);
711 }
712 if let Some(n) = nf {
713 f = f.num_format(n);
714 }
715 f
716}
717
718pub fn set_column_format(
719 store: &mut WorkbookStore,
720 input: SetColumnFormatInput,
721) -> Result<String, anyhow::Error> {
722 let entry = match store.get_mut(&input.workbook_id) {
723 Some(e) => e,
724 None => return Ok(workbook_not_found(store, &input.workbook_id)),
725 };
726 let idx = match find_sheet(&entry.data, &input.sheet_name) {
727 Some(i) => i,
728 None => return Ok(sheet_err(&input.sheet_name)),
729 };
730 let col =
731 zavora_xlsx::utility::col_from_letter(&input.column).map_err(|e| anyhow::anyhow!("{e}"))?;
732 let fmt = build_format(
733 input.bold,
734 input.italic,
735 input.font_size,
736 input.font_color.as_deref(),
737 input.background_color.as_deref(),
738 input.number_format.as_deref(),
739 );
740 entry
741 .data
742 .worksheet(idx)
743 .map_err(|e| anyhow::anyhow!("{e}"))?
744 .set_column_format(col, &fmt);
745 Ok(success_no_data(&format!(
746 "Column {} format set",
747 input.column
748 )))
749}
750
751pub fn set_row_format(
752 store: &mut WorkbookStore,
753 input: SetRowFormatInput,
754) -> Result<String, anyhow::Error> {
755 let entry = match store.get_mut(&input.workbook_id) {
756 Some(e) => e,
757 None => return Ok(workbook_not_found(store, &input.workbook_id)),
758 };
759 let idx = match find_sheet(&entry.data, &input.sheet_name) {
760 Some(i) => i,
761 None => return Ok(sheet_err(&input.sheet_name)),
762 };
763 let fmt = build_format(
764 input.bold,
765 input.italic,
766 input.font_size,
767 input.font_color.as_deref(),
768 input.background_color.as_deref(),
769 input.number_format.as_deref(),
770 );
771 entry
772 .data
773 .worksheet(idx)
774 .map_err(|e| anyhow::anyhow!("{e}"))?
775 .set_row_format(input.row.saturating_sub(1), &fmt);
776 Ok(success_no_data(&format!("Row {} format set", input.row)))
777}
778
779pub fn set_column_hidden(
780 store: &mut WorkbookStore,
781 input: SetColumnHiddenInput,
782) -> Result<String, anyhow::Error> {
783 let entry = match store.get_mut(&input.workbook_id) {
784 Some(e) => e,
785 None => return Ok(workbook_not_found(store, &input.workbook_id)),
786 };
787 let idx = match find_sheet(&entry.data, &input.sheet_name) {
788 Some(i) => i,
789 None => return Ok(sheet_err(&input.sheet_name)),
790 };
791 let col =
792 zavora_xlsx::utility::col_from_letter(&input.column).map_err(|e| anyhow::anyhow!("{e}"))?;
793 entry
794 .data
795 .worksheet(idx)
796 .map_err(|e| anyhow::anyhow!("{e}"))?
797 .set_column_hidden(col, input.hidden);
798 Ok(success_no_data(&format!(
799 "Column {} hidden={}",
800 input.column, input.hidden
801 )))
802}
803
804pub fn set_row_hidden(
805 store: &mut WorkbookStore,
806 input: SetRowHiddenInput,
807) -> Result<String, anyhow::Error> {
808 let entry = match store.get_mut(&input.workbook_id) {
809 Some(e) => e,
810 None => return Ok(workbook_not_found(store, &input.workbook_id)),
811 };
812 let idx = match find_sheet(&entry.data, &input.sheet_name) {
813 Some(i) => i,
814 None => return Ok(sheet_err(&input.sheet_name)),
815 };
816 entry
817 .data
818 .worksheet(idx)
819 .map_err(|e| anyhow::anyhow!("{e}"))?
820 .set_row_hidden(input.row.saturating_sub(1), input.hidden);
821 Ok(success_no_data(&format!(
822 "Row {} hidden={}",
823 input.row, input.hidden
824 )))
825}
826
827pub fn set_column_range_width(
828 store: &mut WorkbookStore,
829 input: SetColumnRangeWidthInput,
830) -> Result<String, anyhow::Error> {
831 let entry = match store.get_mut(&input.workbook_id) {
832 Some(e) => e,
833 None => return Ok(workbook_not_found(store, &input.workbook_id)),
834 };
835 let idx = match find_sheet(&entry.data, &input.sheet_name) {
836 Some(i) => i,
837 None => return Ok(sheet_err(&input.sheet_name)),
838 };
839 let c1 = zavora_xlsx::utility::col_from_letter(&input.first_column)
840 .map_err(|e| anyhow::anyhow!("{e}"))?;
841 let c2 = zavora_xlsx::utility::col_from_letter(&input.last_column)
842 .map_err(|e| anyhow::anyhow!("{e}"))?;
843 entry
844 .data
845 .worksheet(idx)
846 .map_err(|e| anyhow::anyhow!("{e}"))?
847 .set_column_range_width(c1, c2, input.width);
848 Ok(success_no_data(&format!(
849 "Columns {}:{} width set to {}",
850 input.first_column, input.last_column, input.width
851 )))
852}
853
854pub fn set_default_row_height(
855 store: &mut WorkbookStore,
856 input: SetDefaultRowHeightInput,
857) -> Result<String, anyhow::Error> {
858 let entry = match store.get_mut(&input.workbook_id) {
859 Some(e) => e,
860 None => return Ok(workbook_not_found(store, &input.workbook_id)),
861 };
862 let idx = match find_sheet(&entry.data, &input.sheet_name) {
863 Some(i) => i,
864 None => return Ok(sheet_err(&input.sheet_name)),
865 };
866 entry
867 .data
868 .worksheet(idx)
869 .map_err(|e| anyhow::anyhow!("{e}"))?
870 .set_default_row_height(input.height);
871 Ok(success_no_data(&format!(
872 "Default row height set to {}",
873 input.height
874 )))
875}
876
877pub fn set_selection(
878 store: &mut WorkbookStore,
879 input: SetSelectionInput,
880) -> Result<String, anyhow::Error> {
881 let entry = match store.get_mut(&input.workbook_id) {
882 Some(e) => e,
883 None => return Ok(workbook_not_found(store, &input.workbook_id)),
884 };
885 let idx = match find_sheet(&entry.data, &input.sheet_name) {
886 Some(i) => i,
887 None => return Ok(sheet_err(&input.sheet_name)),
888 };
889 let (row, col) =
890 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
891 entry
892 .data
893 .worksheet(idx)
894 .map_err(|e| anyhow::anyhow!("{e}"))?
895 .set_selection(row, col);
896 Ok(success_no_data(&format!("Selection set to {}", input.cell)))
897}
898
899pub fn set_autofilter(
900 store: &mut WorkbookStore,
901 input: SetAutofilterInput,
902) -> Result<String, anyhow::Error> {
903 let entry = match store.get_mut(&input.workbook_id) {
904 Some(e) => e,
905 None => return Ok(workbook_not_found(store, &input.workbook_id)),
906 };
907 let idx = match find_sheet(&entry.data, &input.sheet_name) {
908 Some(i) => i,
909 None => return Ok(sheet_err(&input.sheet_name)),
910 };
911 let (r1, c1, r2, c2) =
912 zavora_xlsx::utility::parse_range_ref(&input.range).map_err(|e| anyhow::anyhow!("{e}"))?;
913 entry
914 .data
915 .worksheet(idx)
916 .map_err(|e| anyhow::anyhow!("{e}"))?
917 .set_autofilter(r1, c1, r2, c2);
918 Ok(success_no_data(&format!(
919 "Autofilter set on {}",
920 input.range
921 )))
922}
923
924pub fn filter_column(
925 store: &mut WorkbookStore,
926 input: FilterColumnInput,
927) -> Result<String, anyhow::Error> {
928 let entry = match store.get_mut(&input.workbook_id) {
929 Some(e) => e,
930 None => return Ok(workbook_not_found(store, &input.workbook_id)),
931 };
932 let idx = match find_sheet(&entry.data, &input.sheet_name) {
933 Some(i) => i,
934 None => return Ok(sheet_err(&input.sheet_name)),
935 };
936 let col =
937 zavora_xlsx::utility::col_from_letter(&input.column).map_err(|e| anyhow::anyhow!("{e}"))?;
938 let strs: Vec<&str> = input.values.iter().map(|s| s.as_str()).collect();
939 entry
940 .data
941 .worksheet(idx)
942 .map_err(|e| anyhow::anyhow!("{e}"))?
943 .filter_column(col, &strs);
944 Ok(success_no_data(&format!(
945 "Filter applied to column {}",
946 input.column
947 )))
948}
949
950pub fn ignore_error(
951 store: &mut WorkbookStore,
952 input: IgnoreErrorInput,
953) -> Result<String, anyhow::Error> {
954 let entry = match store.get_mut(&input.workbook_id) {
955 Some(e) => e,
956 None => return Ok(workbook_not_found(store, &input.workbook_id)),
957 };
958 let idx = match find_sheet(&entry.data, &input.sheet_name) {
959 Some(i) => i,
960 None => return Ok(sheet_err(&input.sheet_name)),
961 };
962 entry
963 .data
964 .worksheet(idx)
965 .map_err(|e| anyhow::anyhow!("{e}"))?
966 .ignore_error(&input.error_type, &input.range);
967 Ok(success_no_data("Error ignored"))
968}
969
970pub fn set_page_breaks(
971 store: &mut WorkbookStore,
972 input: SetPageBreaksInput,
973) -> Result<String, anyhow::Error> {
974 let entry = match store.get_mut(&input.workbook_id) {
975 Some(e) => e,
976 None => return Ok(workbook_not_found(store, &input.workbook_id)),
977 };
978 let idx = match find_sheet(&entry.data, &input.sheet_name) {
979 Some(i) => i,
980 None => return Ok(sheet_err(&input.sheet_name)),
981 };
982 entry
983 .data
984 .worksheet(idx)
985 .map_err(|e| anyhow::anyhow!("{e}"))?
986 .set_page_breaks(&input.row_breaks, &input.col_breaks);
987 Ok(success_no_data("Page breaks set"))
988}
989
990pub fn unprotect_range(
991 store: &mut WorkbookStore,
992 input: UnprotectRangeInput,
993) -> Result<String, anyhow::Error> {
994 let entry = match store.get_mut(&input.workbook_id) {
995 Some(e) => e,
996 None => return Ok(workbook_not_found(store, &input.workbook_id)),
997 };
998 let idx = match find_sheet(&entry.data, &input.sheet_name) {
999 Some(i) => i,
1000 None => return Ok(sheet_err(&input.sheet_name)),
1001 };
1002 let ws = entry
1003 .data
1004 .worksheet(idx)
1005 .map_err(|e| anyhow::anyhow!("{e}"))?;
1006 if let Some(ref pw) = input.password {
1007 ws.unprotect_range_with_password(&input.range, &input.title, pw);
1008 } else {
1009 ws.unprotect_range(&input.range, &input.title);
1010 }
1011 Ok(success_no_data(&format!(
1012 "Range {} unprotected",
1013 input.range
1014 )))
1015}
1016
1017pub fn write_formula(
1018 store: &mut WorkbookStore,
1019 input: WriteFormulaInput,
1020) -> Result<String, anyhow::Error> {
1021 let entry = match store.get_mut(&input.workbook_id) {
1022 Some(e) => e,
1023 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1024 };
1025 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1026 Some(i) => i,
1027 None => return Ok(sheet_err(&input.sheet_name)),
1028 };
1029 let (row, col) =
1030 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1031 let ws = entry
1032 .data
1033 .worksheet(idx)
1034 .map_err(|e| anyhow::anyhow!("{e}"))?;
1035 if let Some(result) = input.cached_result {
1036 ws.write_formula_with_result(row, col, &input.formula, result)
1037 .map_err(|e| anyhow::anyhow!("{e}"))?;
1038 } else {
1039 ws.write_formula(row, col, &input.formula)
1040 .map_err(|e| anyhow::anyhow!("{e}"))?;
1041 }
1042 Ok(success_no_data(&format!(
1043 "Formula written to {}",
1044 input.cell
1045 )))
1046}
1047
1048pub fn write_array_formula(
1049 store: &mut WorkbookStore,
1050 input: WriteArrayFormulaInput,
1051) -> Result<String, anyhow::Error> {
1052 let entry = match store.get_mut(&input.workbook_id) {
1053 Some(e) => e,
1054 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1055 };
1056 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1057 Some(i) => i,
1058 None => return Ok(sheet_err(&input.sheet_name)),
1059 };
1060 let (r1, c1, r2, c2) =
1061 zavora_xlsx::utility::parse_range_ref(&input.range).map_err(|e| anyhow::anyhow!("{e}"))?;
1062 entry
1063 .data
1064 .worksheet(idx)
1065 .map_err(|e| anyhow::anyhow!("{e}"))?
1066 .write_array_formula(r1, c1, r2, c2, &input.formula)
1067 .map_err(|e| anyhow::anyhow!("{e}"))?;
1068 Ok(success_no_data(&format!(
1069 "Array formula written to {}",
1070 input.range
1071 )))
1072}
1073
1074pub fn write_dynamic_formula(
1075 store: &mut WorkbookStore,
1076 input: WriteDynamicFormulaInput,
1077) -> Result<String, anyhow::Error> {
1078 let entry = match store.get_mut(&input.workbook_id) {
1079 Some(e) => e,
1080 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1081 };
1082 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1083 Some(i) => i,
1084 None => return Ok(sheet_err(&input.sheet_name)),
1085 };
1086 let (row, col) =
1087 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1088 entry
1089 .data
1090 .worksheet(idx)
1091 .map_err(|e| anyhow::anyhow!("{e}"))?
1092 .write_dynamic_formula(row, col, &input.formula)
1093 .map_err(|e| anyhow::anyhow!("{e}"))?;
1094 Ok(success_no_data(&format!(
1095 "Dynamic formula written to {}",
1096 input.cell
1097 )))
1098}
1099
1100pub fn write_blank(
1101 store: &mut WorkbookStore,
1102 input: WriteBlankInput,
1103) -> Result<String, anyhow::Error> {
1104 let entry = match store.get_mut(&input.workbook_id) {
1105 Some(e) => e,
1106 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1107 };
1108 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1109 Some(i) => i,
1110 None => return Ok(sheet_err(&input.sheet_name)),
1111 };
1112 let (row, col) =
1113 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1114 let fmt = build_format(
1115 input.bold,
1116 None,
1117 None,
1118 None,
1119 input.background_color.as_deref(),
1120 input.number_format.as_deref(),
1121 );
1122 entry
1123 .data
1124 .worksheet(idx)
1125 .map_err(|e| anyhow::anyhow!("{e}"))?
1126 .write_blank(row, col, &fmt)
1127 .map_err(|e| anyhow::anyhow!("{e}"))?;
1128 Ok(success_no_data(&format!(
1129 "Blank cell written at {}",
1130 input.cell
1131 )))
1132}
1133
1134pub fn clear_cell(
1135 store: &mut WorkbookStore,
1136 input: ClearCellInput,
1137) -> Result<String, anyhow::Error> {
1138 let entry = match store.get_mut(&input.workbook_id) {
1139 Some(e) => e,
1140 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1141 };
1142 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1143 Some(i) => i,
1144 None => return Ok(sheet_err(&input.sheet_name)),
1145 };
1146 let (row, col) =
1147 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1148 entry
1149 .data
1150 .worksheet(idx)
1151 .map_err(|e| anyhow::anyhow!("{e}"))?
1152 .clear_cell(row, col);
1153 Ok(success_no_data(&format!("Cell {} cleared", input.cell)))
1154}
1155
1156pub fn set_calc_mode(
1157 store: &mut WorkbookStore,
1158 input: SetCalcModeInput,
1159) -> Result<String, anyhow::Error> {
1160 let entry = match store.get_mut(&input.workbook_id) {
1161 Some(e) => e,
1162 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1163 };
1164 let mode = match input.mode.as_str() {
1165 "manual" => zavora_xlsx::CalcMode::Manual,
1166 "auto_no_table" => zavora_xlsx::CalcMode::AutoNoTable,
1167 _ => zavora_xlsx::CalcMode::Auto,
1168 };
1169 entry.data.set_calc_mode(mode);
1170 Ok(success_no_data(&format!("Calc mode set to {}", input.mode)))
1171}
1172
1173pub fn set_properties(
1174 store: &mut WorkbookStore,
1175 input: SetPropertiesInput,
1176) -> Result<String, anyhow::Error> {
1177 let entry = match store.get_mut(&input.workbook_id) {
1178 Some(e) => e,
1179 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1180 };
1181 let mut props = entry.data.properties().clone();
1182 if let Some(ref t) = input.title {
1183 props.title = Some(t.clone());
1184 }
1185 if let Some(ref a) = input.author {
1186 props.author = Some(a.clone());
1187 }
1188 if let Some(ref s) = input.subject {
1189 props.subject = Some(s.clone());
1190 }
1191 if let Some(ref c) = input.company {
1192 props.company = Some(c.clone());
1193 }
1194 if let Some(ref d) = input.description {
1195 props.description = Some(d.clone());
1196 }
1197 entry.data.set_properties(props);
1198 Ok(success_no_data("Document properties set"))
1199}
1200
1201pub fn move_worksheet(
1202 store: &mut WorkbookStore,
1203 input: MoveWorksheetInput,
1204) -> Result<String, anyhow::Error> {
1205 let entry = match store.get_mut(&input.workbook_id) {
1206 Some(e) => e,
1207 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1208 };
1209 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1210 Some(i) => i,
1211 None => return Ok(sheet_err(&input.sheet_name)),
1212 };
1213 entry
1214 .data
1215 .move_worksheet(idx, input.to_index)
1216 .map_err(|e| anyhow::anyhow!("{e}"))?;
1217 Ok(success_no_data(&format!(
1218 "Sheet '{}' moved to position {}",
1219 input.sheet_name, input.to_index
1220 )))
1221}
1222
1223pub fn write_internal_link(
1224 store: &mut WorkbookStore,
1225 input: WriteInternalLinkInput,
1226) -> Result<String, anyhow::Error> {
1227 let entry = match store.get_mut(&input.workbook_id) {
1228 Some(e) => e,
1229 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1230 };
1231 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1232 Some(i) => i,
1233 None => return Ok(sheet_err(&input.sheet_name)),
1234 };
1235 let (row, col) =
1236 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1237 entry
1238 .data
1239 .worksheet(idx)
1240 .map_err(|e| anyhow::anyhow!("{e}"))?
1241 .write_internal_link(row, col, &input.location, &input.display_text)
1242 .map_err(|e| anyhow::anyhow!("{e}"))?;
1243 Ok(success_no_data(&format!(
1244 "Internal link written at {}",
1245 input.cell
1246 )))
1247}
1248
1249pub fn configure_workbook(
1254 store: &mut WorkbookStore,
1255 input: ConfigureWorkbookInput,
1256) -> Result<String, anyhow::Error> {
1257 let entry = match store.get_mut(&input.workbook_id) {
1258 Some(e) => e,
1259 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1260 };
1261 if let Some(ref m) = input.calc_mode {
1262 let mode = match m.as_str() {
1263 "manual" => zavora_xlsx::CalcMode::Manual,
1264 "auto_no_table" => zavora_xlsx::CalcMode::AutoNoTable,
1265 _ => zavora_xlsx::CalcMode::Auto,
1266 };
1267 entry.data.set_calc_mode(mode);
1268 }
1269 if let Some(i) = input.active_sheet {
1270 entry.data.set_active_sheet(i);
1271 }
1272 let mut props = entry.data.properties().clone();
1273 if let Some(ref v) = input.title {
1274 props.title = Some(v.clone());
1275 }
1276 if let Some(ref v) = input.author {
1277 props.author = Some(v.clone());
1278 }
1279 if let Some(ref v) = input.subject {
1280 props.subject = Some(v.clone());
1281 }
1282 if let Some(ref v) = input.company {
1283 props.company = Some(v.clone());
1284 }
1285 if let Some(ref v) = input.description {
1286 props.description = Some(v.clone());
1287 }
1288 entry.data.set_properties(props);
1289 Ok(success_no_data("Workbook configured"))
1290}
1291
1292pub fn modify_rows(
1293 store: &mut WorkbookStore,
1294 input: ModifyRowsInput,
1295) -> Result<String, anyhow::Error> {
1296 let entry = match store.get_mut(&input.workbook_id) {
1297 Some(e) => e,
1298 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1299 };
1300 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1301 Some(i) => i,
1302 None => return Ok(sheet_err(&input.sheet_name)),
1303 };
1304 let ws = entry
1305 .data
1306 .worksheet(idx)
1307 .map_err(|e| anyhow::anyhow!("{e}"))?;
1308 let row = input.at_row.saturating_sub(1);
1309 match input.action.as_str() {
1310 "delete" => {
1311 ws.remove_rows(row, input.count)
1312 .map_err(|e| anyhow::anyhow!("{e}"))?;
1313 }
1314 _ => {
1315 ws.insert_rows(row, input.count)
1316 .map_err(|e| anyhow::anyhow!("{e}"))?;
1317 }
1318 }
1319 Ok(success_no_data(&format!(
1320 "{} {} rows at row {}",
1321 input.action, input.count, input.at_row
1322 )))
1323}
1324
1325pub fn modify_columns(
1326 store: &mut WorkbookStore,
1327 input: ModifyColumnsInput,
1328) -> Result<String, anyhow::Error> {
1329 let entry = match store.get_mut(&input.workbook_id) {
1330 Some(e) => e,
1331 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1332 };
1333 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1334 Some(i) => i,
1335 None => return Ok(sheet_err(&input.sheet_name)),
1336 };
1337 let col = zavora_xlsx::utility::col_from_letter(&input.at_column)
1338 .map_err(|e| anyhow::anyhow!("{e}"))?;
1339 let ws = entry
1340 .data
1341 .worksheet(idx)
1342 .map_err(|e| anyhow::anyhow!("{e}"))?;
1343 match input.action.as_str() {
1344 "delete" => {
1345 ws.remove_columns(col, input.count)
1346 .map_err(|e| anyhow::anyhow!("{e}"))?;
1347 }
1348 _ => {
1349 ws.insert_columns(col, input.count)
1350 .map_err(|e| anyhow::anyhow!("{e}"))?;
1351 }
1352 }
1353 Ok(success_no_data(&format!(
1354 "{} {} columns at {}",
1355 input.action, input.count, input.at_column
1356 )))
1357}
1358
1359pub fn write_formula_consolidated(
1360 store: &mut WorkbookStore,
1361 input: WriteFormulaConsolidatedInput,
1362) -> Result<String, anyhow::Error> {
1363 let entry = match store.get_mut(&input.workbook_id) {
1364 Some(e) => e,
1365 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1366 };
1367 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1368 Some(i) => i,
1369 None => return Ok(sheet_err(&input.sheet_name)),
1370 };
1371 let ws = entry
1372 .data
1373 .worksheet(idx)
1374 .map_err(|e| anyhow::anyhow!("{e}"))?;
1375 match input.formula_type.as_deref().unwrap_or("regular") {
1376 "array" => {
1377 let (r1, c1, r2, c2) = zavora_xlsx::utility::parse_range_ref(&input.cell)
1378 .map_err(|e| anyhow::anyhow!("{e}"))?;
1379 ws.write_array_formula(r1, c1, r2, c2, &input.formula)
1380 .map_err(|e| anyhow::anyhow!("{e}"))?;
1381 }
1382 "dynamic" => {
1383 let (row, col) = zavora_xlsx::utility::parse_cell_ref(&input.cell)
1384 .map_err(|e| anyhow::anyhow!("{e}"))?;
1385 ws.write_dynamic_formula(row, col, &input.formula)
1386 .map_err(|e| anyhow::anyhow!("{e}"))?;
1387 }
1388 _ => {
1389 let (row, col) = zavora_xlsx::utility::parse_cell_ref(&input.cell)
1390 .map_err(|e| anyhow::anyhow!("{e}"))?;
1391 if let Some(r) = input.cached_result {
1392 ws.write_formula_with_result(row, col, &input.formula, r)
1393 .map_err(|e| anyhow::anyhow!("{e}"))?;
1394 } else {
1395 ws.write_formula(row, col, &input.formula)
1396 .map_err(|e| anyhow::anyhow!("{e}"))?;
1397 }
1398 }
1399 }
1400 Ok(success_no_data(&format!(
1401 "Formula written to {}",
1402 input.cell
1403 )))
1404}
1405
1406pub fn manage_cell(
1407 store: &mut WorkbookStore,
1408 input: ManageCellInput,
1409) -> Result<String, anyhow::Error> {
1410 let entry = match store.get_mut(&input.workbook_id) {
1411 Some(e) => e,
1412 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1413 };
1414 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1415 Some(i) => i,
1416 None => return Ok(sheet_err(&input.sheet_name)),
1417 };
1418 let (row, col) =
1419 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1420 let ws = entry
1421 .data
1422 .worksheet(idx)
1423 .map_err(|e| anyhow::anyhow!("{e}"))?;
1424 match input.action.as_str() {
1425 "clear" => {
1426 ws.clear_cell(row, col);
1427 }
1428 _ => {
1429 let fmt = build_format(
1430 None,
1431 None,
1432 None,
1433 None,
1434 input.background_color.as_deref(),
1435 input.number_format.as_deref(),
1436 );
1437 ws.write_blank(row, col, &fmt)
1438 .map_err(|e| anyhow::anyhow!("{e}"))?;
1439 }
1440 }
1441 Ok(success_no_data(&format!(
1442 "Cell {} {}",
1443 input.cell, input.action
1444 )))
1445}
1446
1447pub fn manage_comments(
1448 store: &mut WorkbookStore,
1449 input: ManageCommentsInput,
1450) -> Result<String, anyhow::Error> {
1451 let entry = match store.get_mut(&input.workbook_id) {
1452 Some(e) => e,
1453 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1454 };
1455 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1456 Some(i) => i,
1457 None => return Ok(sheet_err(&input.sheet_name)),
1458 };
1459 match input.action.as_str() {
1460 "read" => {
1461 let ws = entry
1462 .data
1463 .worksheet(idx)
1464 .map_err(|e| anyhow::anyhow!("{e}"))?;
1465 let comments: Vec<serde_json::Value> = ws.comments().iter().map(|c|
1466 serde_json::json!({"cell": zavora_xlsx::utility::to_a1(c.row, c.col), "author": c.author, "text": c.text})
1467 ).collect();
1468 Ok(success("Comments read", comments))
1469 }
1470 _ => {
1471 let cell = input.cell.as_deref().unwrap_or("A1");
1472 let text = input.text.as_deref().unwrap_or("");
1473 let (row, col) =
1474 zavora_xlsx::utility::parse_cell_ref(cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1475 let ws = entry
1476 .data
1477 .worksheet(idx)
1478 .map_err(|e| anyhow::anyhow!("{e}"))?;
1479 if let Some(ref a) = input.author {
1480 ws.add_comment_with_author(row, col, text, a);
1481 } else {
1482 ws.add_comment(row, col, text);
1483 }
1484 Ok(success_no_data(&format!("Comment added at {cell}")))
1485 }
1486 }
1487}
1488
1489pub fn manage_defined_names(
1490 store: &mut WorkbookStore,
1491 input: ManageDefinedNamesInput,
1492) -> Result<String, anyhow::Error> {
1493 let entry = match store.get_mut(&input.workbook_id) {
1494 Some(e) => e,
1495 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1496 };
1497 match input.action.as_str() {
1498 "list" => {
1499 let names: Vec<serde_json::Value> = entry
1500 .data
1501 .defined_names()
1502 .iter()
1503 .map(|(n, f)| serde_json::json!({"name": n, "formula": f}))
1504 .collect();
1505 Ok(success("Defined names", names))
1506 }
1507 _ => {
1508 let name = input.name.as_deref().unwrap_or("");
1509 let formula = input.formula.as_deref().unwrap_or("");
1510 entry.data.define_name(name, formula);
1511 Ok(success_no_data(&format!("Defined name '{name}' added")))
1512 }
1513 }
1514}
1515
1516pub fn add_link(store: &mut WorkbookStore, input: AddLinkInput) -> Result<String, anyhow::Error> {
1517 let entry = match store.get_mut(&input.workbook_id) {
1518 Some(e) => e,
1519 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1520 };
1521 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1522 Some(i) => i,
1523 None => return Ok(sheet_err(&input.sheet_name)),
1524 };
1525 let (row, col) =
1526 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1527 let ws = entry
1528 .data
1529 .worksheet(idx)
1530 .map_err(|e| anyhow::anyhow!("{e}"))?;
1531 match input.link_type.as_str() {
1532 "internal" => {
1533 ws.write_internal_link(
1534 row,
1535 col,
1536 &input.target,
1537 input.display_text.as_deref().unwrap_or(&input.target),
1538 )
1539 .map_err(|e| anyhow::anyhow!("{e}"))?;
1540 }
1541 _ => {
1542 ws.write_url(
1543 row,
1544 col,
1545 &input.target,
1546 input.display_text.as_deref().unwrap_or(&input.target),
1547 )
1548 .map_err(|e| anyhow::anyhow!("{e}"))?;
1549 }
1550 }
1551 Ok(success_no_data(&format!("Link added at {}", input.cell)))
1552}
1553
1554pub fn protect_consolidated(
1555 store: &mut WorkbookStore,
1556 input: ProtectInput,
1557) -> Result<String, anyhow::Error> {
1558 let entry = match store.get_mut(&input.workbook_id) {
1559 Some(e) => e,
1560 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1561 };
1562 match input.target.as_str() {
1563 "workbook" => {
1564 if let Some(ref pw) = input.password {
1565 entry.data.protect_with_password(pw);
1566 } else {
1567 entry.data.protect();
1568 }
1569 Ok(success_no_data("Workbook protected"))
1570 }
1571 "unprotect_range" => {
1572 let sn = input.sheet_name.as_deref().unwrap_or("Sheet1");
1573 let idx = match find_sheet(&entry.data, sn) {
1574 Some(i) => i,
1575 None => return Ok(sheet_err(sn)),
1576 };
1577 let ws = entry
1578 .data
1579 .worksheet(idx)
1580 .map_err(|e| anyhow::anyhow!("{e}"))?;
1581 let range = input.range.as_deref().unwrap_or("A1:A1");
1582 let title = input.range_title.as_deref().unwrap_or("Range");
1583 if let Some(ref pw) = input.password {
1584 ws.unprotect_range_with_password(range, title, pw);
1585 } else {
1586 ws.unprotect_range(range, title);
1587 }
1588 Ok(success_no_data(&format!("Range {range} unprotected")))
1589 }
1590 _ => {
1591 let sn = input.sheet_name.as_deref().unwrap_or("Sheet1");
1593 let idx = match find_sheet(&entry.data, sn) {
1594 Some(i) => i,
1595 None => return Ok(sheet_err(sn)),
1596 };
1597 let ws = entry
1598 .data
1599 .worksheet(idx)
1600 .map_err(|e| anyhow::anyhow!("{e}"))?;
1601 if let Some(ref pw) = input.password {
1602 ws.protect_with_password(pw);
1603 } else {
1604 ws.protect();
1605 }
1606 Ok(success_no_data(&format!("Sheet '{sn}' protected")))
1607 }
1608 }
1609}
1610
1611pub fn set_dimensions(
1612 store: &mut WorkbookStore,
1613 input: SetDimensionsInput,
1614) -> Result<String, anyhow::Error> {
1615 let entry = match store.get_mut(&input.workbook_id) {
1616 Some(e) => e,
1617 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1618 };
1619 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1620 Some(i) => i,
1621 None => return Ok(sheet_err(&input.sheet_name)),
1622 };
1623 let ws = entry
1624 .data
1625 .worksheet(idx)
1626 .map_err(|e| anyhow::anyhow!("{e}"))?;
1627 match input.target.as_str() {
1628 "row_height" => {
1629 ws.set_row_height(input.row.unwrap_or(1).saturating_sub(1), input.value)
1630 .map_err(|e| anyhow::anyhow!("{e}"))?;
1631 }
1632 "column_range_width" => {
1633 let c1 =
1634 zavora_xlsx::utility::col_from_letter(input.first_column.as_deref().unwrap_or("A"))
1635 .map_err(|e| anyhow::anyhow!("{e}"))?;
1636 let c2 =
1637 zavora_xlsx::utility::col_from_letter(input.last_column.as_deref().unwrap_or("A"))
1638 .map_err(|e| anyhow::anyhow!("{e}"))?;
1639 ws.set_column_range_width(c1, c2, input.value);
1640 }
1641 "default_row_height" => {
1642 ws.set_default_row_height(input.value);
1643 }
1644 _ => {
1645 let col = zavora_xlsx::utility::col_from_letter(input.column.as_deref().unwrap_or("A"))
1647 .map_err(|e| anyhow::anyhow!("{e}"))?;
1648 ws.set_column_width(col, input.value)
1649 .map_err(|e| anyhow::anyhow!("{e}"))?;
1650 }
1651 }
1652 Ok(success_no_data(&format!(
1653 "{} set to {}",
1654 input.target, input.value
1655 )))
1656}
1657
1658pub fn set_visibility(
1659 store: &mut WorkbookStore,
1660 input: SetVisibilityInput,
1661) -> Result<String, anyhow::Error> {
1662 let entry = match store.get_mut(&input.workbook_id) {
1663 Some(e) => e,
1664 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1665 };
1666 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1667 Some(i) => i,
1668 None => return Ok(sheet_err(&input.sheet_name)),
1669 };
1670 let ws = entry
1671 .data
1672 .worksheet(idx)
1673 .map_err(|e| anyhow::anyhow!("{e}"))?;
1674 match input.target.as_str() {
1675 "row" => {
1676 let r: u32 = input.identifier.parse().unwrap_or(1);
1677 ws.set_row_hidden(r.saturating_sub(1), input.hidden);
1678 }
1679 _ => {
1680 let col = zavora_xlsx::utility::col_from_letter(&input.identifier)
1681 .map_err(|e| anyhow::anyhow!("{e}"))?;
1682 ws.set_column_hidden(col, input.hidden);
1683 }
1684 }
1685 Ok(success_no_data(&format!(
1686 "{} {} hidden={}",
1687 input.target, input.identifier, input.hidden
1688 )))
1689}
1690
1691pub fn set_row_column_format(
1692 store: &mut WorkbookStore,
1693 input: SetRowColumnFormatInput,
1694) -> Result<String, anyhow::Error> {
1695 let entry = match store.get_mut(&input.workbook_id) {
1696 Some(e) => e,
1697 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1698 };
1699 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1700 Some(i) => i,
1701 None => return Ok(sheet_err(&input.sheet_name)),
1702 };
1703 let fmt = build_format(
1704 input.bold,
1705 input.italic,
1706 input.font_size,
1707 input.font_color.as_deref(),
1708 input.background_color.as_deref(),
1709 input.number_format.as_deref(),
1710 );
1711 let ws = entry
1712 .data
1713 .worksheet(idx)
1714 .map_err(|e| anyhow::anyhow!("{e}"))?;
1715 match input.target.as_str() {
1716 "row" => {
1717 let r: u32 = input.identifier.parse().unwrap_or(1);
1718 ws.set_row_format(r.saturating_sub(1), &fmt);
1719 }
1720 _ => {
1721 let col = zavora_xlsx::utility::col_from_letter(&input.identifier)
1722 .map_err(|e| anyhow::anyhow!("{e}"))?;
1723 ws.set_column_format(col, &fmt);
1724 }
1725 }
1726 Ok(success_no_data(&format!(
1727 "{} {} format set",
1728 input.target, input.identifier
1729 )))
1730}
1731
1732pub fn group_consolidated(
1733 store: &mut WorkbookStore,
1734 input: GroupInput,
1735) -> Result<String, anyhow::Error> {
1736 let entry = match store.get_mut(&input.workbook_id) {
1737 Some(e) => e,
1738 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1739 };
1740 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1741 Some(i) => i,
1742 None => return Ok(sheet_err(&input.sheet_name)),
1743 };
1744 let ws = entry
1745 .data
1746 .worksheet(idx)
1747 .map_err(|e| anyhow::anyhow!("{e}"))?;
1748 match input.target.as_str() {
1749 "columns" => {
1750 let c1 = zavora_xlsx::utility::col_from_letter(&input.start)
1751 .map_err(|e| anyhow::anyhow!("{e}"))?;
1752 let c2 = zavora_xlsx::utility::col_from_letter(&input.end)
1753 .map_err(|e| anyhow::anyhow!("{e}"))?;
1754 ws.group_columns(c1, c2, input.level);
1755 }
1756 _ => {
1757 let s: u32 = input.start.parse().unwrap_or(1);
1758 let e: u32 = input.end.parse().unwrap_or(1);
1759 ws.group_rows(s.saturating_sub(1), e.saturating_sub(1), input.level);
1760 }
1761 }
1762 Ok(success_no_data(&format!(
1763 "{} {}-{} grouped at level {}",
1764 input.target, input.start, input.end, input.level
1765 )))
1766}
1767
1768pub fn manage_autofilter(
1769 store: &mut WorkbookStore,
1770 input: ManageAutofilterInput,
1771) -> Result<String, anyhow::Error> {
1772 let entry = match store.get_mut(&input.workbook_id) {
1773 Some(e) => e,
1774 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1775 };
1776 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1777 Some(i) => i,
1778 None => return Ok(sheet_err(&input.sheet_name)),
1779 };
1780 let (r1, c1, r2, c2) =
1781 zavora_xlsx::utility::parse_range_ref(&input.range).map_err(|e| anyhow::anyhow!("{e}"))?;
1782 let ws = entry
1783 .data
1784 .worksheet(idx)
1785 .map_err(|e| anyhow::anyhow!("{e}"))?;
1786 ws.set_autofilter(r1, c1, r2, c2);
1787 if let (Some(ref fc), Some(ref fv)) = (input.filter_column, input.filter_values) {
1788 let col = zavora_xlsx::utility::col_from_letter(fc).map_err(|e| anyhow::anyhow!("{e}"))?;
1789 let strs: Vec<&str> = fv.iter().map(|s| s.as_str()).collect();
1790 ws.filter_column(col, &strs);
1791 }
1792 Ok(success_no_data(&format!(
1793 "Autofilter set on {}",
1794 input.range
1795 )))
1796}
1797
1798fn parse_hex_color(hex: &str) -> [u8; 3] {
1804 let s = hex.trim_start_matches('#');
1805 if s.len() == 6 {
1806 let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0);
1807 let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0);
1808 let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0);
1809 [r, g, b]
1810 } else {
1811 [0, 0, 0]
1812 }
1813}
1814
1815pub fn add_waterfall_chart(
1816 store: &mut WorkbookStore,
1817 input: AddWaterfallChartInput,
1818) -> Result<String, anyhow::Error> {
1819 let entry = match store.get_mut(&input.workbook_id) {
1820 Some(e) => e,
1821 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1822 };
1823 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1824 Some(i) => i,
1825 None => return Ok(sheet_err(&input.sheet_name)),
1826 };
1827 let mut chart = zavora_xlsx::WaterfallChart::new();
1828 if let Some(ref t) = input.title {
1829 chart.set_title(t);
1830 }
1831 if let Some(ref n) = input.series_name {
1832 chart.set_series_name(n);
1833 }
1834 chart.set_width(input.width);
1835 chart.set_height(input.height);
1836 for pt in &input.points {
1837 let point_type = match pt.point_type {
1838 crate::types::enums::WaterfallPointKind::Increase => {
1839 zavora_xlsx::WaterfallPointType::Increase
1840 }
1841 crate::types::enums::WaterfallPointKind::Decrease => {
1842 zavora_xlsx::WaterfallPointType::Decrease
1843 }
1844 crate::types::enums::WaterfallPointKind::Total => {
1845 zavora_xlsx::WaterfallPointType::Total
1846 }
1847 };
1848 chart.add_point(&pt.category, pt.value, point_type);
1849 }
1850 let (row, col) = if let Some(ref c) = input.cell {
1851 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
1852 } else {
1853 (0, 0)
1854 };
1855 entry
1856 .data
1857 .worksheet(idx)
1858 .map_err(|e| anyhow::anyhow!("{e}"))?
1859 .insert_waterfall(row, col, &chart)?;
1860 Ok(success_no_data(&format!(
1861 "Waterfall chart added to '{}'",
1862 input.sheet_name
1863 )))
1864}
1865
1866pub fn add_funnel_chart(
1867 store: &mut WorkbookStore,
1868 input: AddFunnelChartInput,
1869) -> Result<String, anyhow::Error> {
1870 let entry = match store.get_mut(&input.workbook_id) {
1871 Some(e) => e,
1872 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1873 };
1874 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1875 Some(i) => i,
1876 None => return Ok(sheet_err(&input.sheet_name)),
1877 };
1878 let mut chart = zavora_xlsx::FunnelChart::new();
1879 if let Some(ref t) = input.title {
1880 chart.set_title(t);
1881 }
1882 if let Some(ref n) = input.series_name {
1883 chart.set_series_name(n);
1884 }
1885 chart.set_width(input.width);
1886 chart.set_height(input.height);
1887 for pt in &input.points {
1888 chart.add_point(&pt.category, pt.value);
1889 }
1890 let (row, col) = if let Some(ref c) = input.cell {
1891 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
1892 } else {
1893 (0, 0)
1894 };
1895 entry
1896 .data
1897 .worksheet(idx)
1898 .map_err(|e| anyhow::anyhow!("{e}"))?
1899 .insert_funnel(row, col, &chart)?;
1900 Ok(success_no_data(&format!(
1901 "Funnel chart added to '{}'",
1902 input.sheet_name
1903 )))
1904}
1905
1906pub fn add_treemap_chart(
1907 store: &mut WorkbookStore,
1908 input: AddTreemapChartInput,
1909) -> Result<String, anyhow::Error> {
1910 let entry = match store.get_mut(&input.workbook_id) {
1911 Some(e) => e,
1912 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1913 };
1914 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1915 Some(i) => i,
1916 None => return Ok(sheet_err(&input.sheet_name)),
1917 };
1918 let mut chart = zavora_xlsx::TreemapChart::new();
1919 if let Some(ref t) = input.title {
1920 chart.set_title(t);
1921 }
1922 if let Some(ref n) = input.series_name {
1923 chart.set_series_name(n);
1924 }
1925 chart.set_width(input.width);
1926 chart.set_height(input.height);
1927 for pt in &input.points {
1928 if let Some(ref c) = pt.color {
1929 chart.add_point_with_color(&pt.category, pt.value, c.as_str());
1930 } else {
1931 chart.add_point(&pt.category, pt.value);
1932 }
1933 }
1934 let (row, col) = if let Some(ref c) = input.cell {
1935 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
1936 } else {
1937 (0, 0)
1938 };
1939 entry
1940 .data
1941 .worksheet(idx)
1942 .map_err(|e| anyhow::anyhow!("{e}"))?
1943 .insert_treemap(row, col, &chart)?;
1944 Ok(success_no_data(&format!(
1945 "Treemap chart added to '{}'",
1946 input.sheet_name
1947 )))
1948}
1949
1950pub fn add_shape(store: &mut WorkbookStore, input: AddShapeInput) -> Result<String, anyhow::Error> {
1951 let entry = match store.get_mut(&input.workbook_id) {
1952 Some(e) => e,
1953 None => return Ok(workbook_not_found(store, &input.workbook_id)),
1954 };
1955 let idx = match find_sheet(&entry.data, &input.sheet_name) {
1956 Some(i) => i,
1957 None => return Ok(sheet_err(&input.sheet_name)),
1958 };
1959 let st = match input.shape_type {
1960 crate::types::enums::ShapeKind::Rectangle => zavora_xlsx::ShapeType::Rectangle,
1961 crate::types::enums::ShapeKind::RoundedRectangle => {
1962 zavora_xlsx::ShapeType::RoundedRectangle
1963 }
1964 crate::types::enums::ShapeKind::Ellipse => zavora_xlsx::ShapeType::Ellipse,
1965 crate::types::enums::ShapeKind::Triangle => zavora_xlsx::ShapeType::Triangle,
1966 crate::types::enums::ShapeKind::Diamond => zavora_xlsx::ShapeType::Diamond,
1967 crate::types::enums::ShapeKind::Arrow => zavora_xlsx::ShapeType::Arrow,
1968 crate::types::enums::ShapeKind::Callout => zavora_xlsx::ShapeType::Callout,
1969 crate::types::enums::ShapeKind::TextBox => zavora_xlsx::ShapeType::TextBox,
1970 };
1971 let mut shape = zavora_xlsx::Shape::new(st, input.width, input.height);
1972 if let Some(ref t) = input.text {
1973 shape = shape.text(t);
1974 }
1975 if let Some(ref c) = input.fill_color {
1976 shape = shape.fill_color(parse_hex_color(c));
1977 }
1978 if let Some(ref c) = input.outline_color {
1979 shape = shape.outline_color(parse_hex_color(c));
1980 }
1981 if let Some(w) = input.outline_width {
1982 shape = shape.outline_width(w);
1983 }
1984 if let Some(s) = input.font_size {
1985 shape = shape.font_size(s);
1986 }
1987 if input.bold == Some(true) {
1988 shape = shape.bold();
1989 }
1990 let (row, col) =
1991 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
1992 entry
1993 .data
1994 .worksheet(idx)
1995 .map_err(|e| anyhow::anyhow!("{e}"))?
1996 .add_shape(row, col, &shape)?;
1997 Ok(success_no_data(&format!("Shape added at {}", input.cell)))
1998}
1999
2000pub fn set_doc_properties(
2001 store: &mut WorkbookStore,
2002 input: SetDocPropertiesInput,
2003) -> Result<String, anyhow::Error> {
2004 let entry = match store.get_mut(&input.workbook_id) {
2005 Some(e) => e,
2006 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2007 };
2008 let mut props = zavora_xlsx::DocProperties::new();
2009 if let Some(ref v) = input.title {
2010 props = props.title(v);
2011 }
2012 if let Some(ref v) = input.author {
2013 props = props.author(v);
2014 }
2015 if let Some(ref v) = input.subject {
2016 props = props.subject(v);
2017 }
2018 if let Some(ref v) = input.description {
2019 props = props.description(v);
2020 }
2021 if let Some(ref v) = input.keywords {
2022 props = props.keywords(v);
2023 }
2024 if let Some(ref v) = input.category {
2025 props = props.category(v);
2026 }
2027 if let Some(ref v) = input.company {
2028 props = props.company(v);
2029 }
2030 entry.data.set_properties(props);
2031 Ok(success_no_data("Document properties set"))
2032}
2033
2034pub fn add_sunburst_chart(
2041 store: &mut WorkbookStore,
2042 input: AddSunburstChartInput,
2043) -> Result<String, anyhow::Error> {
2044 let entry = match store.get_mut(&input.workbook_id) {
2045 Some(e) => e,
2046 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2047 };
2048 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2049 Some(i) => i,
2050 None => return Ok(sheet_err(&input.sheet_name)),
2051 };
2052 let mut chart = zavora_xlsx::SunburstChart::new();
2053 if let Some(ref t) = input.title {
2054 chart.set_title(t);
2055 }
2056 if let Some(ref n) = input.series_name {
2057 chart.set_series_name(n);
2058 }
2059 chart.set_width(input.width);
2060 chart.set_height(input.height);
2061 let labels: Vec<&str> = input.points.iter().map(|p| p.category.as_str()).collect();
2063 let values: Vec<f64> = input.points.iter().map(|p| p.value).collect();
2064 chart.add_level(&labels);
2065 chart.set_values(&values);
2066 let (row, col) = if let Some(ref c) = input.cell {
2067 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2068 } else {
2069 (0, 0)
2070 };
2071 entry
2072 .data
2073 .worksheet(idx)
2074 .map_err(|e| anyhow::anyhow!("{e}"))?
2075 .insert_sunburst(row, col, &chart)?;
2076 Ok(success_no_data(&format!(
2077 "Sunburst chart added to '{}'",
2078 input.sheet_name
2079 )))
2080}
2081
2082pub fn add_histogram_chart(
2083 store: &mut WorkbookStore,
2084 input: AddHistogramChartInput,
2085) -> Result<String, anyhow::Error> {
2086 let entry = match store.get_mut(&input.workbook_id) {
2087 Some(e) => e,
2088 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2089 };
2090 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2091 Some(i) => i,
2092 None => return Ok(sheet_err(&input.sheet_name)),
2093 };
2094 let mut chart = if input.pareto == Some(true) {
2095 zavora_xlsx::HistogramChart::pareto()
2096 } else {
2097 zavora_xlsx::HistogramChart::new()
2098 };
2099 if let Some(ref t) = input.title {
2100 chart.set_title(t);
2101 }
2102 if let Some(ref n) = input.series_name {
2103 chart.set_series_name(n);
2104 }
2105 chart.set_width(input.width);
2106 chart.set_height(input.height);
2107 if let Some(bc) = input.bin_count {
2108 chart.set_bin_count(bc);
2109 }
2110 if let Some(bw) = input.bin_width {
2111 chart.set_bin_width(bw);
2112 }
2113 let values: Vec<f64> = input.points.iter().map(|p| p.value).collect();
2114 chart.set_values(&values);
2115 let (row, col) = if let Some(ref c) = input.cell {
2116 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2117 } else {
2118 (0, 0)
2119 };
2120 entry
2121 .data
2122 .worksheet(idx)
2123 .map_err(|e| anyhow::anyhow!("{e}"))?
2124 .insert_histogram(row, col, &chart)?;
2125 Ok(success_no_data(&format!(
2126 "Histogram chart added to '{}'",
2127 input.sheet_name
2128 )))
2129}
2130
2131pub fn add_box_whisker_chart(
2132 store: &mut WorkbookStore,
2133 input: AddBoxWhiskerChartInput,
2134) -> Result<String, anyhow::Error> {
2135 let entry = match store.get_mut(&input.workbook_id) {
2136 Some(e) => e,
2137 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2138 };
2139 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2140 Some(i) => i,
2141 None => return Ok(sheet_err(&input.sheet_name)),
2142 };
2143 let mut chart = zavora_xlsx::BoxWhiskerChart::new();
2144 if let Some(ref t) = input.title {
2145 chart.set_title(t);
2146 }
2147 if let Some(ref n) = input.series_name {
2148 chart.set_series_name(n);
2149 }
2150 chart.set_width(input.width);
2151 chart.set_height(input.height);
2152 if let Some(v) = input.show_outliers {
2153 chart.set_show_outliers(v);
2154 }
2155 if let Some(v) = input.show_mean {
2156 chart.set_show_mean_markers(v);
2157 }
2158 if let Some(v) = input.show_inner_points {
2159 chart.set_show_inner_points(v);
2160 }
2161 for pt in &input.points {
2163 chart.add_data_set(&pt.category, &[pt.value]);
2164 }
2165 let (row, col) = if let Some(ref c) = input.cell {
2166 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2167 } else {
2168 (0, 0)
2169 };
2170 entry
2171 .data
2172 .worksheet(idx)
2173 .map_err(|e| anyhow::anyhow!("{e}"))?
2174 .insert_box_whisker(row, col, &chart)?;
2175 Ok(success_no_data(&format!(
2176 "Box & whisker chart added to '{}'",
2177 input.sheet_name
2178 )))
2179}
2180
2181pub fn add_map_chart(
2182 store: &mut WorkbookStore,
2183 input: AddMapChartInput,
2184) -> Result<String, anyhow::Error> {
2185 let entry = match store.get_mut(&input.workbook_id) {
2186 Some(e) => e,
2187 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2188 };
2189 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2190 Some(i) => i,
2191 None => return Ok(sheet_err(&input.sheet_name)),
2192 };
2193 let mut chart = zavora_xlsx::MapChart::new();
2194 if let Some(ref t) = input.title {
2195 chart.set_title(t);
2196 }
2197 if let Some(ref n) = input.series_name {
2198 chart.set_series_name(n);
2199 }
2200 chart.set_width(input.width);
2201 chart.set_height(input.height);
2202 if let Some(ref ml) = input.map_level {
2203 let level = match ml.as_str() {
2204 "region" => zavora_xlsx::MapLevel::Region,
2205 _ => zavora_xlsx::MapLevel::Country,
2206 };
2207 chart.set_map_level(level);
2208 }
2209 for pt in &input.points {
2210 chart.add_point(&pt.category, pt.value);
2211 }
2212 let (row, col) = if let Some(ref c) = input.cell {
2213 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2214 } else {
2215 (0, 0)
2216 };
2217 entry
2218 .data
2219 .worksheet(idx)
2220 .map_err(|e| anyhow::anyhow!("{e}"))?
2221 .insert_map(row, col, &chart)?;
2222 Ok(success_no_data(&format!(
2223 "Map chart added to '{}'",
2224 input.sheet_name
2225 )))
2226}
2227
2228pub fn add_slicer(
2229 store: &mut WorkbookStore,
2230 input: AddSlicerInput,
2231) -> Result<String, anyhow::Error> {
2232 let entry = match store.get_mut(&input.workbook_id) {
2233 Some(e) => e,
2234 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2235 };
2236 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2237 Some(i) => i,
2238 None => return Ok(sheet_err(&input.sheet_name)),
2239 };
2240 let mut slicer = zavora_xlsx::Slicer::new(&input.pivot_table_name, &input.field_name);
2242 if let Some(w) = input.width {
2243 slicer = slicer.set_width(w);
2244 }
2245 if let Some(h) = input.height {
2246 slicer = slicer.set_height(h);
2247 }
2248 let (row, col) = if let Some(ref c) = input.cell {
2249 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2250 } else {
2251 (0, 0)
2252 };
2253 entry
2254 .data
2255 .worksheet(idx)
2256 .map_err(|e| anyhow::anyhow!("{e}"))?
2257 .add_slicer(row, col, &slicer)?;
2258 Ok(success_no_data(&format!(
2259 "Slicer for '{}' added to '{}'",
2260 input.field_name, input.sheet_name
2261 )))
2262}
2263
2264pub fn add_timeline(
2265 store: &mut WorkbookStore,
2266 input: AddTimelineInput,
2267) -> Result<String, anyhow::Error> {
2268 let entry = match store.get_mut(&input.workbook_id) {
2269 Some(e) => e,
2270 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2271 };
2272 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2273 Some(i) => i,
2274 None => return Ok(sheet_err(&input.sheet_name)),
2275 };
2276 let timeline = zavora_xlsx::Timeline::new(&input.pivot_table_name, &input.field_name);
2277 let (row, col) = if let Some(ref c) = input.cell {
2278 zavora_xlsx::utility::parse_cell_ref(c).map_err(|e| anyhow::anyhow!("{e}"))?
2279 } else {
2280 (0, 0)
2281 };
2282 entry
2283 .data
2284 .worksheet(idx)
2285 .map_err(|e| anyhow::anyhow!("{e}"))?
2286 .add_timeline(row, col, &timeline)?;
2287 Ok(success_no_data(&format!(
2288 "Timeline for '{}' added to '{}'",
2289 input.field_name, input.sheet_name
2290 )))
2291}
2292
2293pub fn add_form_control(
2294 store: &mut WorkbookStore,
2295 input: AddFormControlInput,
2296) -> Result<String, anyhow::Error> {
2297 let entry = match store.get_mut(&input.workbook_id) {
2298 Some(e) => e,
2299 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2300 };
2301 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2302 Some(i) => i,
2303 None => return Ok(sheet_err(&input.sheet_name)),
2304 };
2305 let (row, col) =
2306 zavora_xlsx::utility::parse_cell_ref(&input.cell).map_err(|e| anyhow::anyhow!("{e}"))?;
2307 let text = input.text.as_deref().unwrap_or("Control");
2308 let fc = match input.control_type.as_str() {
2309 "checkbox" => {
2310 if let Some(ref cl) = input.cell_link {
2311 zavora_xlsx::FormControl::checkbox_with_link(text, cl)
2312 } else {
2313 zavora_xlsx::FormControl::checkbox(text)
2314 }
2315 }
2316 "dropdown" => {
2317 let items = input
2318 .input_range
2319 .as_deref()
2320 .unwrap_or("")
2321 .split(',')
2322 .map(|s| s.trim().to_string())
2323 .collect();
2324 zavora_xlsx::FormControl::dropdown(items)
2325 }
2326 "spinner" => zavora_xlsx::FormControl::spinner(0, 100, 0),
2327 _ => zavora_xlsx::FormControl::button(text),
2328 };
2329 entry
2330 .data
2331 .worksheet(idx)
2332 .map_err(|e| anyhow::anyhow!("{e}"))?
2333 .add_form_control(row, col, fc);
2334 Ok(success_no_data(&format!(
2335 "Form control '{}' added at {}",
2336 input.control_type, input.cell
2337 )))
2338}
2339
2340pub fn save_workbook_advanced(
2341 store: &mut WorkbookStore,
2342 input: SaveWorkbookAdvancedInput,
2343) -> Result<String, anyhow::Error> {
2344 let entry = match store.get_mut(&input.workbook_id) {
2345 Some(e) => e,
2346 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2347 };
2348 if entry.read_only {
2349 return Ok(error(
2350 ErrorCategory::EngineUnsupported,
2351 "Read-only workbooks cannot be saved",
2352 "Reopen in edit mode.",
2353 ));
2354 }
2355 let _ = entry.data.recalculate();
2356 let path = std::path::Path::new(&input.file_path);
2357 match input.format.as_str() {
2358 "template" => entry
2359 .data
2360 .save_as_template(path)
2361 .map_err(|e| anyhow::anyhow!("{e}"))?,
2362 "encrypted" => {
2363 let pw = input.password.as_deref().unwrap_or("");
2364 entry
2365 .data
2366 .save_encrypted(path, pw)
2367 .map_err(|e| anyhow::anyhow!("{e}"))?;
2368 }
2369 "parallel" => entry
2370 .data
2371 .save_parallel(path)
2372 .map_err(|e| anyhow::anyhow!("{e}"))?,
2373 _ => entry
2374 .data
2375 .save(path)
2376 .map_err(|e| anyhow::anyhow!("{e}"))?,
2377 }
2378 Ok(success_no_data(&format!(
2379 "Workbook saved as {} to {}",
2380 input.format, input.file_path
2381 )))
2382}
2383
2384pub fn open_workbook_encrypted(
2385 store: &mut WorkbookStore,
2386 input: OpenWorkbookEncryptedInput,
2387) -> Result<String, anyhow::Error> {
2388 use crate::store::WorkbookEntry;
2389 use std::time::Instant;
2390
2391 if store.is_full() {
2392 return Ok(error(
2393 ErrorCategory::CapacityExceeded,
2394 "Workbook store is at maximum capacity",
2395 "Save and close an existing workbook first.",
2396 ));
2397 }
2398 let path = std::path::Path::new(&input.file_path);
2399 if !path.exists() {
2400 return Ok(error(
2401 ErrorCategory::NotFound,
2402 &format!("File not found: {}", input.file_path),
2403 "Check the file path.",
2404 ));
2405 }
2406 let wb = zavora_xlsx::Workbook::open_with_password(path, &input.password)
2407 .map_err(|e| anyhow::anyhow!("{e}"))?;
2408 let sheets = crate::engines::zavora::sheet_summaries(&wb);
2409 let entry = WorkbookEntry {
2410 id: String::new(),
2411 data: wb,
2412 read_only: false,
2413 last_access: Instant::now(),
2414 };
2415 let id = store.insert(entry).map_err(|e| anyhow::anyhow!("{}", e))?;
2416 Ok(success(
2417 "Encrypted workbook opened",
2418 crate::types::responses::WorkbookInfo {
2419 workbook_id: id,
2420 engine: "zavora-xlsx".to_string(),
2421 sheets,
2422 },
2423 ))
2424}
2425
2426pub fn manage_named_ranges(
2427 store: &mut WorkbookStore,
2428 input: ManageNamedRangesInput,
2429) -> Result<String, anyhow::Error> {
2430 let entry = match store.get_mut(&input.workbook_id) {
2431 Some(e) => e,
2432 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2433 };
2434 match input.action.as_str() {
2435 "add" => {
2436 let name = input.name.as_deref().unwrap_or("");
2437 let formula = input.formula.as_deref().unwrap_or("");
2438 entry.data.define_name(name, formula);
2439 Ok(success_no_data(&format!("Named range '{}' added", name)))
2440 }
2441 "add_scoped" => {
2442 let name = input.name.as_deref().unwrap_or("");
2443 let formula = input.formula.as_deref().unwrap_or("");
2444 let sheet_idx = input.sheet_index.unwrap_or(0);
2445 entry.data.define_name_scoped(name, formula, sheet_idx);
2446 Ok(success_no_data(&format!(
2447 "Scoped named range '{}' added for sheet {}",
2448 name, sheet_idx
2449 )))
2450 }
2451 "update" => {
2452 let name = input.name.as_deref().unwrap_or("");
2453 let formula = input.formula.as_deref().unwrap_or("");
2454 let _ = entry.data.update_named_range(name, formula);
2455 Ok(success_no_data(&format!("Named range '{}' updated", name)))
2456 }
2457 "remove" => {
2458 let name = input.name.as_deref().unwrap_or("");
2459 let scope = if let Some(idx) = input.sheet_index {
2460 zavora_xlsx::DefinedNameScope::Sheet(idx)
2461 } else {
2462 zavora_xlsx::DefinedNameScope::Workbook
2463 };
2464 let _ = entry.data.remove_named_range(name, &scope);
2465 Ok(success_no_data(&format!("Named range '{}' removed", name)))
2466 }
2467 _ => {
2468 let names: Vec<serde_json::Value> = entry
2470 .data
2471 .defined_names_with_scope()
2472 .iter()
2473 .map(|dn| {
2474 serde_json::json!({
2475 "name": dn.name,
2476 "formula": dn.formula,
2477 "scope": format!("{:?}", dn.scope),
2478 })
2479 })
2480 .collect();
2481 Ok(success("Named ranges listed", names))
2482 }
2483 }
2484}
2485
2486pub fn read_sheet_metadata(
2487 store: &mut WorkbookStore,
2488 input: ReadSheetMetadataInput,
2489) -> Result<String, anyhow::Error> {
2490 let entry = match store.get_mut(&input.workbook_id) {
2491 Some(e) => e,
2492 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2493 };
2494 let idx = match find_sheet(&entry.data, &input.sheet_name) {
2495 Some(i) => i,
2496 None => return Ok(sheet_err(&input.sheet_name)),
2497 };
2498 let ws = entry
2499 .data
2500 .worksheet(idx)
2501 .map_err(|e| anyhow::anyhow!("{e}"))?;
2502
2503 let mut result = serde_json::Map::new();
2504
2505 if input.info == "used_range" || input.info == "all" {
2506 if let Some((r1, c1, r2, c2)) = ws.used_range() {
2507 let range_str = format!(
2508 "{}:{}",
2509 zavora_xlsx::utility::to_a1(r1, c1),
2510 zavora_xlsx::utility::to_a1(r2, c2)
2511 );
2512 result.insert("used_range".into(), serde_json::json!(range_str));
2513 } else {
2514 result.insert("used_range".into(), serde_json::Value::Null);
2515 }
2516 }
2517
2518 if input.info == "hyperlinks" || input.info == "all" {
2519 let links: Vec<serde_json::Value> = ws
2520 .hyperlinks()
2521 .iter()
2522 .map(|h| {
2523 serde_json::json!({
2524 "cell": zavora_xlsx::utility::to_a1(h.row, h.col),
2525 "url": h.url,
2526 "location": h.location,
2527 "tooltip": h.tooltip,
2528 })
2529 })
2530 .collect();
2531 result.insert("hyperlinks".into(), serde_json::json!(links));
2532 }
2533
2534 if input.info == "merge_ranges" || input.info == "all" {
2535 let merges: Vec<String> = ws
2536 .merge_ranges()
2537 .iter()
2538 .map(|(r1, c1, r2, c2)| {
2539 format!(
2540 "{}:{}",
2541 zavora_xlsx::utility::to_a1(*r1, *c1),
2542 zavora_xlsx::utility::to_a1(*r2, *c2)
2543 )
2544 })
2545 .collect();
2546 result.insert("merge_ranges".into(), serde_json::json!(merges));
2547 }
2548
2549 if input.info == "charts" || input.info == "all" {
2550 let charts: Vec<serde_json::Value> = ws
2551 .charts()
2552 .iter()
2553 .map(|c| {
2554 serde_json::json!({
2555 "title": c.title(),
2556 "type": format!("{:?}", c.chart_type()),
2557 "series_count": c.series().len(),
2558 })
2559 })
2560 .collect();
2561 result.insert("charts".into(), serde_json::json!(charts));
2562 }
2563
2564 Ok(success(
2565 "Sheet metadata read",
2566 serde_json::Value::Object(result),
2567 ))
2568}
2569
2570pub fn add_chart_sheet(
2571 store: &mut WorkbookStore,
2572 input: AddChartSheetInput,
2573) -> Result<String, anyhow::Error> {
2574 let entry = match store.get_mut(&input.workbook_id) {
2575 Some(e) => e,
2576 None => return Ok(workbook_not_found(store, &input.workbook_id)),
2577 };
2578 let ct = match input.chart_type {
2579 ChartType::Bar => zavora_xlsx::ChartType::Bar,
2580 ChartType::Column => zavora_xlsx::ChartType::Column,
2581 ChartType::Line => zavora_xlsx::ChartType::Line,
2582 ChartType::Pie => zavora_xlsx::ChartType::Pie,
2583 ChartType::Scatter => zavora_xlsx::ChartType::Scatter,
2584 ChartType::Area => zavora_xlsx::ChartType::Area,
2585 ChartType::Doughnut => zavora_xlsx::ChartType::Doughnut,
2586 };
2587 let mut chart = zavora_xlsx::Chart::new(ct);
2588 if let Some(ref t) = input.title {
2589 chart.set_title(t);
2590 }
2591 if let Some(ref x) = input.x_axis_label {
2592 chart.set_x_axis_name(x);
2593 }
2594 if let Some(ref y) = input.y_axis_label {
2595 chart.set_y_axis_name(y);
2596 }
2597 if let Some(ref lp) = input.legend_position {
2598 chart.set_legend_position(match lp {
2599 LegendPosition::Top => zavora_xlsx::LegendPosition::Top,
2600 LegendPosition::Bottom => zavora_xlsx::LegendPosition::Bottom,
2601 LegendPosition::Left => zavora_xlsx::LegendPosition::Left,
2602 LegendPosition::Right => zavora_xlsx::LegendPosition::Right,
2603 LegendPosition::None => zavora_xlsx::LegendPosition::None,
2604 });
2605 }
2606 if !input.series.is_empty() {
2607 for si in &input.series {
2608 let s = chart.add_series();
2609 s.set_values(&si.values);
2610 if let Some(ref c) = si.categories {
2611 s.set_categories(c);
2612 }
2613 if let Some(ref n) = si.name {
2614 s.set_name(n);
2615 }
2616 }
2617 } else if let Some(ref dr) = input.data_range {
2618 chart.add_series().set_values(dr);
2619 }
2620 entry
2621 .data
2622 .add_chart_sheet(&input.sheet_name, chart)
2623 .map_err(|e| anyhow::anyhow!("{e}"))?;
2624 Ok(success_no_data(&format!(
2625 "Chart sheet '{}' added",
2626 input.sheet_name
2627 )))
2628}