mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
248 lines
7.5 KiB
Elixir
248 lines
7.5 KiB
Elixir
defmodule SpazioSolazzoWeb.Admin.AdminCalendarComponent do
|
|
@moduledoc """
|
|
Admin calendar for managing bookings, visualizing capacity, and selecting date ranges.
|
|
"""
|
|
use SpazioSolazzoWeb, :live_component
|
|
|
|
alias SpazioSolazzo.CalendarExt
|
|
|
|
@doc "Resets the calendar selection state."
|
|
def reset(id) do
|
|
send_update(__MODULE__, id: id, reset: true)
|
|
end
|
|
|
|
def update(%{reset: true}, socket) do
|
|
socket =
|
|
socket
|
|
|> assign(start_date: nil)
|
|
|> assign(end_date: nil)
|
|
|> assign(selected_date: nil)
|
|
|
|
{:ok, socket}
|
|
end
|
|
|
|
def update(assigns, socket) do
|
|
first_day =
|
|
assigns[:first_day_of_month] ||
|
|
socket.assigns[:first_day_of_month] ||
|
|
Date.utc_today() |> Date.beginning_of_month()
|
|
|
|
grid = CalendarExt.build_calendar_grid(first_day)
|
|
|
|
socket =
|
|
socket
|
|
|> assign(assigns)
|
|
|> assign_new(:booking_counts, fn -> %{} end)
|
|
|> assign_new(:multi_day_mode, fn -> false end)
|
|
|> assign_new(:start_date, fn -> nil end)
|
|
|> assign_new(:end_date, fn -> nil end)
|
|
|> assign_new(:selected_date, fn -> nil end)
|
|
|> assign(first_day_of_month: first_day)
|
|
|> assign(grid: grid)
|
|
|
|
{:ok, socket}
|
|
end
|
|
|
|
def handle_event("prev_month", _, socket) do
|
|
new_date = Date.shift(socket.assigns.first_day_of_month, month: -1)
|
|
|
|
grid = CalendarExt.build_calendar_grid(new_date)
|
|
|
|
socket =
|
|
socket
|
|
|> assign(first_day_of_month: new_date)
|
|
|> assign(grid: grid)
|
|
|
|
send(self(), {:change_month, new_date})
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("next_month", _, socket) do
|
|
new_date = Date.shift(socket.assigns.first_day_of_month, month: 1)
|
|
|
|
grid = CalendarExt.build_calendar_grid(new_date)
|
|
|
|
socket =
|
|
socket
|
|
|> assign(first_day_of_month: new_date)
|
|
|> assign(grid: grid)
|
|
|
|
send(self(), {:change_month, new_date})
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("toggle_multi_day", _, socket) do
|
|
new_mode = !socket.assigns.multi_day_mode
|
|
|
|
send(self(), {:multi_day_mode_toggle, new_mode})
|
|
|
|
{:noreply,
|
|
assign(socket,
|
|
multi_day_mode: new_mode,
|
|
start_date: nil,
|
|
end_date: nil,
|
|
selected_date: nil
|
|
)}
|
|
end
|
|
|
|
def handle_event("select_date", %{"date" => d}, socket) do
|
|
date = Date.from_iso8601!(d)
|
|
|
|
if socket.assigns.multi_day_mode do
|
|
handle_multi_select(socket, date)
|
|
else
|
|
send(self(), {:date_selected, date, date})
|
|
{:noreply, assign(socket, selected_date: date, start_date: nil, end_date: nil)}
|
|
end
|
|
end
|
|
|
|
defp handle_multi_select(
|
|
%{assigns: %{start_date: start_date, end_date: end_date}} = socket,
|
|
date
|
|
) do
|
|
cond do
|
|
is_nil(start_date) ->
|
|
# Start Selection
|
|
{:noreply, assign(socket, start_date: date)}
|
|
|
|
is_nil(end_date) ->
|
|
# End Selection (Order correctly)
|
|
{new_start, new_end} =
|
|
if Date.compare(date, start_date) == :lt,
|
|
do: {date, start_date},
|
|
else: {start_date, date}
|
|
|
|
send(self(), {:date_selected, new_start, new_end})
|
|
{:noreply, assign(socket, start_date: new_start, end_date: new_end)}
|
|
|
|
true ->
|
|
# Reset
|
|
{:noreply, assign(socket, start_date: date, end_date: nil)}
|
|
end
|
|
end
|
|
|
|
def render(assigns) do
|
|
~H"""
|
|
<div class="flex flex-col gap-4">
|
|
<%!-- Toolbar --%>
|
|
<div
|
|
class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700"
|
|
phx-click="toggle_multi_day"
|
|
phx-target={@myself}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={@multi_day_mode}
|
|
class="checkbox checkbox-primary checkbox-sm pointer-events-none"
|
|
/>
|
|
<label class="text-sm font-semibold select-none cursor-pointer">
|
|
Enable Multi-Day Selection
|
|
</label>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-2xl p-4 shadow-sm">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<button phx-click="prev_month" phx-target={@myself} class="btn btn-sm btn-ghost btn-circle">
|
|
<.icon name="hero-chevron-left" />
|
|
</button>
|
|
<h4 class="font-bold text-lg capitalize">
|
|
{Calendar.strftime(@first_day_of_month, "%B %Y")}
|
|
</h4>
|
|
<button phx-click="next_month" phx-target={@myself} class="btn btn-sm btn-ghost btn-circle">
|
|
<.icon name="hero-chevron-right" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-7 mb-2 text-center text-xs font-bold text-slate-400 uppercase tracking-wider">
|
|
<span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span><span>Su</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-7 gap-1 md:gap-2">
|
|
<%= for date <- @grid do %>
|
|
<% # Uses the booking_counts passed from parent
|
|
count = Map.get(@booking_counts, date, 0)
|
|
is_current = date.month == @first_day_of_month.month %>
|
|
|
|
<div class={[day_classes(date, assigns), !is_current && "opacity-25 grayscale"]}>
|
|
<%!-- Header Row: Date & Badge --%>
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-xs font-bold">{date.day}</span>
|
|
<%= if count > 0 and is_current do %>
|
|
<.link
|
|
navigate={~p"/admin/bookings?date=#{Date.to_string(date)}"}
|
|
class="badge badge-info badge-xs text-white font-bold hover:scale-110 transition-transform"
|
|
title={"#{count} bookings"}
|
|
>
|
|
{count}
|
|
</.link>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%= if @multi_day_mode do %>
|
|
{if @start_date == date, do: echo_label("Start")}
|
|
{if @end_date == date, do: echo_label("End")}
|
|
<% end %>
|
|
|
|
<%= if Date.compare(date, Date.utc_today()) != :lt do %>
|
|
<button
|
|
phx-click="select_date"
|
|
phx-value-date={date}
|
|
phx-target={@myself}
|
|
class="absolute inset-0 w-full h-full"
|
|
>
|
|
</button>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp day_classes(date, %{
|
|
start_date: start_date,
|
|
end_date: end_date,
|
|
selected_date: selected_date,
|
|
multi_day_mode: multi
|
|
}) do
|
|
is_past = Date.compare(date, Date.utc_today()) == :lt
|
|
is_start = start_date == date
|
|
is_end = end_date == date
|
|
is_sel = selected_date == date
|
|
in_range = CalendarExt.date_in_range?(date, start_date, end_date)
|
|
|
|
base =
|
|
"relative aspect-square flex flex-col p-2 transition-all border border-slate-200 dark:border-slate-700 "
|
|
|
|
cond do
|
|
is_past ->
|
|
base <> "bg-slate-50 dark:bg-slate-800/50 text-slate-300 cursor-not-allowed"
|
|
|
|
is_start ->
|
|
base <> "bg-primary text-white rounded-l-lg z-10 shadow-md"
|
|
|
|
is_end ->
|
|
base <> "bg-primary text-white rounded-r-lg z-10 shadow-md"
|
|
|
|
in_range && multi ->
|
|
base <> "bg-primary/20 text-slate-900 dark:text-white"
|
|
|
|
is_sel ->
|
|
base <> "bg-primary text-white rounded-lg shadow-md"
|
|
|
|
true ->
|
|
base <> "bg-white dark:bg-slate-800 hover:bg-slate-50 text-slate-700 dark:text-slate-200"
|
|
end
|
|
end
|
|
|
|
defp echo_label(text) do
|
|
assigns = %{text: text}
|
|
|
|
~H"""
|
|
<span class="mt-auto text-[10px] uppercase font-black tracking-tighter">{@text}</span>
|
|
"""
|
|
end
|
|
end
|