170aa84a0f
Add indoor_sowing_weeks, direct_sowing_weeks, transplanting_weeks, glasshouse_weeks, harvesting_weeks (integer[]) to backend Cultivar and CreateCultivar structs with INSERT/UPDATE SQL bindings. Frontend PlantingCalendar component rewritten as a compact 52-column Gantt-style grid grouped by month headers. Prefers week data when available, falls back to expanding month data into week ranges. Calendar displayed full-width on cultivar detail page with color legend.
136 lines
5.1 KiB
Rust
136 lines
5.1 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
/// Month boundaries as week ranges (approximate ISO weeks).
|
|
/// Jan=1-4, Feb=5-8, Mar=9-13, Apr=14-17, May=18-22, Jun=23-26,
|
|
/// Jul=27-30, Aug=31-35, Sep=36-39, Oct=40-44, Nov=45-48, Dec=49-52
|
|
const MONTH_WEEK_RANGES: [(u8, u8); 12] = [
|
|
(1, 4), (5, 8), (9, 13), (14, 17), (18, 22), (23, 26),
|
|
(27, 30), (31, 35), (36, 39), (40, 44), (45, 48), (49, 52),
|
|
];
|
|
|
|
const MONTH_LABELS: [&str; 12] = [
|
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
];
|
|
|
|
/// Expand month data (1-12) into week numbers (1-52) for fallback.
|
|
fn months_to_weeks(months: &Option<Vec<i32>>) -> Option<Vec<i32>> {
|
|
let m = months.as_ref()?;
|
|
if m.is_empty() { return None; }
|
|
let mut weeks = Vec::new();
|
|
for &month in m {
|
|
if month >= 1 && month <= 12 {
|
|
let (start, end) = MONTH_WEEK_RANGES[(month - 1) as usize];
|
|
for w in start..=end {
|
|
weeks.push(w as i32);
|
|
}
|
|
}
|
|
}
|
|
weeks.sort();
|
|
weeks.dedup();
|
|
Some(weeks)
|
|
}
|
|
|
|
/// Resolve: prefer week data, fall back to expanding month data.
|
|
fn resolve_weeks(weeks: &Option<Vec<i32>>, months: &Option<Vec<i32>>) -> Option<Vec<i32>> {
|
|
if let Some(w) = weeks {
|
|
if !w.is_empty() {
|
|
return Some(w.clone());
|
|
}
|
|
}
|
|
months_to_weeks(months)
|
|
}
|
|
|
|
#[component]
|
|
pub fn PlantingCalendar(
|
|
indoor_sowing_months: Option<Vec<i32>>,
|
|
direct_sowing_months: Option<Vec<i32>>,
|
|
transplanting_months: Option<Vec<i32>>,
|
|
glasshouse_months: Option<Vec<i32>>,
|
|
harvesting_months: Option<Vec<i32>>,
|
|
indoor_sowing_weeks: Option<Vec<i32>>,
|
|
direct_sowing_weeks: Option<Vec<i32>>,
|
|
transplanting_weeks: Option<Vec<i32>>,
|
|
glasshouse_weeks: Option<Vec<i32>>,
|
|
harvesting_weeks: Option<Vec<i32>>,
|
|
) -> Element {
|
|
let rows: Vec<(&str, &str, Option<Vec<i32>>)> = vec![
|
|
("Indoor Sowing", "cal-indoor", resolve_weeks(&indoor_sowing_weeks, &indoor_sowing_months)),
|
|
("Direct Sowing", "cal-direct", resolve_weeks(&direct_sowing_weeks, &direct_sowing_months)),
|
|
("Transplanting", "cal-transplant", resolve_weeks(&transplanting_weeks, &transplanting_months)),
|
|
("Glasshouse", "cal-glass", resolve_weeks(&glasshouse_weeks, &glasshouse_months)),
|
|
("Harvesting", "cal-harvest", resolve_weeks(&harvesting_weeks, &harvesting_months)),
|
|
];
|
|
|
|
let has_data = rows.iter().any(|(_, _, w)| w.is_some());
|
|
if !has_data {
|
|
return rsx! { p { class: "empty", "No planting calendar data." } };
|
|
}
|
|
|
|
rsx! {
|
|
div { class: "planting-calendar week-calendar",
|
|
// Month header spanning groups of weeks
|
|
div { class: "wcal-month-row",
|
|
div { class: "wcal-label" }
|
|
for (i, label) in MONTH_LABELS.iter().enumerate() {
|
|
{
|
|
let (start, end) = MONTH_WEEK_RANGES[i];
|
|
let span = (end - start + 1) as usize;
|
|
rsx! {
|
|
div {
|
|
class: "wcal-month-header",
|
|
style: "grid-column: span {span};",
|
|
"{label}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Data rows
|
|
for (name, class, weeks) in rows.iter() {
|
|
if weeks.is_some() {
|
|
div { class: "wcal-row",
|
|
div { class: "wcal-label", "{name}" }
|
|
for week in 1..=52i32 {
|
|
{
|
|
let active = weeks.as_ref()
|
|
.map(|w| w.contains(&week))
|
|
.unwrap_or(false);
|
|
rsx! {
|
|
div {
|
|
class: if active { format!("wcal-cell {class} active") } else { "wcal-cell".to_string() },
|
|
title: "W{week}",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Legend
|
|
div { class: "wcal-legend",
|
|
div { class: "wcal-legend-item",
|
|
div { class: "wcal-legend-swatch cal-indoor" }
|
|
span { "Indoor Sowing" }
|
|
}
|
|
div { class: "wcal-legend-item",
|
|
div { class: "wcal-legend-swatch cal-direct" }
|
|
span { "Direct Sowing" }
|
|
}
|
|
div { class: "wcal-legend-item",
|
|
div { class: "wcal-legend-swatch cal-transplant" }
|
|
span { "Transplanting" }
|
|
}
|
|
div { class: "wcal-legend-item",
|
|
div { class: "wcal-legend-swatch cal-glass" }
|
|
span { "Glasshouse" }
|
|
}
|
|
div { class: "wcal-legend-item",
|
|
div { class: "wcal-legend-swatch cal-harvest" }
|
|
span { "Harvesting" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|