tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definitions

feat(tahuantinsuyu): editor inline para cartas libres (F2)

Las cartas libres ahora se pueden editar en su totalidad (fecha,
hora, lugar, lat/lon/alt, TZ, label) desde el menú contextual.
La edición es **in-memory** — la carta se queda como libre tras
el cambio; para persistirla hay que usar "Guardar como…".

Tree:
- Nuevo `Modal::EditFreeChart { source_id, form, error }`
  paralelo al `Modal::EditChart` existente. Reusa la misma
  `ChartForm` (11 TextInputs: name + place + date + time + TZ
  + lat/lon/alt) y la misma función `render_chart_form` para
  pintarlo. El title cambia a "Editar carta libre".
- `open_edit_free_chart_modal(source_id, w, cx)`: lee el entry
  de `self.free_charts` (que ahora trae `birth_data` además
  de id+label), pre-puebla el form, y abre el modal.
- Submit: `build_chart_from_form` parsea + valida; al éxito
  emite nuevo evento `TreeEvent::FreeChartEditConfirmed
  { source_id, birth_data, label }`. Al error, conserva el
  modal con la pill destructiva.
- City picker funciona como antes — el branch de
  `apply_city_preset` se extendió para que reconozca
  `Modal::EditFreeChart` además de Create/Edit.

Modelo:
- `FreeChartEntry` ahora incluye `birth_data: StoredBirthData`
  además de id+label. El shell se lo pasa al setter; el tree
  lo usa para pre-poblar el form sin tener que pedirlo al
  shell.

Shell:
- `push_free_charts_to_tree` clona `birth_data` en cada entry.
- Handler `FreeChartEditConfirmed`: actualiza
  `free_charts[id]` con los nuevos datos + label, re-publica
  al tree, y si la carta editada era la activa, re-renderea
  el wheel.

Menú contextual de "Cartas libres" / `<carta libre>` ahora:
- Editar datos…
- Guardar como…
- Borrar  (no se ofrece sobre sky-now)

10 tests verdes (sin afectar lo testeado).

Próximo y último: F4 — botón "Guardar como…" en cada módulo
overlay (RS, prog, sa, gr) que captura la carta derivada con
un sufijo automático en el contacto original.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

dd836522ab861a796d8ac54b01de05d2ef52ff27 · by sergio · (5 months ago) · parent a83b0396c · browse the tree at this commit

Semantic delta · 10

crates/apps/tahuantinsuyu/src/shell.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs

2 files · +145 −3

diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index c803645..0cd033e 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -249,6 +249,7 @@ impl Shell {
             entries.push(FreeChartEntry {
                 id: FreeChartId::sky_now(),
                 label: c.label.clone(),
+                birth_data: c.birth_data.clone(),
             });
         }
         let mut others: Vec<(&FreeChartId, &Chart)> = self
@@ -261,6 +262,7 @@ impl Shell {
             entries.push(FreeChartEntry {
                 id: id.clone(),
                 label: c.label.clone(),
+                birth_data: c.birth_data.clone(),
             });
         }
         self.tree
@@ -502,6 +504,32 @@ impl Shell {
                 );
                 return;
             }
+            TreeEvent::FreeChartEditConfirmed {
+                source_id,
+                birth_data,
+                label,
+            } => {
+                if let Some(chart) = self.free_charts.get_mut(source_id) {
+                    chart.birth_data = birth_data.clone();
+                    chart.label = label.clone();
+                }
+                self.push_free_charts_to_tree(cx);
+                // Si la carta editada era la activa, re-render.
+                if let Some(current) = self.current_chart.as_mut() {
+                    // Heurística: comparamos por label (ya cambiado al
+                    // que pidió el usuario). Si el label de la activa
+                    // coincide, era esta carta.
+                    if current.label == label.clone()
+                        || current.birth_data.subject_name.as_deref() == Some("Cielo")
+                    {
+                        if let Some(updated) = self.free_charts.get(source_id) {
+                            *current = updated.clone();
+                            self.render_current(cx);
+                        }
+                    }
+                }
+                return;
+            }
             TreeEvent::DeleteFreeChartRequested(id) => {
                 if id.is_sky_now() {
                     return; // no se borra el Cielo
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
index 821b8b6..086d03d 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-tree/src/lib.rs
@@ -79,6 +79,13 @@ pub enum TreeEvent {
         contact: Option<ContactId>,
         new_contact_name: Option<String>,
     },
+    /// Submit del modal "Editar datos" para una carta libre. El
+    /// shell aplica al mapa `free_charts` y re-renderea el wheel.
+    FreeChartEditConfirmed {
+        source_id: FreeChartId,
+        birth_data: StoredBirthData,
+        label: String,
+    },
 }
 
 // =====================================================================
@@ -157,6 +164,15 @@ enum Modal {
         form: ChartForm,
         error: Option<SharedString>,
     },
+    /// Editar los datos (fecha/hora/lugar) de una carta libre.
+    /// Reusa el mismo `ChartForm` que `EditChart`. El submit emite
+    /// `FreeChartEditConfirmed` que el shell aplica al mapa
+    /// `free_charts` y re-renderea el wheel.
+    EditFreeChart {
+        source_id: FreeChartId,
+        form: ChartForm,
+        error: Option<SharedString>,
+    },
     /// Guardar una carta libre como `Chart` persistido. El usuario
     /// elige nombre + contacto destino (existente de la lista o
     /// uno nuevo creado al vuelo). El shell escucha
@@ -227,13 +243,15 @@ pub struct TahuantinsuyuTree {
     free_charts: Vec<FreeChartEntry>,
 }
 
-/// Entrada de la sección "Cartas libres" — id + label visible. La
-/// estructura del Chart en sí vive en el shell (`free_charts` de
-/// `Shell`), no en el tree.
+/// Entrada de la sección "Cartas libres" — id + label visible +
+/// birth_data clonado (para pre-poblar el modal "Editar datos…").
+/// El Chart completo vive en el shell; el tree mantiene esta
+/// proyección compacta para no depender del shell en cada operación.
 #[derive(Clone, Debug)]
 pub struct FreeChartEntry {
     pub id: FreeChartId,
     pub label: String,
+    pub birth_data: StoredBirthData,
 }
 
 /// Preset de ciudad con datos canónicos para autocompletar lat/lon/tz
@@ -777,6 +795,7 @@ impl TahuantinsuyuTree {
         let form = match self.modal.as_mut() {
             Some(Modal::CreateChart { form, .. }) => form,
             Some(Modal::EditChart { form, .. }) => form,
+            Some(Modal::EditFreeChart { form, .. }) => form,
             _ => {
                 self.city_picker_open = false;
                 cx.notify();
@@ -878,6 +897,58 @@ impl TahuantinsuyuTree {
     /// contactos es un snapshot recursivo de toda la jerarquía
     /// (no solo el nivel raíz). El usuario elige uno existente o
     /// deja en "Nuevo contacto" para que se cree uno al confirmar.
+    /// Abre el modal "Editar datos" para una carta libre. Pre-puebla
+    /// `ChartForm` con `birth_data` actual de la entry. Submit emite
+    /// `FreeChartEditConfirmed` que el shell aplica al mapa de
+    /// `free_charts` y re-renderea.
+    fn open_edit_free_chart_modal(
+        &mut self,
+        source_id: FreeChartId,
+        window: &mut Window,
+        cx: &mut Context<'_, Self>,
+    ) {
+        let entry = match self.free_charts.iter().find(|e| e.id == source_id) {
+            Some(e) => e.clone(),
+            None => return,
+        };
+        let bd = &entry.birth_data;
+        let form = ChartForm {
+            name: self.make_input("Etiqueta de la carta", &entry.label, window, cx),
+            place: self.make_input(
+                "Lugar (ciudad, país)",
+                bd.birthplace_label.as_deref().unwrap_or(""),
+                window,
+                cx,
+            ),
+            year: self.make_input("Año", &bd.year.to_string(), window, cx),
+            month: self.make_input("Mes", &bd.month.to_string(), window, cx),
+            day: self.make_input("Día", &bd.day.to_string(), window, cx),
+            hour: self.make_input("Hora (0-23)", &bd.hour.to_string(), window, cx),
+            minute: self.make_input("Minuto", &bd.minute.to_string(), window, cx),
+            tz_offset_min: self.make_input(
+                "TZ offset (min)",
+                &bd.tz_offset_minutes.to_string(),
+                window,
+                cx,
+            ),
+            lat: self.make_input("Latitud (°)", &format!("{}", bd.latitude_deg), window, cx),
+            lon: self.make_input(
+                "Longitud (°)",
+                &format!("{}", bd.longitude_deg),
+                window,
+                cx,
+            ),
+            alt: self.make_input("Altitud (m)", &format!("{}", bd.altitude_m), window, cx),
+        };
+        form.name.update(cx, |i, _| i.request_focus(window));
+        self.modal = Some(Modal::EditFreeChart {
+            source_id,
+            form,
+            error: None,
+        });
+        self.close_menu(cx);
+    }
+
     /// Cambia `selected_contact` del modal `SaveFreeChart` activo
     /// sin recrear los inputs. Permite alternar entre los botones
     /// radio "contacto existente" y "Nuevo contacto…".
@@ -1230,6 +1301,32 @@ impl TahuantinsuyuTree {
                     }
                 }
             }
+            Modal::EditFreeChart {
+                source_id,
+                form,
+                error: _,
+            } => {
+                let _ = value;
+                match build_chart_from_form(&form, cx) {
+                    Ok((birth, label)) => {
+                        cx.emit(TreeEvent::FreeChartEditConfirmed {
+                            source_id,
+                            birth_data: birth,
+                            label,
+                        });
+                        self.modal = None;
+                        cx.notify();
+                    }
+                    Err(msg) => {
+                        self.modal = Some(Modal::EditFreeChart {
+                            source_id,
+                            form,
+                            error: Some(SharedString::from(msg)),
+                        });
+                        cx.notify();
+                    }
+                }
+            }
             Modal::SaveFreeChart {
                 source_id,
                 name,
@@ -1617,6 +1714,14 @@ impl TahuantinsuyuTree {
             }
             MenuTarget::FreeChart(fid) => {
                 let is_sky = fid.is_sky_now();
+                let fid_edit = fid.clone();
+                items = items.child(
+                    menu_item("tts-menu-edit-free", "Editar datos…", theme).on_click(
+                        cx.listener(move |this, _: &ClickEvent, w, cx| {
+                            this.open_edit_free_chart_modal(fid_edit.clone(), w, cx);
+                        }),
+                    ),
+                );
                 let fid_save = fid.clone();
                 items = items.child(
                     menu_item("tts-menu-save-free", "Guardar como…", theme).on_click(
@@ -1708,6 +1813,15 @@ impl TahuantinsuyuTree {
                 &self.city_atlas,
                 cx,
             ),
+            Modal::EditFreeChart { form, error, .. } => render_chart_form(
+                theme,
+                "Editar carta libre",
+                form,
+                error.clone(),
+                self.city_picker_open,
+                &self.city_atlas,
+                cx,
+            ),
             Modal::SaveFreeChart {
                 source_id,
                 name,