botserver/src/sheet/handlers.rs

2057 lines
64 KiB
Rust

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<Arc<AppState>>,
Json(req): Json<SheetAiRequest>,
) -> 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<Arc<AppState>>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
Ok(Json(create_new_spreadsheet()))
}
pub async fn handle_list_sheets(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<SpreadsheetMetadata>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Query(query): Query<SearchQuery>,
) -> Result<Json<Vec<SpreadsheetMetadata>>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Query(query): Query<LoadQuery>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<LoadFromDriveRequest>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<SaveRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<LoadQuery>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<CellUpdateRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<FormatRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<FormulaRequest>,
) -> Result<Json<FormulaResult>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ExportRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
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<ShareRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Path(sheet_id): Path<String>,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<MergeCellsRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<MergeCellsRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<FreezePanesRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<SortRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Option<CellData>>> = 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::<f64>().ok();
let num_b = val_b.parse::<f64>().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<Arc<AppState>>,
Json(req): Json<FilterRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ClearFilterRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ChartRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<DeleteChartRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ConditionalFormatRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<DataValidationRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ValidateCellRequest>,
) -> Result<Json<ValidationResult>, (StatusCode, Json<serde_json::Value>)> {
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::<f64>().is_ok(),
"integer" => value.parse::<i64>().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::<usize>().ok()).unwrap_or(0);
let max = rule.value2.as_ref().and_then(|v| v.parse::<usize>().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<Arc<AppState>>,
Json(req): Json<AddNoteRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
mut multipart: axum::extract::Multipart,
) -> Result<Json<Spreadsheet>, (StatusCode, Json<serde_json::Value>)> {
let mut file_bytes: Option<Vec<u8>> = 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<Arc<AppState>>,
Json(req): Json<AddCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ReplyCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ResolveCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<DeleteCommentRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ListCommentsRequest>,
) -> Result<Json<ListCommentsResponse>, (StatusCode, Json<serde_json::Value>)> {
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::<u32>(), parts[1].parse::<u32>()) {
comments_list.push(CommentWithLocation {
row,
col,
comment: comment.clone(),
});
}
}
}
}
Ok(Json(ListCommentsResponse { comments: comments_list }))
}
pub async fn handle_protect_sheet(
State(state): State<Arc<AppState>>,
Json(req): Json<ProtectSheetRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<UnprotectSheetRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<LockCellsRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<AddExternalLinkRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<RefreshExternalLinkRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<RemoveExternalLinkRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<ListExternalLinksResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<ArrayFormulaRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<DeleteArrayFormulaRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<CreateNamedRangeRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<UpdateNamedRangeRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Json(req): Json<DeleteNamedRangeRequest>,
) -> Result<Json<SaveResponse>, (StatusCode, Json<serde_json::Value>)> {
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<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<ListNamedRangesResponse>, (StatusCode, Json<serde_json::Value>)> {
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 }))
}