feat(tahuantinsuyu): fase 12 — Control::ChartPicker para Synastry
Cualquier carta del DB se puede elegir como partner de sinastría
(no solo hermanas del contacto actual). El módulo SynastryModule
declara un Control::ChartPicker; el panel renderea un dropdown que
abre un popup con todas las cartas; el shell inyecta las opciones y
resuelve el partner desde la selección persistida.
- modules: nueva variante Control::ChartPicker { key, label } sin
default — las opciones las inyecta el host. SynastryModule.controls()
agrega un picker con key="partner_chart_id".
- store: list_all_charts() — query sin filtros, ordenada por label
case-insensitive. Pensada para selectores cross-contact.
- panel:
- ChartOption struct público (id + label).
- chart_options Vec + chart_picker_value HashMap + chart_picker_open
Option (solo uno abierto a la vez).
- APIs públicas set_chart_options / set_chart_picker para sync.
- set_active_kind inicializa picker_value a None ("automático").
- render_chart_picker: botón "▾ label" en bg_button; al click toggle
un popup absolute con (automático) + separador + cada chart_option
clickable. select_picker emite ControlChanged con Value::String(id)
o Value::Null.
- shell:
- new() llama refresh_chart_options al final para popular el panel
desde el boot.
- refresh_chart_options() builds Vec<ChartOption> desde
list_all_charts (label + birth_brief "YYYY-MM-DD · Lugar"). Lo
llamamos también desde TreeEvent::HierarchyChanged para que el
dropdown refleje altas/bajas.
- resolve_synastry_partner reemplaza find_synastry_partner: 1) lee
module_configs["synastry"]["partner_chart_id"] y resuelve el chart;
2) fallback al automático (primera hermana). El fallback significa
que el módulo sigue funcionando sin elegir manualmente.
- sync_panel_from_configs ahora maneja Value::String / Value::Null
→ set_chart_picker, así el picker se restaura al cargar una carta.
Persistencia: el partner_chart_id va al config_json (fase 11), así
que cada carta recuerda con quién hizo sinastría la última vez.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ use tahuantinsuyu_canvas::{
|
||||
};
|
||||
use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose};
|
||||
use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
|
||||
use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
|
||||
use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent};
|
||||
use tahuantinsuyu_store::Store;
|
||||
use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
|
||||
use yahweh_bus::AppBus;
|
||||
@@ -81,7 +81,7 @@ impl Shell {
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
let mut shell = Self {
|
||||
store,
|
||||
bus,
|
||||
tree,
|
||||
@@ -90,7 +90,25 @@ impl Shell {
|
||||
current_chart: None,
|
||||
current_offset_minutes: 0,
|
||||
module_configs: HashMap::new(),
|
||||
}
|
||||
};
|
||||
shell.refresh_chart_options(cx);
|
||||
shell
|
||||
}
|
||||
|
||||
/// Recarga la lista de opciones para los `Control::ChartPicker` y
|
||||
/// la pushea al panel. Llamado al boot + tras cada
|
||||
/// `TreeEvent::HierarchyChanged`.
|
||||
fn refresh_chart_options(&self, cx: &mut Context<Self>) {
|
||||
let charts = self.store.list_all_charts().unwrap_or_default();
|
||||
let options: Vec<ChartOption> = charts
|
||||
.into_iter()
|
||||
.map(|c| ChartOption {
|
||||
id: c.id.to_string(),
|
||||
label: format!("{} — {}", c.label, format_birth_brief(&c.birth_data)),
|
||||
})
|
||||
.collect();
|
||||
self.panel
|
||||
.update(cx, |p, cx| p.set_chart_options(options, cx));
|
||||
}
|
||||
|
||||
fn on_tree_event(&mut self, ev: &TreeEvent, cx: &mut Context<Self>) {
|
||||
@@ -98,6 +116,10 @@ impl Shell {
|
||||
TreeEvent::Selected(s) => s,
|
||||
TreeEvent::Opened(s) => s,
|
||||
TreeEvent::HierarchyChanged => {
|
||||
// La jerarquía cambió (alta/baja de cartas) — refrescar
|
||||
// las opciones del picker para que aparezcan / desaparezcan
|
||||
// en el dropdown.
|
||||
self.refresh_chart_options(cx);
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -211,7 +233,7 @@ impl Shell {
|
||||
});
|
||||
}
|
||||
if module_enabled(&self.module_configs, "synastry") {
|
||||
if let Some(partner) = self.find_synastry_partner() {
|
||||
if let Some(partner) = self.resolve_synastry_partner() {
|
||||
requests.push(PipelineRequest::Synastry {
|
||||
partner_chart: Box::new(partner),
|
||||
});
|
||||
@@ -220,10 +242,22 @@ impl Shell {
|
||||
requests
|
||||
}
|
||||
|
||||
/// Encuentra una carta hermana del contacto actual (cualquier otra
|
||||
/// carta con el mismo `contact_id` ≠ self). `None` si no hay
|
||||
/// hermana — el shell salta el request silenciosamente.
|
||||
fn find_synastry_partner(&self) -> Option<Chart> {
|
||||
/// Resuelve la carta partner para sinastría: 1) si el picker tiene
|
||||
/// un `partner_chart_id` válido en `module_configs`, lo usa; 2)
|
||||
/// si no, cae al automático (primera carta hermana del contacto
|
||||
/// actual). `None` si nada matchea — el request se salta.
|
||||
fn resolve_synastry_partner(&self) -> Option<Chart> {
|
||||
let manual = self
|
||||
.module_configs
|
||||
.get("synastry")
|
||||
.and_then(|c| c.get("partner_chart_id"))
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse::<tahuantinsuyu_model::ChartId>().ok())
|
||||
.and_then(|id| self.store.get_chart(id).ok());
|
||||
manual.or_else(|| self.find_synastry_partner_auto())
|
||||
}
|
||||
|
||||
fn find_synastry_partner_auto(&self) -> Option<Chart> {
|
||||
let current = self.current_chart.as_ref()?;
|
||||
let siblings = self.store.list_charts(current.contact_id).ok()?;
|
||||
siblings.into_iter().find(|c| c.id != current.id)
|
||||
@@ -272,8 +306,8 @@ impl Shell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushea cada toggle/slider del `module_configs` al panel para que
|
||||
/// la UI refleje el estado persistido al cargar una carta.
|
||||
/// Pushea cada toggle/slider/picker del `module_configs` al panel
|
||||
/// para que la UI refleje el estado persistido al cargar una carta.
|
||||
fn sync_panel_from_configs(&mut self, cx: &mut Context<Self>) {
|
||||
let snapshot: Vec<(String, serde_json::Value)> = self
|
||||
.module_configs
|
||||
@@ -288,6 +322,10 @@ impl Shell {
|
||||
p.set_toggle(module_id, key, b, cx);
|
||||
} else if let Some(f) = value.as_f64() {
|
||||
p.set_slider(module_id, key, f, cx);
|
||||
} else if let Some(s) = value.as_str() {
|
||||
p.set_chart_picker(module_id, key, Some(s.to_string()), cx);
|
||||
} else if value.is_null() {
|
||||
p.set_chart_picker(module_id, key, None, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +520,16 @@ impl Shell {
|
||||
// Helpers de module_configs
|
||||
// =====================================================================
|
||||
|
||||
/// Etiqueta breve para mostrar al elegir una carta en el picker:
|
||||
/// `"YYYY-MM-DD · Lugar"` cuando hay lugar, sino solo la fecha.
|
||||
fn format_birth_brief(birth: &tahuantinsuyu_model::StoredBirthData) -> String {
|
||||
let date = format!("{:04}-{:02}-{:02}", birth.year, birth.month, birth.day);
|
||||
match &birth.birthplace_label {
|
||||
Some(p) if !p.is_empty() => format!("{} · {}", date, p),
|
||||
_ => date,
|
||||
}
|
||||
}
|
||||
|
||||
/// Edad en años decimales desde el nacimiento hasta el reloj actual.
|
||||
/// Aproximación: ignora la TZ de nacimiento (no afecta a resolución de
|
||||
/// año) y usa una fracción de año tropical sobre los segundos Unix.
|
||||
|
||||
Reference in New Issue
Block a user