diff --git a/app/page.tsx b/app/page.tsx index fabcaf2..ea1b139 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -561,18 +561,18 @@ function ScribeContent() { try { await tauri.syncUser({ userId: user.id, - firstName: user.name, - lastName: user.lastName, username: user.username, email: user.email }); } catch (syncError) { + console.error('[Page] syncUser failed:', syncError); errorMessage(t("homePage.errors.syncError")); } } else { errorMessage(t("homePage.errors.dbInitError")); } } catch (error) { + console.error('[Page] DB init or sync failed:', error); errorMessage(t("homePage.errors.syncError")); } } diff --git a/src-tauri/src/db/schema.rs b/src-tauri/src/db/schema.rs index 477d49a..d9e3e65 100644 --- a/src-tauri/src/db/schema.rs +++ b/src-tauri/src/db/schema.rs @@ -317,25 +317,15 @@ pub fn initialize_schema(conn: &Connection) -> Result<(), rusqlite::Error> { -- Erit Users CREATE TABLE IF NOT EXISTS erit_users ( user_id TEXT PRIMARY KEY, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, username TEXT NOT NULL, email TEXT NOT NULL, origin_email TEXT NOT NULL, origin_username TEXT NOT NULL, author_name TEXT, origin_author_name TEXT, - plateform TEXT NOT NULL, social_id TEXT, user_group INTEGER NOT NULL DEFAULT 4, - password TEXT, - term_accepted INTEGER NOT NULL DEFAULT 0, - verify_code TEXT, - reg_date INTEGER NOT NULL, - account_verified INTEGER NOT NULL DEFAULT 0, - erite_points INTEGER NOT NULL DEFAULT 100, - stripe_customer_id TEXT, - credits_balance REAL DEFAULT 0 + password TEXT ); -- ========================================================================= diff --git a/src-tauri/src/domains/act/service.rs b/src-tauri/src/domains/act/service.rs index f85350a..b507b24 100644 --- a/src-tauri/src/domains/act/service.rs +++ b/src-tauri/src/domains/act/service.rs @@ -24,15 +24,6 @@ pub struct ActProps { pub chapters: Option>, } -pub struct ActStory { - pub act_id: i64, - pub summary: String, - pub chapter_summary: String, - pub chapter_goal: String, - pub incidents: Vec, - pub plot_points: Vec, -} - #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ActChapter { @@ -47,13 +38,8 @@ pub struct ActChapter { pub goal: String, } -pub struct SyncedActSummary { - pub id: String, - pub last_update: i64, -} - -pub use incident_service::{IncidentProps, IncidentStory}; -pub use plotpoint_service::{PlotPointProps, PlotPointStory}; +pub use incident_service::IncidentProps; +pub use plotpoint_service::PlotPointProps; #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/domains/book/commands.rs b/src-tauri/src/domains/book/commands.rs index cea8052..d146f86 100644 --- a/src-tauri/src/domains/book/commands.rs +++ b/src-tauri/src/domains/book/commands.rs @@ -444,7 +444,7 @@ pub fn export_book(data: ExportBookData, db: State, session: State { let result = export_service::transform_to_epub(&book_data)?; Ok(result.buffer) }, + "epub" => { let result = export_service::transform_to_epub(&book_data, lang)?; Ok(result.buffer) }, "pdf" => { let result = export_service::transform_to_pdf(&book_data)?; Ok(result.buffer) }, "docx" => { let result = export_service::transform_to_docx(&book_data)?; Ok(result.buffer) }, _ => Err(AppError::Validation(if lang == crate::shared::types::Lang::Fr { "Format non supporté.".to_string() } else { "Unsupported format.".to_string() })), diff --git a/src-tauri/src/domains/book/repo.rs b/src-tauri/src/domains/book/repo.rs index 056dbcf..0967972 100644 --- a/src-tauri/src/domains/book/repo.rs +++ b/src-tauri/src/domains/book/repo.rs @@ -9,9 +9,7 @@ pub struct BookQuery { pub book_type: String, pub author_id: String, pub title: String, - pub hashed_title: String, pub sub_title: Option, - pub hashed_sub_title: Option, pub summary: Option, pub serie_id: Option, pub desired_release_date: Option, @@ -86,8 +84,7 @@ pub fn fetch_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult, - pub last_update: i64, - pub chapters: Vec, - pub characters: Vec, - pub locations: Vec, - pub worlds: Vec, - pub incidents: Vec, - pub plot_points: Vec, - pub issues: Vec, - pub act_summaries: Vec, - pub guide_line: Option, - pub ai_guide_line: Option, - pub book_tools: Option, - pub spells: Vec, - pub spell_tags: Vec, -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct BookSyncCompare { @@ -115,43 +91,12 @@ pub struct BookSyncCompare { pub act_summaries: Vec, pub guide_line: bool, pub ai_guide_line: bool, - pub book_tools: bool, + #[serde(rename = "bookTools")] + pub _book_tools: bool, pub spells: Vec, pub spell_tags: Vec, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SyncedChapter { - pub id: String, - pub title: String, - pub last_update: i64, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SyncedCharacter { - pub id: String, - pub name: String, - pub last_update: i64, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SyncedLocation { - pub id: String, - pub name: String, - pub last_update: i64, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SyncedWorld { - pub id: String, - pub name: String, - pub last_update: i64, -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SyncedIncident { @@ -966,8 +911,7 @@ pub fn complete_book_data(conn: &Connection, user_id: &str, book_id: &str, lang: summary: if let Some(ref summary) = book_data.summary { decrypt_data_with_user_key(summary, &user_key)? } else { String::new() }, cover_image, user_infos: BookUserInfos { - first_name: if let Some(ref first_name) = user_infos.first_name { decrypt_data_with_user_key(first_name, &user_key)? } else { String::new() }, - last_name: if let Some(ref last_name) = user_infos.last_name { decrypt_data_with_user_key(last_name, &user_key)? } else { String::new() }, + username: decrypt_data_with_user_key(&user_infos.username, &user_key)?, author_name: if let Some(ref author_name) = user_infos.author_name { decrypt_data_with_user_key(author_name, &user_key)? } else { String::new() }, }, chapters: decrypted_chapters, diff --git a/src-tauri/src/domains/chapter/commands.rs b/src-tauri/src/domains/chapter/commands.rs index 46ba438..f16f52f 100644 --- a/src-tauri/src/domains/chapter/commands.rs +++ b/src-tauri/src/domains/chapter/commands.rs @@ -89,7 +89,6 @@ pub struct SaveChapterContentData { pub version: i64, pub content: Value, pub total_word_count: i64, - pub content_id: String, } #[tauri::command] diff --git a/src-tauri/src/domains/chapter/repo.rs b/src-tauri/src/domains/chapter/repo.rs index 6c1b1d8..377c6ee 100644 --- a/src-tauri/src/domains/chapter/repo.rs +++ b/src-tauri/src/domains/chapter/repo.rs @@ -23,7 +23,7 @@ pub struct ActChapterQuery { } pub struct ChapterStoryQueryResult { - pub chapter_info_id: i64, + pub _chapter_info_id: i64, pub act_id: i64, pub summary: String, pub chapter_summary: String, @@ -47,18 +47,18 @@ pub struct BookChaptersTable { pub author_id: String, pub title: String, pub hashed_title: String, - pub words_count: Option, + pub _words_count: Option, pub chapter_order: i64, pub last_update: i64, } pub struct BookChapterInfosTable { - pub chapter_info_id: String, + pub _chapter_info_id: String, pub chapter_id: String, - pub act_id: Option, - pub incident_id: Option, - pub plot_point_id: Option, - pub book_id: String, + pub _act_id: Option, + pub _incident_id: Option, + pub _plot_point_id: Option, + pub _book_id: String, pub author_id: String, pub summary: Option, pub goal: Option, @@ -75,7 +75,7 @@ pub struct SyncedChapterResult { pub struct SyncedChapterInfoResult { pub chapter_info_id: String, pub chapter_id: Option, - pub book_id: String, + pub _book_id: String, pub last_update: i64, } @@ -97,7 +97,7 @@ pub struct SelectedChapterContentResult { pub title: String, pub chapter_order: i64, pub content: String, - pub version: i64, + pub _version: i64, } #[derive(serde::Deserialize)] @@ -307,7 +307,7 @@ pub fn fetch_chapter_story(conn: &Connection, user_id: &str, chapter_id: &str, l let rows = statement .query_map(params![chapter_id, user_id], |query_row| { Ok(ChapterStoryQueryResult { - chapter_info_id: query_row.get(0)?, act_id: query_row.get(1)?, + _chapter_info_id: query_row.get(0)?, act_id: query_row.get(1)?, summary: query_row.get::<_, Option>(2)?.unwrap_or_default(), chapter_summary: query_row.get::<_, Option>(3)?.unwrap_or_default(), chapter_goal: query_row.get::<_, Option>(4)?.unwrap_or_default(), @@ -393,7 +393,7 @@ pub fn fetch_book_chapters(conn: &Connection, user_id: &str, book_id: &str, lang Ok(BookChaptersTable { chapter_id: query_row.get(0)?, book_id: query_row.get(1)?, author_id: query_row.get(2)?, title: query_row.get(3)?, - hashed_title: query_row.get(4)?, words_count: query_row.get(5)?, + hashed_title: query_row.get(4)?, _words_count: query_row.get(5)?, chapter_order: query_row.get(6)?, last_update: query_row.get(7)?, }) }) @@ -413,9 +413,9 @@ pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &s let rows = statement .query_map(params![user_id, chapter_id], |query_row| { Ok(BookChapterInfosTable { - chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?, - act_id: query_row.get(2)?, incident_id: query_row.get(3)?, - plot_point_id: query_row.get(4)?, book_id: query_row.get(5)?, + _chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?, + _act_id: query_row.get(2)?, _incident_id: query_row.get(3)?, + _plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?, author_id: query_row.get(6)?, summary: query_row.get(7)?, goal: query_row.get(8)?, last_update: query_row.get(9)?, }) @@ -457,7 +457,7 @@ pub fn fetch_synced_chapter_infos(conn: &Connection, user_id: &str, lang: Lang) .query_map(params![user_id], |query_row| { Ok(SyncedChapterInfoResult { chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?, - book_id: query_row.get(2)?, last_update: query_row.get(3)?, + _book_id: query_row.get(2)?, last_update: query_row.get(3)?, }) }) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter infos.".to_string() }))? @@ -500,7 +500,7 @@ pub fn fetch_complete_chapter_by_id(conn: &Connection, chapter_id: &str, lang: L Ok(BookChaptersTable { chapter_id: query_row.get(0)?, book_id: query_row.get(1)?, author_id: query_row.get(2)?, title: query_row.get(3)?, - hashed_title: query_row.get(4)?, words_count: query_row.get(5)?, + hashed_title: query_row.get(4)?, _words_count: query_row.get(5)?, chapter_order: query_row.get(6)?, last_update: query_row.get(7)?, }) }) @@ -520,9 +520,9 @@ pub fn fetch_complete_chapter_info_by_id(conn: &Connection, chapter_info_id: &st let rows = statement .query_map(params![chapter_info_id], |query_row| { Ok(BookChapterInfosTable { - chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?, - act_id: query_row.get(2)?, incident_id: query_row.get(3)?, - plot_point_id: query_row.get(4)?, book_id: query_row.get(5)?, + _chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?, + _act_id: query_row.get(2)?, _incident_id: query_row.get(3)?, + _plot_point_id: query_row.get(4)?, _book_id: query_row.get(5)?, author_id: query_row.get(6)?, summary: query_row.get(7)?, goal: query_row.get(8)?, last_update: query_row.get(9)?, }) @@ -581,7 +581,7 @@ pub fn fetch_selected_chapters_content(conn: &Connection, book_id: &str, selecti Ok(SelectedChapterContentResult { chapter_id: query_row.get(0)?, title: query_row.get(1)?, chapter_order: query_row.get(2)?, content: query_row.get(3)?, - version: query_row.get(4)?, + _version: query_row.get(4)?, }) }) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres sélectionnés.".to_string() } else { "Unable to retrieve selected chapters content.".to_string() }))? diff --git a/src-tauri/src/domains/chapter/service.rs b/src-tauri/src/domains/chapter/service.rs index 095178a..2e0008d 100644 --- a/src-tauri/src/domains/chapter/service.rs +++ b/src-tauri/src/domains/chapter/service.rs @@ -24,10 +24,7 @@ pub struct ChapterContent { pub struct ChapterContentData { pub title: String, - pub chapter_order: i64, pub content: String, - pub words_count: i64, - pub version: i64, } #[derive(Serialize)] @@ -47,24 +44,6 @@ pub struct CompanionContent { pub words_count: i64, } -pub struct SyncedChapter { - pub id: String, - pub name: String, - pub last_update: i64, - pub contents: Vec, - pub info: Option, -} - -pub struct SyncedChapterContent { - pub id: String, - pub last_update: i64, -} - -pub struct SyncedChapterInfo { - pub id: String, - pub last_update: i64, -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CompleteChapterContent { @@ -601,10 +580,7 @@ pub fn get_chapters_or_sheet(book_chapters: &[CompleteChapterContent]) -> Vec Vec, - pub words_count: i64, - pub time_on_it: i64, + pub _words_count: i64, + pub _time_on_it: i64, pub last_update: i64, } @@ -188,8 +188,8 @@ pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id: Ok(BookChapterContentTable { content_id: query_row.get(0)?, chapter_id: query_row.get(1)?, author_id: query_row.get(2)?, version: query_row.get(3)?, - content: query_row.get(4)?, words_count: query_row.get(5)?, - time_on_it: query_row.get(6)?, last_update: query_row.get(7)?, + content: query_row.get(4)?, _words_count: query_row.get(5)?, + _time_on_it: query_row.get(6)?, last_update: query_row.get(7)?, }) }) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))? @@ -261,8 +261,8 @@ pub fn fetch_complete_chapter_content_by_id(conn: &Connection, content_id: &str, Ok(BookChapterContentTable { content_id: query_row.get(0)?, chapter_id: query_row.get(1)?, author_id: query_row.get(2)?, version: query_row.get(3)?, - content: query_row.get(4)?, words_count: query_row.get(5)?, - time_on_it: query_row.get(6)?, last_update: query_row.get(7)?, + content: query_row.get(4)?, _words_count: query_row.get(5)?, + _time_on_it: query_row.get(6)?, last_update: query_row.get(7)?, }) }) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu de chapitre complet.".to_string() } else { "Unable to retrieve complete chapter content.".to_string() }))? diff --git a/src-tauri/src/domains/character/commands.rs b/src-tauri/src/domains/character/commands.rs index 903689a..d228deb 100644 --- a/src-tauri/src/domains/character/commands.rs +++ b/src-tauri/src/domains/character/commands.rs @@ -18,7 +18,6 @@ fn get_session(session: &State) -> Result<(String, Lang), AppError #[serde(rename_all = "camelCase")] pub struct GetCharacterListData { pub book_id: String, - pub enabled: bool, } #[tauri::command] diff --git a/src-tauri/src/domains/character/service.rs b/src-tauri/src/domains/character/service.rs index 4f8dcbd..7adf2f5 100644 --- a/src-tauri/src/domains/character/service.rs +++ b/src-tauri/src/domains/character/service.rs @@ -106,19 +106,6 @@ pub struct CharacterAttribute { pub values: Vec, } -pub struct SyncedCharacter { - pub id: String, - pub name: String, - pub last_update: i64, - pub attributes: Vec, -} - -pub struct SyncedCharacterAttribute { - pub id: String, - pub name: String, - pub last_update: i64, -} - /// Retrieves a list of all characters for a specific book. /// Decrypts character data using the user's encryption key. /// * `conn` - Database connection diff --git a/src-tauri/src/domains/export/service.rs b/src-tauri/src/domains/export/service.rs index fa39e65..4f5fa89 100644 --- a/src-tauri/src/domains/export/service.rs +++ b/src-tauri/src/domains/export/service.rs @@ -10,6 +10,7 @@ use serde_json::Value; use crate::domains::book::service::{CompleteBookData, CompleteChapterContent}; use crate::domains::chapter::service::{get_chapters_or_sheet, tip_tap_to_html, ChapterContentData}; use crate::error::{AppError, AppResult}; +use crate::shared::types::Lang; pub const MAIN_STYLE: &str = r#"h1 { font-size: 24px !important; @@ -25,7 +26,6 @@ p { pub struct ExportResult { pub buffer: Vec, - pub file_name: String, } /// Transforms book data into a DOCX document. @@ -33,7 +33,6 @@ pub struct ExportResult { /// Returns the DOCX buffer and filename. pub fn transform_to_docx(book_data: &CompleteBookData) -> AppResult { let book_title: &str = &book_data.title; - let filename: String = format!("{}.docx", book_title); let mut docx: Docx = Docx::new(); @@ -92,7 +91,7 @@ pub fn transform_to_docx(book_data: &CompleteBookData) -> AppResult AppResult AppResult { let book_title: &str = &book_data.title; - let filename: String = format!("{}.pdf", book_title); let (pdf_document, page_index, layer_index) = PdfDocument::new(book_title, Mm(210.0), Mm(297.0), "Title Page"); let font = pdf_document @@ -150,13 +148,13 @@ pub fn transform_to_pdf(book_data: &CompleteBookData) -> AppResult .save_to_bytes() .map_err(|error| AppError::Internal(format!("PDF generation failed: {}", error)))?; - Ok(ExportResult { buffer, file_name: filename }) + Ok(ExportResult { buffer }) } /// Transforms book data into an EPUB document. /// * `book_data` - The complete book data to export /// Returns the EPUB buffer and filename. -pub fn transform_to_epub(book_data: &CompleteBookData) -> AppResult { +pub fn transform_to_epub(book_data: &CompleteBookData, lang: Lang) -> AppResult { let book_title: &str = &book_data.title; let book_id: &str = &book_data.book_id; @@ -179,8 +177,11 @@ pub fn transform_to_epub(book_data: &CompleteBookData) -> AppResult AppResult, pub themes: Option, pub verbe_tense: Option, pub narrative_type: Option, pub langue: Option, pub dialogue_type: Option, - pub tone: Option, + pub _tone: Option, pub atmosphere: Option, pub current_resume: Option, - pub meta: String, + pub _meta: String, } /// Fetches the guideline for a specific book. @@ -211,8 +211,8 @@ pub fn fetch_guide_line_ai(conn: &Connection, user_id: &str, book_id: &str, lang global_resume: query_row.get(2)?, atmosphere: query_row.get(3)?, verbe_tense: query_row.get(4)?, langue: query_row.get(5)?, themes: query_row.get(6)?, current_resume: query_row.get(7)?, - user_id: user_id.to_string(), book_id: book_id.to_string(), - tone: None, meta: String::new(), + _user_id: user_id.to_string(), _book_id: book_id.to_string(), + _tone: None, _meta: String::new(), }) }) .map_err(|error| match error { diff --git a/src-tauri/src/domains/guideline/service.rs b/src-tauri/src/domains/guideline/service.rs index 9fc46ff..ecde5d8 100644 --- a/src-tauri/src/domains/guideline/service.rs +++ b/src-tauri/src/domains/guideline/service.rs @@ -8,16 +8,6 @@ use crate::error::{AppError, AppResult}; use crate::helpers::timestamp_in_seconds; use crate::shared::types::Lang; -/// Represents the synced guideline data for a book. -pub struct SyncedGuideLine { - pub last_update: i64, -} - -/// Represents the synced AI guideline data for a book. -pub struct SyncedAIGuideLine { - pub last_update: i64, -} - /// Represents the decrypted guideline properties for a book. #[derive(Serialize)] #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/domains/incident/service.rs b/src-tauri/src/domains/incident/service.rs index ee5fa57..572ec95 100644 --- a/src-tauri/src/domains/incident/service.rs +++ b/src-tauri/src/domains/incident/service.rs @@ -10,21 +10,6 @@ use crate::error::AppResult; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; -/// Represents the story details of an incident within a chapter. -pub struct IncidentStory { - pub incident_title: String, - pub incident_summary: String, - pub chapter_summary: String, - pub chapter_goal: String, -} - -/// Represents a synced incident with minimal information for comparison. -pub struct SyncedIncident { - pub id: String, - pub name: String, - pub last_update: i64, -} - #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IncidentProps { diff --git a/src-tauri/src/domains/issue/service.rs b/src-tauri/src/domains/issue/service.rs index 78110b5..8cd72b7 100644 --- a/src-tauri/src/domains/issue/service.rs +++ b/src-tauri/src/domains/issue/service.rs @@ -9,13 +9,6 @@ use crate::error::AppResult; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; -/// Represents a synced issue with its metadata. -pub struct SyncedIssue { - pub id: String, - pub name: String, - pub last_update: i64, -} - #[derive(Serialize)] pub struct IssueProps { pub id: String, diff --git a/src-tauri/src/domains/location/commands.rs b/src-tauri/src/domains/location/commands.rs index 4c037ef..21a6b68 100644 --- a/src-tauri/src/domains/location/commands.rs +++ b/src-tauri/src/domains/location/commands.rs @@ -18,7 +18,6 @@ fn get_session(session: &State) -> Result<(String, Lang), AppError #[serde(rename_all = "camelCase")] pub struct GetAllLocationsData { pub book_id: String, - pub enabled: bool, } #[tauri::command] diff --git a/src-tauri/src/domains/location/repo.rs b/src-tauri/src/domains/location/repo.rs index 6b76733..076eec5 100644 --- a/src-tauri/src/domains/location/repo.rs +++ b/src-tauri/src/domains/location/repo.rs @@ -24,13 +24,6 @@ pub struct LocationElementQueryResult { pub element_description: Option, } -pub struct LocationByTagResult { - pub element_name: String, - pub element_description: Option, - pub sub_elem_name: Option, - pub sub_elem_description: Option, -} - pub struct BookLocationTable { pub loc_id: String, pub book_id: String, @@ -349,51 +342,6 @@ pub fn fetch_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang /// * `conn` - Database connection /// * `user_id` - The user's unique identifier /// * `locations` - An array of location tag IDs to search for -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of locations matching the provided tags. -/// Errors if no tags are provided or no locations are found. -pub fn fetch_locations_by_tags(conn: &Connection, user_id: &str, locations: &[String], lang: Lang) -> AppResult> { - if locations.is_empty() { - return Err(AppError::Validation(if lang == Lang::Fr { "Aucun tag fourni.".to_string() } else { "No tags provided.".to_string() })); - } - - let location_placeholders: String = locations.iter().map(|_| "?").collect::>().join(","); - let query = format!("SELECT el.element_name, el.element_description, se.sub_elem_name, se.sub_elem_description FROM location_element AS el LEFT JOIN location_sub_element AS se ON el.element_id = se.element_id WHERE el.user_id = ?1 AND (el.element_id IN ({}) OR se.sub_element_id IN ({}))", location_placeholders, location_placeholders); - - let mut statement = conn - .prepare(&query) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?; - - let mut param_values: Vec> = Vec::new(); - param_values.push(Box::new(user_id.to_string())); - for location in locations { - param_values.push(Box::new(location.clone())); - } - for location in locations { - param_values.push(Box::new(location.clone())); - } - let param_refs: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|parameter| parameter.as_ref() as &dyn rusqlite::types::ToSql).collect(); - - let locations_by_tags = statement - .query_map(param_refs.as_slice(), |query_row| { - Ok(LocationByTagResult { - element_name: query_row.get(0)?, - element_description: query_row.get(1)?, - sub_elem_name: query_row.get(2)?, - sub_elem_description: query_row.get(3)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?; - - if locations_by_tags.is_empty() { - return Err(AppError::NotFound(if lang == Lang::Fr { "Aucun emplacement trouvé avec ces tags.".to_string() } else { "No locations found with these tags.".to_string() })); - } - - Ok(locations_by_tags) -} - /// Checks if a location exists in the database. /// * `conn` - Database connection /// * `user_id` - The user's unique identifier diff --git a/src-tauri/src/domains/location/service.rs b/src-tauri/src/domains/location/service.rs index 86ab42d..65a1bcb 100644 --- a/src-tauri/src/domains/location/service.rs +++ b/src-tauri/src/domains/location/service.rs @@ -41,29 +41,6 @@ pub struct LocationListResponse { pub enabled: bool, } -/// Synced location sub-element (lightweight, for comparison). -pub struct SyncedLocationSubElement { - pub id: String, - pub name: String, - pub last_update: i64, -} - -/// Synced location element (lightweight, for comparison). -pub struct SyncedLocationElement { - pub id: String, - pub name: String, - pub last_update: i64, - pub sub_elements: Vec, -} - -/// Synced location (lightweight, for comparison). -pub struct SyncedLocation { - pub id: String, - pub name: String, - pub last_update: i64, - pub elements: Vec, -} - /// Retrieves all locations for a given user and book. /// * `conn` - Database connection /// * `user_id` - The user's unique identifier @@ -383,80 +360,3 @@ pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang: Ok(sub_elements) } -/// Retrieves location elements filtered by specific tag IDs. -/// * `conn` - Database connection -/// * `user_id` - The user's unique identifier -/// * `locations` - Array of location tag IDs to filter by -/// * `lang` - The language for error messages -/// Returns an array of elements with their associated sub-elements. -pub fn get_locations_by_tags(conn: &Connection, user_id: &str, locations: &[String], lang: Lang) -> AppResult> { - let location_tag_records: Vec = repo::fetch_locations_by_tags(conn, user_id, locations, lang)?; - if location_tag_records.is_empty() { - return Ok(vec![]); - } - - let user_key: String = get_user_encryption_key(user_id)?; - let mut location_elements: Vec = Vec::new(); - - for record in &location_tag_records { - let element_index: Option = location_elements.iter().position(|elem| elem.name == record.element_name); - - let element_idx: usize = match element_index { - Some(idx) => idx, - None => { - let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?; - let decrypted_description: String = if let Some(ref element_description) = record.element_description { - decrypt_data_with_user_key(element_description, &user_key)? - } else { - String::new() - }; - location_elements.push(Element { - id: String::new(), - name: decrypted_name, - description: decrypted_description, - sub_elements: vec![], - }); - location_elements.len() - 1 - } - }; - - if let Some(ref sub_elem_name) = record.sub_elem_name { - let sub_element_exists: bool = location_elements[element_idx].sub_elements.iter().any(|sub| sub.name == *sub_elem_name); - if !sub_element_exists { - let decrypted_name: String = decrypt_data_with_user_key(sub_elem_name, &user_key)?; - let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description { - decrypt_data_with_user_key(sub_elem_description, &user_key)? - } else { - String::new() - }; - location_elements[element_idx].sub_elements.push(SubElement { - id: String::new(), - name: decrypted_name, - description: decrypted_description, - }); - } - } - } - - Ok(location_elements) -} - -/// Generates a formatted description string from an array of location elements. -/// * `locations` - Array of location elements to describe -/// Returns a formatted string with location names and descriptions. -pub fn locations_description(locations: &[Element]) -> String { - locations - .iter() - .map(|location| { - let mut description_fields: Vec = Vec::new(); - if !location.name.is_empty() { - description_fields.push(format!("Nom : {}", location.name)); - } - if !location.description.is_empty() { - description_fields.push(format!("Description : {}", location.description)); - } - description_fields.join("\n") - }) - .collect::>() - .join("\n\n") -} diff --git a/src-tauri/src/domains/offline/commands.rs b/src-tauri/src/domains/offline/commands.rs index 0242d5f..41b105f 100644 --- a/src-tauri/src/domains/offline/commands.rs +++ b/src-tauri/src/domains/offline/commands.rs @@ -31,10 +31,7 @@ pub struct OfflineModeStatus { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -pub struct OfflineModeData { - pub enabled: bool, - pub sync_interval_days: Option, -} +pub struct OfflineModeData {} #[derive(Serialize)] #[serde(rename_all = "camelCase")] diff --git a/src-tauri/src/domains/plotpoint/repo.rs b/src-tauri/src/domains/plotpoint/repo.rs index 1387398..77e5330 100644 --- a/src-tauri/src/domains/plotpoint/repo.rs +++ b/src-tauri/src/domains/plotpoint/repo.rs @@ -10,7 +10,7 @@ pub struct BookPlotPointsTable { pub summary: Option, pub linked_incident_id: Option, pub author_id: String, - pub book_id: String, + pub _book_id: String, pub last_update: i64, } @@ -152,7 +152,7 @@ pub fn fetch_book_plot_points(conn: &Connection, user_id: &str, book_id: &str, l summary: query_row.get(3)?, linked_incident_id: query_row.get(4)?, author_id: query_row.get(5)?, - book_id: query_row.get(6)?, + _book_id: query_row.get(6)?, last_update: query_row.get(7)?, }) }) @@ -235,7 +235,7 @@ pub fn fetch_complete_plot_point_by_id(conn: &Connection, plot_point_id: &str, l summary: query_row.get(3)?, linked_incident_id: query_row.get(4)?, author_id: query_row.get(5)?, - book_id: query_row.get(6)?, + _book_id: query_row.get(6)?, last_update: query_row.get(7)?, }) }) diff --git a/src-tauri/src/domains/plotpoint/service.rs b/src-tauri/src/domains/plotpoint/service.rs index 7f22912..3e5fcdf 100644 --- a/src-tauri/src/domains/plotpoint/service.rs +++ b/src-tauri/src/domains/plotpoint/service.rs @@ -10,14 +10,6 @@ use crate::error::AppResult; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; -/// Represents the story details associated with a plot point. -pub struct PlotPointStory { - pub plot_title: String, - pub plot_summary: String, - pub chapter_summary: String, - pub chapter_goal: String, -} - #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PlotPointProps { @@ -28,13 +20,6 @@ pub struct PlotPointProps { pub chapters: Option>, } -/// Represents a synced plot point with minimal information. -pub struct SyncedPlotPoint { - pub id: String, - pub name: String, - pub last_update: i64, -} - /// Retrieves all plot points for a specific book with their associated chapters. /// Decrypts plot point titles and summaries using the user's encryption key. /// * `conn` - Database connection diff --git a/src-tauri/src/domains/series/repo.rs b/src-tauri/src/domains/series/repo.rs index f1b7e44..0e904a5 100644 --- a/src-tauri/src/domains/series/repo.rs +++ b/src-tauri/src/domains/series/repo.rs @@ -5,16 +5,16 @@ use crate::shared::types::Lang; pub struct SeriesResult { pub series_id: String, - pub user_id: String, + pub _user_id: String, pub name: String, - pub hashed_name: String, + pub _hashed_name: String, pub description: Option, pub cover_image: Option, - pub last_update: i64, + pub _last_update: i64, } pub struct SeriesBookResult { - pub series_id: String, + pub _series_id: String, pub book_id: String, pub book_order: i64, pub title: String, @@ -105,10 +105,10 @@ pub fn fetch_series_by_id(conn: &Connection, user_id: &str, series_id: &str, lan let series = statement .query_row(params![series_id, user_id], |query_row| { Ok(SeriesResult { - series_id: query_row.get(0)?, user_id: query_row.get(1)?, - name: query_row.get(2)?, hashed_name: query_row.get(3)?, + series_id: query_row.get(0)?, _user_id: query_row.get(1)?, + name: query_row.get(2)?, _hashed_name: query_row.get(3)?, description: query_row.get(4)?, cover_image: query_row.get(5)?, - last_update: query_row.get(6)?, + _last_update: query_row.get(6)?, }) }); @@ -199,7 +199,7 @@ pub fn fetch_series_books(conn: &Connection, user_id: &str, series_id: &str, lan let books = statement .query_map(params![series_id, user_id], |query_row| { Ok(SeriesBookResult { - series_id: query_row.get(0)?, book_id: query_row.get(1)?, + _series_id: query_row.get(0)?, book_id: query_row.get(1)?, book_order: query_row.get(2)?, title: query_row.get(3)?, cover_image: query_row.get(4)?, }) @@ -403,32 +403,6 @@ pub fn fetch_synced_series_books(conn: &Connection, user_id: &str, lang: Lang) - Ok(books) } -/// Fetches a complete series by ID for sync. -/// * `conn` - Database connection -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array containing the series. -pub fn fetch_complete_series_by_id(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?; - - let series = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesTableResult { - series_id: query_row.get(0)?, user_id: query_row.get(1)?, - name: query_row.get(2)?, hashed_name: query_row.get(3)?, - description: query_row.get(4)?, cover_image: query_row.get(5)?, - last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?; - - Ok(series) -} - /// Inserts a series for sync purposes. /// * `conn` - Database connection /// * `series_id` - The unique identifier of the series @@ -498,27 +472,6 @@ pub fn insert_sync_series_book(conn: &Connection, series_id: &str, book_id: &str Ok(insert_result > 0) } -/// Checks if a series-book relationship exists. -/// * `conn` - Database connection -/// * `series_id` - The unique identifier of the series -/// * `book_id` - The unique identifier of the book -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the relationship exists. -pub fn is_series_book_exist(conn: &Connection, series_id: &str, book_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_books WHERE series_id = ?1 AND book_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la liaison série-livre.".to_string() } else { "Unable to check series-book.".to_string() }))?; - - let exists = statement - .query_row(params![series_id, book_id], |_query_row| Ok(true)); - - match exists { - Ok(_) => Ok(true), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), - Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la liaison série-livre.".to_string() } else { "Unable to check series-book.".to_string() })), - } -} - /// Checks if a series exists for a user (alias for is_series_exist). /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user diff --git a/src-tauri/src/domains/series/service.rs b/src-tauri/src/domains/series/service.rs index cc02dca..d34748a 100644 --- a/src-tauri/src/domains/series/service.rs +++ b/src-tauri/src/domains/series/service.rs @@ -9,15 +9,6 @@ use crate::error::{AppError, AppResult}; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SeriesProps { - pub id: String, - pub name: String, - pub description: String, - pub cover_image: Option, -} - #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SeriesDetailProps { diff --git a/src-tauri/src/domains/series_character/repo.rs b/src-tauri/src/domains/series_character/repo.rs index 4063504..cfa9514 100644 --- a/src-tauri/src/domains/series_character/repo.rs +++ b/src-tauri/src/domains/series_character/repo.rs @@ -1,746 +1,592 @@ -use rusqlite::{params, Connection, OptionalExtension}; - -use crate::error::{AppError, AppResult}; -use crate::shared::types::Lang; - -pub struct SeriesCharacterResult { - pub character_id: String, - pub first_name: String, - pub last_name: String, - pub nickname: Option, - pub age: Option, - pub gender: Option, - pub species: Option, - pub nationality: Option, - pub status: Option, - pub title: String, - pub category: String, - pub image: String, - pub role: String, - pub biography: String, - pub history: String, - pub speech_pattern: Option, - pub catchphrase: Option, - pub residence: Option, - pub notes: Option, - pub color: Option, -} - -pub struct SeriesCharacterAttributeResult { - pub attr_id: String, - pub attribute_name: String, - pub attribute_value: String, -} - -pub struct SeriesCharactersTableResult { - pub character_id: String, - pub series_id: String, - pub user_id: String, - pub first_name: String, - pub last_name: Option, - pub nickname: Option, - pub age: Option, - pub gender: Option, - pub species: Option, - pub nationality: Option, - pub status: Option, - pub title: Option, - pub category: String, - pub image: Option, - pub role: Option, - pub biography: Option, - pub history: Option, - pub speech_pattern: Option, - pub catchphrase: Option, - pub residence: Option, - pub notes: Option, - pub color: Option, - pub last_update: i64, -} - -pub struct SeriesCharacterAttributesTableResult { - pub attr_id: String, - pub character_id: String, - pub user_id: String, - pub attribute_name: String, - pub attribute_value: String, - pub last_update: i64, -} - -pub struct SyncedSeriesCharacterResult { - pub character_id: String, - pub series_id: String, - pub first_name: String, - pub last_update: i64, -} - -pub struct SyncedSeriesCharacterAttributeResult { - pub attr_id: String, - pub character_id: String, - pub attribute_name: String, - pub last_update: i64, -} - -/// Fetches all characters for a specific series owned by the user. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of character results. -pub fn fetch_characters(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color FROM series_characters WHERE series_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de la s\u{00e9}rie.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?; - - let characters = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesCharacterResult { - character_id: query_row.get(0)?, first_name: query_row.get(1)?, - last_name: query_row.get(2)?, nickname: query_row.get(3)?, - age: query_row.get(4)?, gender: query_row.get(5)?, - species: query_row.get(6)?, nationality: query_row.get(7)?, - status: query_row.get(8)?, title: query_row.get(9)?, - category: query_row.get(10)?, image: query_row.get(11)?, - role: query_row.get(12)?, biography: query_row.get(13)?, - history: query_row.get(14)?, speech_pattern: query_row.get(15)?, - catchphrase: query_row.get(16)?, residence: query_row.get(17)?, - notes: query_row.get(18)?, color: query_row.get(19)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de la s\u{00e9}rie.".to_string() } else { "Unable to retrieve series characters.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de la s\u{00e9}rie.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?; - - Ok(characters) -} - -/// Adds a new character to a series. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `encrypted_name` - The encrypted first name -/// * `encrypted_last_name` - The encrypted last name -/// * `encrypted_nickname` - The encrypted nickname -/// * `encrypted_age` - The encrypted age -/// * `encrypted_gender` - The encrypted gender -/// * `encrypted_species` - The encrypted species -/// * `encrypted_nationality` - The encrypted nationality -/// * `encrypted_status` - The encrypted status -/// * `encrypted_title` - The encrypted title -/// * `encrypted_category` - The encrypted category -/// * `encrypted_image` - The encrypted image -/// * `encrypted_role` - The encrypted role -/// * `encrypted_biography` - The encrypted biography -/// * `encrypted_history` - The encrypted history -/// * `encrypted_speech_pattern` - The encrypted speech pattern -/// * `encrypted_catchphrase` - The encrypted catchphrase -/// * `encrypted_residence` - The encrypted residence -/// * `encrypted_notes` - The encrypted notes -/// * `encrypted_color` - The encrypted color -/// * `series_id` - The unique identifier of the series -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns the character ID if insertion was successful. -pub fn add_new_character( - conn: &Connection, user_id: &str, character_id: &str, encrypted_name: &str, - encrypted_last_name: Option<&str>, encrypted_nickname: Option<&str>, encrypted_age: Option<&str>, - encrypted_gender: Option<&str>, encrypted_species: Option<&str>, encrypted_nationality: Option<&str>, - encrypted_status: Option<&str>, encrypted_title: Option<&str>, encrypted_category: Option<&str>, - encrypted_image: Option<&str>, encrypted_role: Option<&str>, encrypted_biography: Option<&str>, - encrypted_history: Option<&str>, encrypted_speech_pattern: Option<&str>, encrypted_catchphrase: Option<&str>, - encrypted_residence: Option<&str>, encrypted_notes: Option<&str>, encrypted_color: Option<&str>, - series_id: &str, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23)", - params![character_id, series_id, user_id, encrypted_name, encrypted_last_name, encrypted_nickname, encrypted_age, encrypted_gender, encrypted_species, encrypted_nationality, encrypted_status, encrypted_category, encrypted_title, encrypted_image, encrypted_role, encrypted_biography, encrypted_history, encrypted_speech_pattern, encrypted_catchphrase, encrypted_residence, encrypted_notes, encrypted_color, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le personnage.".to_string() } else { "Unable to add character.".to_string() }))?; - - if insert_result > 0 { - Ok(character_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du personnage.".to_string() } else { "Error adding character.".to_string() })) - } -} - -/// Inserts a new attribute for a series character. -/// * `conn` - Database connection -/// * `attribute_id` - The unique identifier of the attribute -/// * `character_id` - The unique identifier of the character -/// * `user_id` - The unique identifier of the user -/// * `attribute_type` - The attribute type/name -/// * `name` - The attribute value -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns the attribute ID if insertion was successful. -pub fn insert_attribute( - conn: &Connection, attribute_id: &str, character_id: &str, user_id: &str, - attribute_type: &str, name: &str, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![attribute_id, character_id, user_id, attribute_type, name, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'attribut.".to_string() } else { "Unable to add attribute.".to_string() }))?; - - if insert_result > 0 { - Ok(attribute_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'attribut.".to_string() } else { "Error adding attribute.".to_string() })) - } -} - -/// Updates an existing series character's information. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `encrypted_name` - The encrypted first name -/// * `encrypted_last_name` - The encrypted last name -/// * `encrypted_nickname` - The encrypted nickname -/// * `encrypted_age` - The encrypted age -/// * `encrypted_gender` - The encrypted gender -/// * `encrypted_species` - The encrypted species -/// * `encrypted_nationality` - The encrypted nationality -/// * `encrypted_status` - The encrypted status -/// * `encrypted_title` - The encrypted title -/// * `encrypted_category` - The encrypted category -/// * `encrypted_image` - The encrypted image -/// * `encrypted_role` - The encrypted role -/// * `encrypted_biography` - The encrypted biography -/// * `encrypted_history` - The encrypted history -/// * `encrypted_speech_pattern` - The encrypted speech pattern -/// * `encrypted_catchphrase` - The encrypted catchphrase -/// * `encrypted_residence` - The encrypted residence -/// * `encrypted_notes` - The encrypted notes -/// * `encrypted_color` - The encrypted color -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the update was successful, false otherwise. -pub fn update_character( - conn: &Connection, user_id: &str, character_id: &str, encrypted_name: &str, - encrypted_last_name: Option<&str>, encrypted_nickname: Option<&str>, encrypted_age: Option<&str>, - encrypted_gender: Option<&str>, encrypted_species: Option<&str>, encrypted_nationality: Option<&str>, - encrypted_status: Option<&str>, encrypted_title: Option<&str>, encrypted_category: Option<&str>, - encrypted_image: Option<&str>, encrypted_role: Option<&str>, encrypted_biography: Option<&str>, - encrypted_history: Option<&str>, encrypted_speech_pattern: Option<&str>, encrypted_catchphrase: Option<&str>, - encrypted_residence: Option<&str>, encrypted_notes: Option<&str>, encrypted_color: Option<&str>, - last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_characters SET first_name = ?1, last_name = ?2, nickname = ?3, age = ?4, gender = ?5, species = ?6, nationality = ?7, status = ?8, title = ?9, category = ?10, image = ?11, role = ?12, biography = ?13, history = ?14, speech_pattern = ?15, catchphrase = ?16, residence = ?17, notes = ?18, color = ?19, last_update = ?20 WHERE character_id = ?21 AND user_id = ?22", - params![encrypted_name, encrypted_last_name, encrypted_nickname, encrypted_age, encrypted_gender, encrypted_species, encrypted_nationality, encrypted_status, encrypted_title, encrypted_category, encrypted_image, encrypted_role, encrypted_biography, encrypted_history, encrypted_speech_pattern, encrypted_catchphrase, encrypted_residence, encrypted_notes, encrypted_color, last_update, character_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre \u{00e0} jour le personnage.".to_string() } else { "Unable to update character.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Deletes a series character and all its related data via CASCADE. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the deletion was successful, false otherwise. -pub fn delete_character(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult { - // Delete attributes first - conn.execute("DELETE FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2", params![character_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?; - - // Delete character - let delete_result = conn - .execute("DELETE FROM series_characters WHERE character_id = ?1 AND user_id = ?2", params![character_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Deletes an attribute from a series character. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `attribute_id` - The unique identifier of the attribute -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the deletion was successful, false otherwise. -pub fn delete_attribute(conn: &Connection, user_id: &str, attribute_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute("DELETE FROM series_characters_attributes WHERE attr_id = ?1 AND user_id = ?2", params![attribute_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'attribut.".to_string() } else { "Unable to delete attribute.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Fetches all attributes for a specific series character. -/// * `conn` - Database connection -/// * `character_id` - The unique identifier of the character -/// * `user_id` - The unique identifier of the user -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of attribute results. -pub fn fetch_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT attr_id, attribute_name, attribute_value FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?; - - let attributes = statement - .query_map(params![character_id, user_id], |query_row| { - Ok(SeriesCharacterAttributeResult { attr_id: query_row.get(0)?, attribute_name: query_row.get(1)?, attribute_value: query_row.get(2)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?; - - Ok(attributes) -} - -/// Checks if a series character exists. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the character exists, false otherwise. -pub fn is_character_exist(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_characters WHERE character_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?; - - let existence_check = statement - .query_row(params![character_id, user_id], |_query_row| Ok(true)) - .optional() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?; - - Ok(existence_check.is_some()) -} - -/// Fetches all characters for a series for sync. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full character table results. -pub fn fetch_series_characters_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?; - - let characters = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesCharactersTableResult { - character_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, first_name: query_row.get(3)?, - last_name: query_row.get(4)?, nickname: query_row.get(5)?, - age: query_row.get(6)?, gender: query_row.get(7)?, - species: query_row.get(8)?, nationality: query_row.get(9)?, - status: query_row.get(10)?, title: query_row.get(11)?, - category: query_row.get(12)?, image: query_row.get(13)?, - role: query_row.get(14)?, biography: query_row.get(15)?, - history: query_row.get(16)?, speech_pattern: query_row.get(17)?, - catchphrase: query_row.get(18)?, residence: query_row.get(19)?, - notes: query_row.get(20)?, color: query_row.get(21)?, - last_update: query_row.get(22)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?; - - Ok(characters) -} - -/// Fetches all attributes for a character for sync. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full attribute table results. -pub fn fetch_series_character_attributes_table(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?; - - let attributes = statement - .query_map(params![character_id, user_id], |query_row| { - Ok(SeriesCharacterAttributesTableResult { - attr_id: query_row.get(0)?, character_id: query_row.get(1)?, - user_id: query_row.get(2)?, attribute_name: query_row.get(3)?, - attribute_value: query_row.get(4)?, last_update: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?; - - Ok(attributes) -} - -/// Fetches all series characters for a user for sync comparison. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of synced character results. -pub fn fetch_synced_series_characters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT character_id, series_id, first_name, last_update FROM series_characters WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de s\u{00e9}rie pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?; - - let characters = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesCharacterResult { character_id: query_row.get(0)?, series_id: query_row.get(1)?, first_name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de s\u{00e9}rie pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de s\u{00e9}rie pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?; - - Ok(characters) -} - -/// Fetches all series character attributes for a user for sync comparison. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of synced attribute results. -pub fn fetch_synced_series_character_attributes(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT attr_id, character_id, attribute_name, last_update FROM series_characters_attributes WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?; - - let attributes = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesCharacterAttributeResult { attr_id: query_row.get(0)?, character_id: query_row.get(1)?, attribute_name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?; - - Ok(attributes) -} - -/// Fetches a complete character by ID for sync. -/// * `conn` - Database connection -/// * `character_id` - The unique identifier of the character -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full character table results. -pub fn fetch_complete_character_by_id(conn: &Connection, character_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE character_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?; - - let characters = statement - .query_map(params![character_id], |query_row| { - Ok(SeriesCharactersTableResult { - character_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, first_name: query_row.get(3)?, - last_name: query_row.get(4)?, nickname: query_row.get(5)?, - age: query_row.get(6)?, gender: query_row.get(7)?, - species: query_row.get(8)?, nationality: query_row.get(9)?, - status: query_row.get(10)?, title: query_row.get(11)?, - category: query_row.get(12)?, image: query_row.get(13)?, - role: query_row.get(14)?, biography: query_row.get(15)?, - history: query_row.get(16)?, speech_pattern: query_row.get(17)?, - catchphrase: query_row.get(18)?, residence: query_row.get(19)?, - notes: query_row.get(20)?, color: query_row.get(21)?, - last_update: query_row.get(22)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?; - - Ok(characters) -} - -/// Fetches a complete character attribute by ID for sync. -/// * `conn` - Database connection -/// * `attr_id` - The unique identifier of the attribute -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full attribute table results. -pub fn fetch_complete_attribute_by_id(conn: &Connection, attr_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE attr_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer l'attribut complet.".to_string() } else { "Unable to retrieve complete attribute.".to_string() }))?; - - let attributes = statement - .query_map(params![attr_id], |query_row| { - Ok(SeriesCharacterAttributesTableResult { - attr_id: query_row.get(0)?, character_id: query_row.get(1)?, - user_id: query_row.get(2)?, attribute_name: query_row.get(3)?, - attribute_value: query_row.get(4)?, last_update: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer l'attribut complet.".to_string() } else { "Unable to retrieve complete attribute.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer l'attribut complet.".to_string() } else { "Unable to retrieve complete attribute.".to_string() }))?; - - Ok(attributes) -} - -/// Inserts a series character for sync. -/// * `conn` - Database connection -/// * `character_id` - The unique identifier of the character -/// * `series_id` - The unique identifier of the series -/// * `user_id` - The unique identifier of the user -/// * `first_name` - The character's first name -/// * `last_name` - The character's last name -/// * `nickname` - The character's nickname -/// * `age` - The character's age -/// * `gender` - The character's gender -/// * `species` - The character's species -/// * `nationality` - The character's nationality -/// * `status` - The character's status -/// * `category` - The character's category -/// * `title` - The character's title -/// * `image` - The character's image -/// * `role` - The character's role -/// * `biography` - The character's biography -/// * `history` - The character's history -/// * `speech_pattern` - The character's speech pattern -/// * `catchphrase` - The character's catchphrase -/// * `residence` - The character's residence -/// * `notes` - The character's notes -/// * `color` - The character's color -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the insertion was successful, false otherwise. -pub fn insert_sync_series_character( - conn: &Connection, character_id: &str, series_id: &str, user_id: &str, first_name: &str, - last_name: Option<&str>, nickname: Option<&str>, age: Option<&str>, gender: Option<&str>, - species: Option<&str>, nationality: Option<&str>, status: Option<&str>, category: &str, - title: Option<&str>, image: Option<&str>, role: Option<&str>, biography: Option<&str>, - history: Option<&str>, speech_pattern: Option<&str>, catchphrase: Option<&str>, - residence: Option<&str>, notes: Option<&str>, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23) ON CONFLICT(character_id) DO UPDATE SET first_name = excluded.first_name, last_name = excluded.last_name, nickname = excluded.nickname, age = excluded.age, gender = excluded.gender, species = excluded.species, nationality = excluded.nationality, status = excluded.status, category = excluded.category, title = excluded.title, image = excluded.image, role = excluded.role, biography = excluded.biography, history = excluded.history, speech_pattern = excluded.speech_pattern, catchphrase = excluded.catchphrase, residence = excluded.residence, notes = excluded.notes, color = excluded.color, last_update = excluded.last_update", - params![character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ins\u{00e9}rer le personnage pour sync.".to_string() } else { "Unable to insert character for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series character for sync. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `first_name` - The character's first name -/// * `last_name` - The character's last name -/// * `nickname` - The character's nickname -/// * `age` - The character's age -/// * `gender` - The character's gender -/// * `species` - The character's species -/// * `nationality` - The character's nationality -/// * `status` - The character's status -/// * `category` - The character's category -/// * `title` - The character's title -/// * `image` - The character's image -/// * `role` - The character's role -/// * `biography` - The character's biography -/// * `history` - The character's history -/// * `speech_pattern` - The character's speech pattern -/// * `catchphrase` - The character's catchphrase -/// * `residence` - The character's residence -/// * `notes` - The character's notes -/// * `color` - The character's color -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the update was successful, false otherwise. -pub fn update_sync_series_character( - conn: &Connection, user_id: &str, character_id: &str, first_name: &str, - last_name: Option<&str>, nickname: Option<&str>, age: Option<&str>, gender: Option<&str>, - species: Option<&str>, nationality: Option<&str>, status: Option<&str>, category: &str, - title: Option<&str>, image: Option<&str>, role: Option<&str>, biography: Option<&str>, - history: Option<&str>, speech_pattern: Option<&str>, catchphrase: Option<&str>, - residence: Option<&str>, notes: Option<&str>, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_characters SET first_name = ?1, last_name = ?2, nickname = ?3, age = ?4, gender = ?5, species = ?6, nationality = ?7, status = ?8, category = ?9, title = ?10, image = ?11, role = ?12, biography = ?13, history = ?14, speech_pattern = ?15, catchphrase = ?16, residence = ?17, notes = ?18, color = ?19, last_update = ?20 WHERE character_id = ?21 AND user_id = ?22", - params![first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update, character_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre \u{00e0} jour le personnage pour sync.".to_string() } else { "Unable to update character for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series character attribute for sync. -/// * `conn` - Database connection -/// * `attr_id` - The unique identifier of the attribute -/// * `character_id` - The unique identifier of the character -/// * `user_id` - The unique identifier of the user -/// * `attribute_name` - The attribute name -/// * `attribute_value` - The attribute value -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the insertion was successful, false otherwise. -pub fn insert_sync_series_character_attribute( - conn: &Connection, attr_id: &str, character_id: &str, user_id: &str, - attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(attr_id) DO UPDATE SET attribute_name = excluded.attribute_name, attribute_value = excluded.attribute_value, last_update = excluded.last_update", - params![attr_id, character_id, user_id, attribute_name, attribute_value, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ins\u{00e9}rer l'attribut pour sync.".to_string() } else { "Unable to insert attribute for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Checks if a series character attribute exists. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `attr_id` - The unique identifier of the attribute -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the attribute exists, false otherwise. -pub fn is_attribute_exist(conn: &Connection, user_id: &str, attr_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_characters_attributes WHERE attr_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence de l'attribut.".to_string() } else { "Unable to check attribute existence.".to_string() }))?; - - let existence_check = statement - .query_row(params![attr_id, user_id], |_query_row| Ok(true)) - .optional() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence de l'attribut.".to_string() } else { "Unable to check attribute existence.".to_string() }))?; - - Ok(existence_check.is_some()) -} - -/// Updates a series character attribute for sync. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `attr_id` - The unique identifier of the attribute -/// * `attribute_name` - The attribute name -/// * `attribute_value` - The attribute value -/// * `last_update` - The timestamp for the last update -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the update was successful, false otherwise. -pub fn update_sync_series_character_attribute( - conn: &Connection, user_id: &str, attr_id: &str, attribute_name: &str, - attribute_value: &str, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_characters_attributes SET attribute_name = ?1, attribute_value = ?2, last_update = ?3 WHERE attr_id = ?4 AND user_id = ?5", - params![attribute_name, attribute_value, last_update, attr_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre \u{00e0} jour l'attribut pour sync.".to_string() } else { "Unable to update attribute for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Fetches all characters for a series for sync (without user filter). -/// * `conn` - Database connection -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full character table results. -pub fn fetch_characters_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?; - - let characters = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesCharactersTableResult { - character_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, first_name: query_row.get(3)?, - last_name: query_row.get(4)?, nickname: query_row.get(5)?, - age: query_row.get(6)?, gender: query_row.get(7)?, - species: query_row.get(8)?, nationality: query_row.get(9)?, - status: query_row.get(10)?, title: query_row.get(11)?, - category: query_row.get(12)?, image: query_row.get(13)?, - role: query_row.get(14)?, biography: query_row.get(15)?, - history: query_row.get(16)?, speech_pattern: query_row.get(17)?, - catchphrase: query_row.get(18)?, residence: query_row.get(19)?, - notes: query_row.get(20)?, color: query_row.get(21)?, - last_update: query_row.get(22)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?; - - Ok(characters) -} - -/// Fetches all character attributes for a series for sync (without user filter). -/// * `conn` - Database connection -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full attribute table results. -pub fn fetch_character_attributes_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?; - - let attributes = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesCharacterAttributesTableResult { - attr_id: query_row.get(0)?, character_id: query_row.get(1)?, - user_id: query_row.get(2)?, attribute_name: query_row.get(3)?, - attribute_value: query_row.get(4)?, last_update: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?; - - Ok(attributes) -} - -/// Fetches all characters for a series (alias for fetch_series_characters_table). -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full character table results. -pub fn fetch_series_characters(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - fetch_series_characters_table(conn, user_id, series_id, lang) -} - -/// Fetches all character attributes for a series by series ID. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `series_id` - The unique identifier of the series -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns an array of full attribute table results. -pub fn fetch_series_character_attributes_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?1 AND sc.user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs par s\u{00e9}rie.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?; - - let attributes = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesCharacterAttributesTableResult { - attr_id: query_row.get(0)?, character_id: query_row.get(1)?, - user_id: query_row.get(2)?, attribute_name: query_row.get(3)?, - attribute_value: query_row.get(4)?, last_update: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs par s\u{00e9}rie.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs par s\u{00e9}rie.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?; - - Ok(attributes) -} - -/// Checks if a series character exists (alias for is_character_exist). -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `character_id` - The unique identifier of the character -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the character exists, false otherwise. -pub fn series_character_exists(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult { - is_character_exist(conn, user_id, character_id, lang) -} - -/// Checks if a series character attribute exists (alias for is_attribute_exist). -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `attr_id` - The unique identifier of the attribute -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the attribute exists, false otherwise. -pub fn series_character_attribute_exists(conn: &Connection, user_id: &str, attr_id: &str, lang: Lang) -> AppResult { - is_attribute_exist(conn, user_id, attr_id, lang) -} +use rusqlite::{params, Connection, OptionalExtension}; + +use crate::error::{AppError, AppResult}; +use crate::shared::types::Lang; + +pub struct SeriesCharacterResult { + pub character_id: String, + pub first_name: String, + pub last_name: String, + pub nickname: Option, + pub age: Option, + pub gender: Option, + pub species: Option, + pub nationality: Option, + pub status: Option, + pub title: String, + pub category: String, + pub image: String, + pub role: String, + pub biography: String, + pub history: String, + pub speech_pattern: Option, + pub catchphrase: Option, + pub residence: Option, + pub notes: Option, + pub color: Option, +} + +pub struct SeriesCharacterAttributeResult { + pub attr_id: String, + pub _attribute_name: String, + pub attribute_value: String, +} + +pub struct SeriesCharactersTableResult { + pub character_id: String, + pub series_id: String, + pub user_id: String, + pub first_name: String, + pub last_name: Option, + pub nickname: Option, + pub age: Option, + pub gender: Option, + pub species: Option, + pub nationality: Option, + pub status: Option, + pub title: Option, + pub category: String, + pub image: Option, + pub role: Option, + pub biography: Option, + pub history: Option, + pub speech_pattern: Option, + pub catchphrase: Option, + pub residence: Option, + pub notes: Option, + pub color: Option, + pub last_update: i64, +} + +pub struct SeriesCharacterAttributesTableResult { + pub attr_id: String, + pub character_id: String, + pub user_id: String, + pub attribute_name: String, + pub attribute_value: String, + pub last_update: i64, +} + +pub struct SyncedSeriesCharacterResult { + pub character_id: String, + pub series_id: String, + pub first_name: String, + pub last_update: i64, +} + +pub struct SyncedSeriesCharacterAttributeResult { + pub attr_id: String, + pub character_id: String, + pub attribute_name: String, + pub last_update: i64, +} + +/// Fetches all characters for a specific series owned by the user. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `series_id` - The unique identifier of the series +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns an array of character results. +pub fn fetch_characters(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color FROM series_characters WHERE series_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages de la série.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?; + + let characters = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesCharacterResult { + character_id: query_row.get(0)?, first_name: query_row.get(1)?, + last_name: query_row.get(2)?, nickname: query_row.get(3)?, + age: query_row.get(4)?, gender: query_row.get(5)?, + species: query_row.get(6)?, nationality: query_row.get(7)?, + status: query_row.get(8)?, title: query_row.get(9)?, + category: query_row.get(10)?, image: query_row.get(11)?, + role: query_row.get(12)?, biography: query_row.get(13)?, + history: query_row.get(14)?, speech_pattern: query_row.get(15)?, + catchphrase: query_row.get(16)?, residence: query_row.get(17)?, + notes: query_row.get(18)?, color: query_row.get(19)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages de la série.".to_string() } else { "Unable to retrieve series characters.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages de la série.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?; + + Ok(characters) +} + +/// Adds a new character to a series. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `character_id` - The unique identifier of the character +/// * `encrypted_name` - The encrypted first name +/// * `encrypted_last_name` - The encrypted last name +/// * `encrypted_nickname` - The encrypted nickname +/// * `encrypted_age` - The encrypted age +/// * `encrypted_gender` - The encrypted gender +/// * `encrypted_species` - The encrypted species +/// * `encrypted_nationality` - The encrypted nationality +/// * `encrypted_status` - The encrypted status +/// * `encrypted_title` - The encrypted title +/// * `encrypted_category` - The encrypted category +/// * `encrypted_image` - The encrypted image +/// * `encrypted_role` - The encrypted role +/// * `encrypted_biography` - The encrypted biography +/// * `encrypted_history` - The encrypted history +/// * `encrypted_speech_pattern` - The encrypted speech pattern +/// * `encrypted_catchphrase` - The encrypted catchphrase +/// * `encrypted_residence` - The encrypted residence +/// * `encrypted_notes` - The encrypted notes +/// * `encrypted_color` - The encrypted color +/// * `series_id` - The unique identifier of the series +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns the character ID if insertion was successful. +pub fn add_new_character( + conn: &Connection, user_id: &str, character_id: &str, encrypted_name: &str, + encrypted_last_name: Option<&str>, encrypted_nickname: Option<&str>, encrypted_age: Option<&str>, + encrypted_gender: Option<&str>, encrypted_species: Option<&str>, encrypted_nationality: Option<&str>, + encrypted_status: Option<&str>, encrypted_title: Option<&str>, encrypted_category: Option<&str>, + encrypted_image: Option<&str>, encrypted_role: Option<&str>, encrypted_biography: Option<&str>, + encrypted_history: Option<&str>, encrypted_speech_pattern: Option<&str>, encrypted_catchphrase: Option<&str>, + encrypted_residence: Option<&str>, encrypted_notes: Option<&str>, encrypted_color: Option<&str>, + series_id: &str, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23)", + params![character_id, series_id, user_id, encrypted_name, encrypted_last_name, encrypted_nickname, encrypted_age, encrypted_gender, encrypted_species, encrypted_nationality, encrypted_status, encrypted_category, encrypted_title, encrypted_image, encrypted_role, encrypted_biography, encrypted_history, encrypted_speech_pattern, encrypted_catchphrase, encrypted_residence, encrypted_notes, encrypted_color, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le personnage.".to_string() } else { "Unable to add character.".to_string() }))?; + + if insert_result > 0 { + Ok(character_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du personnage.".to_string() } else { "Error adding character.".to_string() })) + } +} + +/// Inserts a new attribute for a series character. +/// * `conn` - Database connection +/// * `attribute_id` - The unique identifier of the attribute +/// * `character_id` - The unique identifier of the character +/// * `user_id` - The unique identifier of the user +/// * `attribute_type` - The attribute type/name +/// * `name` - The attribute value +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns the attribute ID if insertion was successful. +pub fn insert_attribute( + conn: &Connection, attribute_id: &str, character_id: &str, user_id: &str, + attribute_type: &str, name: &str, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![attribute_id, character_id, user_id, attribute_type, name, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'attribut.".to_string() } else { "Unable to add attribute.".to_string() }))?; + + if insert_result > 0 { + Ok(attribute_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'attribut.".to_string() } else { "Error adding attribute.".to_string() })) + } +} + +/// Updates an existing series character's information. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `character_id` - The unique identifier of the character +/// * `encrypted_name` - The encrypted first name +/// * `encrypted_last_name` - The encrypted last name +/// * `encrypted_nickname` - The encrypted nickname +/// * `encrypted_age` - The encrypted age +/// * `encrypted_gender` - The encrypted gender +/// * `encrypted_species` - The encrypted species +/// * `encrypted_nationality` - The encrypted nationality +/// * `encrypted_status` - The encrypted status +/// * `encrypted_title` - The encrypted title +/// * `encrypted_category` - The encrypted category +/// * `encrypted_image` - The encrypted image +/// * `encrypted_role` - The encrypted role +/// * `encrypted_biography` - The encrypted biography +/// * `encrypted_history` - The encrypted history +/// * `encrypted_speech_pattern` - The encrypted speech pattern +/// * `encrypted_catchphrase` - The encrypted catchphrase +/// * `encrypted_residence` - The encrypted residence +/// * `encrypted_notes` - The encrypted notes +/// * `encrypted_color` - The encrypted color +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the update was successful, false otherwise. +pub fn update_character( + conn: &Connection, user_id: &str, character_id: &str, encrypted_name: &str, + encrypted_last_name: Option<&str>, encrypted_nickname: Option<&str>, encrypted_age: Option<&str>, + encrypted_gender: Option<&str>, encrypted_species: Option<&str>, encrypted_nationality: Option<&str>, + encrypted_status: Option<&str>, encrypted_title: Option<&str>, encrypted_category: Option<&str>, + encrypted_image: Option<&str>, encrypted_role: Option<&str>, encrypted_biography: Option<&str>, + encrypted_history: Option<&str>, encrypted_speech_pattern: Option<&str>, encrypted_catchphrase: Option<&str>, + encrypted_residence: Option<&str>, encrypted_notes: Option<&str>, encrypted_color: Option<&str>, + last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_characters SET first_name = ?1, last_name = ?2, nickname = ?3, age = ?4, gender = ?5, species = ?6, nationality = ?7, status = ?8, title = ?9, category = ?10, image = ?11, role = ?12, biography = ?13, history = ?14, speech_pattern = ?15, catchphrase = ?16, residence = ?17, notes = ?18, color = ?19, last_update = ?20 WHERE character_id = ?21 AND user_id = ?22", + params![encrypted_name, encrypted_last_name, encrypted_nickname, encrypted_age, encrypted_gender, encrypted_species, encrypted_nationality, encrypted_status, encrypted_title, encrypted_category, encrypted_image, encrypted_role, encrypted_biography, encrypted_history, encrypted_speech_pattern, encrypted_catchphrase, encrypted_residence, encrypted_notes, encrypted_color, last_update, character_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le personnage.".to_string() } else { "Unable to update character.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Deletes a series character and all its related data via CASCADE. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `character_id` - The unique identifier of the character +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the deletion was successful, false otherwise. +pub fn delete_character(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult { + // Delete attributes first + conn.execute("DELETE FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2", params![character_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?; + + // Delete character + let delete_result = conn + .execute("DELETE FROM series_characters WHERE character_id = ?1 AND user_id = ?2", params![character_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Deletes an attribute from a series character. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `attribute_id` - The unique identifier of the attribute +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the deletion was successful, false otherwise. +pub fn delete_attribute(conn: &Connection, user_id: &str, attribute_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute("DELETE FROM series_characters_attributes WHERE attr_id = ?1 AND user_id = ?2", params![attribute_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'attribut.".to_string() } else { "Unable to delete attribute.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Fetches all attributes for a specific series character. +/// * `conn` - Database connection +/// * `character_id` - The unique identifier of the character +/// * `user_id` - The unique identifier of the user +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns an array of attribute results. +pub fn fetch_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT attr_id, attribute_name, attribute_value FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?; + + let attributes = statement + .query_map(params![character_id, user_id], |query_row| { + Ok(SeriesCharacterAttributeResult { attr_id: query_row.get(0)?, _attribute_name: query_row.get(1)?, attribute_value: query_row.get(2)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?; + + Ok(attributes) +} + +/// Checks if a series character exists. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `character_id` - The unique identifier of the character +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the character exists, false otherwise. +pub fn is_character_exist(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT 1 FROM series_characters WHERE character_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?; + + let existence_check = statement + .query_row(params![character_id, user_id], |_query_row| Ok(true)) + .optional() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?; + + Ok(existence_check.is_some()) +} + +/// Fetches all characters for a series for sync. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `series_id` - The unique identifier of the series +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns an array of full character table results. +pub fn fetch_series_characters_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?; + + let characters = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesCharactersTableResult { + character_id: query_row.get(0)?, series_id: query_row.get(1)?, + user_id: query_row.get(2)?, first_name: query_row.get(3)?, + last_name: query_row.get(4)?, nickname: query_row.get(5)?, + age: query_row.get(6)?, gender: query_row.get(7)?, + species: query_row.get(8)?, nationality: query_row.get(9)?, + status: query_row.get(10)?, title: query_row.get(11)?, + category: query_row.get(12)?, image: query_row.get(13)?, + role: query_row.get(14)?, biography: query_row.get(15)?, + history: query_row.get(16)?, speech_pattern: query_row.get(17)?, + catchphrase: query_row.get(18)?, residence: query_row.get(19)?, + notes: query_row.get(20)?, color: query_row.get(21)?, + last_update: query_row.get(22)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?; + + Ok(characters) +} + +/// Fetches all series characters for a user for sync comparison. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns an array of synced character results. +pub fn fetch_synced_series_characters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT character_id, series_id, first_name, last_update FROM series_characters WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages de série pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?; + + let characters = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesCharacterResult { character_id: query_row.get(0)?, series_id: query_row.get(1)?, first_name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages de série pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages de série pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?; + + Ok(characters) +} + +/// Fetches all series character attributes for a user for sync comparison. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns an array of synced attribute results. +pub fn fetch_synced_series_character_attributes(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT attr_id, character_id, attribute_name, last_update FROM series_characters_attributes WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?; + + let attributes = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesCharacterAttributeResult { attr_id: query_row.get(0)?, character_id: query_row.get(1)?, attribute_name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?; + + Ok(attributes) +} + +/// Inserts a series character for sync. +/// * `conn` - Database connection +/// * `character_id` - The unique identifier of the character +/// * `series_id` - The unique identifier of the series +/// * `user_id` - The unique identifier of the user +/// * `first_name` - The character's first name +/// * `last_name` - The character's last name +/// * `nickname` - The character's nickname +/// * `age` - The character's age +/// * `gender` - The character's gender +/// * `species` - The character's species +/// * `nationality` - The character's nationality +/// * `status` - The character's status +/// * `category` - The character's category +/// * `title` - The character's title +/// * `image` - The character's image +/// * `role` - The character's role +/// * `biography` - The character's biography +/// * `history` - The character's history +/// * `speech_pattern` - The character's speech pattern +/// * `catchphrase` - The character's catchphrase +/// * `residence` - The character's residence +/// * `notes` - The character's notes +/// * `color` - The character's color +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the insertion was successful, false otherwise. +pub fn insert_sync_series_character( + conn: &Connection, character_id: &str, series_id: &str, user_id: &str, first_name: &str, + last_name: Option<&str>, nickname: Option<&str>, age: Option<&str>, gender: Option<&str>, + species: Option<&str>, nationality: Option<&str>, status: Option<&str>, category: &str, + title: Option<&str>, image: Option<&str>, role: Option<&str>, biography: Option<&str>, + history: Option<&str>, speech_pattern: Option<&str>, catchphrase: Option<&str>, + residence: Option<&str>, notes: Option<&str>, color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23) ON CONFLICT(character_id) DO UPDATE SET first_name = excluded.first_name, last_name = excluded.last_name, nickname = excluded.nickname, age = excluded.age, gender = excluded.gender, species = excluded.species, nationality = excluded.nationality, status = excluded.status, category = excluded.category, title = excluded.title, image = excluded.image, role = excluded.role, biography = excluded.biography, history = excluded.history, speech_pattern = excluded.speech_pattern, catchphrase = excluded.catchphrase, residence = excluded.residence, notes = excluded.notes, color = excluded.color, last_update = excluded.last_update", + params![character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le personnage pour sync.".to_string() } else { "Unable to insert character for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Updates a series character for sync. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `character_id` - The unique identifier of the character +/// * `first_name` - The character's first name +/// * `last_name` - The character's last name +/// * `nickname` - The character's nickname +/// * `age` - The character's age +/// * `gender` - The character's gender +/// * `species` - The character's species +/// * `nationality` - The character's nationality +/// * `status` - The character's status +/// * `category` - The character's category +/// * `title` - The character's title +/// * `image` - The character's image +/// * `role` - The character's role +/// * `biography` - The character's biography +/// * `history` - The character's history +/// * `speech_pattern` - The character's speech pattern +/// * `catchphrase` - The character's catchphrase +/// * `residence` - The character's residence +/// * `notes` - The character's notes +/// * `color` - The character's color +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the update was successful, false otherwise. +pub fn update_sync_series_character( + conn: &Connection, user_id: &str, character_id: &str, first_name: &str, + last_name: Option<&str>, nickname: Option<&str>, age: Option<&str>, gender: Option<&str>, + species: Option<&str>, nationality: Option<&str>, status: Option<&str>, category: &str, + title: Option<&str>, image: Option<&str>, role: Option<&str>, biography: Option<&str>, + history: Option<&str>, speech_pattern: Option<&str>, catchphrase: Option<&str>, + residence: Option<&str>, notes: Option<&str>, color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_characters SET first_name = ?1, last_name = ?2, nickname = ?3, age = ?4, gender = ?5, species = ?6, nationality = ?7, status = ?8, category = ?9, title = ?10, image = ?11, role = ?12, biography = ?13, history = ?14, speech_pattern = ?15, catchphrase = ?16, residence = ?17, notes = ?18, color = ?19, last_update = ?20 WHERE character_id = ?21 AND user_id = ?22", + params![first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update, character_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le personnage pour sync.".to_string() } else { "Unable to update character for sync.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Inserts a series character attribute for sync. +/// * `conn` - Database connection +/// * `attr_id` - The unique identifier of the attribute +/// * `character_id` - The unique identifier of the character +/// * `user_id` - The unique identifier of the user +/// * `attribute_name` - The attribute name +/// * `attribute_value` - The attribute value +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the insertion was successful, false otherwise. +pub fn insert_sync_series_character_attribute( + conn: &Connection, attr_id: &str, character_id: &str, user_id: &str, + attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(attr_id) DO UPDATE SET attribute_name = excluded.attribute_name, attribute_value = excluded.attribute_value, last_update = excluded.last_update", + params![attr_id, character_id, user_id, attribute_name, attribute_value, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'attribut pour sync.".to_string() } else { "Unable to insert attribute for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Checks if a series character attribute exists. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `attr_id` - The unique identifier of the attribute +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the attribute exists, false otherwise. +pub fn is_attribute_exist(conn: &Connection, user_id: &str, attr_id: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT 1 FROM series_characters_attributes WHERE attr_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'attribut.".to_string() } else { "Unable to check attribute existence.".to_string() }))?; + + let existence_check = statement + .query_row(params![attr_id, user_id], |_query_row| Ok(true)) + .optional() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'attribut.".to_string() } else { "Unable to check attribute existence.".to_string() }))?; + + Ok(existence_check.is_some()) +} + +/// Updates a series character attribute for sync. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `attr_id` - The unique identifier of the attribute +/// * `attribute_name` - The attribute name +/// * `attribute_value` - The attribute value +/// * `last_update` - The timestamp for the last update +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the update was successful, false otherwise. +pub fn update_sync_series_character_attribute( + conn: &Connection, user_id: &str, attr_id: &str, attribute_name: &str, + attribute_value: &str, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_characters_attributes SET attribute_name = ?1, attribute_value = ?2, last_update = ?3 WHERE attr_id = ?4 AND user_id = ?5", + params![attribute_name, attribute_value, last_update, attr_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'attribut pour sync.".to_string() } else { "Unable to update attribute for sync.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Fetches all character attributes for a series by series ID. +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `series_id` - The unique identifier of the series +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns an array of full attribute table results. +pub fn fetch_series_character_attributes_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?1 AND sc.user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs par série.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?; + + let attributes = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesCharacterAttributesTableResult { + attr_id: query_row.get(0)?, character_id: query_row.get(1)?, + user_id: query_row.get(2)?, attribute_name: query_row.get(3)?, + attribute_value: query_row.get(4)?, last_update: query_row.get(5)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs par série.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs par série.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?; + + Ok(attributes) +} + +/// Checks if a series character exists (alias for is_character_exist). +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `character_id` - The unique identifier of the character +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the character exists, false otherwise. +pub fn series_character_exists(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult { + is_character_exist(conn, user_id, character_id, lang) +} + +/// Checks if a series character attribute exists (alias for is_attribute_exist). +/// * `conn` - Database connection +/// * `user_id` - The unique identifier of the user +/// * `attr_id` - The unique identifier of the attribute +/// * `lang` - The language for error messages ("fr" or "en") +/// Returns true if the attribute exists, false otherwise. +pub fn series_character_attribute_exists(conn: &Connection, user_id: &str, attr_id: &str, lang: Lang) -> AppResult { + is_attribute_exist(conn, user_id, attr_id, lang) +} diff --git a/src-tauri/src/domains/series_location/repo.rs b/src-tauri/src/domains/series_location/repo.rs index e323211..b3d4cce 100644 --- a/src-tauri/src/domains/series_location/repo.rs +++ b/src-tauri/src/domains/series_location/repo.rs @@ -1,583 +1,474 @@ -use rusqlite::{params, Connection}; - -use crate::error::{AppError, AppResult}; -use crate::shared::types::Lang; - -pub struct SeriesLocationResult { - pub loc_id: String, - pub loc_name: String, -} - -pub struct SeriesLocationElementResult { - pub element_id: String, - pub location_id: String, - pub element_name: String, - pub element_description: String, -} - -pub struct SeriesLocationSubElementResult { - pub sub_element_id: String, - pub element_id: String, - pub sub_elem_name: String, - pub sub_elem_description: String, -} - -pub struct SeriesLocationsTableResult { - pub loc_id: String, - pub series_id: String, - pub user_id: String, - pub loc_name: String, - pub loc_original_name: String, - pub last_update: i64, -} - -pub struct SeriesLocationElementsTableResult { - pub element_id: String, - pub location_id: String, - pub user_id: String, - pub element_name: String, - pub original_name: String, - pub element_description: Option, - pub last_update: i64, -} - -pub struct SeriesLocationSubElementsTableResult { - pub sub_element_id: String, - pub element_id: String, - pub user_id: String, - pub sub_elem_name: String, - pub original_name: String, - pub sub_elem_description: Option, - pub last_update: i64, -} - -pub struct SyncedSeriesLocationResult { - pub loc_id: String, - pub series_id: String, - pub loc_name: String, - pub last_update: i64, -} - -pub struct SyncedSeriesLocationElementResult { - pub element_id: String, - pub location_id: String, - pub element_name: String, - pub last_update: i64, -} - -pub struct SyncedSeriesLocationSubElementResult { - pub sub_element_id: String, - pub element_id: String, - pub sub_elem_name: String, - pub last_update: i64, -} - -/// Fetches all locations for a series. -pub fn fetch_locations(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT loc_id, loc_name FROM series_locations WHERE user_id = ?1 AND series_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?; - - let locations = statement - .query_map(params![user_id, series_id], |query_row| { - Ok(SeriesLocationResult { loc_id: query_row.get(0)?, loc_name: query_row.get(1)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?; - - Ok(locations) -} - -/// Fetches all elements for a location. -pub fn fetch_elements(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT element_id, location_id, element_name, element_description FROM series_location_elements WHERE user_id = ?1 AND location_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments.".to_string() } else { "Unable to retrieve elements.".to_string() }))?; - - let elements = statement - .query_map(params![user_id, location_id], |query_row| { - Ok(SeriesLocationElementResult { element_id: query_row.get(0)?, location_id: query_row.get(1)?, element_name: query_row.get(2)?, element_description: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments.".to_string() } else { "Unable to retrieve elements.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments.".to_string() } else { "Unable to retrieve elements.".to_string() }))?; - - Ok(elements) -} - -/// Fetches all sub-elements for an element. -pub fn fetch_sub_elements(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT sub_element_id, element_id, sub_elem_name, sub_elem_description FROM series_location_sub_elements WHERE user_id = ?1 AND element_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments.".to_string() } else { "Unable to retrieve sub-elements.".to_string() }))?; - - let sub_elements = statement - .query_map(params![user_id, element_id], |query_row| { - Ok(SeriesLocationSubElementResult { sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, sub_elem_name: query_row.get(2)?, sub_elem_description: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments.".to_string() } else { "Unable to retrieve sub-elements.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments.".to_string() } else { "Unable to retrieve sub-elements.".to_string() }))?; - - Ok(sub_elements) -} - -/// Inserts a new location section. -pub fn insert_location( - conn: &Connection, location_id: &str, series_id: &str, user_id: &str, - encrypted_name: &str, original_name: &str, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![location_id, series_id, user_id, encrypted_name, original_name, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le lieu.".to_string() } else { "Unable to add location.".to_string() }))?; - - if insert_result > 0 { - Ok(location_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du lieu.".to_string() } else { "Error adding location.".to_string() })) - } -} - -/// Inserts a new element. -pub fn insert_element( - conn: &Connection, element_id: &str, location_id: &str, user_id: &str, - encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![element_id, location_id, user_id, encrypted_name, original_name, description, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément.".to_string() } else { "Unable to add element.".to_string() }))?; - - if insert_result > 0 { - Ok(element_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout de l'élément.".to_string() } else { "Error adding element.".to_string() })) - } -} - -/// Inserts a new sub-element. -pub fn insert_sub_element( - conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, - encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![sub_element_id, element_id, user_id, encrypted_name, original_name, description, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sous-élément.".to_string() } else { "Unable to add sub-element.".to_string() }))?; - - if insert_result > 0 { - Ok(sub_element_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du sous-élément.".to_string() } else { "Error adding sub-element.".to_string() })) - } -} - -/// Deletes a location section. -pub fn delete_location(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute("DELETE FROM series_locations WHERE loc_id = ?1 AND user_id = ?2", params![location_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le lieu.".to_string() } else { "Unable to delete location.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Deletes an element. -pub fn delete_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute("DELETE FROM series_location_elements WHERE element_id = ?1 AND user_id = ?2", params![element_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément.".to_string() } else { "Unable to delete element.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Deletes a sub-element. -pub fn delete_sub_element(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute("DELETE FROM series_location_sub_elements WHERE sub_element_id = ?1 AND user_id = ?2", params![sub_element_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le sous-élément.".to_string() } else { "Unable to delete sub-element.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Fetches all locations for a series for sync. -pub fn fetch_series_locations_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))?; - - let locations = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesLocationsTableResult { - loc_id: query_row.get(0)?, series_id: query_row.get(1)?, user_id: query_row.get(2)?, - loc_name: query_row.get(3)?, loc_original_name: query_row.get(4)?, last_update: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))?; - - Ok(locations) -} - -/// Fetches all series locations for a user for sync comparison. -pub fn fetch_synced_series_locations(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT loc_id, series_id, loc_name, last_update FROM series_locations WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux de série pour sync.".to_string() } else { "Unable to retrieve series locations for sync.".to_string() }))?; - - let locations = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesLocationResult { loc_id: query_row.get(0)?, series_id: query_row.get(1)?, loc_name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux de série pour sync.".to_string() } else { "Unable to retrieve series locations for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux de série pour sync.".to_string() } else { "Unable to retrieve series locations for sync.".to_string() }))?; - - Ok(locations) -} - -/// Fetches all series location elements for a user for sync comparison. -pub fn fetch_synced_series_location_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT element_id, location_id, element_name, last_update FROM series_location_elements WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))?; - - let elements = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesLocationElementResult { element_id: query_row.get(0)?, location_id: query_row.get(1)?, element_name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))?; - - Ok(elements) -} - -/// Fetches all series location sub-elements for a user for sync comparison. -pub fn fetch_synced_series_location_sub_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT sub_element_id, element_id, sub_elem_name, last_update FROM series_location_sub_elements WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))?; - - let sub_elements = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesLocationSubElementResult { sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, sub_elem_name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))?; - - Ok(sub_elements) -} - -/// Checks if a location exists. -pub fn is_location_exist(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult { - let exists: bool = conn - .prepare("SELECT 1 FROM series_locations WHERE loc_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du lieu.".to_string() } else { "Unable to check location existence.".to_string() }))? - .exists(params![location_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du lieu.".to_string() } else { "Unable to check location existence.".to_string() }))?; - - Ok(exists) -} - -/// Checks if a location element exists. -pub fn is_location_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { - let exists: bool = conn - .prepare("SELECT 1 FROM series_location_elements WHERE element_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))? - .exists(params![element_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?; - - Ok(exists) -} - -/// Checks if a location sub-element exists. -pub fn is_location_sub_element_exist(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult { - let exists: bool = conn - .prepare("SELECT 1 FROM series_location_sub_elements WHERE sub_element_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sous-élément.".to_string() } else { "Unable to check sub-element existence.".to_string() }))? - .exists(params![sub_element_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sous-élément.".to_string() } else { "Unable to check sub-element existence.".to_string() }))?; - - Ok(exists) -} - -/// Inserts a series location for sync. -pub fn insert_sync_location( - conn: &Connection, location_id: &str, series_id: &str, user_id: &str, - loc_name: &str, loc_original_name: &str, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(loc_id) DO UPDATE SET loc_name = excluded.loc_name, loc_original_name = excluded.loc_original_name, last_update = excluded.last_update", - params![location_id, series_id, user_id, loc_name, loc_original_name, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le lieu pour sync.".to_string() } else { "Unable to insert location for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series location for sync. -pub fn update_sync_location(conn: &Connection, user_id: &str, location_id: &str, loc_name: &str, loc_original_name: &str, last_update: i64, lang: Lang) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_locations SET loc_name = ?1, loc_original_name = ?2, last_update = ?3 WHERE loc_id = ?4 AND user_id = ?5", - params![loc_name, loc_original_name, last_update, location_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le lieu pour sync.".to_string() } else { "Unable to update location for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series location element for sync. -pub fn insert_sync_location_element( - conn: &Connection, element_id: &str, location_id: &str, user_id: &str, - element_name: &str, original_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(element_id) DO UPDATE SET element_name = excluded.element_name, original_name = excluded.original_name, element_description = excluded.element_description, last_update = excluded.last_update", - params![element_id, location_id, user_id, element_name, original_name, element_description, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément de lieu pour sync.".to_string() } else { "Unable to insert location element for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series location element for sync. -pub fn update_sync_location_element( - conn: &Connection, user_id: &str, element_id: &str, element_name: &str, - original_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_location_elements SET element_name = ?1, original_name = ?2, element_description = ?3, last_update = ?4 WHERE element_id = ?5 AND user_id = ?6", - params![element_name, original_name, element_description, last_update, element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de lieu pour sync.".to_string() } else { "Unable to update location element for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series location sub-element for sync. -pub fn insert_sync_location_sub_element( - conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, - sub_elem_name: &str, original_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(sub_element_id) DO UPDATE SET sub_elem_name = excluded.sub_elem_name, original_name = excluded.original_name, sub_elem_description = excluded.sub_elem_description, last_update = excluded.last_update", - params![sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le sous-élément pour sync.".to_string() } else { "Unable to insert sub-element for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series location sub-element for sync. -pub fn update_sync_location_sub_element( - conn: &Connection, user_id: &str, sub_element_id: &str, sub_elem_name: &str, - original_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_location_sub_elements SET sub_elem_name = ?1, original_name = ?2, sub_elem_description = ?3, last_update = ?4 WHERE sub_element_id = ?5 AND user_id = ?6", - params![sub_elem_name, original_name, sub_elem_description, last_update, sub_element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sous-élément pour sync.".to_string() } else { "Unable to update sub-element for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Fetches all locations for a series for sync (without user filter). -pub fn fetch_locations_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))?; - - let locations = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesLocationsTableResult { - loc_id: query_row.get(0)?, series_id: query_row.get(1)?, user_id: query_row.get(2)?, - loc_name: query_row.get(3)?, loc_original_name: query_row.get(4)?, last_update: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))?; - - Ok(locations) -} - -/// Fetches all location elements for a series for sync (without user filter). -pub fn fetch_location_elements_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))?; - - let elements = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesLocationElementsTableResult { - element_id: query_row.get(0)?, location_id: query_row.get(1)?, user_id: query_row.get(2)?, - element_name: query_row.get(3)?, original_name: query_row.get(4)?, - element_description: query_row.get(5)?, last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))?; - - Ok(elements) -} - -/// Fetches all location sub-elements for a series for sync (without user filter). -pub fn fetch_location_sub_elements_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))?; - - let sub_elements = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesLocationSubElementsTableResult { - sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, user_id: query_row.get(2)?, - sub_elem_name: query_row.get(3)?, original_name: query_row.get(4)?, - sub_elem_description: query_row.get(5)?, last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))?; - - Ok(sub_elements) -} - -/// Fetches all locations for a series (alias for fetch_series_locations_table). -pub fn fetch_series_locations(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - fetch_series_locations_table(conn, user_id, series_id, lang) -} - -/// Fetches all location elements for a series by series ID. -pub fn fetch_series_location_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?1 AND sl.user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu par série.".to_string() } else { "Unable to retrieve location elements by series.".to_string() }))?; - - let elements = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesLocationElementsTableResult { - element_id: query_row.get(0)?, location_id: query_row.get(1)?, user_id: query_row.get(2)?, - element_name: query_row.get(3)?, original_name: query_row.get(4)?, - element_description: query_row.get(5)?, last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu par série.".to_string() } else { "Unable to retrieve location elements by series.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu par série.".to_string() } else { "Unable to retrieve location elements by series.".to_string() }))?; - - Ok(elements) -} - -/// Fetches all location sub-elements for a series by series ID. -pub fn fetch_series_location_sub_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?1 AND sl.user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu par série.".to_string() } else { "Unable to retrieve location sub-elements by series.".to_string() }))?; - - let sub_elements = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesLocationSubElementsTableResult { - sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, user_id: query_row.get(2)?, - sub_elem_name: query_row.get(3)?, original_name: query_row.get(4)?, - sub_elem_description: query_row.get(5)?, last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu par série.".to_string() } else { "Unable to retrieve location sub-elements by series.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu par série.".to_string() } else { "Unable to retrieve location sub-elements by series.".to_string() }))?; - - Ok(sub_elements) -} - -/// Checks if a series location exists (alias for is_location_exist). -pub fn series_location_exists(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult { - is_location_exist(conn, user_id, location_id, lang) -} - -/// Checks if a series location element exists (alias for is_location_element_exist). -pub fn series_location_element_exists(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { - is_location_element_exist(conn, user_id, element_id, lang) -} - -/// Checks if a series location sub-element exists (alias for is_location_sub_element_exist). -pub fn series_location_sub_element_exists(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult { - is_location_sub_element_exist(conn, user_id, sub_element_id, lang) -} - -/// Inserts a series location for sync (alias with compatible signature). -pub fn insert_sync_series_location( - conn: &Connection, location_id: &str, series_id: &str, user_id: &str, - loc_name: &str, loc_original_name: &str, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_location(conn, location_id, series_id, user_id, loc_name, loc_original_name, last_update, lang) -} - -/// Updates a series location for sync (without original_name). -pub fn update_sync_series_location(conn: &Connection, location_id: &str, user_id: &str, loc_name: &str, last_update: i64, lang: Lang) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_locations SET loc_name = ?1, last_update = ?2 WHERE loc_id = ?3 AND user_id = ?4", - params![loc_name, last_update, location_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le lieu série pour sync.".to_string() } else { "Unable to update series location for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series location element for sync (alias with compatible signature). -pub fn insert_sync_series_location_element( - conn: &Connection, element_id: &str, location_id: &str, user_id: &str, - element_name: &str, original_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_location_element(conn, element_id, location_id, user_id, element_name, original_name, element_description, last_update, lang) -} - -/// Updates a series location element for sync (without original_name). -pub fn update_sync_series_location_element(conn: &Connection, element_id: &str, user_id: &str, element_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_location_elements SET element_name = ?1, element_description = ?2, last_update = ?3 WHERE element_id = ?4 AND user_id = ?5", - params![element_name, element_description, last_update, element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de lieu série pour sync.".to_string() } else { "Unable to update series location element for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series location sub-element for sync (alias with compatible signature). -pub fn insert_sync_series_location_sub_element( - conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, - sub_elem_name: &str, original_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_location_sub_element(conn, sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update, lang) -} - -/// Updates a series location sub-element for sync (without original_name). -pub fn update_sync_series_location_sub_element(conn: &Connection, sub_element_id: &str, user_id: &str, sub_elem_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_location_sub_elements SET sub_elem_name = ?1, sub_elem_description = ?2, last_update = ?3 WHERE sub_element_id = ?4 AND user_id = ?5", - params![sub_elem_name, sub_elem_description, last_update, sub_element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sous-élément série pour sync.".to_string() } else { "Unable to update series sub-element for sync.".to_string() }))?; - - Ok(update_result > 0) -} +use rusqlite::{params, Connection}; + +use crate::error::{AppError, AppResult}; +use crate::shared::types::Lang; + +pub struct SeriesLocationResult { + pub loc_id: String, + pub loc_name: String, +} + +pub struct SeriesLocationElementResult { + pub element_id: String, + pub _location_id: String, + pub element_name: String, + pub element_description: String, +} + +pub struct SeriesLocationSubElementResult { + pub sub_element_id: String, + pub _element_id: String, + pub sub_elem_name: String, + pub sub_elem_description: String, +} + +pub struct SeriesLocationsTableResult { + pub loc_id: String, + pub series_id: String, + pub user_id: String, + pub loc_name: String, + pub loc_original_name: String, + pub last_update: i64, +} + +pub struct SeriesLocationElementsTableResult { + pub element_id: String, + pub location_id: String, + pub user_id: String, + pub element_name: String, + pub original_name: String, + pub element_description: Option, + pub last_update: i64, +} + +pub struct SeriesLocationSubElementsTableResult { + pub sub_element_id: String, + pub element_id: String, + pub user_id: String, + pub sub_elem_name: String, + pub original_name: String, + pub sub_elem_description: Option, + pub last_update: i64, +} + +pub struct SyncedSeriesLocationResult { + pub loc_id: String, + pub series_id: String, + pub loc_name: String, + pub last_update: i64, +} + +pub struct SyncedSeriesLocationElementResult { + pub element_id: String, + pub location_id: String, + pub element_name: String, + pub last_update: i64, +} + +pub struct SyncedSeriesLocationSubElementResult { + pub sub_element_id: String, + pub element_id: String, + pub sub_elem_name: String, + pub last_update: i64, +} + +/// Fetches all locations for a series. +pub fn fetch_locations(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT loc_id, loc_name FROM series_locations WHERE user_id = ?1 AND series_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?; + + let locations = statement + .query_map(params![user_id, series_id], |query_row| { + Ok(SeriesLocationResult { loc_id: query_row.get(0)?, loc_name: query_row.get(1)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?; + + Ok(locations) +} + +/// Fetches all elements for a location. +pub fn fetch_elements(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT element_id, location_id, element_name, element_description FROM series_location_elements WHERE user_id = ?1 AND location_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments.".to_string() } else { "Unable to retrieve elements.".to_string() }))?; + + let elements = statement + .query_map(params![user_id, location_id], |query_row| { + Ok(SeriesLocationElementResult { element_id: query_row.get(0)?, _location_id: query_row.get(1)?, element_name: query_row.get(2)?, element_description: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments.".to_string() } else { "Unable to retrieve elements.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments.".to_string() } else { "Unable to retrieve elements.".to_string() }))?; + + Ok(elements) +} + +/// Fetches all sub-elements for an element. +pub fn fetch_sub_elements(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT sub_element_id, element_id, sub_elem_name, sub_elem_description FROM series_location_sub_elements WHERE user_id = ?1 AND element_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments.".to_string() } else { "Unable to retrieve sub-elements.".to_string() }))?; + + let sub_elements = statement + .query_map(params![user_id, element_id], |query_row| { + Ok(SeriesLocationSubElementResult { sub_element_id: query_row.get(0)?, _element_id: query_row.get(1)?, sub_elem_name: query_row.get(2)?, sub_elem_description: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments.".to_string() } else { "Unable to retrieve sub-elements.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments.".to_string() } else { "Unable to retrieve sub-elements.".to_string() }))?; + + Ok(sub_elements) +} + +/// Inserts a new location section. +pub fn insert_location( + conn: &Connection, location_id: &str, series_id: &str, user_id: &str, + encrypted_name: &str, original_name: &str, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![location_id, series_id, user_id, encrypted_name, original_name, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le lieu.".to_string() } else { "Unable to add location.".to_string() }))?; + + if insert_result > 0 { + Ok(location_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du lieu.".to_string() } else { "Error adding location.".to_string() })) + } +} + +/// Inserts a new element. +pub fn insert_element( + conn: &Connection, element_id: &str, location_id: &str, user_id: &str, + encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![element_id, location_id, user_id, encrypted_name, original_name, description, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément.".to_string() } else { "Unable to add element.".to_string() }))?; + + if insert_result > 0 { + Ok(element_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout de l'élément.".to_string() } else { "Error adding element.".to_string() })) + } +} + +/// Inserts a new sub-element. +pub fn insert_sub_element( + conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, + encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![sub_element_id, element_id, user_id, encrypted_name, original_name, description, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sous-élément.".to_string() } else { "Unable to add sub-element.".to_string() }))?; + + if insert_result > 0 { + Ok(sub_element_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du sous-élément.".to_string() } else { "Error adding sub-element.".to_string() })) + } +} + +/// Deletes a location section. +pub fn delete_location(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute("DELETE FROM series_locations WHERE loc_id = ?1 AND user_id = ?2", params![location_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le lieu.".to_string() } else { "Unable to delete location.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Deletes an element. +pub fn delete_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute("DELETE FROM series_location_elements WHERE element_id = ?1 AND user_id = ?2", params![element_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément.".to_string() } else { "Unable to delete element.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Deletes a sub-element. +pub fn delete_sub_element(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute("DELETE FROM series_location_sub_elements WHERE sub_element_id = ?1 AND user_id = ?2", params![sub_element_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le sous-élément.".to_string() } else { "Unable to delete sub-element.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Fetches all locations for a series for sync. +pub fn fetch_series_locations_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))?; + + let locations = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesLocationsTableResult { + loc_id: query_row.get(0)?, series_id: query_row.get(1)?, user_id: query_row.get(2)?, + loc_name: query_row.get(3)?, loc_original_name: query_row.get(4)?, last_update: query_row.get(5)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux pour sync.".to_string() } else { "Unable to retrieve locations for sync.".to_string() }))?; + + Ok(locations) +} + +/// Fetches all series locations for a user for sync comparison. +pub fn fetch_synced_series_locations(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT loc_id, series_id, loc_name, last_update FROM series_locations WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux de série pour sync.".to_string() } else { "Unable to retrieve series locations for sync.".to_string() }))?; + + let locations = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesLocationResult { loc_id: query_row.get(0)?, series_id: query_row.get(1)?, loc_name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux de série pour sync.".to_string() } else { "Unable to retrieve series locations for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux de série pour sync.".to_string() } else { "Unable to retrieve series locations for sync.".to_string() }))?; + + Ok(locations) +} + +/// Fetches all series location elements for a user for sync comparison. +pub fn fetch_synced_series_location_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT element_id, location_id, element_name, last_update FROM series_location_elements WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))?; + + let elements = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesLocationElementResult { element_id: query_row.get(0)?, location_id: query_row.get(1)?, element_name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location elements for sync.".to_string() }))?; + + Ok(elements) +} + +/// Fetches all series location sub-elements for a user for sync comparison. +pub fn fetch_synced_series_location_sub_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT sub_element_id, element_id, sub_elem_name, last_update FROM series_location_sub_elements WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))?; + + let sub_elements = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesLocationSubElementResult { sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, sub_elem_name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu pour sync.".to_string() } else { "Unable to retrieve location sub-elements for sync.".to_string() }))?; + + Ok(sub_elements) +} + +/// Checks if a location exists. +pub fn is_location_exist(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult { + let exists: bool = conn + .prepare("SELECT 1 FROM series_locations WHERE loc_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du lieu.".to_string() } else { "Unable to check location existence.".to_string() }))? + .exists(params![location_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du lieu.".to_string() } else { "Unable to check location existence.".to_string() }))?; + + Ok(exists) +} + +/// Checks if a location element exists. +pub fn is_location_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { + let exists: bool = conn + .prepare("SELECT 1 FROM series_location_elements WHERE element_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))? + .exists(params![element_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?; + + Ok(exists) +} + +/// Checks if a location sub-element exists. +pub fn is_location_sub_element_exist(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult { + let exists: bool = conn + .prepare("SELECT 1 FROM series_location_sub_elements WHERE sub_element_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sous-élément.".to_string() } else { "Unable to check sub-element existence.".to_string() }))? + .exists(params![sub_element_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sous-élément.".to_string() } else { "Unable to check sub-element existence.".to_string() }))?; + + Ok(exists) +} + +/// Inserts a series location for sync. +pub fn insert_sync_location( + conn: &Connection, location_id: &str, series_id: &str, user_id: &str, + loc_name: &str, loc_original_name: &str, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(loc_id) DO UPDATE SET loc_name = excluded.loc_name, loc_original_name = excluded.loc_original_name, last_update = excluded.last_update", + params![location_id, series_id, user_id, loc_name, loc_original_name, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le lieu pour sync.".to_string() } else { "Unable to insert location for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Inserts a series location element for sync. +pub fn insert_sync_location_element( + conn: &Connection, element_id: &str, location_id: &str, user_id: &str, + element_name: &str, original_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(element_id) DO UPDATE SET element_name = excluded.element_name, original_name = excluded.original_name, element_description = excluded.element_description, last_update = excluded.last_update", + params![element_id, location_id, user_id, element_name, original_name, element_description, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément de lieu pour sync.".to_string() } else { "Unable to insert location element for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Inserts a series location sub-element for sync. +pub fn insert_sync_location_sub_element( + conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, + sub_elem_name: &str, original_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(sub_element_id) DO UPDATE SET sub_elem_name = excluded.sub_elem_name, original_name = excluded.original_name, sub_elem_description = excluded.sub_elem_description, last_update = excluded.last_update", + params![sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le sous-élément pour sync.".to_string() } else { "Unable to insert sub-element for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Fetches all location elements for a series by series ID. +pub fn fetch_series_location_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?1 AND sl.user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu par série.".to_string() } else { "Unable to retrieve location elements by series.".to_string() }))?; + + let elements = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesLocationElementsTableResult { + element_id: query_row.get(0)?, location_id: query_row.get(1)?, user_id: query_row.get(2)?, + element_name: query_row.get(3)?, original_name: query_row.get(4)?, + element_description: query_row.get(5)?, last_update: query_row.get(6)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu par série.".to_string() } else { "Unable to retrieve location elements by series.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu par série.".to_string() } else { "Unable to retrieve location elements by series.".to_string() }))?; + + Ok(elements) +} + +/// Fetches all location sub-elements for a series by series ID. +pub fn fetch_series_location_sub_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?1 AND sl.user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu par série.".to_string() } else { "Unable to retrieve location sub-elements by series.".to_string() }))?; + + let sub_elements = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesLocationSubElementsTableResult { + sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, user_id: query_row.get(2)?, + sub_elem_name: query_row.get(3)?, original_name: query_row.get(4)?, + sub_elem_description: query_row.get(5)?, last_update: query_row.get(6)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu par série.".to_string() } else { "Unable to retrieve location sub-elements by series.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu par série.".to_string() } else { "Unable to retrieve location sub-elements by series.".to_string() }))?; + + Ok(sub_elements) +} + +/// Checks if a series location exists (alias for is_location_exist). +pub fn series_location_exists(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult { + is_location_exist(conn, user_id, location_id, lang) +} + +/// Checks if a series location element exists (alias for is_location_element_exist). +pub fn series_location_element_exists(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { + is_location_element_exist(conn, user_id, element_id, lang) +} + +/// Checks if a series location sub-element exists (alias for is_location_sub_element_exist). +pub fn series_location_sub_element_exists(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult { + is_location_sub_element_exist(conn, user_id, sub_element_id, lang) +} + +/// Inserts a series location for sync (alias with compatible signature). +pub fn insert_sync_series_location( + conn: &Connection, location_id: &str, series_id: &str, user_id: &str, + loc_name: &str, loc_original_name: &str, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_location(conn, location_id, series_id, user_id, loc_name, loc_original_name, last_update, lang) +} + +/// Updates a series location for sync (without original_name). +pub fn update_sync_series_location(conn: &Connection, location_id: &str, user_id: &str, loc_name: &str, last_update: i64, lang: Lang) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_locations SET loc_name = ?1, last_update = ?2 WHERE loc_id = ?3 AND user_id = ?4", + params![loc_name, last_update, location_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le lieu série pour sync.".to_string() } else { "Unable to update series location for sync.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Inserts a series location element for sync (alias with compatible signature). +pub fn insert_sync_series_location_element( + conn: &Connection, element_id: &str, location_id: &str, user_id: &str, + element_name: &str, original_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_location_element(conn, element_id, location_id, user_id, element_name, original_name, element_description, last_update, lang) +} + +/// Updates a series location element for sync (without original_name). +pub fn update_sync_series_location_element(conn: &Connection, element_id: &str, user_id: &str, element_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_location_elements SET element_name = ?1, element_description = ?2, last_update = ?3 WHERE element_id = ?4 AND user_id = ?5", + params![element_name, element_description, last_update, element_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de lieu série pour sync.".to_string() } else { "Unable to update series location element for sync.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Inserts a series location sub-element for sync (alias with compatible signature). +pub fn insert_sync_series_location_sub_element( + conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, + sub_elem_name: &str, original_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_location_sub_element(conn, sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update, lang) +} + +/// Updates a series location sub-element for sync (without original_name). +pub fn update_sync_series_location_sub_element(conn: &Connection, sub_element_id: &str, user_id: &str, sub_elem_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_location_sub_elements SET sub_elem_name = ?1, sub_elem_description = ?2, last_update = ?3 WHERE sub_element_id = ?4 AND user_id = ?5", + params![sub_elem_name, sub_elem_description, last_update, sub_element_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sous-élément série pour sync.".to_string() } else { "Unable to update series sub-element for sync.".to_string() }))?; + + Ok(update_result > 0) +} diff --git a/src-tauri/src/domains/series_spell/repo.rs b/src-tauri/src/domains/series_spell/repo.rs index b4122a9..cb2ff85 100644 --- a/src-tauri/src/domains/series_spell/repo.rs +++ b/src-tauri/src/domains/series_spell/repo.rs @@ -1,599 +1,415 @@ -use rusqlite::{params, Connection}; - -use crate::error::{AppError, AppResult}; -use crate::shared::types::Lang; - -pub struct SeriesSpellResult { - pub spell_id: String, - pub series_id: String, - pub name: String, - pub description: String, - pub appearance: String, - pub tags: String, - pub power_level: Option, - pub components: Option, - pub limitations: Option, - pub notes: Option, -} - -pub struct SeriesSpellTagResult { - pub tag_id: String, - pub name: String, - pub color: Option, -} - -pub struct SeriesSpellsTableResult { - pub spell_id: String, - pub series_id: String, - pub user_id: String, - pub name: String, - pub name_hash: String, - pub description: Option, - pub appearance: Option, - pub tags: Option, - pub power_level: Option, - pub components: Option, - pub limitations: Option, - pub notes: Option, - pub last_update: i64, -} - -pub struct SeriesSpellTagsTableResult { - pub tag_id: String, - pub series_id: String, - pub user_id: String, - pub name: String, - pub hashed_name: String, - pub color: Option, - pub last_update: i64, -} - -pub struct SyncedSeriesSpellResult { - pub spell_id: String, - pub series_id: String, - pub name: String, - pub last_update: i64, -} - -pub struct SyncedSeriesSpellTagResult { - pub tag_id: String, - pub series_id: String, - pub name: String, - pub last_update: i64, -} - -/// Fetches all spells for a specific series. -pub fn fetch_spells(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=?1 AND series_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?; - - let spells = statement - .query_map(params![user_id, series_id], |query_row| { - Ok(SeriesSpellResult { - spell_id: query_row.get(0)?, series_id: query_row.get(1)?, - name: query_row.get(2)?, description: query_row.get(3)?, - appearance: query_row.get(4)?, tags: query_row.get(5)?, - power_level: query_row.get(6)?, components: query_row.get(7)?, - limitations: query_row.get(8)?, notes: query_row.get(9)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?; - - Ok(spells) -} - -/// Fetches a single spell by its ID. -pub fn fetch_spell_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=?1 AND spell_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?; - - let spell = statement - .query_row(params![user_id, spell_id], |query_row| { - Ok(SeriesSpellResult { - spell_id: query_row.get(0)?, series_id: query_row.get(1)?, - name: query_row.get(2)?, description: query_row.get(3)?, - appearance: query_row.get(4)?, tags: query_row.get(5)?, - power_level: query_row.get(6)?, components: query_row.get(7)?, - limitations: query_row.get(8)?, notes: query_row.get(9)?, - }) - }); - - match spell { - Ok(spell) => Ok(Some(spell)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() })), - } -} - -/// Inserts a new spell. -pub fn insert_spell( - conn: &Connection, spell_id: &str, series_id: &str, user_id: &str, name: &str, name_hash: &str, - description: &str, appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, - limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - params![spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sort.".to_string() } else { "Unable to add spell.".to_string() }))?; - - if insert_result > 0 { - Ok(spell_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du sort.".to_string() } else { "Error adding spell.".to_string() })) - } -} - -/// Updates an existing spell. -pub fn update_spell( - conn: &Connection, user_id: &str, spell_id: &str, name: &str, name_hash: &str, description: &str, - appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, - limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_spells SET name=?1, name_hash=?2, description=?3, appearance=?4, tags=?5, power_level=?6, components=?7, limitations=?8, notes=?9, last_update=?10 WHERE spell_id=?11 AND user_id=?12", - params![name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort.".to_string() } else { "Unable to update spell.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Deletes a spell. -pub fn delete_spell(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute("DELETE FROM series_spells WHERE spell_id=?1 AND user_id=?2", params![spell_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le sort.".to_string() } else { "Unable to delete spell.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Fetches all spell tags for a series. -pub fn fetch_tags(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT tag_id, name, color FROM series_spell_tags WHERE user_id=?1 AND series_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags.".to_string() } else { "Unable to retrieve tags.".to_string() }))?; - - let tags = statement - .query_map(params![user_id, series_id], |query_row| { - Ok(SeriesSpellTagResult { tag_id: query_row.get(0)?, name: query_row.get(1)?, color: query_row.get(2)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags.".to_string() } else { "Unable to retrieve tags.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags.".to_string() } else { "Unable to retrieve tags.".to_string() }))?; - - Ok(tags) -} - -/// Inserts a new spell tag. -pub fn insert_tag( - conn: &Connection, tag_id: &str, series_id: &str, user_id: &str, name: &str, - hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![tag_id, series_id, user_id, name, hashed_name, color, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le tag.".to_string() } else { "Unable to add tag.".to_string() }))?; - - if insert_result > 0 { - Ok(tag_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du tag.".to_string() } else { "Error adding tag.".to_string() })) - } -} - -/// Updates an existing spell tag. -pub fn update_tag( - conn: &Connection, user_id: &str, tag_id: &str, name: &str, - hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_spell_tags SET name=?1, hashed_name=?2, color=?3, last_update=?4 WHERE tag_id=?5 AND user_id=?6", - params![name, hashed_name, color, last_update, tag_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le tag.".to_string() } else { "Unable to update tag.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Deletes a spell tag. -pub fn delete_tag(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute("DELETE FROM series_spell_tags WHERE tag_id=?1 AND user_id=?2", params![tag_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le tag.".to_string() } else { "Unable to delete tag.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Checks if a spell exists. -pub fn is_spell_exist(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_spells WHERE spell_id=?1 AND user_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sort.".to_string() } else { "Unable to check spell existence.".to_string() }))?; - - let result = statement.query_row(params![spell_id, user_id], |_query_row| Ok(())); - - match result { - Ok(_) => Ok(true), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), - Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sort.".to_string() } else { "Unable to check spell existence.".to_string() })), - } -} - -/// Fetches all spells for a series for sync. -pub fn fetch_series_spells_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))?; - - let spells = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesSpellsTableResult { - spell_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - name_hash: query_row.get(4)?, description: query_row.get(5)?, - appearance: query_row.get(6)?, tags: query_row.get(7)?, - power_level: query_row.get(8)?, components: query_row.get(9)?, - limitations: query_row.get(10)?, notes: query_row.get(11)?, - last_update: query_row.get(12)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))?; - - Ok(spells) -} - -/// Fetches all spell tags for a series for sync. -pub fn fetch_series_spell_tags_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; - - let tags = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesSpellTagsTableResult { - tag_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, color: query_row.get(5)?, - last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; - - Ok(tags) -} - -/// Fetches all series spells for a user for sync comparison. -pub fn fetch_synced_series_spells(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, name, last_update FROM series_spells WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts de série pour sync.".to_string() } else { "Unable to retrieve series spells for sync.".to_string() }))?; - - let spells = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesSpellResult { spell_id: query_row.get(0)?, series_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts de série pour sync.".to_string() } else { "Unable to retrieve series spells for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts de série pour sync.".to_string() } else { "Unable to retrieve series spells for sync.".to_string() }))?; - - Ok(spells) -} - -/// Fetches all series spell tags for a user for sync comparison. -pub fn fetch_synced_series_spell_tags(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT tag_id, series_id, name, last_update FROM series_spell_tags WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; - - let tags = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesSpellTagResult { tag_id: query_row.get(0)?, series_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; - - Ok(tags) -} - -/// Fetches a complete spell by ID for sync. -pub fn fetch_spell_table_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE spell_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort complet.".to_string() } else { "Unable to retrieve complete spell.".to_string() }))?; - - let spell = statement - .query_row(params![spell_id, user_id], |query_row| { - Ok(SeriesSpellsTableResult { - spell_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - name_hash: query_row.get(4)?, description: query_row.get(5)?, - appearance: query_row.get(6)?, tags: query_row.get(7)?, - power_level: query_row.get(8)?, components: query_row.get(9)?, - limitations: query_row.get(10)?, notes: query_row.get(11)?, - last_update: query_row.get(12)?, - }) - }); - - match spell { - Ok(spell) => Ok(Some(spell)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort complet.".to_string() } else { "Unable to retrieve complete spell.".to_string() })), - } -} - -/// Fetches a complete spell tag by ID for sync. -pub fn fetch_spell_tag_table_by_id(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE tag_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le tag complet.".to_string() } else { "Unable to retrieve complete tag.".to_string() }))?; - - let tag = statement - .query_row(params![tag_id, user_id], |query_row| { - Ok(SeriesSpellTagsTableResult { - tag_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, color: query_row.get(5)?, - last_update: query_row.get(6)?, - }) - }); - - match tag { - Ok(tag) => Ok(Some(tag)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le tag complet.".to_string() } else { "Unable to retrieve complete tag.".to_string() })), - } -} - -/// Checks if a spell tag exists. -pub fn is_spell_tag_exist(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_spell_tags WHERE tag_id=?1 AND user_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du tag.".to_string() } else { "Unable to check tag existence.".to_string() }))?; - - let result = statement.query_row(params![tag_id, user_id], |_query_row| Ok(())); - - match result { - Ok(_) => Ok(true), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), - Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du tag.".to_string() } else { "Unable to check tag existence.".to_string() })), - } -} - -/// Inserts a series spell for sync. -pub fn insert_sync_spell( - conn: &Connection, spell_id: &str, series_id: &str, user_id: &str, name: &str, name_hash: &str, - description: &str, appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, - limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13) ON CONFLICT(spell_id) DO UPDATE SET name = excluded.name, name_hash = excluded.name_hash, description = excluded.description, appearance = excluded.appearance, tags = excluded.tags, power_level = excluded.power_level, components = excluded.components, limitations = excluded.limitations, notes = excluded.notes, last_update = excluded.last_update", - params![spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le sort pour sync.".to_string() } else { "Unable to insert spell for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series spell for sync. -pub fn update_sync_spell( - conn: &Connection, user_id: &str, spell_id: &str, name: &str, name_hash: &str, description: &str, - appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, - limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_spells SET name = ?1, name_hash = ?2, description = ?3, appearance = ?4, tags = ?5, power_level = ?6, components = ?7, limitations = ?8, notes = ?9, last_update = ?10 WHERE spell_id = ?11 AND user_id = ?12", - params![name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort pour sync.".to_string() } else { "Unable to update spell for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series spell tag for sync. -pub fn insert_sync_spell_tag( - conn: &Connection, tag_id: &str, series_id: &str, user_id: &str, name: &str, - hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(tag_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, color = excluded.color, last_update = excluded.last_update", - params![tag_id, series_id, user_id, name, hashed_name, color, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le tag pour sync.".to_string() } else { "Unable to insert tag for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series spell tag for sync. -pub fn update_sync_spell_tag( - conn: &Connection, user_id: &str, tag_id: &str, name: &str, - hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_spell_tags SET name = ?1, hashed_name = ?2, color = ?3, last_update = ?4 WHERE tag_id = ?5 AND user_id = ?6", - params![name, hashed_name, color, last_update, tag_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le tag pour sync.".to_string() } else { "Unable to update tag for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Fetches all spells for a series for sync (without user filter). -pub fn fetch_spells_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))?; - - let spells = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesSpellsTableResult { - spell_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - name_hash: query_row.get(4)?, description: query_row.get(5)?, - appearance: query_row.get(6)?, tags: query_row.get(7)?, - power_level: query_row.get(8)?, components: query_row.get(9)?, - limitations: query_row.get(10)?, notes: query_row.get(11)?, - last_update: query_row.get(12)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))?; - - Ok(spells) -} - -/// Fetches all spell tags for a series for sync (without user filter). -pub fn fetch_spell_tags_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; - - let tags = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesSpellTagsTableResult { - tag_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, color: query_row.get(5)?, - last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; - - Ok(tags) -} - -/// Fetches all spells for a series (alias for fetch_series_spells_table). -pub fn fetch_series_spells(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - fetch_series_spells_table(conn, user_id, series_id, lang) -} - -/// Fetches all spell tags for a series (alias for fetch_series_spell_tags_table). -pub fn fetch_series_spell_tags(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - fetch_series_spell_tags_table(conn, user_id, series_id, lang) -} - -/// Checks if a series spell exists (alias for is_spell_exist). -pub fn series_spell_exists(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { - is_spell_exist(conn, user_id, spell_id, lang) -} - -/// Checks if a series spell tag exists (alias for is_spell_tag_exist). -pub fn series_spell_tag_exists(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult { - is_spell_tag_exist(conn, user_id, tag_id, lang) -} - -/// Fetches a complete spell by ID for sync (array format). -pub fn fetch_complete_spell_by_id(conn: &Connection, spell_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE spell_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort complet.".to_string() } else { "Unable to retrieve complete spell.".to_string() }))?; - - let spells = statement - .query_map(params![spell_id], |query_row| { - Ok(SeriesSpellsTableResult { - spell_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - name_hash: query_row.get(4)?, description: query_row.get(5)?, - appearance: query_row.get(6)?, tags: query_row.get(7)?, - power_level: query_row.get(8)?, components: query_row.get(9)?, - limitations: query_row.get(10)?, notes: query_row.get(11)?, - last_update: query_row.get(12)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort complet.".to_string() } else { "Unable to retrieve complete spell.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort complet.".to_string() } else { "Unable to retrieve complete spell.".to_string() }))?; - - Ok(spells) -} - -/// Fetches a complete spell tag by ID for sync (array format). -pub fn fetch_complete_spell_tag_by_id(conn: &Connection, tag_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE tag_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le tag complet.".to_string() } else { "Unable to retrieve complete tag.".to_string() }))?; - - let tags = statement - .query_map(params![tag_id], |query_row| { - Ok(SeriesSpellTagsTableResult { - tag_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, color: query_row.get(5)?, - last_update: query_row.get(6)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le tag complet.".to_string() } else { "Unable to retrieve complete tag.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le tag complet.".to_string() } else { "Unable to retrieve complete tag.".to_string() }))?; - - Ok(tags) -} - -/// Inserts a series spell for sync (alias with compatible signature). -pub fn insert_sync_series_spell( - conn: &Connection, spell_id: &str, series_id: &str, user_id: &str, name: &str, name_hash: &str, - description: &str, appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, - limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_spell(conn, spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, lang) -} - -/// Updates a series spell for sync (simplified signature). -pub fn update_sync_series_spell( - conn: &Connection, spell_id: &str, user_id: &str, name: &str, description: &str, appearance: &str, - tags: &str, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>, - notes: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_spells SET name = ?1, description = ?2, appearance = ?3, tags = ?4, power_level = ?5, components = ?6, limitations = ?7, notes = ?8, last_update = ?9 WHERE spell_id = ?10 AND user_id = ?11", - params![name, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort série pour sync.".to_string() } else { "Unable to update series spell for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series spell tag for sync (alias with compatible signature). -pub fn insert_sync_series_spell_tag( - conn: &Connection, tag_id: &str, series_id: &str, user_id: &str, name: &str, - hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_spell_tag(conn, tag_id, series_id, user_id, name, hashed_name, color, last_update, lang) -} - -/// Updates a series spell tag for sync (simplified signature). -pub fn update_sync_series_spell_tag( - conn: &Connection, tag_id: &str, user_id: &str, name: &str, - color: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_spell_tags SET name = ?1, color = ?2, last_update = ?3 WHERE tag_id = ?4 AND user_id = ?5", - params![name, color, last_update, tag_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le tag série pour sync.".to_string() } else { "Unable to update series tag for sync.".to_string() }))?; - - Ok(update_result > 0) -} +use rusqlite::{params, Connection}; + +use crate::error::{AppError, AppResult}; +use crate::shared::types::Lang; + +pub struct SeriesSpellResult { + pub spell_id: String, + pub _series_id: String, + pub name: String, + pub description: String, + pub appearance: String, + pub tags: String, + pub power_level: Option, + pub components: Option, + pub limitations: Option, + pub notes: Option, +} + +pub struct SeriesSpellTagResult { + pub tag_id: String, + pub name: String, + pub color: Option, +} + +pub struct SeriesSpellsTableResult { + pub spell_id: String, + pub series_id: String, + pub user_id: String, + pub name: String, + pub name_hash: String, + pub description: Option, + pub appearance: Option, + pub tags: Option, + pub power_level: Option, + pub components: Option, + pub limitations: Option, + pub notes: Option, + pub last_update: i64, +} + +pub struct SeriesSpellTagsTableResult { + pub tag_id: String, + pub series_id: String, + pub user_id: String, + pub name: String, + pub hashed_name: String, + pub color: Option, + pub last_update: i64, +} + +pub struct SyncedSeriesSpellResult { + pub spell_id: String, + pub series_id: String, + pub name: String, + pub last_update: i64, +} + +pub struct SyncedSeriesSpellTagResult { + pub tag_id: String, + pub series_id: String, + pub name: String, + pub last_update: i64, +} + +/// Fetches all spells for a specific series. +pub fn fetch_spells(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=?1 AND series_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?; + + let spells = statement + .query_map(params![user_id, series_id], |query_row| { + Ok(SeriesSpellResult { + spell_id: query_row.get(0)?, _series_id: query_row.get(1)?, + name: query_row.get(2)?, description: query_row.get(3)?, + appearance: query_row.get(4)?, tags: query_row.get(5)?, + power_level: query_row.get(6)?, components: query_row.get(7)?, + limitations: query_row.get(8)?, notes: query_row.get(9)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts.".to_string() } else { "Unable to retrieve spells.".to_string() }))?; + + Ok(spells) +} + +/// Fetches a single spell by its ID. +pub fn fetch_spell_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=?1 AND spell_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() }))?; + + let spell = statement + .query_row(params![user_id, spell_id], |query_row| { + Ok(SeriesSpellResult { + spell_id: query_row.get(0)?, _series_id: query_row.get(1)?, + name: query_row.get(2)?, description: query_row.get(3)?, + appearance: query_row.get(4)?, tags: query_row.get(5)?, + power_level: query_row.get(6)?, components: query_row.get(7)?, + limitations: query_row.get(8)?, notes: query_row.get(9)?, + }) + }); + + match spell { + Ok(spell) => Ok(Some(spell)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sort.".to_string() } else { "Unable to retrieve spell.".to_string() })), + } +} + +/// Inserts a new spell. +pub fn insert_spell( + conn: &Connection, spell_id: &str, series_id: &str, user_id: &str, name: &str, name_hash: &str, + description: &str, appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, + limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + params![spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sort.".to_string() } else { "Unable to add spell.".to_string() }))?; + + if insert_result > 0 { + Ok(spell_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du sort.".to_string() } else { "Error adding spell.".to_string() })) + } +} + +/// Updates an existing spell. +pub fn update_spell( + conn: &Connection, user_id: &str, spell_id: &str, name: &str, name_hash: &str, description: &str, + appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, + limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_spells SET name=?1, name_hash=?2, description=?3, appearance=?4, tags=?5, power_level=?6, components=?7, limitations=?8, notes=?9, last_update=?10 WHERE spell_id=?11 AND user_id=?12", + params![name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort.".to_string() } else { "Unable to update spell.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Deletes a spell. +pub fn delete_spell(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute("DELETE FROM series_spells WHERE spell_id=?1 AND user_id=?2", params![spell_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le sort.".to_string() } else { "Unable to delete spell.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Fetches all spell tags for a series. +pub fn fetch_tags(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT tag_id, name, color FROM series_spell_tags WHERE user_id=?1 AND series_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags.".to_string() } else { "Unable to retrieve tags.".to_string() }))?; + + let tags = statement + .query_map(params![user_id, series_id], |query_row| { + Ok(SeriesSpellTagResult { tag_id: query_row.get(0)?, name: query_row.get(1)?, color: query_row.get(2)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags.".to_string() } else { "Unable to retrieve tags.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags.".to_string() } else { "Unable to retrieve tags.".to_string() }))?; + + Ok(tags) +} + +/// Inserts a new spell tag. +pub fn insert_tag( + conn: &Connection, tag_id: &str, series_id: &str, user_id: &str, name: &str, + hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![tag_id, series_id, user_id, name, hashed_name, color, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le tag.".to_string() } else { "Unable to add tag.".to_string() }))?; + + if insert_result > 0 { + Ok(tag_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du tag.".to_string() } else { "Error adding tag.".to_string() })) + } +} + +/// Updates an existing spell tag. +pub fn update_tag( + conn: &Connection, user_id: &str, tag_id: &str, name: &str, + hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_spell_tags SET name=?1, hashed_name=?2, color=?3, last_update=?4 WHERE tag_id=?5 AND user_id=?6", + params![name, hashed_name, color, last_update, tag_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le tag.".to_string() } else { "Unable to update tag.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Deletes a spell tag. +pub fn delete_tag(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute("DELETE FROM series_spell_tags WHERE tag_id=?1 AND user_id=?2", params![tag_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le tag.".to_string() } else { "Unable to delete tag.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Checks if a spell exists. +pub fn is_spell_exist(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT 1 FROM series_spells WHERE spell_id=?1 AND user_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sort.".to_string() } else { "Unable to check spell existence.".to_string() }))?; + + let result = statement.query_row(params![spell_id, user_id], |_query_row| Ok(())); + + match result { + Ok(_) => Ok(true), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), + Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sort.".to_string() } else { "Unable to check spell existence.".to_string() })), + } +} + +/// Fetches all spells for a series for sync. +pub fn fetch_series_spells_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))?; + + let spells = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesSpellsTableResult { + spell_id: query_row.get(0)?, series_id: query_row.get(1)?, + user_id: query_row.get(2)?, name: query_row.get(3)?, + name_hash: query_row.get(4)?, description: query_row.get(5)?, + appearance: query_row.get(6)?, tags: query_row.get(7)?, + power_level: query_row.get(8)?, components: query_row.get(9)?, + limitations: query_row.get(10)?, notes: query_row.get(11)?, + last_update: query_row.get(12)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts pour sync.".to_string() } else { "Unable to retrieve spells for sync.".to_string() }))?; + + Ok(spells) +} + +/// Fetches all spell tags for a series for sync. +pub fn fetch_series_spell_tags_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; + + let tags = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesSpellTagsTableResult { + tag_id: query_row.get(0)?, series_id: query_row.get(1)?, + user_id: query_row.get(2)?, name: query_row.get(3)?, + hashed_name: query_row.get(4)?, color: query_row.get(5)?, + last_update: query_row.get(6)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; + + Ok(tags) +} + +/// Fetches all series spells for a user for sync comparison. +pub fn fetch_synced_series_spells(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT spell_id, series_id, name, last_update FROM series_spells WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts de série pour sync.".to_string() } else { "Unable to retrieve series spells for sync.".to_string() }))?; + + let spells = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesSpellResult { spell_id: query_row.get(0)?, series_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts de série pour sync.".to_string() } else { "Unable to retrieve series spells for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sorts de série pour sync.".to_string() } else { "Unable to retrieve series spells for sync.".to_string() }))?; + + Ok(spells) +} + +/// Fetches all series spell tags for a user for sync comparison. +pub fn fetch_synced_series_spell_tags(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT tag_id, series_id, name, last_update FROM series_spell_tags WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; + + let tags = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesSpellTagResult { tag_id: query_row.get(0)?, series_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags de sort pour sync.".to_string() } else { "Unable to retrieve spell tags for sync.".to_string() }))?; + + Ok(tags) +} + +/// Checks if a spell tag exists. +pub fn is_spell_tag_exist(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT 1 FROM series_spell_tags WHERE tag_id=?1 AND user_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du tag.".to_string() } else { "Unable to check tag existence.".to_string() }))?; + + let result = statement.query_row(params![tag_id, user_id], |_query_row| Ok(())); + + match result { + Ok(_) => Ok(true), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), + Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du tag.".to_string() } else { "Unable to check tag existence.".to_string() })), + } +} + +/// Inserts a series spell for sync. +pub fn insert_sync_spell( + conn: &Connection, spell_id: &str, series_id: &str, user_id: &str, name: &str, name_hash: &str, + description: &str, appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, + limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13) ON CONFLICT(spell_id) DO UPDATE SET name = excluded.name, name_hash = excluded.name_hash, description = excluded.description, appearance = excluded.appearance, tags = excluded.tags, power_level = excluded.power_level, components = excluded.components, limitations = excluded.limitations, notes = excluded.notes, last_update = excluded.last_update", + params![spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le sort pour sync.".to_string() } else { "Unable to insert spell for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Inserts a series spell tag for sync. +pub fn insert_sync_spell_tag( + conn: &Connection, tag_id: &str, series_id: &str, user_id: &str, name: &str, + hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(tag_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, color = excluded.color, last_update = excluded.last_update", + params![tag_id, series_id, user_id, name, hashed_name, color, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le tag pour sync.".to_string() } else { "Unable to insert tag for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Checks if a series spell exists (alias for is_spell_exist). +pub fn series_spell_exists(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { + is_spell_exist(conn, user_id, spell_id, lang) +} + +/// Checks if a series spell tag exists (alias for is_spell_tag_exist). +pub fn series_spell_tag_exists(conn: &Connection, user_id: &str, tag_id: &str, lang: Lang) -> AppResult { + is_spell_tag_exist(conn, user_id, tag_id, lang) +} + +/// Inserts a series spell for sync (alias with compatible signature). +pub fn insert_sync_series_spell( + conn: &Connection, spell_id: &str, series_id: &str, user_id: &str, name: &str, name_hash: &str, + description: &str, appearance: &str, tags: &str, power_level: Option<&str>, components: Option<&str>, + limitations: Option<&str>, notes: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_spell(conn, spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update, lang) +} + +/// Updates a series spell for sync (simplified signature). +pub fn update_sync_series_spell( + conn: &Connection, spell_id: &str, user_id: &str, name: &str, description: &str, appearance: &str, + tags: &str, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>, + notes: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_spells SET name = ?1, description = ?2, appearance = ?3, tags = ?4, power_level = ?5, components = ?6, limitations = ?7, notes = ?8, last_update = ?9 WHERE spell_id = ?10 AND user_id = ?11", + params![name, description, appearance, tags, power_level, components, limitations, notes, last_update, spell_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sort série pour sync.".to_string() } else { "Unable to update series spell for sync.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Inserts a series spell tag for sync (alias with compatible signature). +pub fn insert_sync_series_spell_tag( + conn: &Connection, tag_id: &str, series_id: &str, user_id: &str, name: &str, + hashed_name: &str, color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_spell_tag(conn, tag_id, series_id, user_id, name, hashed_name, color, last_update, lang) +} + +/// Updates a series spell tag for sync (simplified signature). +pub fn update_sync_series_spell_tag( + conn: &Connection, tag_id: &str, user_id: &str, name: &str, + color: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_spell_tags SET name = ?1, color = ?2, last_update = ?3 WHERE tag_id = ?4 AND user_id = ?5", + params![name, color, last_update, tag_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le tag série pour sync.".to_string() } else { "Unable to update series tag for sync.".to_string() }))?; + + Ok(update_result > 0) +} diff --git a/src-tauri/src/domains/series_world/repo.rs b/src-tauri/src/domains/series_world/repo.rs index 754a6b3..0a538ea 100644 --- a/src-tauri/src/domains/series_world/repo.rs +++ b/src-tauri/src/domains/series_world/repo.rs @@ -1,499 +1,349 @@ -use rusqlite::{params, Connection}; - -use crate::error::{AppError, AppResult}; -use crate::shared::types::Lang; - -pub struct SeriesWorldResult { - pub world_id: String, - pub world_name: String, - pub history: Option, - pub politics: Option, - pub economy: Option, - pub religion: Option, - pub languages: Option, - pub element_id: Option, - pub element_name: Option, - pub element_description: Option, - pub element_type: Option, -} - -pub struct SeriesWorldsTableResult { - pub world_id: String, - pub series_id: String, - pub user_id: String, - pub name: String, - pub hashed_name: String, - pub history: Option, - pub politics: Option, - pub economy: Option, - pub religion: Option, - pub languages: Option, - pub last_update: i64, -} - -pub struct SeriesWorldElementsTableResult { - pub element_id: String, - pub world_id: String, - pub user_id: String, - pub element_type: i64, - pub name: String, - pub original_name: String, - pub description: Option, - pub last_update: i64, -} - -pub struct SyncedSeriesWorldResult { - pub world_id: String, - pub series_id: String, - pub name: String, - pub last_update: i64, -} - -pub struct SyncedSeriesWorldElementResult { - pub element_id: String, - pub world_id: String, - pub name: String, - pub last_update: i64, -} - -/// Checks if a world with the given hashed name already exists for a user and series. -pub fn check_world_exist(conn: &Connection, user_id: &str, series_id: &str, world_name: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT world_id FROM series_worlds WHERE user_id=?1 AND series_id=?2 AND hashed_name=?3") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?; - - let exists = statement - .exists(params![user_id, series_id, world_name]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?; - - Ok(exists) -} - -/// Inserts a new world into the series. -pub fn insert_new_world(conn: &Connection, world_id: &str, user_id: &str, series_id: &str, encrypted_name: &str, hashed_name: &str, last_update: i64, lang: Lang) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_worlds (world_id, user_id, series_id, name, hashed_name, last_update) VALUES (?1,?2,?3,?4,?5,?6)", - params![world_id, user_id, series_id, encrypted_name, hashed_name, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le monde.".to_string() } else { "Unable to add world.".to_string() }))?; - - if insert_result > 0 { - Ok(world_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du monde.".to_string() } else { "Error adding world.".to_string() })) - } -} - -/// Fetches all worlds and their elements for a given series. -pub fn fetch_worlds(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM series_worlds AS world LEFT JOIN series_world_elements AS element ON world.world_id = element.world_id WHERE world.user_id = ?1 AND world.series_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?; - - let worlds = statement - .query_map(params![user_id, series_id], |query_row| { - Ok(SeriesWorldResult { - world_id: query_row.get(0)?, world_name: query_row.get(1)?, - history: query_row.get(2)?, politics: query_row.get(3)?, - economy: query_row.get(4)?, religion: query_row.get(5)?, - languages: query_row.get(6)?, element_id: query_row.get(7)?, - element_name: query_row.get(8)?, element_description: query_row.get(9)?, - element_type: query_row.get(10)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?; - - Ok(worlds) -} - -/// Updates a world's information. -pub fn update_world( - conn: &Connection, user_id: &str, world_id: &str, encrypted_name: &str, hashed_name: &str, - history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, - languages: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_worlds SET name=?1, hashed_name=?2, history=?3, politics=?4, economy=?5, religion=?6, languages=?7, last_update=?8 WHERE world_id=?9 AND user_id=?10", - params![encrypted_name, hashed_name, history, politics, economy, religion, languages, last_update, world_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde.".to_string() } else { "Unable to update world.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a new element for a world. -pub fn insert_element( - conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64, - encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)", - params![element_id, world_id, user_id, element_type, encrypted_name, original_name, description, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément.".to_string() } else { "Unable to add element.".to_string() }))?; - - if insert_result > 0 { - Ok(element_id.to_string()) - } else { - Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout de l'élément.".to_string() } else { "Error adding element.".to_string() })) - } -} - -/// Deletes an element from a world. -pub fn delete_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute( - "DELETE FROM series_world_elements WHERE element_id=?1 AND user_id=?2", - params![element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément.".to_string() } else { "Unable to delete element.".to_string() }))?; - - Ok(delete_result > 0) -} - -/// Fetches all worlds for a series for sync. -pub fn fetch_series_worlds_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?; - - let worlds = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesWorldsTableResult { - world_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, history: query_row.get(5)?, - politics: query_row.get(6)?, economy: query_row.get(7)?, - religion: query_row.get(8)?, languages: query_row.get(9)?, - last_update: query_row.get(10)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?; - - Ok(worlds) -} - -/// Fetches all elements for a world for sync. -pub fn fetch_series_world_elements_table(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE world_id = ?1 AND user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; - - let elements = statement - .query_map(params![world_id, user_id], |query_row| { - Ok(SeriesWorldElementsTableResult { - element_id: query_row.get(0)?, world_id: query_row.get(1)?, - user_id: query_row.get(2)?, element_type: query_row.get(3)?, - name: query_row.get(4)?, original_name: query_row.get(5)?, - description: query_row.get(6)?, last_update: query_row.get(7)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; - - Ok(elements) -} - -/// Fetches all series worlds for a user for sync comparison. -pub fn fetch_synced_series_worlds(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT world_id, series_id, name, last_update FROM series_worlds WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?; - - let worlds = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesWorldResult { world_id: query_row.get(0)?, series_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?; - - Ok(worlds) -} - -/// Fetches all series world elements for a user for sync comparison. -pub fn fetch_synced_series_world_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT element_id, world_id, name, last_update FROM series_world_elements WHERE user_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; - - let elements = statement - .query_map(params![user_id], |query_row| { - Ok(SyncedSeriesWorldElementResult { element_id: query_row.get(0)?, world_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; - - Ok(elements) -} - -/// Fetches a complete world by ID for sync. -pub fn fetch_complete_world_by_id(conn: &Connection, world_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE world_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le monde complet.".to_string() } else { "Unable to retrieve complete world.".to_string() }))?; - - let worlds = statement - .query_map(params![world_id], |query_row| { - Ok(SeriesWorldsTableResult { - world_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, history: query_row.get(5)?, - politics: query_row.get(6)?, economy: query_row.get(7)?, - religion: query_row.get(8)?, languages: query_row.get(9)?, - last_update: query_row.get(10)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le monde complet.".to_string() } else { "Unable to retrieve complete world.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le monde complet.".to_string() } else { "Unable to retrieve complete world.".to_string() }))?; - - Ok(worlds) -} - -/// Fetches a complete world element by ID for sync. -pub fn fetch_complete_world_element_by_id(conn: &Connection, element_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE element_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de monde complet.".to_string() } else { "Unable to retrieve complete world element.".to_string() }))?; - - let elements = statement - .query_map(params![element_id], |query_row| { - Ok(SeriesWorldElementsTableResult { - element_id: query_row.get(0)?, world_id: query_row.get(1)?, - user_id: query_row.get(2)?, element_type: query_row.get(3)?, - name: query_row.get(4)?, original_name: query_row.get(5)?, - description: query_row.get(6)?, last_update: query_row.get(7)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de monde complet.".to_string() } else { "Unable to retrieve complete world element.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de monde complet.".to_string() } else { "Unable to retrieve complete world element.".to_string() }))?; - - Ok(elements) -} - -/// Checks if a world exists. -pub fn is_world_exist(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_worlds WHERE world_id=?1 AND user_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?; - - let exists = statement - .exists(params![world_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?; - - Ok(exists) -} - -/// Checks if a world element exists. -pub fn is_world_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM series_world_elements WHERE element_id=?1 AND user_id=?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?; - - let exists = statement - .exists(params![element_id, user_id]) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?; - - Ok(exists) -} - -/// Inserts a series world for sync. -pub fn insert_sync_world( - conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str, - history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, - languages: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_worlds (world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(world_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, history = excluded.history, politics = excluded.politics, economy = excluded.economy, religion = excluded.religion, languages = excluded.languages, last_update = excluded.last_update", - params![world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le monde pour sync.".to_string() } else { "Unable to insert world for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series world for sync. -pub fn update_sync_world( - conn: &Connection, user_id: &str, world_id: &str, name: &str, hashed_name: &str, - history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, - languages: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_worlds SET name = ?1, hashed_name = ?2, history = ?3, politics = ?4, economy = ?5, religion = ?6, languages = ?7, last_update = ?8 WHERE world_id = ?9 AND user_id = ?10", - params![name, hashed_name, history, politics, economy, religion, languages, last_update, world_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde pour sync.".to_string() } else { "Unable to update world for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series world element for sync. -pub fn insert_sync_world_element( - conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64, - name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let insert_result = conn - .execute( - "INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(element_id) DO UPDATE SET element_type = excluded.element_type, name = excluded.name, original_name = excluded.original_name, description = excluded.description, last_update = excluded.last_update", - params![element_id, world_id, user_id, element_type, name, original_name, description, last_update], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément de monde pour sync.".to_string() } else { "Unable to insert world element for sync.".to_string() }))?; - - Ok(insert_result > 0) -} - -/// Updates a series world element for sync. -pub fn update_sync_world_element( - conn: &Connection, user_id: &str, element_id: &str, element_type: i64, name: &str, - original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_world_elements SET element_type = ?1, name = ?2, original_name = ?3, description = ?4, last_update = ?5 WHERE element_id = ?6 AND user_id = ?7", - params![element_type, name, original_name, description, last_update, element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de monde pour sync.".to_string() } else { "Unable to update world element for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Fetches all worlds for a series for sync (without user filter). -pub fn fetch_worlds_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?; - - let worlds = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesWorldsTableResult { - world_id: query_row.get(0)?, series_id: query_row.get(1)?, - user_id: query_row.get(2)?, name: query_row.get(3)?, - hashed_name: query_row.get(4)?, history: query_row.get(5)?, - politics: query_row.get(6)?, economy: query_row.get(7)?, - religion: query_row.get(8)?, languages: query_row.get(9)?, - last_update: query_row.get(10)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?; - - Ok(worlds) -} - -/// Fetches all world elements for a series for sync (via series_worlds join). -pub fn fetch_world_elements_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; - - let elements = statement - .query_map(params![series_id], |query_row| { - Ok(SeriesWorldElementsTableResult { - element_id: query_row.get(0)?, world_id: query_row.get(1)?, - user_id: query_row.get(2)?, element_type: query_row.get(3)?, - name: query_row.get(4)?, original_name: query_row.get(5)?, - description: query_row.get(6)?, last_update: query_row.get(7)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; - - Ok(elements) -} - -/// Fetches all worlds for a series (alias for fetch_series_worlds_table). -pub fn fetch_series_worlds(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - fetch_series_worlds_table(conn, user_id, series_id, lang) -} - -/// Fetches all world elements for a series by series ID. -pub fn fetch_series_world_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?1 AND sw.user_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?; - - let elements = statement - .query_map(params![series_id, user_id], |query_row| { - Ok(SeriesWorldElementsTableResult { - element_id: query_row.get(0)?, world_id: query_row.get(1)?, - user_id: query_row.get(2)?, element_type: query_row.get(3)?, - name: query_row.get(4)?, original_name: query_row.get(5)?, - description: query_row.get(6)?, last_update: query_row.get(7)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?; - - Ok(elements) -} - -/// Checks if a series world exists (alias for is_world_exist). -pub fn series_world_exists(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult { - is_world_exist(conn, user_id, world_id, lang) -} - -/// Checks if a series world element exists (alias for is_world_element_exist). -pub fn series_world_element_exists(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { - is_world_element_exist(conn, user_id, element_id, lang) -} - -/// Inserts a series world for sync (alias with compatible signature). -pub fn insert_sync_series_world( - conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str, - history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, - languages: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_world(conn, world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update, lang) -} - -/// Updates a series world for sync (without hashed_name). -pub fn update_sync_series_world( - conn: &Connection, world_id: &str, user_id: &str, name: &str, history: Option<&str>, - politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, languages: Option<&str>, - last_update: i64, lang: Lang, -) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_worlds SET name = ?1, history = ?2, politics = ?3, economy = ?4, religion = ?5, languages = ?6, last_update = ?7 WHERE world_id = ?8 AND user_id = ?9", - params![name, history, politics, economy, religion, languages, last_update, world_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde série pour sync.".to_string() } else { "Unable to update series world for sync.".to_string() }))?; - - Ok(update_result > 0) -} - -/// Inserts a series world element for sync (alias with compatible signature). -pub fn insert_sync_series_world_element( - conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64, - name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, -) -> AppResult { - insert_sync_world_element(conn, element_id, world_id, user_id, element_type, name, original_name, description, last_update, lang) -} - -/// Updates a series world element for sync (without element_type and original_name). -pub fn update_sync_series_world_element(conn: &Connection, element_id: &str, user_id: &str, name: &str, description: Option<&str>, last_update: i64, lang: Lang) -> AppResult { - let update_result = conn - .execute( - "UPDATE series_world_elements SET name = ?1, description = ?2, last_update = ?3 WHERE element_id = ?4 AND user_id = ?5", - params![name, description, last_update, element_id, user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de monde série pour sync.".to_string() } else { "Unable to update series world element for sync.".to_string() }))?; - - Ok(update_result > 0) -} +use rusqlite::{params, Connection}; + +use crate::error::{AppError, AppResult}; +use crate::shared::types::Lang; + +pub struct SeriesWorldResult { + pub world_id: String, + pub world_name: String, + pub history: Option, + pub politics: Option, + pub economy: Option, + pub religion: Option, + pub languages: Option, + pub element_id: Option, + pub element_name: Option, + pub element_description: Option, + pub element_type: Option, +} + +pub struct SeriesWorldsTableResult { + pub world_id: String, + pub series_id: String, + pub user_id: String, + pub name: String, + pub hashed_name: String, + pub history: Option, + pub politics: Option, + pub economy: Option, + pub religion: Option, + pub languages: Option, + pub last_update: i64, +} + +pub struct SeriesWorldElementsTableResult { + pub element_id: String, + pub world_id: String, + pub user_id: String, + pub element_type: i64, + pub name: String, + pub original_name: String, + pub description: Option, + pub last_update: i64, +} + +pub struct SyncedSeriesWorldResult { + pub world_id: String, + pub series_id: String, + pub name: String, + pub last_update: i64, +} + +pub struct SyncedSeriesWorldElementResult { + pub element_id: String, + pub world_id: String, + pub name: String, + pub last_update: i64, +} + +/// Checks if a world with the given hashed name already exists for a user and series. +pub fn check_world_exist(conn: &Connection, user_id: &str, series_id: &str, world_name: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT world_id FROM series_worlds WHERE user_id=?1 AND series_id=?2 AND hashed_name=?3") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?; + + let exists = statement + .exists(params![user_id, series_id, world_name]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to verify world existence.".to_string() }))?; + + Ok(exists) +} + +/// Inserts a new world into the series. +pub fn insert_new_world(conn: &Connection, world_id: &str, user_id: &str, series_id: &str, encrypted_name: &str, hashed_name: &str, last_update: i64, lang: Lang) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_worlds (world_id, user_id, series_id, name, hashed_name, last_update) VALUES (?1,?2,?3,?4,?5,?6)", + params![world_id, user_id, series_id, encrypted_name, hashed_name, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le monde.".to_string() } else { "Unable to add world.".to_string() }))?; + + if insert_result > 0 { + Ok(world_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du monde.".to_string() } else { "Error adding world.".to_string() })) + } +} + +/// Fetches all worlds and their elements for a given series. +pub fn fetch_worlds(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM series_worlds AS world LEFT JOIN series_world_elements AS element ON world.world_id = element.world_id WHERE world.user_id = ?1 AND world.series_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?; + + let worlds = statement + .query_map(params![user_id, series_id], |query_row| { + Ok(SeriesWorldResult { + world_id: query_row.get(0)?, world_name: query_row.get(1)?, + history: query_row.get(2)?, politics: query_row.get(3)?, + economy: query_row.get(4)?, religion: query_row.get(5)?, + languages: query_row.get(6)?, element_id: query_row.get(7)?, + element_name: query_row.get(8)?, element_description: query_row.get(9)?, + element_type: query_row.get(10)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes.".to_string() } else { "Unable to retrieve worlds.".to_string() }))?; + + Ok(worlds) +} + +/// Updates a world's information. +pub fn update_world( + conn: &Connection, user_id: &str, world_id: &str, encrypted_name: &str, hashed_name: &str, + history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, + languages: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_worlds SET name=?1, hashed_name=?2, history=?3, politics=?4, economy=?5, religion=?6, languages=?7, last_update=?8 WHERE world_id=?9 AND user_id=?10", + params![encrypted_name, hashed_name, history, politics, economy, religion, languages, last_update, world_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde.".to_string() } else { "Unable to update world.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Inserts a new element for a world. +pub fn insert_element( + conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64, + encrypted_name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)", + params![element_id, world_id, user_id, element_type, encrypted_name, original_name, description, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément.".to_string() } else { "Unable to add element.".to_string() }))?; + + if insert_result > 0 { + Ok(element_id.to_string()) + } else { + Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout de l'élément.".to_string() } else { "Error adding element.".to_string() })) + } +} + +/// Deletes an element from a world. +pub fn delete_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { + let delete_result = conn + .execute( + "DELETE FROM series_world_elements WHERE element_id=?1 AND user_id=?2", + params![element_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément.".to_string() } else { "Unable to delete element.".to_string() }))?; + + Ok(delete_result > 0) +} + +/// Fetches all worlds for a series for sync. +pub fn fetch_series_worlds_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?1 AND user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?; + + let worlds = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesWorldsTableResult { + world_id: query_row.get(0)?, series_id: query_row.get(1)?, + user_id: query_row.get(2)?, name: query_row.get(3)?, + hashed_name: query_row.get(4)?, history: query_row.get(5)?, + politics: query_row.get(6)?, economy: query_row.get(7)?, + religion: query_row.get(8)?, languages: query_row.get(9)?, + last_update: query_row.get(10)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes pour sync.".to_string() } else { "Unable to retrieve worlds for sync.".to_string() }))?; + + Ok(worlds) +} + +/// Fetches all series worlds for a user for sync comparison. +pub fn fetch_synced_series_worlds(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT world_id, series_id, name, last_update FROM series_worlds WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?; + + let worlds = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesWorldResult { world_id: query_row.get(0)?, series_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les mondes de série pour sync.".to_string() } else { "Unable to retrieve series worlds for sync.".to_string() }))?; + + Ok(worlds) +} + +/// Fetches all series world elements for a user for sync comparison. +pub fn fetch_synced_series_world_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT element_id, world_id, name, last_update FROM series_world_elements WHERE user_id = ?1") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; + + let elements = statement + .query_map(params![user_id], |query_row| { + Ok(SyncedSeriesWorldElementResult { element_id: query_row.get(0)?, world_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde pour sync.".to_string() } else { "Unable to retrieve world elements for sync.".to_string() }))?; + + Ok(elements) +} + +/// Checks if a world exists. +pub fn is_world_exist(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT 1 FROM series_worlds WHERE world_id=?1 AND user_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?; + + let exists = statement + .exists(params![world_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du monde.".to_string() } else { "Unable to check world existence.".to_string() }))?; + + Ok(exists) +} + +/// Checks if a world element exists. +pub fn is_world_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { + let mut statement = conn + .prepare("SELECT 1 FROM series_world_elements WHERE element_id=?1 AND user_id=?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?; + + let exists = statement + .exists(params![element_id, user_id]) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément.".to_string() } else { "Unable to check element existence.".to_string() }))?; + + Ok(exists) +} + +/// Inserts a series world for sync. +pub fn insert_sync_world( + conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str, + history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, + languages: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_worlds (world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) ON CONFLICT(world_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, history = excluded.history, politics = excluded.politics, economy = excluded.economy, religion = excluded.religion, languages = excluded.languages, last_update = excluded.last_update", + params![world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le monde pour sync.".to_string() } else { "Unable to insert world for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Inserts a series world element for sync. +pub fn insert_sync_world_element( + conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64, + name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + let insert_result = conn + .execute( + "INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(element_id) DO UPDATE SET element_type = excluded.element_type, name = excluded.name, original_name = excluded.original_name, description = excluded.description, last_update = excluded.last_update", + params![element_id, world_id, user_id, element_type, name, original_name, description, last_update], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément de monde pour sync.".to_string() } else { "Unable to insert world element for sync.".to_string() }))?; + + Ok(insert_result > 0) +} + +/// Fetches all world elements for a series by series ID. +pub fn fetch_series_world_elements_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult> { + let mut statement = conn + .prepare("SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?1 AND sw.user_id = ?2") + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?; + + let elements = statement + .query_map(params![series_id, user_id], |query_row| { + Ok(SeriesWorldElementsTableResult { + element_id: query_row.get(0)?, world_id: query_row.get(1)?, + user_id: query_row.get(2)?, element_type: query_row.get(3)?, + name: query_row.get(4)?, original_name: query_row.get(5)?, + description: query_row.get(6)?, last_update: query_row.get(7)?, + }) + }) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))? + .collect::, _>>() + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de monde par série.".to_string() } else { "Unable to retrieve world elements by series.".to_string() }))?; + + Ok(elements) +} + +/// Checks if a series world exists (alias for is_world_exist). +pub fn series_world_exists(conn: &Connection, user_id: &str, world_id: &str, lang: Lang) -> AppResult { + is_world_exist(conn, user_id, world_id, lang) +} + +/// Checks if a series world element exists (alias for is_world_element_exist). +pub fn series_world_element_exists(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult { + is_world_element_exist(conn, user_id, element_id, lang) +} + +/// Inserts a series world for sync (alias with compatible signature). +pub fn insert_sync_series_world( + conn: &Connection, world_id: &str, series_id: &str, user_id: &str, name: &str, hashed_name: &str, + history: Option<&str>, politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, + languages: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_world(conn, world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update, lang) +} + +/// Updates a series world for sync (without hashed_name). +pub fn update_sync_series_world( + conn: &Connection, world_id: &str, user_id: &str, name: &str, history: Option<&str>, + politics: Option<&str>, economy: Option<&str>, religion: Option<&str>, languages: Option<&str>, + last_update: i64, lang: Lang, +) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_worlds SET name = ?1, history = ?2, politics = ?3, economy = ?4, religion = ?5, languages = ?6, last_update = ?7 WHERE world_id = ?8 AND user_id = ?9", + params![name, history, politics, economy, religion, languages, last_update, world_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le monde série pour sync.".to_string() } else { "Unable to update series world for sync.".to_string() }))?; + + Ok(update_result > 0) +} + +/// Inserts a series world element for sync (alias with compatible signature). +pub fn insert_sync_series_world_element( + conn: &Connection, element_id: &str, world_id: &str, user_id: &str, element_type: i64, + name: &str, original_name: &str, description: Option<&str>, last_update: i64, lang: Lang, +) -> AppResult { + insert_sync_world_element(conn, element_id, world_id, user_id, element_type, name, original_name, description, last_update, lang) +} + +/// Updates a series world element for sync (without element_type and original_name). +pub fn update_sync_series_world_element(conn: &Connection, element_id: &str, user_id: &str, name: &str, description: Option<&str>, last_update: i64, lang: Lang) -> AppResult { + let update_result = conn + .execute( + "UPDATE series_world_elements SET name = ?1, description = ?2, last_update = ?3 WHERE element_id = ?4 AND user_id = ?5", + params![name, description, last_update, element_id, user_id], + ) + .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément de monde série pour sync.".to_string() } else { "Unable to update series world element for sync.".to_string() }))?; + + Ok(update_result > 0) +} diff --git a/src-tauri/src/domains/spell/commands.rs b/src-tauri/src/domains/spell/commands.rs index 64a6a92..6ec09dc 100644 --- a/src-tauri/src/domains/spell/commands.rs +++ b/src-tauri/src/domains/spell/commands.rs @@ -18,7 +18,6 @@ fn get_session(session: &State) -> Result<(String, Lang), AppError #[serde(rename_all = "camelCase")] pub struct GetSpellListData { pub book_id: String, - pub enabled: bool, } #[tauri::command] diff --git a/src-tauri/src/domains/spell/repo.rs b/src-tauri/src/domains/spell/repo.rs index 42d89ce..89819cf 100644 --- a/src-tauri/src/domains/spell/repo.rs +++ b/src-tauri/src/domains/spell/repo.rs @@ -5,7 +5,7 @@ use crate::shared::types::Lang; pub struct SpellResult { pub spell_id: String, - pub book_id: String, + pub _book_id: String, pub name: String, pub description: Option, pub appearance: Option, @@ -54,7 +54,7 @@ pub fn fetch_spells(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) let spells = statement .query_map(params![user_id, book_id], |query_row| { Ok(SpellResult { - spell_id: query_row.get(0)?, book_id: query_row.get(1)?, + spell_id: query_row.get(0)?, _book_id: query_row.get(1)?, name: query_row.get(2)?, description: query_row.get(3)?, appearance: query_row.get(4)?, tags: query_row.get(5)?, power_level: query_row.get(6)?, components: query_row.get(7)?, @@ -83,7 +83,7 @@ pub fn fetch_spell_by_id(conn: &Connection, user_id: &str, spell_id: &str, lang: let spells = statement .query_map(params![user_id, spell_id], |query_row| { Ok(SpellResult { - spell_id: query_row.get(0)?, book_id: query_row.get(1)?, + spell_id: query_row.get(0)?, _book_id: query_row.get(1)?, name: query_row.get(2)?, description: query_row.get(3)?, appearance: query_row.get(4)?, tags: query_row.get(5)?, power_level: query_row.get(6)?, components: query_row.get(7)?, diff --git a/src-tauri/src/domains/spell/service.rs b/src-tauri/src/domains/spell/service.rs index 1943635..c99a440 100644 --- a/src-tauri/src/domains/spell/service.rs +++ b/src-tauri/src/domains/spell/service.rs @@ -52,18 +52,6 @@ pub struct SpellListResponse { pub tags: Vec, } -pub struct SyncedSpell { - pub id: String, - pub name: String, - pub last_update: i64, -} - -pub struct SyncedSpellTag { - pub id: String, - pub name: String, - pub last_update: i64, -} - /// Retrieves all spell tags for a specific book. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user diff --git a/src-tauri/src/domains/spell_tag/repo.rs b/src-tauri/src/domains/spell_tag/repo.rs index d4e4827..368473b 100644 --- a/src-tauri/src/domains/spell_tag/repo.rs +++ b/src-tauri/src/domains/spell_tag/repo.rs @@ -5,7 +5,7 @@ use crate::shared::types::Lang; pub struct SpellTagResult { pub tag_id: String, - pub book_id: String, + pub _book_id: String, pub name: String, pub color: Option, } @@ -36,7 +36,7 @@ pub fn fetch_spell_tags(conn: &Connection, user_id: &str, book_id: &str, lang: L let rows = statement .query_map(params![user_id, book_id], |query_row| { Ok(SpellTagResult { - tag_id: query_row.get(0)?, book_id: query_row.get(1)?, + tag_id: query_row.get(0)?, _book_id: query_row.get(1)?, name: query_row.get(2)?, color: query_row.get(3)?, }) }) diff --git a/src-tauri/src/domains/sync/service.rs b/src-tauri/src/domains/sync/service.rs index 33768bc..a5fdf8b 100644 --- a/src-tauri/src/domains/sync/service.rs +++ b/src-tauri/src/domains/sync/service.rs @@ -143,30 +143,6 @@ pub struct SyncedBookFull { pub spell_tags: Vec, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SyncCharacterData { - pub first_name: String, - pub last_name: Option, - pub nickname: Option, - pub age: Option, - pub gender: Option, - pub species: Option, - pub nationality: Option, - pub status: Option, - pub category: String, - pub title: Option, - pub image: Option, - pub role: Option, - pub biography: Option, - pub history: Option, - pub speech_pattern: Option, - pub catchphrase: Option, - pub residence: Option, - pub notes: Option, - pub color: Option, -} - /// Retrieves a complete book with all its associated entities for synchronization. /// Decrypts all encrypted fields using the user's encryption key. /// * `conn` - Database connection diff --git a/src-tauri/src/domains/tombstone/mod.rs b/src-tauri/src/domains/tombstone/mod.rs index 2ea4548..158404f 100644 --- a/src-tauri/src/domains/tombstone/mod.rs +++ b/src-tauri/src/domains/tombstone/mod.rs @@ -1,3 +1,2 @@ pub mod commands; pub mod repo; -pub mod service; diff --git a/src-tauri/src/domains/tombstone/repo.rs b/src-tauri/src/domains/tombstone/repo.rs index 592cef1..48e48c5 100644 --- a/src-tauri/src/domains/tombstone/repo.rs +++ b/src-tauri/src/domains/tombstone/repo.rs @@ -4,11 +4,11 @@ use crate::error::{AppError, AppResult}; use crate::shared::types::Lang; pub struct RemovedItemRecord { - pub removal_id: String, + pub _removal_id: String, pub table_name: String, pub entity_id: String, pub book_id: Option, - pub user_id: String, + pub _user_id: String, pub deleted_at: i64, } @@ -51,9 +51,9 @@ pub fn get_deletions_since(conn: &Connection, user_id: &str, since: i64, lang: L let records = statement .query_map(params![user_id, since], |query_row| { Ok(RemovedItemRecord { - removal_id: query_row.get(0)?, table_name: query_row.get(1)?, + _removal_id: query_row.get(0)?, table_name: query_row.get(1)?, entity_id: query_row.get(2)?, book_id: query_row.get(3)?, - user_id: query_row.get(4)?, deleted_at: query_row.get(5)?, + _user_id: query_row.get(4)?, deleted_at: query_row.get(5)?, }) }) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions.".to_string() } else { "Unable to retrieve deletions.".to_string() }))? @@ -63,63 +63,3 @@ pub fn get_deletions_since(conn: &Connection, user_id: &str, since: i64, lang: L Ok(records) } -/// Checks if an entity was previously deleted. -/// * `conn` - Database connection -/// * `table_name` - The table name -/// * `entity_id` - The entity ID -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the entity was deleted locally. -pub fn was_deleted(conn: &Connection, table_name: &str, entity_id: &str, lang: Lang) -> AppResult { - let mut statement = conn - .prepare("SELECT 1 FROM removed_items WHERE table_name = ?1 AND entity_id = ?2 LIMIT 1") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier si l'élément a été supprimé.".to_string() } else { "Unable to check if item was deleted.".to_string() }))?; - - let exists = statement - .query_row(params![table_name, entity_id], |_query_row| Ok(true)) - .unwrap_or(false); - - Ok(exists) -} - -/// Retrieves all tracked deletions for a specific book. -/// * `conn` - Database connection -/// * `user_id` - The user ID -/// * `book_id` - The book ID -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns array of removed item records for that book. -pub fn get_deletions_for_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { - let mut statement = conn - .prepare("SELECT removal_id, table_name, entity_id, book_id, user_id, deleted_at FROM removed_items WHERE user_id = ?1 AND book_id = ?2") - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions pour ce livre.".to_string() } else { "Unable to retrieve deletions for this book.".to_string() }))?; - - let records = statement - .query_map(params![user_id, book_id], |query_row| { - Ok(RemovedItemRecord { - removal_id: query_row.get(0)?, table_name: query_row.get(1)?, - entity_id: query_row.get(2)?, book_id: query_row.get(3)?, - user_id: query_row.get(4)?, deleted_at: query_row.get(5)?, - }) - }) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions pour ce livre.".to_string() } else { "Unable to retrieve deletions for this book.".to_string() }))? - .collect::, _>>() - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les suppressions pour ce livre.".to_string() } else { "Unable to retrieve deletions for this book.".to_string() }))?; - - Ok(records) -} - -/// Clears all deletion records for a user. -/// WARNING: Only use this when wiping user data completely. -/// * `conn` - Database connection -/// * `user_id` - The user ID -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if cleared successfully. -pub fn clear_all_for_user(conn: &Connection, user_id: &str, lang: Lang) -> AppResult { - let delete_result = conn - .execute( - "DELETE FROM removed_items WHERE user_id = ?1", - params![user_id], - ) - .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer les enregistrements de suppression.".to_string() } else { "Unable to clear deletion records.".to_string() }))?; - - Ok(delete_result > 0) -} diff --git a/src-tauri/src/domains/tombstone/service.rs b/src-tauri/src/domains/tombstone/service.rs index ed600ff..e69de29 100644 --- a/src-tauri/src/domains/tombstone/service.rs +++ b/src-tauri/src/domains/tombstone/service.rs @@ -1,25 +0,0 @@ -use rusqlite::Connection; - -use crate::domains::tombstone::repo; -use crate::error::AppResult; -use crate::helpers::create_unique_id; -use crate::shared::types::Lang; - -/// Records a deleted item for sync tracking. -/// Must be called BEFORE the actual deletion from the source table. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `book_id` - The book ID (None for series items) -/// * `table_name` - The name of the table from which the item is deleted -/// * `entity_id` - The UUID of the deleted entity -/// * `deleted_at` - The timestamp of deletion (from UI via timestamp_in_seconds()) -/// * `lang` - The language for error messages ("fr" or "en") -/// Returns true if the record was inserted successfully. -pub fn delete_tracker( - conn: &Connection, user_id: &str, book_id: Option<&str>, table_name: &str, - entity_id: &str, deleted_at: i64, lang: Lang, -) -> AppResult { - let removal_id: String = create_unique_id(None); - - repo::insert(conn, &removal_id, table_name, entity_id, book_id, user_id, deleted_at, lang) -} diff --git a/src-tauri/src/domains/user/commands.rs b/src-tauri/src/domains/user/commands.rs index 25c8c07..4824f40 100644 --- a/src-tauri/src/domains/user/commands.rs +++ b/src-tauri/src/domains/user/commands.rs @@ -8,6 +8,14 @@ use crate::domains::user::service; use crate::error::AppError; use crate::shared::session::SessionState; +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateUserData { + pub username: String, + pub email: String, + pub author_name: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct InitUserData { @@ -25,8 +33,6 @@ pub struct InitUserResult { #[serde(rename_all = "camelCase")] pub struct SyncUserData { pub user_id: String, - pub first_name: String, - pub last_name: String, pub username: String, pub email: String, } @@ -35,14 +41,10 @@ pub struct SyncUserData { #[serde(rename_all = "camelCase")] pub struct UserInfoResponse { pub id: String, - pub name: String, - pub last_name: String, pub username: String, pub email: String, - pub account_verified: bool, pub author_name: String, pub group_id: i64, - pub terms_accepted: bool, } #[tauri::command] @@ -62,6 +64,11 @@ pub fn init_user(data: InitUserData, db: State, session: State, session: State) -> Resu Ok(UserInfoResponse { id: user_info.id, - name: user_info.name, - last_name: user_info.last_name, username: user_info.username, email: user_info.email, - account_verified: user_info.account_verified, author_name: user_info.author_name, group_id: user_info.group_id, - terms_accepted: user_info.terms_accepted, }) } @@ -160,6 +163,20 @@ pub fn sync_user(data: SyncUserData, db: State, session: State, session: State) -> Result { + let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?; + let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string(); + let lang = session_guard.lang; + drop(session_guard); + + let user_key = key_manager::get_user_encryption_key(&user_id)?; + let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?; + let conn = db_manager.get_connection(&user_id)?; + + service::update_user_infos(conn, &user_key, &user_id, &data.username, &data.email, data.author_name.as_deref(), lang) +} diff --git a/src-tauri/src/domains/user/repo.rs b/src-tauri/src/domains/user/repo.rs index 9649287..7e08cc5 100644 --- a/src-tauri/src/domains/user/repo.rs +++ b/src-tauri/src/domains/user/repo.rs @@ -4,45 +4,18 @@ use crate::error::{AppError, AppResult}; use crate::shared::types::Lang; pub struct UserInfosQueryResponse { - pub first_name: String, - pub last_name: String, pub username: String, pub email: String, - pub plateform: String, - pub term_accepted: i64, - pub account_verified: i64, pub author_name: Option, - pub rite_points: i64, pub user_group: i64, } pub struct UserAccountQuery { - pub first_name: Option, - pub last_name: Option, pub username: String, pub author_name: Option, pub email: String, } -pub struct CredentialResponse { - pub valid: bool, - pub message: Option, - pub user: Option, -} - -pub struct UserResponse { - pub id: String, - pub name: String, - pub last_name: String, - pub username: String, - pub email: String, - pub account_verified: bool, -} - -pub struct GuideTourResult { - pub step_tour: String, -} - /// Inserts a new user into the database. /// * `conn` - Database connection /// * `user_id` - The unique identifier for the user @@ -56,21 +29,18 @@ pub struct GuideTourResult { /// Returns the user's UUID if insertion was successful. /// Errors if the user cannot be registered. pub fn insert_user( - conn: &Connection, user_id: &str, first_name: &str, last_name: &str, username: &str, - origin_username: &str, email: &str, origin_email: &str, lang: Lang, + conn: &Connection, user_id: &str, username: &str, origin_username: &str, + email: &str, origin_email: &str, lang: Lang, ) -> AppResult { - let reg_date = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|duration| duration.as_millis() as i64).unwrap_or(0); - - // Try INSERT first; if user already exists, UPDATE their info instead let insert_result = conn.execute( - "INSERT OR IGNORE INTO erit_users (user_id, first_name, last_name, username, email, origin_email, origin_username, plateform, term_accepted, account_verified, reg_date) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)", - params![user_id, first_name, last_name, username, email, origin_email, origin_username, "desktop", 0, 1, reg_date], + "INSERT OR IGNORE INTO erit_users (user_id, username, email, origin_email, origin_username) VALUES (?1,?2,?3,?4,?5)", + params![user_id, username, email, origin_email, origin_username], ).map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'enregistrer l'utilisateur.".to_string() } else { "Unable to register user.".to_string() }))?; if insert_result == 0 { conn.execute( - "UPDATE erit_users SET first_name = ?1, last_name = ?2, username = ?3, email = ?4, origin_email = ?5, origin_username = ?6 WHERE user_id = ?7", - params![first_name, last_name, username, email, origin_email, origin_username, user_id], + "UPDATE erit_users SET username = ?1, email = ?2, origin_email = ?3, origin_username = ?4 WHERE user_id = ?5", + params![username, email, origin_email, origin_username, user_id], ).map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'utilisateur.".to_string() } else { "Unable to update user.".to_string() }))?; } @@ -85,17 +55,14 @@ pub fn insert_user( /// Errors if the user is not found or cannot be retrieved. pub fn fetch_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult { let mut statement = conn - .prepare("SELECT first_name, last_name, username, email, plateform, term_accepted, account_verified, author_name, erite_points AS rite_points, user_group FROM erit_users WHERE user_id = ?1") + .prepare("SELECT username, email, author_name, user_group FROM erit_users WHERE user_id = ?1") .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations utilisateur.".to_string() } else { "Unable to retrieve user information.".to_string() }))?; let user_info = statement .query_row(params![user_id], |query_row| { Ok(UserInfosQueryResponse { - first_name: query_row.get(0)?, last_name: query_row.get(1)?, - username: query_row.get(2)?, email: query_row.get(3)?, - plateform: query_row.get(4)?, term_accepted: query_row.get(5)?, - account_verified: query_row.get(6)?, author_name: query_row.get(7)?, - rite_points: query_row.get(8)?, user_group: query_row.get(9)?, + username: query_row.get(0)?, email: query_row.get(1)?, + author_name: query_row.get(2)?, user_group: query_row.get(3)?, }) }) .map_err(|error| match error { @@ -121,13 +88,13 @@ pub fn fetch_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResu /// Returns true if the update was successful, false otherwise. /// Errors if the update fails. pub fn update_user_infos( - conn: &Connection, user_id: &str, first_name: &str, last_name: &str, username: &str, - origin_username: &str, email: &str, origin_email: &str, original_author_name: &str, author_name: &str, lang: Lang, + conn: &Connection, user_id: &str, username: &str, origin_username: &str, + email: &str, origin_email: &str, original_author_name: &str, author_name: &str, lang: Lang, ) -> AppResult { let update_result = conn .execute( - "UPDATE erit_users SET first_name = ?1, last_name = ?2, username = ?3, email = ?4, origin_username = ?5, origin_author_name = ?6, author_name = ?7 WHERE user_id = ?8 AND origin_email = ?9", - params![first_name, last_name, username, email, origin_username, original_author_name, author_name, user_id, origin_email], + "UPDATE erit_users SET username = ?1, email = ?2, origin_username = ?3, origin_author_name = ?4, author_name = ?5 WHERE user_id = ?6 AND origin_email = ?7", + params![username, email, origin_username, original_author_name, author_name, user_id, origin_email], ) .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les informations utilisateur.".to_string() } else { "Unable to update user information.".to_string() }))?; @@ -142,12 +109,12 @@ pub fn update_user_infos( /// Errors if the account is not found or cannot be retrieved. pub fn fetch_account_information(conn: &Connection, user_id: &str, lang: Lang) -> AppResult { let mut statement = conn - .prepare("SELECT first_name, last_name, username, author_name, email FROM erit_users WHERE user_id = ?1") + .prepare("SELECT username, author_name, email FROM erit_users WHERE user_id = ?1") .map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du compte.".to_string() } else { "Unable to retrieve account information.".to_string() }))?; let account_info = statement .query_row(params![user_id], |query_row| { - Ok(UserAccountQuery { first_name: query_row.get(0)?, last_name: query_row.get(1)?, username: query_row.get(2)?, author_name: query_row.get(3)?, email: query_row.get(4)? }) + Ok(UserAccountQuery { username: query_row.get(0)?, author_name: query_row.get(1)?, email: query_row.get(2)? }) }) .map_err(|error| match error { rusqlite::Error::QueryReturnedNoRows => AppError::NotFound(if lang == Lang::Fr { "Compte non trouvé.".to_string() } else { "Account not found.".to_string() }), diff --git a/src-tauri/src/domains/user/service.rs b/src-tauri/src/domains/user/service.rs index 58d91ec..698fcec 100644 --- a/src-tauri/src/domains/user/service.rs +++ b/src-tauri/src/domains/user/service.rs @@ -1,156 +1,61 @@ -use rusqlite::Connection; - -use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element}; -use crate::crypto::key_manager::get_user_encryption_key; -use crate::domains::user::repo; -use crate::error::AppResult; -use crate::shared::types::Lang; - -pub struct UserAccount { - pub first_name: String, - pub last_name: String, - pub username: String, - pub author_name: String, - pub email: String, -} - -pub struct GuideTour { - pub key: String, - pub value: bool, -} - -pub struct BookSummary { - pub book_id: String, - pub title: String, - pub sub_title: Option, -} - -pub struct UserInfoResponse { - pub id: String, - pub name: String, - pub last_name: String, - pub username: String, - pub email: String, - pub account_verified: bool, - pub author_name: String, - pub group_id: i64, - pub terms_accepted: bool, - pub guide_tour: Vec, -} - -/// Retrieves complete user information including associated books. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user to fetch -/// * `lang` - The language for error messages -/// Returns the complete user information response. -/// Errors if the user is not found or decryption fails. -pub fn return_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult { - let user_infos_data: repo::UserInfosQueryResponse = repo::fetch_user_infos(conn, user_id, lang)?; - let user_encryption_key: String = get_user_encryption_key(user_id)?; - - let first_name: String = decrypt_data_with_user_key(&user_infos_data.first_name, &user_encryption_key)?; - let last_name: String = decrypt_data_with_user_key(&user_infos_data.last_name, &user_encryption_key)?; - let username: String = decrypt_data_with_user_key(&user_infos_data.username, &user_encryption_key)?; - let email: String = decrypt_data_with_user_key(&user_infos_data.email, &user_encryption_key)?; - let account_verified: bool = user_infos_data.account_verified == 1; - let author_name: String = if let Some(ref author_name_val) = user_infos_data.author_name { decrypt_data_with_user_key(author_name_val, &user_encryption_key)? } else { String::new() }; - let group_id: i64 = user_infos_data.user_group; - let terms_accepted: bool = user_infos_data.term_accepted == 1; - let guide_tour_status: Vec = vec![]; - - Ok(UserInfoResponse { - id: user_id.to_string(), - name: first_name, - last_name, - username, - email, - account_verified, - author_name, - group_id, - terms_accepted, - guide_tour: guide_tour_status, - }) -} - -/// Creates a new user in the database with encrypted personal information. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier for the new user -/// * `first_name` - The user's first name (will be encrypted) -/// * `last_name` - The user's last name (will be encrypted) -/// * `username` - The user's username (will be encrypted and hashed) -/// * `email` - The user's email address (will be encrypted and hashed) -/// * `not_encrypt_password` - The user's password in plain text (unused in current implementation) -/// * `lang` - The preferred language for the user -/// Returns the created user's identifier. -/// Errors if encryption or insertion fails. -pub fn add_user( - conn: &Connection, user_id: &str, first_name: &str, last_name: &str, username: &str, - email: &str, _not_encrypt_password: &str, lang: Lang, -) -> AppResult { - let user_encryption_key: String = get_user_encryption_key(user_id)?; - let encrypted_first_name: String = encrypt_data_with_user_key(first_name, &user_encryption_key)?; - let encrypted_last_name: String = encrypt_data_with_user_key(last_name, &user_encryption_key)?; - let encrypted_username: String = encrypt_data_with_user_key(username, &user_encryption_key)?; - let encrypted_email: String = encrypt_data_with_user_key(email, &user_encryption_key)?; - let hashed_email: String = hash_element(email); - let hashed_username: String = hash_element(username); - - repo::insert_user(conn, user_id, &encrypted_first_name, &encrypted_last_name, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, lang) -} - -/// Updates an existing user's profile information in the database. -/// * `conn` - Database connection -/// * `user_key` - The encryption key for the user's data -/// * `user_id` - The unique identifier of the user to update -/// * `first_name` - The updated first name (will be encrypted) -/// * `last_name` - The updated last name (will be encrypted) -/// * `username` - The updated username (will be encrypted and hashed) -/// * `email` - The updated email address (will be encrypted and hashed) -/// * `author_name` - The optional author/pen name (will be encrypted and hashed if provided) -/// * `lang` - The preferred language for the user -/// Returns true if the update was successful. -/// Errors if encryption or update fails. -pub fn update_user_infos( - conn: &Connection, user_key: &str, user_id: &str, first_name: &str, last_name: &str, - username: &str, email: &str, author_name: Option<&str>, lang: Lang, -) -> AppResult { - let encrypted_first_name: String = encrypt_data_with_user_key(first_name, user_key)?; - let encrypted_last_name: String = encrypt_data_with_user_key(last_name, user_key)?; - let encrypted_username: String = encrypt_data_with_user_key(username, user_key)?; - let encrypted_email: String = encrypt_data_with_user_key(email, user_key)?; - let hashed_email: String = hash_element(email); - let hashed_username: String = hash_element(username); - let mut encrypted_author_name: String = String::new(); - let mut hashed_author_name: String = String::new(); - if let Some(author_name_val) = author_name { - encrypted_author_name = encrypt_data_with_user_key(author_name_val, user_key)?; - hashed_author_name = hash_element(author_name_val); - } - - repo::update_user_infos(conn, user_id, &encrypted_first_name, &encrypted_last_name, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, &hashed_author_name, &encrypted_author_name, lang) -} - -/// Retrieves and decrypts the user's account information from the database. -/// * `conn` - Database connection -/// * `user_id` - The unique identifier of the user -/// * `lang` - The language for error messages -/// Returns the decrypted user account information. -/// Errors if the user is not found or decryption fails. -pub fn get_user_account_information(conn: &Connection, user_id: &str, lang: Lang) -> AppResult { - let account_data: repo::UserAccountQuery = repo::fetch_account_information(conn, user_id, lang)?; - let user_encryption_key: String = get_user_encryption_key(user_id)?; - - let decrypted_first_name: String = if let Some(ref first_name) = account_data.first_name { decrypt_data_with_user_key(first_name, &user_encryption_key)? } else { String::new() }; - let decrypted_last_name: String = if let Some(ref last_name) = account_data.last_name { decrypt_data_with_user_key(last_name, &user_encryption_key)? } else { String::new() }; - let decrypted_username: String = decrypt_data_with_user_key(&account_data.username, &user_encryption_key)?; - let decrypted_author_name: String = if let Some(ref author_name) = account_data.author_name { decrypt_data_with_user_key(author_name, &user_encryption_key)? } else { String::new() }; - let decrypted_email: String = decrypt_data_with_user_key(&account_data.email, &user_encryption_key)?; - - Ok(UserAccount { - first_name: decrypted_first_name, - last_name: decrypted_last_name, - username: decrypted_username, - author_name: decrypted_author_name, - email: decrypted_email, - }) -} +use rusqlite::Connection; + +use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element}; +use crate::crypto::key_manager::get_user_encryption_key; +use crate::domains::user::repo; +use crate::error::AppResult; +use crate::shared::types::Lang; + +pub struct UserInfoResponse { + pub id: String, + pub username: String, + pub email: String, + pub author_name: String, + pub group_id: i64, +} + +/// Retrieves complete user information including associated books. +pub fn return_user_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult { + let user_infos_data: repo::UserInfosQueryResponse = repo::fetch_user_infos(conn, user_id, lang)?; + let user_encryption_key: String = get_user_encryption_key(user_id)?; + + let username: String = decrypt_data_with_user_key(&user_infos_data.username, &user_encryption_key)?; + let email: String = decrypt_data_with_user_key(&user_infos_data.email, &user_encryption_key)?; + let author_name: String = if let Some(ref author_name_val) = user_infos_data.author_name { decrypt_data_with_user_key(author_name_val, &user_encryption_key)? } else { String::new() }; + + Ok(UserInfoResponse { + id: user_id.to_string(), + username, email, author_name, + group_id: user_infos_data.user_group, + }) +} + +/// Creates a new user in the database with encrypted personal information. +pub fn add_user(conn: &Connection, user_id: &str, username: &str, email: &str, lang: Lang) -> AppResult { + let user_encryption_key: String = get_user_encryption_key(user_id)?; + let encrypted_username: String = encrypt_data_with_user_key(username, &user_encryption_key)?; + let encrypted_email: String = encrypt_data_with_user_key(email, &user_encryption_key)?; + let hashed_email: String = hash_element(email); + let hashed_username: String = hash_element(username); + + repo::insert_user(conn, user_id, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, lang) +} + +/// Updates an existing user's profile information in the database. +pub fn update_user_infos( + conn: &Connection, user_key: &str, user_id: &str, + username: &str, email: &str, author_name: Option<&str>, lang: Lang, +) -> AppResult { + let encrypted_username: String = encrypt_data_with_user_key(username, user_key)?; + let encrypted_email: String = encrypt_data_with_user_key(email, user_key)?; + let hashed_email: String = hash_element(email); + let hashed_username: String = hash_element(username); + let mut encrypted_author_name: String = String::new(); + let mut hashed_author_name: String = String::new(); + if let Some(author_name_val) = author_name { + encrypted_author_name = encrypt_data_with_user_key(author_name_val, user_key)?; + hashed_author_name = hash_element(author_name_val); + } + + repo::update_user_infos(conn, user_id, &encrypted_username, &hashed_username, &encrypted_email, &hashed_email, &hashed_author_name, &encrypted_author_name, lang) +} diff --git a/src-tauri/src/domains/world/service.rs b/src-tauri/src/domains/world/service.rs index 3062098..32f2e34 100644 --- a/src-tauri/src/domains/world/service.rs +++ b/src-tauri/src/domains/world/service.rs @@ -10,23 +10,6 @@ use crate::error::{AppError, AppResult}; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; -/// Represents a synced world with its elements for synchronization. -#[derive(Debug, Serialize, Deserialize)] -pub struct SyncedWorld { - pub id: String, - pub name: String, - pub last_update: i64, - pub elements: Vec, -} - -/// Represents a synced world element for synchronization. -#[derive(Debug, Serialize, Deserialize)] -pub struct SyncedWorldElement { - pub id: String, - pub name: String, - pub last_update: i64, -} - /// Represents a single world element with its properties. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct WorldElement { diff --git a/src-tauri/src/helpers.rs b/src-tauri/src/helpers.rs index 4fcab44..1a5f2df 100644 --- a/src-tauri/src/helpers.rs +++ b/src-tauri/src/helpers.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use regex::Regex; @@ -20,20 +19,6 @@ pub fn create_unique_id(existing_id: Option<&str>) -> String { } } -/// Returns the current date as an ISO 8601 string. -/// Equivalent to TS `System.getCurrentDate()`. -pub fn get_current_date() -> String { - Utc::now().to_rfc3339() -} - -/// Converts an ISO date string to a MySQL-compatible date (YYYY-MM-DD). -/// Equivalent to TS `System.dateToMySqlDate()`. -pub fn date_to_mysql_date(iso_date_string: &str) -> String { - match chrono::DateTime::parse_from_rfc3339(iso_date_string) { - Ok(date_object) => date_object.format("%Y-%m-%d").to_string(), - Err(_) => iso_date_string.to_string(), - } -} /// Converts HTML content to plain text by stripping tags and decoding entities. /// Equivalent to TS `System.htmlToText()`. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f6ccc35..030e683 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,6 +32,7 @@ pub fn run() { domains::user::commands::get_platform, domains::user::commands::get_user_info, domains::user::commands::sync_user, + domains::user::commands::update_user_info, // ─── Offline ─────────────────────────────────── domains::offline::commands::offline_pin_set, domains::offline::commands::offline_pin_verify, diff --git a/src-tauri/src/shared/mod.rs b/src-tauri/src/shared/mod.rs index 425f8bf..a3f9952 100644 --- a/src-tauri/src/shared/mod.rs +++ b/src-tauri/src/shared/mod.rs @@ -1,3 +1,2 @@ -pub mod ai_models; pub mod session; pub mod types;