diff --git a/src/docs/handlers.rs b/src/docs/handlers.rs index b39655721..6e14638a3 100644 --- a/src/docs/handlers.rs +++ b/src/docs/handlers.rs @@ -1,21 +1,21 @@ use crate::docs::storage::{ create_new_document, delete_document_from_drive, get_current_user_id, - list_documents_from_drive, load_document_from_drive, save_document_to_drive, + list_documents_from_drive, load_document_from_drive, save_document, save_document_to_drive, }; use crate::docs::types::{ DocsSaveRequest, DocsSaveResponse, DocsAiRequest, DocsAiResponse, Document, DocumentMetadata, SearchQuery, TemplateResponse, }; -use crate::docs::utils::{convert_to_html, detect_document_format, html_to_markdown, markdown_to_html, rtf_to_html, strip_html}; +use crate::docs::utils::{detect_document_format, html_to_markdown, markdown_to_html, rtf_to_html, strip_html}; use crate::docs::types::{ AcceptRejectAllRequest, AcceptRejectChangeRequest, AddCommentRequest, AddEndnoteRequest, AddFootnoteRequest, ApplyStyleRequest, CompareDocumentsRequest, CompareDocumentsResponse, CommentReply, ComparisonSummary, CreateStyleRequest, DeleteCommentRequest, DeleteEndnoteRequest, DeleteFootnoteRequest, DeleteStyleRequest, DocumentComment, DocumentComparison, DocumentDiff, - DocumentStyle, EnableTrackChangesRequest, Endnote, Footnote, GenerateTocRequest, + EnableTrackChangesRequest, Endnote, Footnote, GenerateTocRequest, GetOutlineRequest, ListCommentsResponse, ListEndnotesResponse, ListFootnotesResponse, ListStylesResponse, ListTrackChangesResponse, OutlineItem, OutlineResponse, ReplyCommentRequest, - ResolveCommentRequest, TableOfContents, TocEntry, TocResponse, TrackChange, UpdateEndnoteRequest, + ResolveCommentRequest, TableOfContents, TocEntry, TocResponse, UpdateEndnoteRequest, UpdateFootnoteRequest, UpdateStyleRequest, UpdateTocRequest, }; use crate::shared::state::AppState; @@ -25,6 +25,7 @@ use axum::{ response::IntoResponse, Json, }; +use chrono::Utc; use docx_rs::{AlignmentType, Docx, Paragraph, Run}; use log::error; use std::sync::Arc; @@ -601,7 +602,7 @@ pub async fn handle_add_comment( comments.push(comment.clone()); doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -650,7 +651,7 @@ pub async fn handle_reply_comment( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -692,7 +693,7 @@ pub async fn handle_resolve_comment( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -728,7 +729,7 @@ pub async fn handle_delete_comment( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -788,7 +789,7 @@ pub async fn handle_enable_track_changes( doc.track_changes_enabled = req.enabled; doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -829,7 +830,7 @@ pub async fn handle_accept_reject_change( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -867,7 +868,7 @@ pub async fn handle_accept_reject_all( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -929,7 +930,7 @@ pub async fn handle_generate_toc( let mut entries = Vec::new(); let content = &doc.content; - let mut position = 0; + for level in 1..=req.max_level { let tag = format!(""); @@ -955,7 +956,7 @@ pub async fn handle_generate_toc( break; } } - position = search_pos; + } entries.sort_by_key(|e| e.position); @@ -972,7 +973,7 @@ pub async fn handle_generate_toc( doc.toc = Some(toc.clone()); doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1056,7 +1057,7 @@ pub async fn handle_add_footnote( footnotes.push(footnote.clone()); doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1097,7 +1098,7 @@ pub async fn handle_update_footnote( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1136,7 +1137,7 @@ pub async fn handle_delete_footnote( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1206,7 +1207,7 @@ pub async fn handle_add_endnote( endnotes.push(endnote.clone()); doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1264,7 +1265,7 @@ pub async fn handle_update_endnote( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1303,7 +1304,7 @@ pub async fn handle_delete_endnote( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1364,7 +1365,7 @@ pub async fn handle_create_style( styles.push(req.style.clone()); doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1405,7 +1406,7 @@ pub async fn handle_update_style( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1441,7 +1442,7 @@ pub async fn handle_delete_style( } doc.updated_at = Utc::now(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), @@ -1620,11 +1621,12 @@ pub async fn handle_import_document( .to_string(); let user_id = get_current_user_id(); - let mut doc = create_new_document(&title); + let mut doc = create_new_document(); + doc.title = title; doc.content = content; doc.owner_id = user_id.clone(); - if let Err(e) = save_document_to_drive(&state, &user_id, &doc).await { + if let Err(e) = save_document(&state, &user_id, &doc).await { return Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e })), diff --git a/src/docs/storage.rs b/src/docs/storage.rs index e2efa285d..eec66b548 100644 --- a/src/docs/storage.rs +++ b/src/docs/storage.rs @@ -107,6 +107,13 @@ pub async fn load_docx_from_bytes( updated_at: Utc::now(), collaborators: Vec::new(), version: 1, + track_changes: None, + comments: None, + footnotes: None, + endnotes: None, + styles: None, + toc: None, + track_changes_enabled: false, }) } @@ -188,29 +195,27 @@ pub fn convert_docx_to_html(bytes: &[u8]) -> Result { docx_rs::DocumentChild::Table(table) => { html.push_str(""); for row in &table.rows { - if let docx_rs::TableChild::TableRow(tr) = row { - html.push_str(""); - for cell in &tr.cells { - if let docx_rs::TableRowChild::TableCell(tc) = cell { - html.push_str(""); + for cell in &tr.cells { + let docx_rs::TableRowChild::TableCell(tc) = cell; + html.push_str(""); } } - html.push_str(""); + html.push_str(""); } + html.push_str(""); } html.push_str("
"); - for para in &tc.children { - if let docx_rs::TableCellContent::Paragraph(p) = para { - for content in &p.children { - if let docx_rs::ParagraphChild::Run(run) = content { - for child in &run.children { - if let docx_rs::RunChild::Text(text) = child { - html.push_str(&escape_html(&text.text)); - } - } + let docx_rs::TableChild::TableRow(tr) = row; + html.push_str("
"); + for para in &tc.children { + if let docx_rs::TableCellContent::Paragraph(p) = para { + for content in &p.children { + if let docx_rs::ParagraphChild::Run(run) = content { + for child in &run.children { + if let docx_rs::RunChild::Text(text) = child { + html.push_str(&escape_html(&text.text)); } } } } - html.push_str("
"); } @@ -374,6 +379,14 @@ pub async fn save_document_to_drive( Ok(doc_path) } +pub async fn save_document( + state: &Arc, + user_identifier: &str, + doc: &Document, +) -> Result { + save_document_to_drive(state, user_identifier, &doc.id, &doc.title, &doc.content).await +} + pub async fn load_document_from_drive( state: &Arc, user_identifier: &str, @@ -447,6 +460,13 @@ pub async fn load_document_from_drive( updated_at, collaborators: Vec::new(), version: 1, + track_changes: None, + comments: None, + footnotes: None, + endnotes: None, + styles: None, + toc: None, + track_changes_enabled: false, })) } @@ -553,6 +573,13 @@ pub fn create_new_document() -> Document { updated_at: Utc::now(), collaborators: Vec::new(), version: 1, + track_changes: None, + comments: None, + footnotes: None, + endnotes: None, + styles: None, + toc: None, + track_changes_enabled: false, } } diff --git a/src/docs/utils.rs b/src/docs/utils.rs index d2316a937..528cbb17c 100644 --- a/src/docs/utils.rs +++ b/src/docs/utils.rs @@ -1,5 +1,4 @@ use chrono::{DateTime, Duration, Utc}; -use std::collections::HashMap; pub fn format_document_list_item( id: &str, @@ -379,8 +378,6 @@ pub fn html_to_rtf(html: &str) -> String { rtf.push_str("{\\fonttbl{\\f0 Arial;}}\n"); rtf.push_str("\\f0\\fs24\n"); - let plain = strip_html(html); - let mut result = html.to_string(); result = result.replace("", "\\b "); result = result.replace("", "\\b0 "); diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 50aa2b6bc..7acb72cb2 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -220,7 +220,6 @@ pub fn configure() -> Router> { .route("/api/docs/convert", post(document_processing::convert_document)) .route("/api/docs/fill", post(document_processing::fill_document)) .route("/api/docs/export", post(document_processing::export_document)) - .route("/api/docs/import", post(document_processing::import_document)) } pub async fn open_file( diff --git a/src/security/protection/lmd.rs b/src/security/protection/lmd.rs index 345404a91..9475bb76a 100644 --- a/src/security/protection/lmd.rs +++ b/src/security/protection/lmd.rs @@ -4,7 +4,6 @@ use tracing::info; use crate::security::command_guard::SafeCommand; use super::manager::{Finding, FindingSeverity, ScanResultStatus}; -const LMD_LOG_DIR: &str = "/usr/local/maldetect/logs"; const LMD_QUARANTINE_DIR: &str = "/usr/local/maldetect/quarantine"; pub async fn run_scan(path: Option<&str>) -> Result<(ScanResultStatus, Vec, String)> { diff --git a/src/security/protection/lynis.rs b/src/security/protection/lynis.rs index 52e4e391f..d9d488c8f 100644 --- a/src/security/protection/lynis.rs +++ b/src/security/protection/lynis.rs @@ -5,7 +5,6 @@ use crate::security::command_guard::SafeCommand; use super::manager::{Finding, FindingSeverity, ScanResultStatus}; const LYNIS_REPORT_PATH: &str = "/var/log/lynis-report.dat"; -const LYNIS_LOG_PATH: &str = "/var/log/lynis.log"; pub async fn run_scan() -> Result<(ScanResultStatus, Vec, String)> { info!("Running Lynis security audit"); diff --git a/src/security/protection/suricata.rs b/src/security/protection/suricata.rs index e42524dae..a32abf7c4 100644 --- a/src/security/protection/suricata.rs +++ b/src/security/protection/suricata.rs @@ -7,7 +7,6 @@ use crate::security::command_guard::SafeCommand; use super::manager::{Finding, FindingSeverity, ScanResultStatus}; const SURICATA_EVE_LOG: &str = "/var/log/suricata/eve.json"; -const SURICATA_FAST_LOG: &str = "/var/log/suricata/fast.log"; const SURICATA_RULES_DIR: &str = "/var/lib/suricata/rules"; pub async fn get_alerts() -> Result<(ScanResultStatus, Vec, String)> { diff --git a/src/sheet/export.rs b/src/sheet/export.rs index c38d47d3e..ec135448e 100644 --- a/src/sheet/export.rs +++ b/src/sheet/export.rs @@ -1,7 +1,6 @@ use base64::Engine; use crate::sheet::types::{CellStyle, Spreadsheet}; use rust_xlsxwriter::{Color, Format, FormatAlign, Workbook}; -use std::io::Cursor; pub fn export_to_xlsx(sheet: &Spreadsheet) -> Result { let mut workbook = Workbook::new(); @@ -186,7 +185,7 @@ pub fn export_to_html(sheet: &Spreadsheet) -> String { "#); - for (ws_idx, ws) in sheet.worksheets.iter().enumerate() { + for ws in &sheet.worksheets { html.push_str(&format!("

{}

\n", ws.name)); html.push_str("\n"); diff --git a/src/sheet/handlers.rs b/src/sheet/handlers.rs deleted file mode 100644 index c7473a8c5..000000000 --- a/src/sheet/handlers.rs +++ /dev/null @@ -1,2057 +0,0 @@ -use crate::shared::state::AppState; -use crate::sheet::collaboration::broadcast_sheet_change; -use crate::sheet::export::{export_to_csv, export_to_html, export_to_json, export_to_markdown, export_to_ods, export_to_xlsx}; -use crate::sheet::formulas::evaluate_formula; -use crate::sheet::storage::{ - create_new_spreadsheet, delete_sheet_from_drive, get_current_user_id, import_spreadsheet_bytes, - list_sheets_from_drive, load_sheet_by_id, load_sheet_from_drive, parse_csv_to_worksheets, - parse_excel_to_worksheets, save_sheet_to_drive, -}; -use crate::sheet::types::{ - AddCommentRequest, AddExternalLinkRequest, AddNoteRequest, ArrayFormula, ArrayFormulaRequest, - CellComment, CellData, CellUpdateRequest, ChartConfig, ChartOptions, ChartPosition, - ChartRequest, ClearFilterRequest, CommentReply, CommentWithLocation, ConditionalFormatRequest, - ConditionalFormatRule, CreateNamedRangeRequest, DataValidationRequest, DeleteArrayFormulaRequest, - DeleteChartRequest, DeleteCommentRequest, DeleteNamedRangeRequest, ExportRequest, ExternalLink, - FilterConfig, FilterRequest, FormatRequest, FormulaRequest, FormulaResult, FreezePanesRequest, - ListCommentsRequest, ListCommentsResponse, ListExternalLinksResponse, ListNamedRangesRequest, - ListNamedRangesResponse, LoadFromDriveRequest, LoadQuery, LockCellsRequest, MergeCellsRequest, - MergedCell, NamedRange, ProtectSheetRequest, RefreshExternalLinkRequest, RemoveExternalLinkRequest, - ReplyCommentRequest, ResolveCommentRequest, SaveRequest, SaveResponse, SearchQuery, ShareRequest, - SheetAiRequest, SheetAiResponse, SheetProtection, SortRequest, Spreadsheet, SpreadsheetMetadata, - UnprotectSheetRequest, UpdateNamedRangeRequest, ValidateCellRequest, ValidationResult, - ValidationRule, Worksheet, -}; -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, - Json, -}; -use chrono::Utc; -use log::error; -use std::collections::HashMap; -use std::sync::Arc; -use uuid::Uuid; - -pub async fn handle_sheet_ai( - State(_state): State>, - Json(req): Json, -) -> impl IntoResponse { - let command = req.command.to_lowercase(); - - let response = if command.contains("sum") { - "I can help you sum values. Select a range and use the SUM formula, or I've added a SUM formula below your selection." - } else if command.contains("average") || command.contains("avg") { - "I can calculate averages. Select a range and use the AVERAGE formula." - } else if command.contains("chart") { - "To create a chart, select your data range first, then choose the chart type from the Chart menu." - } else if command.contains("sort") { - "I can sort your data. Select the range you want to sort, then specify ascending or descending order." - } else if command.contains("format") || command.contains("currency") || command.contains("percent") { - "I've applied the formatting to your selected cells." - } else if command.contains("bold") || command.contains("italic") { - "I've applied the text formatting to your selected cells." - } else if command.contains("filter") { - "I've enabled filtering on your data. Use the dropdown arrows in the header row to filter." - } else if command.contains("freeze") { - "I've frozen the specified rows/columns so they stay visible when scrolling." - } else if command.contains("merge") { - "I've merged the selected cells into one." - } else if command.contains("clear") { - "I've cleared the content from the selected cells." - } else if command.contains("help") { - "I can help you with:\n• Sum/Average columns\n• Format as currency or percent\n• Bold/Italic formatting\n• Sort data\n• Create charts\n• Filter data\n• Freeze panes\n• Merge cells" - } else { - "I understand you want help with your spreadsheet. Try commands like 'sum column B', 'format as currency', 'sort ascending', or 'create a chart'." - }; - - Json(SheetAiResponse { - response: response.to_string(), - action: None, - data: None, - }) -} - -pub async fn handle_new_sheet( - State(_state): State>, -) -> Result, (StatusCode, Json)> { - Ok(Json(create_new_spreadsheet())) -} - -pub async fn handle_list_sheets( - State(state): State>, -) -> Result>, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - match list_sheets_from_drive(&state, &user_id).await { - Ok(sheets) => Ok(Json(sheets)), - Err(e) => { - error!("Failed to list sheets: {}", e); - Ok(Json(Vec::new())) - } - } -} - -pub async fn handle_search_sheets( - State(state): State>, - Query(query): Query, -) -> Result>, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - let sheets = match list_sheets_from_drive(&state, &user_id).await { - Ok(s) => s, - Err(_) => Vec::new(), - }; - - let filtered = if let Some(q) = query.q { - let q_lower = q.to_lowercase(); - sheets - .into_iter() - .filter(|s| s.name.to_lowercase().contains(&q_lower)) - .collect() - } else { - sheets - }; - - Ok(Json(filtered)) -} - -pub async fn handle_load_sheet( - State(state): State>, - Query(query): Query, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - match load_sheet_from_drive(&state, &user_id, &query.id).await { - Ok(sheet) => Ok(Json(sheet)), - Err(e) => Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )), - } -} - -pub async fn handle_load_from_drive( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let drive = state.drive.as_ref().ok_or_else(|| { - ( - StatusCode::SERVICE_UNAVAILABLE, - Json(serde_json::json!({ "error": "Drive not available" })), - ) - })?; - - let result = drive - .get_object() - .bucket(&req.bucket) - .key(&req.path) - .send() - .await - .map_err(|e| { - ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": format!("File not found: {e}") })), - ) - })?; - - let bytes = result - .body - .collect() - .await - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": format!("Failed to read file: {e}") })), - ) - })? - .into_bytes(); - - let ext = req.path.rsplit('.').next().unwrap_or("").to_lowercase(); - let file_name = req.path.rsplit('/').next().unwrap_or("Spreadsheet"); - let sheet_name = file_name - .rsplit('.') - .last() - .unwrap_or("Spreadsheet") - .to_string(); - - let worksheets = match ext.as_str() { - "csv" | "tsv" => { - let delimiter = if ext == "tsv" { b'\t' } else { b',' }; - parse_csv_to_worksheets(&bytes, delimiter, &sheet_name).map_err(|e| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": e })), - ) - })? - } - "xlsx" | "xls" | "ods" | "xlsb" | "xlsm" => { - parse_excel_to_worksheets(&bytes, &ext).map_err(|e| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": e })), - ) - })? - } - _ => { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": format!("Unsupported format: .{ext}") })), - )); - } - }; - - let user_id = get_current_user_id(); - let sheet = Spreadsheet { - id: Uuid::new_v4().to_string(), - name: sheet_name, - owner_id: user_id, - worksheets, - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - Ok(Json(sheet)) -} - -pub async fn handle_save_sheet( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - let sheet_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string()); - - let sheet = Spreadsheet { - id: sheet_id.clone(), - name: req.name, - owner_id: user_id.clone(), - worksheets: req.worksheets, - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: sheet_id, - success: true, - message: Some("Sheet saved successfully".to_string()), - })) -} - -pub async fn handle_delete_sheet( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - if let Err(e) = delete_sheet_from_drive(&state, &user_id, &req.id).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.id.unwrap_or_default(), - success: true, - message: Some("Sheet deleted".to_string()), - })) -} - -pub async fn handle_update_cell( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - let (value, formula) = if req.value.starts_with('=') { - let result = evaluate_formula(&req.value, worksheet); - (Some(result.value), Some(req.value.clone())) - } else { - (Some(req.value.clone()), None) - }; - - let cell = worksheet.data.entry(key).or_insert_with(|| CellData { - value: None, - formula: None, - style: None, - format: None, - note: None, - }); - - cell.value = value; - cell.formula = formula; - - sheet.updated_at = Utc::now(); - - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - broadcast_sheet_change( - &req.sheet_id, - &user_id, - "User", - req.row, - req.col, - &req.value, - req.worksheet_index, - ) - .await; - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Cell updated".to_string()), - })) -} - -pub async fn handle_format_cells( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - - for row in req.start_row..=req.end_row { - for col in req.start_col..=req.end_col { - let key = format!("{},{}", row, col); - let cell = worksheet.data.entry(key).or_insert_with(|| CellData { - value: None, - formula: None, - style: None, - format: None, - note: None, - }); - cell.style = Some(req.style.clone()); - } - } - - sheet.updated_at = Utc::now(); - - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Format applied".to_string()), - })) -} - -pub async fn handle_evaluate_formula( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - - let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(_) => { - return Ok(Json(evaluate_formula( - &req.formula, - &Worksheet { - name: "temp".to_string(), - data: HashMap::new(), - column_widths: None, - row_heights: None, - frozen_rows: None, - frozen_cols: None, - merged_cells: None, - filters: None, - hidden_rows: None, - validations: None, - conditional_formats: None, - charts: None, - }, - ))) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let result = evaluate_formula(&req.formula, &sheet.worksheets[req.worksheet_index]); - Ok(Json(result)) -} - -pub async fn handle_export_sheet( - State(state): State>, - Json(req): Json, -) -> Result)> { - let user_id = get_current_user_id(); - - let sheet = match load_sheet_by_id(&state, &user_id, &req.id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - match req.format.as_str() { - "csv" => { - let csv = export_to_csv(&sheet); - Ok(([(axum::http::header::CONTENT_TYPE, "text/csv")], csv)) - } - "xlsx" => { - let xlsx = export_to_xlsx(&sheet).map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - ) - })?; - Ok(( - [( - axum::http::header::CONTENT_TYPE, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - )], - xlsx, - )) - } - "json" => { - let json = export_to_json(&sheet); - Ok(([(axum::http::header::CONTENT_TYPE, "application/json")], json)) - } - "html" => { - let html = export_to_html(&sheet); - Ok(([(axum::http::header::CONTENT_TYPE, "text/html")], html)) - } - "ods" => { - let ods = export_to_ods(&sheet).map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - ) - })?; - Ok(( - [( - axum::http::header::CONTENT_TYPE, - "application/vnd.oasis.opendocument.spreadsheet", - )], - ods, - )) - } - "md" | "markdown" => { - let md = export_to_markdown(&sheet); - Ok(([(axum::http::header::CONTENT_TYPE, "text/markdown")], md)) - } - _ => Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Unsupported format" })), - )), - } -} - -pub async fn handle_share_sheet( - Json(req): Json, -) -> Result, (StatusCode, Json)> { - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some(format!("Shared with {} as {}", req.email, req.permission)), - })) -} - -pub async fn handle_get_sheet_by_id( - State(state): State>, - Path(sheet_id): Path, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - match load_sheet_by_id(&state, &user_id, &sheet_id).await { - Ok(sheet) => Ok(Json(sheet)), - Err(e) => Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )), - } -} - -pub async fn handle_merge_cells( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let merged = MergedCell { - start_row: req.start_row, - start_col: req.start_col, - end_row: req.end_row, - end_col: req.end_col, - }; - - let merged_cells = worksheet.merged_cells.get_or_insert_with(Vec::new); - merged_cells.push(merged); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Cells merged".to_string()), - })) -} - -pub async fn handle_unmerge_cells( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - if let Some(ref mut merged_cells) = worksheet.merged_cells { - merged_cells.retain(|m| { - !(m.start_row == req.start_row - && m.start_col == req.start_col - && m.end_row == req.end_row - && m.end_col == req.end_col) - }); - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Cells unmerged".to_string()), - })) -} - -pub async fn handle_freeze_panes( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - worksheet.frozen_rows = Some(req.frozen_rows); - worksheet.frozen_cols = Some(req.frozen_cols); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Panes frozen".to_string()), - })) -} - -pub async fn handle_sort_range( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - - let mut rows: Vec>> = Vec::new(); - for row in req.start_row..=req.end_row { - let mut row_data = Vec::new(); - for col in req.start_col..=req.end_col { - let key = format!("{},{}", row, col); - row_data.push(worksheet.data.get(&key).cloned()); - } - rows.push(row_data); - } - - let sort_col_idx = (req.sort_col - req.start_col) as usize; - rows.sort_by(|a, b| { - let val_a = a - .get(sort_col_idx) - .and_then(|c| c.as_ref()) - .and_then(|c| c.value.clone()) - .unwrap_or_default(); - let val_b = b - .get(sort_col_idx) - .and_then(|c| c.as_ref()) - .and_then(|c| c.value.clone()) - .unwrap_or_default(); - - let num_a = val_a.parse::().ok(); - let num_b = val_b.parse::().ok(); - - let cmp = match (num_a, num_b) { - (Some(na), Some(nb)) => na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal), - _ => val_a.cmp(&val_b), - }; - - if req.ascending { - cmp - } else { - cmp.reverse() - } - }); - - for (row_offset, row_data) in rows.iter().enumerate() { - for (col_offset, cell) in row_data.iter().enumerate() { - let key = format!( - "{},{}", - req.start_row + row_offset as u32, - req.start_col + col_offset as u32 - ); - if let Some(c) = cell { - worksheet.data.insert(key, c.clone()); - } else { - worksheet.data.remove(&key); - } - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Range sorted".to_string()), - })) -} - -pub async fn handle_filter_data( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let filters = worksheet.filters.get_or_insert_with(HashMap::new); - - filters.insert( - req.col, - FilterConfig { - filter_type: req.filter_type, - values: req.values, - condition: req.condition, - value1: req.value1, - value2: req.value2, - }, - ); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Filter applied".to_string()), - })) -} - -pub async fn handle_clear_filter( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - if let Some(ref mut filters) = worksheet.filters { - if let Some(col) = req.col { - filters.remove(&col); - } else { - filters.clear(); - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Filter cleared".to_string()), - })) -} - -pub async fn handle_create_chart( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let chart = ChartConfig { - id: Uuid::new_v4().to_string(), - chart_type: req.chart_type, - title: req.title.unwrap_or_else(|| "Chart".to_string()), - data_range: req.data_range, - label_range: req.label_range.unwrap_or_default(), - position: req.position.unwrap_or(ChartPosition { - row: 0, - col: 5, - width: 400, - height: 300, - }), - options: ChartOptions::default(), - datasets: vec![], - labels: vec![], - }; - - let charts = worksheet.charts.get_or_insert_with(Vec::new); - charts.push(chart); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Chart created".to_string()), - })) -} - -pub async fn handle_delete_chart( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - if let Some(ref mut charts) = worksheet.charts { - charts.retain(|c| c.id != req.chart_id); - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Chart deleted".to_string()), - })) -} - -pub async fn handle_conditional_format( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let rule = ConditionalFormatRule { - id: Uuid::new_v4().to_string(), - start_row: req.start_row, - start_col: req.start_col, - end_row: req.end_row, - end_col: req.end_col, - rule_type: req.rule_type, - condition: req.condition, - style: req.style, - priority: 1, - }; - - let formats = worksheet.conditional_formats.get_or_insert_with(Vec::new); - formats.push(rule); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Conditional format applied".to_string()), - })) -} - -pub async fn handle_data_validation( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let validations = worksheet.validations.get_or_insert_with(HashMap::new); - - for row in req.start_row..=req.end_row { - for col in req.start_col..=req.end_col { - let key = format!("{},{}", row, col); - validations.insert( - key, - ValidationRule { - validation_type: req.validation_type.clone(), - operator: req.operator.clone(), - value1: req.value1.clone(), - value2: req.value2.clone(), - allowed_values: req.allowed_values.clone(), - error_title: None, - error_message: req.error_message.clone(), - input_title: None, - input_message: None, - }, - ); - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Data validation applied".to_string()), - })) -} - -pub async fn handle_validate_cell( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - if let Some(ref validations) = worksheet.validations { - if let Some(rule) = validations.get(&key) { - let result = validate_value(&req.value, rule); - return Ok(Json(result)); - } - } - - Ok(Json(ValidationResult { - valid: true, - error_message: None, - })) -} - -fn validate_value(value: &str, rule: &ValidationRule) -> ValidationResult { - let valid = match rule.validation_type.as_str() { - "number" => value.parse::().is_ok(), - "integer" => value.parse::().is_ok(), - "list" => rule - .allowed_values - .as_ref() - .map(|v| v.contains(&value.to_string())) - .unwrap_or(true), - "date" => chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok(), - "text_length" => { - let len = value.len(); - let min = rule.value1.as_ref().and_then(|v| v.parse::().ok()).unwrap_or(0); - let max = rule.value2.as_ref().and_then(|v| v.parse::().ok()).unwrap_or(usize::MAX); - len >= min && len <= max - } - _ => true, - }; - - ValidationResult { - valid, - error_message: if valid { - None - } else { - rule.error_message.clone().or_else(|| Some("Invalid value".to_string())) - }, - } -} - -pub async fn handle_add_note( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - let cell = worksheet.data.entry(key).or_insert_with(|| CellData { - value: None, - formula: None, - style: None, - format: None, - note: None, - }); - cell.note = Some(req.note); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Note added".to_string()), - })) -} - -pub async fn handle_import_sheet( - State(state): State>, - mut multipart: axum::extract::Multipart, -) -> Result, (StatusCode, Json)> { - let mut file_bytes: Option> = None; - let mut filename = "import.xlsx".to_string(); - - while let Ok(Some(field)) = multipart.next_field().await { - if field.name() == Some("file") { - filename = field.file_name().unwrap_or("import.xlsx").to_string(); - if let Ok(bytes) = field.bytes().await { - file_bytes = Some(bytes.to_vec()); - } - } - } - - let bytes = file_bytes.ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "No file uploaded" })), - ) - })?; - - let mut sheet = import_spreadsheet_bytes(&bytes, &filename).map_err(|e| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": e })), - ) - })?; - - let user_id = get_current_user_id(); - sheet.owner_id = user_id.clone(); - - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(sheet)) -} - -pub async fn handle_add_comment( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - let comment = CellComment { - id: Uuid::new_v4().to_string(), - author_id: user_id.clone(), - author_name: "User".to_string(), - content: req.content, - created_at: Utc::now(), - updated_at: Utc::now(), - replies: vec![], - resolved: false, - }; - - let comments = worksheet.comments.get_or_insert_with(HashMap::new); - comments.insert(key.clone(), comment); - - let cell = worksheet.data.entry(key).or_insert_with(|| CellData { - value: None, - formula: None, - style: None, - format: None, - note: None, - locked: None, - has_comment: None, - array_formula_id: None, - }); - cell.has_comment = Some(true); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Comment added".to_string()), - })) -} - -pub async fn handle_reply_comment( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - if let Some(comments) = &mut worksheet.comments { - if let Some(comment) = comments.get_mut(&key) { - if comment.id == req.comment_id { - let reply = CommentReply { - id: Uuid::new_v4().to_string(), - author_id: user_id.clone(), - author_name: "User".to_string(), - content: req.content, - created_at: Utc::now(), - }; - comment.replies.push(reply); - comment.updated_at = Utc::now(); - } - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Reply added".to_string()), - })) -} - -pub async fn handle_resolve_comment( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - if let Some(comments) = &mut worksheet.comments { - if let Some(comment) = comments.get_mut(&key) { - if comment.id == req.comment_id { - comment.resolved = req.resolved; - comment.updated_at = Utc::now(); - } - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Comment resolved".to_string()), - })) -} - -pub async fn handle_delete_comment( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let key = format!("{},{}", req.row, req.col); - - if let Some(comments) = &mut worksheet.comments { - comments.remove(&key); - } - - if let Some(cell) = worksheet.data.get_mut(&key) { - cell.has_comment = Some(false); - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Comment deleted".to_string()), - })) -} - -pub async fn handle_list_comments( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &sheet.worksheets[req.worksheet_index]; - let mut comments_list = vec![]; - - if let Some(comments) = &worksheet.comments { - for (key, comment) in comments { - let parts: Vec<&str> = key.split(',').collect(); - if parts.len() == 2 { - if let (Ok(row), Ok(col)) = (parts[0].parse::(), parts[1].parse::()) { - comments_list.push(CommentWithLocation { - row, - col, - comment: comment.clone(), - }); - } - } - } - } - - Ok(Json(ListCommentsResponse { comments: comments_list })) -} - -pub async fn handle_protect_sheet( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let mut protection = req.protection; - if let Some(password) = req.password { - protection.password_hash = Some(format!("{:x}", md5::compute(password.as_bytes()))); - } - - sheet.worksheets[req.worksheet_index].protection = Some(protection); - sheet.updated_at = Utc::now(); - - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Sheet protected".to_string()), - })) -} - -pub async fn handle_unprotect_sheet( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - if let Some(protection) = &worksheet.protection { - if let Some(hash) = &protection.password_hash { - if let Some(password) = &req.password { - let provided_hash = format!("{:x}", md5::compute(password.as_bytes())); - if &provided_hash != hash { - return Err(( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ "error": "Invalid password" })), - )); - } - } else { - return Err(( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ "error": "Password required" })), - )); - } - } - } - - worksheet.protection = None; - sheet.updated_at = Utc::now(); - - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Sheet unprotected".to_string()), - })) -} - -pub async fn handle_lock_cells( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - - for row in req.start_row..=req.end_row { - for col in req.start_col..=req.end_col { - let key = format!("{row},{col}"); - let cell = worksheet.data.entry(key).or_insert_with(|| CellData { - value: None, - formula: None, - style: None, - format: None, - note: None, - locked: None, - has_comment: None, - array_formula_id: None, - }); - cell.locked = Some(req.locked); - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some(if req.locked { "Cells locked" } else { "Cells unlocked" }.to_string()), - })) -} - -pub async fn handle_add_external_link( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - let link = ExternalLink { - id: Uuid::new_v4().to_string(), - source_path: req.source_path, - link_type: req.link_type, - target_sheet: req.target_sheet, - target_range: req.target_range, - status: "active".to_string(), - last_updated: Utc::now(), - }; - - let links = sheet.external_links.get_or_insert_with(Vec::new); - links.push(link); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("External link added".to_string()), - })) -} - -pub async fn handle_refresh_external_link( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if let Some(links) = &mut sheet.external_links { - for link in links.iter_mut() { - if link.id == req.link_id { - link.last_updated = Utc::now(); - link.status = "refreshed".to_string(); - } - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("External link refreshed".to_string()), - })) -} - -pub async fn handle_remove_external_link( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if let Some(links) = &mut sheet.external_links { - links.retain(|link| link.id != req.link_id); - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("External link removed".to_string()), - })) -} - -pub async fn handle_list_external_links( - State(state): State>, - Query(params): Query>, -) -> Result, (StatusCode, Json)> { - let sheet_id = params.get("sheet_id").cloned().unwrap_or_default(); - let user_id = get_current_user_id(); - let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - let links = sheet.external_links.unwrap_or_default(); - Ok(Json(ListExternalLinksResponse { links })) -} - -pub async fn handle_array_formula( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let array_formula_id = Uuid::new_v4().to_string(); - let array_formula = ArrayFormula { - id: array_formula_id.clone(), - formula: req.formula.clone(), - start_row: req.start_row, - start_col: req.start_col, - end_row: req.end_row, - end_col: req.end_col, - is_dynamic: req.formula.starts_with('=') && req.formula.contains('#'), - }; - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - let array_formulas = worksheet.array_formulas.get_or_insert_with(Vec::new); - array_formulas.push(array_formula); - - for row in req.start_row..=req.end_row { - for col in req.start_col..=req.end_col { - let key = format!("{row},{col}"); - let cell = worksheet.data.entry(key).or_insert_with(|| CellData { - value: None, - formula: None, - style: None, - format: None, - note: None, - locked: None, - has_comment: None, - array_formula_id: None, - }); - cell.array_formula_id = Some(array_formula_id.clone()); - if row == req.start_row && col == req.start_col { - cell.formula = Some(format!("{{{}}}", req.formula)); - } - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Array formula created".to_string()), - })) -} - -pub async fn handle_delete_array_formula( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if req.worksheet_index >= sheet.worksheets.len() { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": "Invalid worksheet index" })), - )); - } - - let worksheet = &mut sheet.worksheets[req.worksheet_index]; - - if let Some(array_formulas) = &mut worksheet.array_formulas { - array_formulas.retain(|af| af.id != req.array_formula_id); - } - - for cell in worksheet.data.values_mut() { - if cell.array_formula_id.as_ref() == Some(&req.array_formula_id) { - cell.array_formula_id = None; - cell.formula = None; - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Array formula deleted".to_string()), - })) -} - -pub async fn handle_create_named_range( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - let named_range = NamedRange { - id: Uuid::new_v4().to_string(), - name: req.name, - scope: req.scope, - worksheet_index: req.worksheet_index, - start_row: req.start_row, - start_col: req.start_col, - end_row: req.end_row, - end_col: req.end_col, - comment: req.comment, - }; - - let named_ranges = sheet.named_ranges.get_or_insert_with(Vec::new); - named_ranges.push(named_range); - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Named range created".to_string()), - })) -} - -pub async fn handle_update_named_range( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if let Some(named_ranges) = &mut sheet.named_ranges { - for range in named_ranges.iter_mut() { - if range.id == req.range_id { - if let Some(name) = req.name { - range.name = name; - } - if let Some(start_row) = req.start_row { - range.start_row = start_row; - } - if let Some(start_col) = req.start_col { - range.start_col = start_col; - } - if let Some(end_row) = req.end_row { - range.end_row = end_row; - } - if let Some(end_col) = req.end_col { - range.end_col = end_col; - } - if let Some(comment) = req.comment { - range.comment = Some(comment); - } - } - } - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Named range updated".to_string()), - })) -} - -pub async fn handle_delete_named_range( - State(state): State>, - Json(req): Json, -) -> Result, (StatusCode, Json)> { - let user_id = get_current_user_id(); - let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - if let Some(named_ranges) = &mut sheet.named_ranges { - named_ranges.retain(|r| r.id != req.range_id); - } - - sheet.updated_at = Utc::now(); - if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": e })), - )); - } - - Ok(Json(SaveResponse { - id: req.sheet_id, - success: true, - message: Some("Named range deleted".to_string()), - })) -} - -pub async fn handle_list_named_ranges( - State(state): State>, - Query(params): Query>, -) -> Result, (StatusCode, Json)> { - let sheet_id = params.get("sheet_id").cloned().unwrap_or_default(); - let user_id = get_current_user_id(); - let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await { - Ok(s) => s, - Err(e) => { - return Err(( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ "error": e })), - )) - } - }; - - let ranges = sheet.named_ranges.unwrap_or_default(); - Ok(Json(ListNamedRangesResponse { ranges })) -} diff --git a/src/sheet/handlers/advanced.rs b/src/sheet/handlers/advanced.rs new file mode 100644 index 000000000..a5c5654d9 --- /dev/null +++ b/src/sheet/handlers/advanced.rs @@ -0,0 +1,601 @@ +use crate::shared::state::AppState; +use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive}; +use crate::sheet::types::{ + AddExternalLinkRequest, ArrayFormula, ArrayFormulaRequest, CellData, + CreateNamedRangeRequest, DeleteArrayFormulaRequest, DeleteNamedRangeRequest, ExternalLink, + ListExternalLinksResponse, ListNamedRangesResponse, LockCellsRequest, NamedRange, + ProtectSheetRequest, RefreshExternalLinkRequest, RemoveExternalLinkRequest, SaveResponse, + UnprotectSheetRequest, UpdateNamedRangeRequest, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + Json, +}; +use chrono::Utc; +use std::collections::HashMap; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::Arc; +use uuid::Uuid; + +pub async fn handle_protect_sheet( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let mut protection = req.protection; + if let Some(password) = req.password { + let mut hasher = DefaultHasher::new(); + password.hash(&mut hasher); + protection.password_hash = Some(format!("{:x}", hasher.finish())); + } + + sheet.worksheets[req.worksheet_index].protection = Some(protection); + sheet.updated_at = Utc::now(); + + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Sheet protected".to_string()), + })) +} + +pub async fn handle_unprotect_sheet( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + if let Some(protection) = &worksheet.protection { + if let Some(hash) = &protection.password_hash { + if let Some(password) = &req.password { + let mut hasher = DefaultHasher::new(); + password.hash(&mut hasher); + let provided_hash = format!("{:x}", hasher.finish()); + if &provided_hash != hash { + return Err(( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ "error": "Invalid password" })), + )); + } + } else { + return Err(( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ "error": "Password required" })), + )); + } + } + } + + worksheet.protection = None; + sheet.updated_at = Utc::now(); + + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Sheet unprotected".to_string()), + })) +} + +pub async fn handle_lock_cells( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + + for row in req.start_row..=req.end_row { + for col in req.start_col..=req.end_col { + let key = format!("{row},{col}"); + let cell = worksheet.data.entry(key).or_insert_with(|| CellData { + value: None, + formula: None, + style: None, + format: None, + note: None, + locked: None, + has_comment: None, + array_formula_id: None, + }); + cell.locked = Some(req.locked); + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some( + if req.locked { + "Cells locked" + } else { + "Cells unlocked" + } + .to_string(), + ), + })) +} + +pub async fn handle_add_external_link( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + let link = ExternalLink { + id: Uuid::new_v4().to_string(), + source_path: req.source_path, + link_type: req.link_type, + target_sheet: req.target_sheet, + target_range: req.target_range, + status: "active".to_string(), + last_updated: Utc::now(), + }; + + let links = sheet.external_links.get_or_insert_with(Vec::new); + links.push(link); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("External link added".to_string()), + })) +} + +pub async fn handle_refresh_external_link( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if let Some(links) = &mut sheet.external_links { + for link in links.iter_mut() { + if link.id == req.link_id { + link.last_updated = Utc::now(); + link.status = "refreshed".to_string(); + } + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("External link refreshed".to_string()), + })) +} + +pub async fn handle_remove_external_link( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if let Some(links) = &mut sheet.external_links { + links.retain(|link| link.id != req.link_id); + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("External link removed".to_string()), + })) +} + +pub async fn handle_list_external_links( + State(state): State>, + Query(params): Query>, +) -> Result, (StatusCode, Json)> { + let sheet_id = params.get("sheet_id").cloned().unwrap_or_default(); + let user_id = get_current_user_id(); + let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + let links = sheet.external_links.unwrap_or_default(); + Ok(Json(ListExternalLinksResponse { links })) +} + +pub async fn handle_array_formula( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let array_formula_id = Uuid::new_v4().to_string(); + let array_formula = ArrayFormula { + id: array_formula_id.clone(), + formula: req.formula.clone(), + start_row: req.start_row, + start_col: req.start_col, + end_row: req.end_row, + end_col: req.end_col, + is_dynamic: req.formula.starts_with('=') && req.formula.contains('#'), + }; + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let array_formulas = worksheet.array_formulas.get_or_insert_with(Vec::new); + array_formulas.push(array_formula); + + for row in req.start_row..=req.end_row { + for col in req.start_col..=req.end_col { + let key = format!("{row},{col}"); + let cell = worksheet.data.entry(key).or_insert_with(|| CellData { + value: None, + formula: None, + style: None, + format: None, + note: None, + locked: None, + has_comment: None, + array_formula_id: None, + }); + cell.array_formula_id = Some(array_formula_id.clone()); + if row == req.start_row && col == req.start_col { + cell.formula = Some(format!("{{{}}}", req.formula)); + } + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Array formula created".to_string()), + })) +} + +pub async fn handle_delete_array_formula( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + + if let Some(array_formulas) = &mut worksheet.array_formulas { + array_formulas.retain(|af| af.id != req.array_formula_id); + } + + for cell in worksheet.data.values_mut() { + if cell.array_formula_id.as_ref() == Some(&req.array_formula_id) { + cell.array_formula_id = None; + cell.formula = None; + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Array formula deleted".to_string()), + })) +} + +pub async fn handle_create_named_range( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + let named_range = NamedRange { + id: Uuid::new_v4().to_string(), + name: req.name, + scope: req.scope, + worksheet_index: req.worksheet_index, + start_row: req.start_row, + start_col: req.start_col, + end_row: req.end_row, + end_col: req.end_col, + comment: req.comment, + }; + + let named_ranges = sheet.named_ranges.get_or_insert_with(Vec::new); + named_ranges.push(named_range); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Named range created".to_string()), + })) +} + +pub async fn handle_update_named_range( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if let Some(named_ranges) = &mut sheet.named_ranges { + for range in named_ranges.iter_mut() { + if range.id == req.range_id { + if let Some(ref name) = req.name { + range.name = name.clone(); + } + if let Some(start_row) = req.start_row { + range.start_row = start_row; + } + if let Some(start_col) = req.start_col { + range.start_col = start_col; + } + if let Some(end_row) = req.end_row { + range.end_row = end_row; + } + if let Some(end_col) = req.end_col { + range.end_col = end_col; + } + if let Some(ref comment) = req.comment { + range.comment = Some(comment.clone()); + } + } + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Named range updated".to_string()), + })) +} + +pub async fn handle_delete_named_range( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if let Some(named_ranges) = &mut sheet.named_ranges { + named_ranges.retain(|r| r.id != req.range_id); + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Named range deleted".to_string()), + })) +} + +pub async fn handle_list_named_ranges( + State(state): State>, + Query(params): Query>, +) -> Result, (StatusCode, Json)> { + let sheet_id = params.get("sheet_id").cloned().unwrap_or_default(); + let user_id = get_current_user_id(); + let sheet = match load_sheet_by_id(&state, &user_id, &sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + let ranges = sheet.named_ranges.unwrap_or_default(); + Ok(Json(ListNamedRangesResponse { ranges })) +} diff --git a/src/sheet/handlers/ai.rs b/src/sheet/handlers/ai.rs new file mode 100644 index 000000000..2fb283d47 --- /dev/null +++ b/src/sheet/handlers/ai.rs @@ -0,0 +1,43 @@ +use crate::shared::state::AppState; +use crate::sheet::types::{SheetAiRequest, SheetAiResponse}; +use axum::{extract::State, response::IntoResponse, Json}; +use std::sync::Arc; + +pub async fn handle_sheet_ai( + State(_state): State>, + Json(req): Json, +) -> impl IntoResponse { + let command = req.command.to_lowercase(); + + let response = if command.contains("sum") { + "I can help you sum values. Select a range and use the SUM formula, or I've added a SUM formula below your selection." + } else if command.contains("average") || command.contains("avg") { + "I can calculate averages. Select a range and use the AVERAGE formula." + } else if command.contains("chart") { + "To create a chart, select your data range first, then choose the chart type from the Chart menu." + } else if command.contains("sort") { + "I can sort your data. Select the range you want to sort, then specify ascending or descending order." + } else if command.contains("format") || command.contains("currency") || command.contains("percent") { + "I've applied the formatting to your selected cells." + } else if command.contains("bold") || command.contains("italic") { + "I've applied the text formatting to your selected cells." + } else if command.contains("filter") { + "I've enabled filtering on your data. Use the dropdown arrows in the header row to filter." + } else if command.contains("freeze") { + "I've frozen the specified rows/columns so they stay visible when scrolling." + } else if command.contains("merge") { + "I've merged the selected cells into one." + } else if command.contains("clear") { + "I've cleared the content from the selected cells." + } else if command.contains("help") { + "I can help you with:\n• Sum/Average columns\n• Format as currency or percent\n• Bold/Italic formatting\n• Sort data\n• Create charts\n• Filter data\n• Freeze panes\n• Merge cells" + } else { + "I understand you want help with your spreadsheet. Try commands like 'sum column B', 'format as currency', 'sort ascending', or 'create a chart'." + }; + + Json(SheetAiResponse { + response: response.to_string(), + action: None, + data: None, + }) +} diff --git a/src/sheet/handlers/cell_ops.rs b/src/sheet/handlers/cell_ops.rs new file mode 100644 index 000000000..d64c09490 --- /dev/null +++ b/src/sheet/handlers/cell_ops.rs @@ -0,0 +1,323 @@ +use crate::shared::state::AppState; +use crate::sheet::collaboration::broadcast_sheet_change; +use crate::sheet::formulas::evaluate_formula; +use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive}; +use crate::sheet::types::{ + CellData, CellUpdateRequest, FormatRequest, FormulaRequest, FormulaResult, FreezePanesRequest, + MergeCellsRequest, MergedCell, SaveResponse, Worksheet, +}; +use axum::{extract::State, http::StatusCode, Json}; +use chrono::Utc; +use std::collections::HashMap; +use std::sync::Arc; + +pub async fn handle_update_cell( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + let (value, formula) = if req.value.starts_with('=') { + let result = evaluate_formula(&req.value, worksheet); + (Some(result.value), Some(req.value.clone())) + } else { + (Some(req.value.clone()), None) + }; + + let cell = worksheet.data.entry(key).or_insert_with(|| CellData { + value: None, + formula: None, + style: None, + format: None, + note: None, + locked: None, + has_comment: None, + array_formula_id: None, + }); + + cell.value = value; + cell.formula = formula; + + sheet.updated_at = Utc::now(); + + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + broadcast_sheet_change( + &req.sheet_id, + &user_id, + "User", + req.row, + req.col, + &req.value, + req.worksheet_index, + ) + .await; + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Cell updated".to_string()), + })) +} + +pub async fn handle_format_cells( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + + for row in req.start_row..=req.end_row { + for col in req.start_col..=req.end_col { + let key = format!("{},{}", row, col); + let cell = worksheet.data.entry(key).or_insert_with(|| CellData { + value: None, + formula: None, + style: None, + format: None, + note: None, + locked: None, + has_comment: None, + array_formula_id: None, + }); + cell.style = Some(req.style.clone()); + } + } + + sheet.updated_at = Utc::now(); + + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Format applied".to_string()), + })) +} + +pub async fn handle_evaluate_formula( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(_) => { + return Ok(Json(evaluate_formula( + &req.formula, + &Worksheet { + name: "temp".to_string(), + data: HashMap::new(), + column_widths: None, + row_heights: None, + frozen_rows: None, + frozen_cols: None, + merged_cells: None, + filters: None, + hidden_rows: None, + validations: None, + conditional_formats: None, + charts: None, + comments: None, + protection: None, + array_formulas: None, + }, + ))) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let result = evaluate_formula(&req.formula, &sheet.worksheets[req.worksheet_index]); + Ok(Json(result)) +} + +pub async fn handle_merge_cells( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let merged = MergedCell { + start_row: req.start_row, + start_col: req.start_col, + end_row: req.end_row, + end_col: req.end_col, + }; + + let merged_cells = worksheet.merged_cells.get_or_insert_with(Vec::new); + merged_cells.push(merged); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Cells merged".to_string()), + })) +} + +pub async fn handle_unmerge_cells( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + if let Some(ref mut merged_cells) = worksheet.merged_cells { + merged_cells.retain(|m| { + !(m.start_row == req.start_row + && m.start_col == req.start_col + && m.end_row == req.end_row + && m.end_col == req.end_col) + }); + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Cells unmerged".to_string()), + })) +} + +pub async fn handle_freeze_panes( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + worksheet.frozen_rows = Some(req.frozen_rows); + worksheet.frozen_cols = Some(req.frozen_cols); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Panes frozen".to_string()), + })) +} diff --git a/src/sheet/handlers/crud.rs b/src/sheet/handlers/crud.rs new file mode 100644 index 000000000..e23883a8c --- /dev/null +++ b/src/sheet/handlers/crud.rs @@ -0,0 +1,358 @@ +use crate::shared::state::AppState; +use crate::sheet::export::{ + export_to_csv, export_to_html, export_to_json, export_to_markdown, export_to_ods, + export_to_xlsx, +}; +use crate::sheet::storage::{ + create_new_spreadsheet, delete_sheet_from_drive, get_current_user_id, import_spreadsheet_bytes, + list_sheets_from_drive, load_sheet_by_id, load_sheet_from_drive, parse_csv_to_worksheets, + parse_excel_to_worksheets, save_sheet_to_drive, +}; +use crate::sheet::types::{ + ExportRequest, LoadFromDriveRequest, LoadQuery, SaveRequest, SaveResponse, SearchQuery, + ShareRequest, Spreadsheet, SpreadsheetMetadata, +}; +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use chrono::Utc; +use log::error; +use std::sync::Arc; +use uuid::Uuid; + +pub async fn handle_new_sheet( + State(_state): State>, +) -> Result, (StatusCode, Json)> { + Ok(Json(create_new_spreadsheet())) +} + +pub async fn handle_list_sheets( + State(state): State>, +) -> Result>, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + match list_sheets_from_drive(&state, &user_id).await { + Ok(sheets) => Ok(Json(sheets)), + Err(e) => { + error!("Failed to list sheets: {}", e); + Ok(Json(Vec::new())) + } + } +} + +pub async fn handle_search_sheets( + State(state): State>, + Query(query): Query, +) -> Result>, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + let sheets = match list_sheets_from_drive(&state, &user_id).await { + Ok(s) => s, + Err(_) => Vec::new(), + }; + + let filtered = if let Some(q) = query.q { + let q_lower = q.to_lowercase(); + sheets + .into_iter() + .filter(|s| s.name.to_lowercase().contains(&q_lower)) + .collect() + } else { + sheets + }; + + Ok(Json(filtered)) +} + +pub async fn handle_load_sheet( + State(state): State>, + Query(query): Query, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + match load_sheet_from_drive(&state, &user_id, &query.id).await { + Ok(sheet) => Ok(Json(sheet)), + Err(e) => Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )), + } +} + +pub async fn handle_load_from_drive( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let drive = state.drive.as_ref().ok_or_else(|| { + ( + StatusCode::SERVICE_UNAVAILABLE, + Json(serde_json::json!({ "error": "Drive not available" })), + ) + })?; + + let result = drive + .get_object() + .bucket(&req.bucket) + .key(&req.path) + .send() + .await + .map_err(|e| { + ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": format!("File not found: {e}") })), + ) + })?; + + let bytes = result + .body + .collect() + .await + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": format!("Failed to read file: {e}") })), + ) + })? + .into_bytes(); + + let ext = req.path.rsplit('.').next().unwrap_or("").to_lowercase(); + let file_name = req.path.rsplit('/').next().unwrap_or("Spreadsheet"); + let sheet_name = file_name + .rsplit('.') + .last() + .unwrap_or("Spreadsheet") + .to_string(); + + let worksheets = match ext.as_str() { + "csv" | "tsv" => { + let delimiter = if ext == "tsv" { b'\t' } else { b',' }; + parse_csv_to_worksheets(&bytes, delimiter, &sheet_name).map_err(|e| { + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": e })), + ) + })? + } + "xlsx" | "xls" | "ods" | "xlsb" | "xlsm" => { + parse_excel_to_worksheets(&bytes, &ext).map_err(|e| { + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": e })), + ) + })? + } + _ => { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": format!("Unsupported format: .{ext}") })), + )); + } + }; + + let user_id = get_current_user_id(); + let sheet = Spreadsheet { + id: Uuid::new_v4().to_string(), + name: sheet_name, + owner_id: user_id, + worksheets, + created_at: Utc::now(), + updated_at: Utc::now(), + named_ranges: None, + external_links: None, + }; + + Ok(Json(sheet)) +} + +pub async fn handle_save_sheet( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + let sheet_id = req.id.unwrap_or_else(|| Uuid::new_v4().to_string()); + + let sheet = Spreadsheet { + id: sheet_id.clone(), + name: req.name, + owner_id: user_id.clone(), + worksheets: req.worksheets, + created_at: Utc::now(), + updated_at: Utc::now(), + named_ranges: None, + external_links: None, + }; + + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: sheet_id, + success: true, + message: Some("Sheet saved successfully".to_string()), + })) +} + +pub async fn handle_delete_sheet( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + + if let Err(e) = delete_sheet_from_drive(&state, &user_id, &req.id).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.id.unwrap_or_default(), + success: true, + message: Some("Sheet deleted".to_string()), + })) +} + +pub async fn handle_get_sheet_by_id( + State(state): State>, + Path(sheet_id): Path, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + match load_sheet_by_id(&state, &user_id, &sheet_id).await { + Ok(sheet) => Ok(Json(sheet)), + Err(e) => Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )), + } +} + +pub async fn handle_share_sheet( + Json(req): Json, +) -> Result, (StatusCode, Json)> { + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some(format!("Shared with {} as {}", req.email, req.permission)), + })) +} + +pub async fn handle_export_sheet( + State(state): State>, + Json(req): Json, +) -> Result)> { + let user_id = get_current_user_id(); + + let sheet = match load_sheet_by_id(&state, &user_id, &req.id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + match req.format.as_str() { + "csv" => { + let csv = export_to_csv(&sheet); + Ok(([(axum::http::header::CONTENT_TYPE, "text/csv")], csv)) + } + "xlsx" => { + let xlsx = export_to_xlsx(&sheet).map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + ) + })?; + Ok(( + [( + axum::http::header::CONTENT_TYPE, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + )], + xlsx, + )) + } + "json" => { + let json = export_to_json(&sheet); + Ok(([(axum::http::header::CONTENT_TYPE, "application/json")], json)) + } + "html" => { + let html = export_to_html(&sheet); + Ok(([(axum::http::header::CONTENT_TYPE, "text/html")], html)) + } + "ods" => { + let ods = export_to_ods(&sheet).map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + ) + })?; + Ok(( + [( + axum::http::header::CONTENT_TYPE, + "application/vnd.oasis.opendocument.spreadsheet", + )], + ods, + )) + } + "md" | "markdown" => { + let md = export_to_markdown(&sheet); + Ok(([(axum::http::header::CONTENT_TYPE, "text/markdown")], md)) + } + _ => Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Unsupported format" })), + )), + } +} + +pub async fn handle_import_sheet( + State(state): State>, + mut multipart: axum::extract::Multipart, +) -> Result, (StatusCode, Json)> { + let mut file_bytes: Option> = None; + let mut filename = "import.xlsx".to_string(); + + while let Ok(Some(field)) = multipart.next_field().await { + if field.name() == Some("file") { + filename = field.file_name().unwrap_or("import.xlsx").to_string(); + if let Ok(bytes) = field.bytes().await { + file_bytes = Some(bytes.to_vec()); + } + } + } + + let bytes = file_bytes.ok_or_else(|| { + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "No file uploaded" })), + ) + })?; + + let mut sheet = import_spreadsheet_bytes(&bytes, &filename).map_err(|e| { + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": e })), + ) + })?; + + let user_id = get_current_user_id(); + sheet.owner_id = user_id.clone(); + + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(sheet)) +} diff --git a/src/sheet/handlers/data_ops.rs b/src/sheet/handlers/data_ops.rs new file mode 100644 index 000000000..7c66ffd4d --- /dev/null +++ b/src/sheet/handlers/data_ops.rs @@ -0,0 +1,353 @@ +use crate::shared::state::AppState; +use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive}; +use crate::sheet::types::{ + CellData, ChartConfig, ChartOptions, ChartPosition, ChartRequest, ClearFilterRequest, + ConditionalFormatRequest, ConditionalFormatRule, DeleteChartRequest, FilterConfig, + FilterRequest, SaveResponse, SortRequest, +}; +use axum::{extract::State, http::StatusCode, Json}; +use chrono::Utc; +use std::sync::Arc; +use uuid::Uuid; + +pub async fn handle_sort_range( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + + let mut rows: Vec>> = Vec::new(); + for row in req.start_row..=req.end_row { + let mut row_data = Vec::new(); + for col in req.start_col..=req.end_col { + let key = format!("{},{}", row, col); + row_data.push(worksheet.data.get(&key).cloned()); + } + rows.push(row_data); + } + + let sort_col_idx = (req.sort_col - req.start_col) as usize; + rows.sort_by(|a, b| { + let val_a = a + .get(sort_col_idx) + .and_then(|c| c.as_ref()) + .and_then(|c| c.value.clone()) + .unwrap_or_default(); + let val_b = b + .get(sort_col_idx) + .and_then(|c| c.as_ref()) + .and_then(|c| c.value.clone()) + .unwrap_or_default(); + + let num_a = val_a.parse::().ok(); + let num_b = val_b.parse::().ok(); + + let cmp = match (num_a, num_b) { + (Some(na), Some(nb)) => na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal), + _ => val_a.cmp(&val_b), + }; + + if req.ascending { + cmp + } else { + cmp.reverse() + } + }); + + for (row_offset, row_data) in rows.iter().enumerate() { + for (col_offset, cell) in row_data.iter().enumerate() { + let key = format!( + "{},{}", + req.start_row + row_offset as u32, + req.start_col + col_offset as u32 + ); + if let Some(c) = cell { + worksheet.data.insert(key, c.clone()); + } else { + worksheet.data.remove(&key); + } + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Range sorted".to_string()), + })) +} + +pub async fn handle_filter_data( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let filters = worksheet.filters.get_or_insert_with(std::collections::HashMap::new); + + filters.insert( + req.col, + FilterConfig { + filter_type: req.filter_type, + values: req.values, + condition: req.condition, + value1: req.value1, + value2: req.value2, + }, + ); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Filter applied".to_string()), + })) +} + +pub async fn handle_clear_filter( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + if let Some(ref mut filters) = worksheet.filters { + if let Some(col) = req.col { + filters.remove(&col); + } else { + filters.clear(); + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Filter cleared".to_string()), + })) +} + +pub async fn handle_create_chart( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let chart = ChartConfig { + id: Uuid::new_v4().to_string(), + chart_type: req.chart_type, + title: req.title.unwrap_or_else(|| "Chart".to_string()), + data_range: req.data_range, + label_range: req.label_range.unwrap_or_default(), + position: req.position.unwrap_or(ChartPosition { + row: 0, + col: 5, + width: 400, + height: 300, + }), + options: ChartOptions::default(), + datasets: vec![], + labels: vec![], + }; + + let charts = worksheet.charts.get_or_insert_with(Vec::new); + charts.push(chart); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Chart created".to_string()), + })) +} + +pub async fn handle_delete_chart( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + if let Some(ref mut charts) = worksheet.charts { + charts.retain(|c| c.id != req.chart_id); + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Chart deleted".to_string()), + })) +} + +pub async fn handle_conditional_format( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let rule = ConditionalFormatRule { + id: Uuid::new_v4().to_string(), + start_row: req.start_row, + start_col: req.start_col, + end_row: req.end_row, + end_col: req.end_col, + rule_type: req.rule_type, + condition: req.condition, + style: req.style, + priority: 1, + }; + + let formats = worksheet.conditional_formats.get_or_insert_with(Vec::new); + formats.push(rule); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Conditional format applied".to_string()), + })) +} diff --git a/src/sheet/handlers/mod.rs b/src/sheet/handlers/mod.rs new file mode 100644 index 000000000..9c44b080d --- /dev/null +++ b/src/sheet/handlers/mod.rs @@ -0,0 +1,32 @@ +pub mod advanced; +pub mod ai; +pub mod cell_ops; +pub mod crud; +pub mod data_ops; +pub mod validation; + +pub use advanced::{ + handle_add_external_link, handle_array_formula, handle_create_named_range, + handle_delete_array_formula, handle_delete_named_range, handle_list_external_links, + handle_list_named_ranges, handle_lock_cells, handle_protect_sheet, + handle_refresh_external_link, handle_remove_external_link, handle_unprotect_sheet, + handle_update_named_range, +}; +pub use ai::handle_sheet_ai; +pub use cell_ops::{ + handle_evaluate_formula, handle_format_cells, handle_freeze_panes, handle_merge_cells, + handle_unmerge_cells, handle_update_cell, +}; +pub use crud::{ + handle_delete_sheet, handle_export_sheet, handle_get_sheet_by_id, handle_import_sheet, + handle_list_sheets, handle_load_from_drive, handle_load_sheet, handle_new_sheet, + handle_save_sheet, handle_search_sheets, handle_share_sheet, +}; +pub use data_ops::{ + handle_clear_filter, handle_conditional_format, handle_create_chart, handle_delete_chart, + handle_filter_data, handle_sort_range, +}; +pub use validation::{ + handle_add_comment, handle_add_note, handle_data_validation, handle_delete_comment, + handle_list_comments, handle_reply_comment, handle_resolve_comment, handle_validate_cell, +}; diff --git a/src/sheet/handlers/validation.rs b/src/sheet/handlers/validation.rs new file mode 100644 index 000000000..e6487bad4 --- /dev/null +++ b/src/sheet/handlers/validation.rs @@ -0,0 +1,469 @@ +use crate::shared::state::AppState; +use crate::sheet::storage::{get_current_user_id, load_sheet_by_id, save_sheet_to_drive}; +use crate::sheet::types::{ + AddCommentRequest, AddNoteRequest, CellComment, CellData, CommentReply, CommentWithLocation, + DataValidationRequest, DeleteCommentRequest, ListCommentsRequest, ListCommentsResponse, + ReplyCommentRequest, ResolveCommentRequest, SaveResponse, ValidateCellRequest, + ValidationResult, ValidationRule, +}; +use axum::{extract::State, http::StatusCode, Json}; +use chrono::Utc; +use std::sync::Arc; +use uuid::Uuid; + +pub async fn handle_data_validation( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let validations = worksheet + .validations + .get_or_insert_with(std::collections::HashMap::new); + + for row in req.start_row..=req.end_row { + for col in req.start_col..=req.end_col { + let key = format!("{},{}", row, col); + validations.insert( + key, + ValidationRule { + validation_type: req.validation_type.clone(), + operator: req.operator.clone(), + value1: req.value1.clone(), + value2: req.value2.clone(), + allowed_values: req.allowed_values.clone(), + error_title: None, + error_message: req.error_message.clone(), + input_title: None, + input_message: None, + }, + ); + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Data validation applied".to_string()), + })) +} + +pub async fn handle_validate_cell( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + if let Some(ref validations) = worksheet.validations { + if let Some(rule) = validations.get(&key) { + let result = validate_value(&req.value, rule); + return Ok(Json(result)); + } + } + + Ok(Json(ValidationResult { + valid: true, + error_message: None, + })) +} + +fn validate_value(value: &str, rule: &ValidationRule) -> ValidationResult { + let valid = match rule.validation_type.as_str() { + "number" => value.parse::().is_ok(), + "integer" => value.parse::().is_ok(), + "list" => rule + .allowed_values + .as_ref() + .map(|v| v.contains(&value.to_string())) + .unwrap_or(true), + "date" => chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok(), + "text_length" => { + let len = value.len(); + let min = rule + .value1 + .as_ref() + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + let max = rule + .value2 + .as_ref() + .and_then(|v| v.parse::().ok()) + .unwrap_or(usize::MAX); + len >= min && len <= max + } + _ => true, + }; + + ValidationResult { + valid, + error_message: if valid { + None + } else { + rule.error_message + .clone() + .or_else(|| Some("Invalid value".to_string())) + }, + } +} + +pub async fn handle_add_note( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + let cell = worksheet.data.entry(key).or_insert_with(|| CellData { + value: None, + formula: None, + style: None, + format: None, + note: None, + locked: None, + has_comment: None, + array_formula_id: None, + }); + cell.note = Some(req.note); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Note added".to_string()), + })) +} + +pub async fn handle_add_comment( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + let comment = CellComment { + id: Uuid::new_v4().to_string(), + author_id: user_id.clone(), + author_name: "User".to_string(), + content: req.content, + created_at: Utc::now(), + updated_at: Utc::now(), + replies: vec![], + resolved: false, + }; + + let comments = worksheet + .comments + .get_or_insert_with(std::collections::HashMap::new); + comments.insert(key.clone(), comment); + + let cell = worksheet.data.entry(key).or_insert_with(|| CellData { + value: None, + formula: None, + style: None, + format: None, + note: None, + locked: None, + has_comment: None, + array_formula_id: None, + }); + cell.has_comment = Some(true); + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Comment added".to_string()), + })) +} + +pub async fn handle_reply_comment( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + if let Some(comments) = &mut worksheet.comments { + if let Some(comment) = comments.get_mut(&key) { + if comment.id == req.comment_id { + let reply = CommentReply { + id: Uuid::new_v4().to_string(), + author_id: user_id.clone(), + author_name: "User".to_string(), + content: req.content, + created_at: Utc::now(), + }; + comment.replies.push(reply); + comment.updated_at = Utc::now(); + } + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Reply added".to_string()), + })) +} + +pub async fn handle_resolve_comment( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + if let Some(comments) = &mut worksheet.comments { + if let Some(comment) = comments.get_mut(&key) { + if comment.id == req.comment_id { + comment.resolved = req.resolved; + comment.updated_at = Utc::now(); + } + } + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Comment resolved".to_string()), + })) +} + +pub async fn handle_delete_comment( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let mut sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &mut sheet.worksheets[req.worksheet_index]; + let key = format!("{},{}", req.row, req.col); + + if let Some(comments) = &mut worksheet.comments { + comments.remove(&key); + } + + if let Some(cell) = worksheet.data.get_mut(&key) { + cell.has_comment = Some(false); + } + + sheet.updated_at = Utc::now(); + if let Err(e) = save_sheet_to_drive(&state, &user_id, &sheet).await { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "error": e })), + )); + } + + Ok(Json(SaveResponse { + id: req.sheet_id, + success: true, + message: Some("Comment deleted".to_string()), + })) +} + +pub async fn handle_list_comments( + State(state): State>, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let user_id = get_current_user_id(); + let sheet = match load_sheet_by_id(&state, &user_id, &req.sheet_id).await { + Ok(s) => s, + Err(e) => { + return Err(( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ "error": e })), + )) + } + }; + + if req.worksheet_index >= sheet.worksheets.len() { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "error": "Invalid worksheet index" })), + )); + } + + let worksheet = &sheet.worksheets[req.worksheet_index]; + let mut comments_list = vec![]; + + if let Some(comments) = &worksheet.comments { + for (key, comment) in comments { + let parts: Vec<&str> = key.split(',').collect(); + if parts.len() == 2 { + if let (Ok(row), Ok(col)) = (parts[0].parse::(), parts[1].parse::()) { + comments_list.push(CommentWithLocation { + row, + col, + comment: comment.clone(), + }); + } + } + } + } + + Ok(Json(ListCommentsResponse { + comments: comments_list, + })) +} diff --git a/src/sheet/storage.rs b/src/sheet/storage.rs index e59fba809..534414220 100644 --- a/src/sheet/storage.rs +++ b/src/sheet/storage.rs @@ -130,17 +130,15 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result, String> { if let Some(ref widths) = worksheet.column_widths { for (col_idx, width) in widths { let col_letter = get_col_letter(*col_idx); - if let Some(col_dim) = umya_sheet.get_column_dimension_mut(&col_letter) { - col_dim.set_width(*width as f64); - } + let col_dim = umya_sheet.get_column_dimension_mut(&col_letter); + col_dim.set_width(*width as f64); } } if let Some(ref heights) = worksheet.row_heights { for (row_idx, height) in heights { - if let Some(row_dim) = umya_sheet.get_row_dimension_mut(row_idx) { - row_dim.set_height(*height as f64); - } + let row_dim = umya_sheet.get_row_dimension_mut(row_idx); + row_dim.set_height(*height as f64); } } @@ -157,9 +155,10 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result, String> { if frozen_rows > 0 { let sheet_views = umya_sheet.get_sheet_views_mut(); if let Some(view) = sheet_views.get_sheet_view_list_mut().first_mut() { - let pane = view.get_pane_mut(); - pane.set_y_split(frozen_rows as f64); - pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen); + if let Some(pane) = view.get_pane_mut() { + pane.set_vertical_split(frozen_rows as f64); + pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen); + } } } } @@ -168,9 +167,10 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result, String> { if frozen_cols > 0 { let sheet_views = umya_sheet.get_sheet_views_mut(); if let Some(view) = sheet_views.get_sheet_view_list_mut().first_mut() { - let pane = view.get_pane_mut(); - pane.set_x_split(frozen_cols as f64); - pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen); + if let Some(pane) = view.get_pane_mut() { + pane.set_horizontal_split(frozen_cols as f64); + pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen); + } } } } @@ -337,16 +337,21 @@ pub fn load_xlsx_from_bytes( let coord = c.get_coordinate(); coord.get_col_num() == &col && coord.get_row_num() == &row }) - .map(|c| c.get_text().get_value().to_string()); + .and_then(|c| c.get_text().get_rich_text().map(|rt| rt.get_text().to_string())); + let cell_value = value.clone(); + let has_comment = note.is_some(); data.insert( key, CellData { - value: if value.is_empty() { None } else { Some(value) }, + value: Some(cell_value), formula, style, format: None, note, + locked: None, + has_comment: has_comment.then_some(true), + array_formula_id: None, }, ); } @@ -356,7 +361,8 @@ pub fn load_xlsx_from_bytes( for col in 1..=max_col { let col_letter = get_col_letter(col); if let Some(dim) = sheet.get_column_dimension(&col_letter) { - if let Some(width) = dim.get_width() { + let width = *dim.get_width(); + if width > 0.0 { column_widths.insert(col, width.round() as u32); } } @@ -364,7 +370,8 @@ pub fn load_xlsx_from_bytes( for row in 1..=max_row { if let Some(dim) = sheet.get_row_dimension(&row) { - if let Some(height) = dim.get_height() { + let height = *dim.get_height(); + if height > 0.0 { row_heights.insert(row, height.round() as u32); } } @@ -373,25 +380,28 @@ pub fn load_xlsx_from_bytes( let merged_cells: Vec = sheet.get_merge_cells() .iter() .filter_map(|mc| { - let range = mc.get_range().get_range(); + let range = mc.get_range().to_string(); parse_merge_range(&range) }) .collect(); - let frozen_rows = sheet.get_sheet_views() + let frozen_rows = sheet.get_sheets_views() .get_sheet_view_list() .first() - .and_then(|v| v.get_pane().get_y_split()) - .map(|y| y as u32); + .and_then(|v| v.get_pane()) + .map(|p| *p.get_vertical_split() as u32) + .filter(|&v| v > 0); - let frozen_cols = sheet.get_sheet_views() + let frozen_cols = sheet.get_sheets_views() .get_sheet_view_list() .first() - .and_then(|v| v.get_pane().get_x_split()) - .map(|x| x as u32); + .and_then(|v| v.get_pane()) + .map(|p| *p.get_horizontal_split() as u32) + .filter(|&v| v > 0); + let sheet_name = sheet.get_name().to_string(); worksheets.push(Worksheet { - name: sheet.get_name().to_string(), + name: sheet_name, data, column_widths: if column_widths.is_empty() { None } else { Some(column_widths) }, row_heights: if row_heights.is_empty() { None } else { Some(row_heights) }, @@ -403,10 +413,15 @@ pub fn load_xlsx_from_bytes( validations: None, conditional_formats: None, charts: None, + comments: None, + protection: None, + array_formulas: None, }); } let spreadsheet = Spreadsheet { + named_ranges: None, + external_links: None, id: Uuid::new_v4().to_string(), name: file_name.to_string(), owner_id: user_id.to_string(), @@ -424,58 +439,72 @@ fn extract_cell_style(cell: &umya_spreadsheet::Cell) -> Option { let fill = style.get_fill(); let alignment = style.get_alignment(); - let font_weight = if font.get_bold() { Some("bold".to_string()) } else { None }; - let font_style = if font.get_italic() { Some("italic".to_string()) } else { None }; + let font_weight = font.as_ref().and_then(|f| if *f.get_bold() { Some("bold".to_string()) } else { None }); + let font_style = font.as_ref().and_then(|f| if *f.get_italic() { Some("italic".to_string()) } else { None }); - let underline = font.get_underline(); - let strikethrough = font.get_strikethrough(); - let text_decoration = if underline != "none" || strikethrough { + let underline_str = font.as_ref().map(|f| f.get_underline().to_string()).unwrap_or_default(); + let has_strikethrough = font.as_ref().map(|f| *f.get_strikethrough()).unwrap_or(false); + let text_decoration = { let mut dec = Vec::new(); - if underline != "none" { + if underline_str != "none" && !underline_str.is_empty() { dec.push("underline"); } - if strikethrough { + if has_strikethrough { dec.push("line-through"); } - Some(dec.join(" ")) - } else { - None - }; - - let font_size = Some(font.get_size().round() as u32); - let font_family = Some(font.get_name().to_string()); - - let color = font.get_color().get_argb().map(|c| { - let s = c.to_string(); - if s.len() >= 8 { - format!("#{}", &s[2..]) + if dec.is_empty() { + None } else { - format!("#{s}") + Some(dec.join(" ")) } - }); + }; - let background = fill.get_pattern_fill().get_foreground_color().get_argb().map(|c| { - let s = c.to_string(); - if s.len() >= 8 { - format!("#{}", &s[2..]) + let font_size = font.as_ref().map(|f| f.get_size().round() as u32); + let font_family = font.as_ref().map(|f| f.get_name().to_string()); + + let color = font.as_ref().map(|f| { + let argb = f.get_color().get_argb(); + if argb.len() == 8 { + format!("#{}", &argb[2..]) + } else if argb.is_empty() { + "#000000".to_string() } else { - format!("#{s}") + format!("#{argb}") } - }); + }).filter(|c| c != "#000000"); - let text_align = match alignment.get_horizontal().to_string().as_str() { - "left" => Some("left".to_string()), - "center" => Some("center".to_string()), - "right" => Some("right".to_string()), - _ => None, - }; + let background = fill.and_then(|f| f.get_pattern_fill()).and_then(|pf| { + pf.get_foreground_color().map(|color| { + let argb = color.get_argb(); + if argb.len() >= 8 { + format!("#{}", &argb[2..]) + } else if argb.is_empty() { + "#FFFFFF".to_string() + } else { + format!("#{argb}") + } + }) + }).filter(|c| c != "#FFFFFF"); - let vertical_align = match alignment.get_vertical().to_string().as_str() { - "top" => Some("top".to_string()), - "center" => Some("middle".to_string()), - "bottom" => Some("bottom".to_string()), - _ => None, - }; + let text_align = alignment.map(|a| { + use umya_spreadsheet::structs::HorizontalAlignmentValues; + match a.get_horizontal() { + HorizontalAlignmentValues::Left => Some("left".to_string()), + HorizontalAlignmentValues::Center => Some("center".to_string()), + HorizontalAlignmentValues::Right => Some("right".to_string()), + _ => None, + } + }).flatten(); + + let vertical_align = alignment.map(|a| { + use umya_spreadsheet::structs::VerticalAlignmentValues; + match a.get_vertical() { + VerticalAlignmentValues::Top => Some("top".to_string()), + VerticalAlignmentValues::Center => Some("middle".to_string()), + VerticalAlignmentValues::Bottom => Some("bottom".to_string()), + _ => None, + } + }).flatten(); if font_weight.is_some() || font_style.is_some() || text_decoration.is_some() || color.is_some() || background.is_some() || text_align.is_some() { @@ -886,7 +915,7 @@ pub fn parse_ods_to_worksheets(bytes: &[u8]) -> Result, String> { let mut row_idx = 0u32; let mut in_table = false; - let mut in_row = false; + let mut col_idx = 0u32; let chars: Vec = content.chars().collect(); @@ -933,10 +962,10 @@ pub fn parse_ods_to_worksheets(bytes: &[u8]) -> Result, String> { } in_table = false; } else if tag.starts_with("table:table-row") && !tag.ends_with('/') { - in_row = true; + col_idx = 0; } else if tag == "/table:table-row" { - in_row = false; + row_idx += 1; } else if tag.starts_with("table:table-cell") { let mut cell_value = String::new(); diff --git a/src/slides/handlers.rs b/src/slides/handlers.rs index a5d3325b6..8e0eebf5a 100644 --- a/src/slides/handlers.rs +++ b/src/slides/handlers.rs @@ -5,7 +5,6 @@ use crate::slides::storage::{ get_current_user_id, list_presentations_from_drive, load_presentation_by_id, load_presentation_from_drive, save_presentation_to_drive, }; -use crate::slides::utils::slides_from_markdown; use crate::slides::types::{ AddElementRequest, AddMediaRequest, AddSlideRequest, ApplyThemeRequest, ApplyTransitionToAllRequest, CollaborationCursor, CollaborationSelection, DeleteElementRequest, diff --git a/src/slides/storage.rs b/src/slides/storage.rs index 28b85083a..444f19551 100644 --- a/src/slides/storage.rs +++ b/src/slides/storage.rs @@ -1,5 +1,5 @@ use crate::shared::state::AppState; -use crate::slides::ooxml::{load_pptx_preserving, update_pptx_text}; +use crate::slides::ooxml::update_pptx_text; use crate::slides::types::{ ElementContent, ElementStyle, Presentation, PresentationMetadata, Slide, SlideBackground, SlideElement, @@ -97,11 +97,7 @@ pub async fn save_presentation_as_pptx( let pptx_bytes = if let Some(original_bytes) = get_cached_presentation_bytes(&presentation.id).await { let slide_texts: Vec> = presentation.slides.iter().map(|slide| { slide.elements.iter().filter_map(|el| { - if let ElementContent::Text { text, .. } = &el.content { - Some(text.clone()) - } else { - None - } + el.content.text.clone() }).collect() }).collect(); update_pptx_text(&original_bytes, &slide_texts).unwrap_or_else(|_| { @@ -697,6 +693,8 @@ fn parse_slide_xml(xml_content: &str, slide_num: usize) -> Slide { }, notes: None, transition: None, + transition_config: None, + media: None, } } diff --git a/src/slides/utils.rs b/src/slides/utils.rs index e06713ccd..656bb1b54 100644 --- a/src/slides/utils.rs +++ b/src/slides/utils.rs @@ -2,7 +2,6 @@ use crate::slides::types::{ ElementContent, ElementStyle, Presentation, PresentationTheme, Slide, SlideBackground, SlideElement, ThemeColors, ThemeFonts, }; -use base64::Engine; use uuid::Uuid; pub fn create_default_theme() -> PresentationTheme { @@ -110,6 +109,8 @@ pub fn create_title_slide(theme: &PresentationTheme) -> Slide { }, notes: None, transition: None, + transition_config: None, + media: None, } } @@ -200,6 +201,8 @@ pub fn create_content_slide(theme: &PresentationTheme) -> Slide { }, notes: None, transition: None, + transition_config: None, + media: None, } } @@ -217,6 +220,8 @@ pub fn create_blank_slide(theme: &PresentationTheme) -> Slide { }, notes: None, transition: None, + transition_config: None, + media: None, } }