towerops/lib/towerops_web/live/schedule_live/index.html.heex

272 lines
11 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="schedules"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="flex items-center justify-between mb-1">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{t("On-Call & Schedules")}</h1>
<div class="flex items-center gap-2">
<%= if @tab == "escalation-policies" do %>
<.link
navigate={~p"/schedules/escalation-policies/new"}
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("New Policy")}
</.link>
<% else %>
<.link
navigate={~p"/schedules/new"}
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("New Schedule")}
</.link>
<% end %>
</div>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t("Define on-call rotations and escalation policies for alert notifications.")}
</p>
<div class="flex items-center gap-2 mb-6">
<.link
patch={~p"/schedules?tab=schedules"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@tab == "schedules" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@tab != "schedules" &&
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Schedules")}
</.link>
<.link
patch={~p"/schedules?tab=escalation-policies"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@tab == "escalation-policies" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@tab != "escalation-policies" &&
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Escalation Policies")}
</.link>
</div>
<%= if @tab == "escalation-policies" do %>
<%= if Enum.empty?(@policies) do %>
<div class="flex items-center justify-center py-16">
<div class="text-center">
<.icon
name="hero-arrow-trending-up"
class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4"
/>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{t("No escalation policies")}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Create an escalation policy to define how alerts are escalated to on-call users.")}
</p>
</div>
</div>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Name")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Levels")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-900">
<%= for policy <- @policies do %>
<tr
class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors cursor-pointer"
phx-click={JS.navigate(~p"/schedules/escalation-policies/#{policy.id}")}
>
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
{policy.name}
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{length(policy.rules)}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<% else %>
<%!-- Search input --%>
<form phx-change="search" phx-submit="search" class="mb-4">
<div class="relative max-w-md">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
<.icon name="hero-magnifying-glass" class="h-4 w-4 text-gray-400" />
</span>
<input
type="text"
name="search"
value={@search}
placeholder={t("Search schedules…")}
phx-debounce="300"
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm pl-9 py-1.5"
/>
</div>
</form>
<%!-- Date navigation header --%>
<div class="flex items-center justify-between flex-wrap gap-3 mb-4">
<div class="flex items-center gap-2">
<button
phx-click="today"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{t("Today")}
</button>
<button
phx-click="prev"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-700"
title={t("Previous range")}
>
<.icon name="hero-chevron-left" class="h-4 w-4" />
</button>
<button
phx-click="next"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-700"
title={t("Next range")}
>
<.icon name="hero-chevron-right" class="h-4 w-4" />
</button>
<span class="ml-2 text-sm font-semibold text-gray-900 dark:text-white">
{window_label(@start_date, @range_days)}
</span>
</div>
<form phx-change="set_range" class="inline-flex">
<select
name="range"
class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm py-1.5"
>
<option value="7" selected={@range_days == 7}>{t("1 Week")}</option>
<option value="14" selected={@range_days == 14}>{t("2 Weeks")}</option>
<option value="28" selected={@range_days == 28}>{t("4 Weeks")}</option>
</select>
</form>
</div>
<%= if Enum.empty?(@schedules) do %>
<div class="flex items-center justify-center py-16">
<div class="text-center">
<.icon
name="hero-calendar-days"
class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4"
/>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{t("No schedules")}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Create an on-call schedule to define rotation coverage.")}
</p>
</div>
</div>
<% else %>
<div class="space-y-4">
<%= for schedule <- @schedules do %>
<% days = days_in_window(@start_date, @range_days) %>
<% today_idx = today_offset(@start_date, @range_days) %>
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4">
<div class="flex items-center justify-between mb-3">
<div>
<.link
navigate={~p"/schedules/#{schedule.id}"}
class="text-base font-semibold text-blue-600 dark:text-blue-400 hover:underline"
>
{schedule.name}
</.link>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{schedule.timezone}
</p>
</div>
<div class="text-right">
<p class="text-xs text-gray-500 dark:text-gray-400">{t("On-Call Now")}</p>
<%= if schedule.current_on_call do %>
<span class="inline-flex items-center gap-1.5">
<span class={[
"h-2 w-2 rounded-full",
schedule.current_on_call_color
]}></span>
<span class="text-sm font-medium text-gray-900 dark:text-white">
{schedule.current_on_call_name}
</span>
</span>
<% else %>
<span class="text-sm text-gray-400 dark:text-gray-500">{t("No one")}</span>
<% end %>
</div>
</div>
<%!-- Day header row --%>
<div
class="grid gap-px text-xs text-gray-500 dark:text-gray-400 mb-1"
style={"grid-template-columns: repeat(#{@range_days}, minmax(0, 1fr));"}
>
<%= for {day, idx} <- Enum.with_index(days) do %>
<div class={[
"px-1 py-0.5 text-center",
today_idx == idx && "font-bold text-red-600 dark:text-red-400"
]}>
<div class="leading-tight">{Calendar.strftime(day, "%a")}</div>
<div class="leading-tight">{Calendar.strftime(day, "%-d")}</div>
</div>
<% end %>
</div>
<%!-- Gantt strip --%>
<div
class="relative grid gap-px h-8 rounded-md overflow-hidden bg-gray-100 dark:bg-gray-800"
style={"grid-template-columns: repeat(#{@range_days}, minmax(0, 1fr));"}
>
<%= for segment <- schedule.segments do %>
<div
class={[
"flex items-center justify-center text-white text-xs font-medium truncate px-1",
segment.color_class
]}
style={"grid-column: #{segment.day_offset + 1} / span #{segment.day_span};"}
title={segment.label}
>
{segment.label}
</div>
<% end %>
<%= if today_idx do %>
<div
class="pointer-events-none absolute top-0 bottom-0 w-px bg-red-500"
style={"left: calc((100% / #{@range_days}) * #{today_idx});"}
>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<.pagination
meta={@pagination}
path={~p"/schedules"}
params={
%{
"tab" => @tab,
"start" => Date.to_iso8601(@start_date),
"range" => Integer.to_string(@range_days),
"search" => @search
}
}
/>
<% end %>
<% end %>
</Layouts.authenticated>