use super::catalog::check_masks;
use super::dom_id;
use super::list::ListItem;
use crate::action::{ActionMeta, DataQuery, PageRequest, Result};
use crate::ctx::{BoxFuture, Ctx};
use crate::grid::{FetchFn, fetch_via};
use crate::i18n::Text;
use crate::model::Model;
use crate::render::Render;
use crate::screen::{Component, GridSource, LoadedPage, Node};
use crate::time::{Date, DateTime};
use serde_json::{Value as Json, json};
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a day of the week",
label = "return a Date, an Option<Date> or a weekday number (u8, 1 = Monday to 7 = Sunday)",
note = "for example `.day(|e: &Event| e.date)` or `.day(|c: &Class| c.weekday)`"
)]
pub trait WeekDay {
fn weekday(&self) -> Option<u8>;
}
impl WeekDay for Date {
fn weekday(&self) -> Option<u8> {
Some(Date::weekday(self))
}
}
impl WeekDay for Option<Date> {
fn weekday(&self) -> Option<u8> {
self.as_ref().map(Date::weekday)
}
}
impl WeekDay for u8 {
fn weekday(&self) -> Option<u8> {
(1..=7).contains(self).then_some(*self)
}
}
type DayFn<E> = Box<dyn Fn(&E) -> Option<u8> + Send + Sync>;
type ItemFn<E> = Box<dyn Fn(&E) -> ListItem + Send + Sync>;
pub const MAX_WEEK_ROWS: u32 = 500;
pub struct WeekSchedule<E: Model> {
action: ActionMeta,
fetch: FetchFn<E>,
day: Option<DayFn<E>>,
item: Option<ItemFn<E>>,
title: Option<Text>,
limit: u32,
}
impl<E: Model> WeekSchedule<E> {
pub fn of<A>(_source: A) -> Self
where
A: crate::action::Action<Input = DataQuery, Output = Result<crate::action::Page<E>>>,
{
WeekSchedule {
action: A::META,
fetch: fetch_via::<A, E>,
day: None,
item: None,
title: None,
limit: 100,
}
}
pub fn day<D: WeekDay>(mut self, day: impl Fn(&E) -> D + Send + Sync + 'static) -> Self {
self.day = Some(Box::new(move |row| day(row).weekday()));
self
}
pub fn item(mut self, build: impl Fn(&E) -> ListItem + Send + Sync + 'static) -> Self {
self.item = Some(Box::new(build));
self
}
pub fn title(mut self, title: impl Into<Text>) -> Self {
self.title = Some(title.into());
self
}
pub fn limit(mut self, rows: u32) -> Self {
self.limit = rows.clamp(1, MAX_WEEK_ROWS);
self
}
fn allow(&self, query: &mut DataQuery) {
*query = DataQuery {
page: PageRequest::Offset {
offset: 0,
limit: self.limit,
},
time_zone: query.time_zone,
..DataQuery::default()
};
}
fn days(&self, out: &mut String, r: &Render, page: Option<&LoadedPage>) {
let today = r
.ctx
.map(|c| c.today())
.unwrap_or_else(|| r.tz.today(DateTime::now()))
.weekday();
let first = Date::first_weekday(r.locale);
let rows = page.and_then(|p| p.items.downcast_ref::<Vec<E>>());
let id = dom_id(r, "week");
let t = |key: &str| r.text(&Text::key(format!("zeyn.ui.week.{key}")));
for i in 0..7u8 {
let day = (first - 1 + i) % 7 + 1;
let is_today = day == today;
out.push_str(&format!(
r#"<section class="z-week-day{}" aria-labelledby="{id}-{day}"><h3 class="z-week-name" id="{id}-{day}">{}{}</h3>"#,
if is_today { " z-week-today" } else { "" },
t(&format!("days.{day}")),
if is_today { format!(r#" <span class="z-week-badge">{}</span>"#, t("today")) } else { String::new() }
));
let mut items = String::new();
if let (Some(rows), Some(place), Some(item)) = (rows, &self.day, &self.item) {
for row in rows
.iter()
.filter(|row| place(row) == Some(day))
{
item(row).html(&mut items, r);
}
}
if items.is_empty() {
out.push_str(&format!(r#"<p class="z-week-empty">{}</p>"#, t("empty")));
} else {
out.push_str(&format!(r#"<ul class="z-list z-week-items">{items}</ul>"#));
}
out.push_str("</section>");
}
}
fn render_with(&self, out: &mut String, r: &Render, page: Option<&LoadedPage>) {
let id = dom_id(r, "week");
let (title, labelled) = match &self.title {
Some(t) => (
format!(
r#"<h2 class="z-cards-title" id="{id}-title">{}</h2>"#,
r.text(t)
),
format!(r#" aria-labelledby="{id}-title""#),
),
None => (
String::new(),
format!(
r#" aria-label="{}""#,
r.text(&Text::key("zeyn.ui.week.label"))
),
),
};
out.push_str(&format!(
r#"<section class="z-week"{labelled}>{title}<div class="z-week-days">"#
));
self.days(out, r, page);
out.push_str("</div></section>");
}
}
impl<E: Model> GridSource for WeekSchedule<E> {
fn action(&self) -> ActionMeta {
self.action
}
fn first_query(&self, _view: Option<&crate::grid::DataGridView>) -> DataQuery {
let mut query = DataQuery::default();
self.allow(&mut query);
query
}
fn clean_view(&self, _view: crate::grid::DataGridView) -> crate::grid::DataGridView {
crate::grid::DataGridView::default()
}
fn head_html(&self, _r: &Render, _view: Option<&crate::grid::DataGridView>) -> String {
String::new()
}
fn fetch(&self, ctx: Ctx, mut query: DataQuery) -> BoxFuture<Result<LoadedPage>> {
self.allow(&mut query);
query.time_zone = ctx.time_zone();
let fetch = self.fetch;
Box::pin(async move {
check_masks::<E>(&ctx)?;
let page = fetch(ctx, query).await?;
Ok(LoadedPage {
count: page.items.len(),
total: page.total,
next: None,
offset: 0,
totals: Vec::new(),
items: Box::new(page.items),
view: None,
saved_views: false,
depth: 0,
path: Vec::new(),
groups: None,
parents: Vec::new(),
})
})
}
fn rows_html(&self, page: &LoadedPage, r: &Render) -> String {
let mut out = String::new();
self.days(&mut out, r, Some(page));
out
}
fn foot_html(&self, _page: &LoadedPage, _r: &Render) -> Option<String> {
None
}
fn render_loaded(&self, out: &mut String, r: &Render, page: &LoadedPage) {
self.render_with(out, r, Some(page));
}
fn tool_schema(&self) -> Json {
json!({ "type": "object", "description": "Rows of the week. The server sets the query; nothing can be chosen." })
}
fn fields(&self) -> &'static [crate::model::FieldMeta] {
E::fields()
}
}
impl<E: Model> Component for WeekSchedule<E> {
fn render(&self, out: &mut String, r: &Render) {
self.render_with(out, r, None);
}
fn schema(&self) -> Json {
json!({ "type": "week_schedule", "model": E::NAME, "action": self.action, "title": self.title, "limit": self.limit })
}
fn grid(&self) -> Option<&dyn GridSource> {
Some(self)
}
}
impl<E: Model> From<WeekSchedule<E>> for Node {
fn from(w: WeekSchedule<E>) -> Self {
Node::new(w)
}
}