Skip to main content

excel_mcp_server/tools/
expanded.rs

1//! New tools: page setup, comments, hyperlinks, defined names, sheet settings,
2//! active sheet, insert/delete rows/cols, grouping, protection, autofit,
3//! enhanced charts, pivot tables, read comments, rich text.
4
5use 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
22// ── Batch 1 ──
23
24pub 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
231// ── Batch 2 ──
232
233pub 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
451// ── Batch 3 ──
452
453pub 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    // Add series
499    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
617// ── Batch 4 ──
618
619pub 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
684// ══════════════════════════════════════════════════════════════════
685// Batch 5–8: Remaining 22 tools
686// ══════════════════════════════════════════════════════════════════
687
688fn 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
1249// ══════════════════════════════════════════════════════════════════
1250// Consolidated tools (replacing multiple separate tools)
1251// ══════════════════════════════════════════════════════════════════
1252
1253pub 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            // "sheet"
1592            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            // "column_width"
1646            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
1798// ══════════════════════════════════════════════════════════════════
1799// Batch 9: Feature-parity tools (waterfall, funnel, treemap, shape, doc properties)
1800// ══════════════════════════════════════════════════════════════════
1801
1802/// Parse a hex color string like "#FF0000" into [u8; 3].
1803fn 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
2034// ══════════════════════════════════════════════════════════════════
2035// v0.2.0: New tools — 4 chart types, slicers, timelines, form controls,
2036// advanced save, named ranges CRUD, sheet metadata, chart sheet,
2037// chart enhancements, protection options
2038// ══════════════════════════════════════════════════════════════════
2039
2040pub 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    // SunburstChart uses add_level for hierarchy labels and set_values for leaf sizes
2062    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    // BoxWhiskerChart uses add_data_set(category, &[f64]) for each box
2162    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    // Slicer uses builder pattern (takes ownership)
2241    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            // "list"
2469            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}