632 lines
27 KiB
Text
632 lines
27 KiB
Text
<Layouts.authenticated
|
||
flash={@flash}
|
||
current_scope={@current_scope}
|
||
active_page="schedules"
|
||
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
||
>
|
||
<div class="flex flex-wrap items-start justify-between gap-3 mb-6">
|
||
<div class="flex items-center gap-3 min-w-0">
|
||
<.link
|
||
navigate={~p"/schedules"}
|
||
class="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||
>
|
||
<.icon name="hero-arrow-left" class="h-5 w-5" />
|
||
</.link>
|
||
<div class="min-w-0">
|
||
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">{@schedule.name}</h1>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">{@schedule.timezone}</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2 shrink-0">
|
||
<.link
|
||
navigate={~p"/schedules/#{@schedule.id}/edit"}
|
||
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||
>
|
||
<.icon name="hero-pencil" class="h-4 w-4" />
|
||
<span class="hidden sm:inline">{t("Edit")}</span>
|
||
</.link>
|
||
<button
|
||
phx-click="delete"
|
||
data-confirm={t("Are you sure you want to delete this schedule?")}
|
||
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-red-600 dark:text-red-400 border border-gray-300 dark:border-gray-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||
>
|
||
<.icon name="hero-trash" class="h-4 w-4" />
|
||
<span class="hidden sm:inline">{t("Delete")}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<%= if @schedule.description do %>
|
||
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">{@schedule.description}</p>
|
||
<% end %>
|
||
|
||
<%!-- On-Call Status --%>
|
||
<div class="mb-8 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4">
|
||
<div class="flex items-center gap-3">
|
||
<div class={[
|
||
"h-10 w-10 rounded-full flex items-center justify-center",
|
||
@on_call && "bg-green-100 dark:bg-green-900/40",
|
||
!@on_call && "bg-gray-100 dark:bg-gray-800"
|
||
]}>
|
||
<.icon
|
||
name="hero-user"
|
||
class={[
|
||
"h-5 w-5",
|
||
@on_call && "text-green-600 dark:text-green-400",
|
||
!@on_call && "text-gray-400 dark:text-gray-500"
|
||
]}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||
{t("Currently On-Call")}
|
||
</p>
|
||
<%= if @on_call do %>
|
||
<p class="text-sm font-semibold text-gray-900 dark:text-white">
|
||
{display_name(@on_call)}
|
||
</p>
|
||
<% else %>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||
{t("No one is currently on-call")}
|
||
</p>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Timeline --%>
|
||
<div class="mb-8">
|
||
<%!-- Timeline navigation --%>
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-2">
|
||
<button
|
||
phx-click="timeline_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-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||
>
|
||
{t("Today")}
|
||
</button>
|
||
<button
|
||
phx-click="timeline_prev"
|
||
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||
>
|
||
<.icon name="hero-chevron-left" class="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
phx-click="timeline_next"
|
||
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||
>
|
||
<.icon name="hero-chevron-right" class="h-4 w-4" />
|
||
</button>
|
||
<span class="text-sm font-semibold text-gray-900 dark:text-white ml-2">
|
||
{Calendar.strftime(@timeline_start, "%b %-d")} – {Calendar.strftime(
|
||
@timeline_end,
|
||
"%b %-d, %Y"
|
||
)}
|
||
</span>
|
||
</div>
|
||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||
{t("2 Weeks")}
|
||
</span>
|
||
</div>
|
||
|
||
<%!-- Layer timelines --%>
|
||
<%= if @layer_timelines != [] do %>
|
||
<div class="overflow-x-auto">
|
||
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 mb-4 min-w-[320px]">
|
||
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
|
||
{t("Configuration Layers")}
|
||
</h3>
|
||
|
||
<%= for lt <- @layer_timelines do %>
|
||
<div class="mb-4 last:mb-0">
|
||
<%!-- Day headers --%>
|
||
<div class="flex mb-1">
|
||
<div class="w-28 shrink-0"></div>
|
||
<div class="flex-1 flex">
|
||
<%= for day <- @timeline_days do %>
|
||
<div class="flex-1 text-center border-l first:border-l-0 border-gray-200 dark:border-gray-700">
|
||
<span class="text-[10px] text-gray-400 dark:text-gray-500">
|
||
{Calendar.strftime(day, "%-d %a")}
|
||
</span>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<%!-- Timeline bar --%>
|
||
<div class="flex items-center">
|
||
<div class="w-28 shrink-0 pr-2 text-right">
|
||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 truncate">
|
||
{lt.layer.name}
|
||
</span>
|
||
</div>
|
||
<div class="flex-1 relative h-8 bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
|
||
<%= for span <- lt.spans do %>
|
||
<div
|
||
class={[
|
||
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
|
||
@user_colors[span.user.id] || "bg-gray-400"
|
||
]}
|
||
style={"left: #{span.start_offset / span.total_hours * 100}%; width: #{span.duration / span.total_hours * 100}%;"}
|
||
>
|
||
<span class="text-[10px] font-medium text-white truncate">
|
||
{display_name(span.user)}
|
||
</span>
|
||
</div>
|
||
<% end %>
|
||
<%!-- Today marker --%>
|
||
<% today_offset = Date.diff(Date.utc_today(), @timeline_start) %>
|
||
<%= if today_offset >= 0 and today_offset < @timeline_days do %>
|
||
<div
|
||
class="absolute inset-y-0 w-px bg-red-500 z-10"
|
||
style={"left: #{(today_offset * 24 + DateTime.utc_now().hour) / (length(@timeline_days) * 24) * 100}%;"}
|
||
>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Final Schedule --%>
|
||
<div class="overflow-x-auto">
|
||
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 min-w-[320px]">
|
||
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
|
||
{t("Final Schedule")}
|
||
</h3>
|
||
<%!-- Day headers --%>
|
||
<div class="flex mb-1">
|
||
<div class="w-28 shrink-0"></div>
|
||
<div class="flex-1 flex">
|
||
<%= for day <- @timeline_days do %>
|
||
<div class="flex-1 text-center border-l first:border-l-0 border-gray-200 dark:border-gray-700">
|
||
<span class="text-[10px] text-gray-400 dark:text-gray-500">
|
||
{Calendar.strftime(day, "%-d %a")}
|
||
</span>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
<%!-- Timeline bar --%>
|
||
<div class="flex items-center">
|
||
<div class="w-28 shrink-0"></div>
|
||
<div class="flex-1 relative h-8 bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
|
||
<%= for span <- @final_spans do %>
|
||
<div
|
||
class={[
|
||
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
|
||
@user_colors[span.user.id] || "bg-gray-400"
|
||
]}
|
||
style={"left: #{span.start_offset / span.total_hours * 100}%; width: #{span.duration / span.total_hours * 100}%;"}
|
||
>
|
||
<span class="text-[10px] font-medium text-white truncate">
|
||
{display_name(span.user)}
|
||
</span>
|
||
</div>
|
||
<% end %>
|
||
<%!-- Today marker --%>
|
||
<% today_offset = Date.diff(Date.utc_today(), @timeline_start) %>
|
||
<%= if today_offset >= 0 and today_offset < @timeline_days do %>
|
||
<div
|
||
class="absolute inset-y-0 w-px bg-red-500 z-10"
|
||
style={"left: #{(today_offset * 24 + DateTime.utc_now().hour) / (length(@timeline_days) * 24) * 100}%;"}
|
||
>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%!-- Layers --%>
|
||
<div class="mb-8">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{t("Layers")}</h2>
|
||
<button
|
||
phx-click="toggle_add_layer"
|
||
class="inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500"
|
||
>
|
||
<.icon name="hero-plus" class="h-4 w-4" />
|
||
{t("Add Layer")}
|
||
</button>
|
||
</div>
|
||
|
||
<%= if @show_add_layer do %>
|
||
<div class="mb-4 rounded-lg border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10 p-4">
|
||
<.form for={@layer_form} phx-change="validate_layer" phx-submit="save_layer">
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
||
<.input
|
||
field={@layer_form[:name]}
|
||
type="text"
|
||
label={t("Layer Name")}
|
||
placeholder="Layer 1"
|
||
/>
|
||
<.input
|
||
field={@layer_form[:rotation_type]}
|
||
type="select"
|
||
label={t("Rotation Type")}
|
||
options={[{t("Daily"), "daily"}, {t("Weekly"), "weekly"}, {t("Custom"), "custom"}]}
|
||
/>
|
||
<.input
|
||
field={@layer_form[:rotation_interval]}
|
||
type="number"
|
||
label={t("Rotation Interval")}
|
||
min="1"
|
||
/>
|
||
<.input field={@layer_form[:handoff_time]} type="time" label={t("Handoff Time")} />
|
||
<.input
|
||
field={@layer_form[:handoff_day]}
|
||
type="select"
|
||
label={t("Handoff Day")}
|
||
options={[
|
||
{t("Sunday"), "0"},
|
||
{t("Monday"), "1"},
|
||
{t("Tuesday"), "2"},
|
||
{t("Wednesday"), "3"},
|
||
{t("Thursday"), "4"},
|
||
{t("Friday"), "5"},
|
||
{t("Saturday"), "6"}
|
||
]}
|
||
/>
|
||
<.input
|
||
field={@layer_form[:start_date]}
|
||
type="datetime-local"
|
||
label={t("Start Date")}
|
||
/>
|
||
</div>
|
||
|
||
<%!-- User selection for new layer --%>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
{t("Members")}
|
||
</label>
|
||
<%= if @new_layer_members != [] do %>
|
||
<div class="flex flex-wrap gap-2 mb-2">
|
||
<%= for user <- @new_layer_members do %>
|
||
<span class="inline-flex items-center gap-1 rounded-full bg-blue-100 dark:bg-blue-900/40 pl-2.5 pr-1 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">
|
||
{display_name(user)}
|
||
<button
|
||
type="button"
|
||
phx-click="new_layer_remove_user"
|
||
phx-value-id={user.id}
|
||
class="ml-0.5 rounded-full p-0.5 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
|
||
>
|
||
<.icon name="hero-x-mark" class="h-3 w-3" />
|
||
</button>
|
||
</span>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
<% new_member_ids = Enum.map(@new_layer_members, & &1.id) %>
|
||
<% available = Enum.reject(@org_users, &(&1.id in new_member_ids)) %>
|
||
<%= unless Enum.empty?(available) do %>
|
||
<select
|
||
phx-change="new_layer_add_user"
|
||
name="user_id"
|
||
class="block w-full max-w-xs rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm py-1.5"
|
||
>
|
||
<option value="">{t("Select a user")}</option>
|
||
<%= for user <- available do %>
|
||
<option value={user.id}>{display_name(user)}</option>
|
||
<% end %>
|
||
</select>
|
||
<% end %>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2">
|
||
<.button type="submit" phx-disable-with={t("Saving...")}>
|
||
{t("Add Layer")}
|
||
</.button>
|
||
<button
|
||
type="button"
|
||
phx-click="toggle_add_layer"
|
||
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||
>
|
||
{t("Cancel")}
|
||
</button>
|
||
</div>
|
||
</.form>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%= if Enum.empty?(@schedule.layers) do %>
|
||
<div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-6 text-center">
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||
{t("No layers configured. Add a layer to define rotation coverage.")}
|
||
</p>
|
||
</div>
|
||
<% else %>
|
||
<% sorted_layers = Enum.sort_by(@schedule.layers, & &1.position) %>
|
||
<% layer_count = length(sorted_layers) %>
|
||
<div
|
||
id="layer-list"
|
||
class="space-y-4"
|
||
phx-hook="SortableList"
|
||
data-event="reorder_layers"
|
||
>
|
||
<%= for {layer, idx} <- Enum.with_index(sorted_layers) do %>
|
||
<div
|
||
data-id={layer.id}
|
||
draggable="true"
|
||
class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 cursor-move"
|
||
>
|
||
<div class="flex items-start justify-between mb-3 gap-3">
|
||
<div class="flex items-start gap-2 min-w-0">
|
||
<.icon name="hero-bars-3" class="h-4 w-4 text-gray-400 mt-0.5 shrink-0" />
|
||
<div class="min-w-0">
|
||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white truncate">
|
||
{layer.name}
|
||
</h3>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||
{String.capitalize(layer.rotation_type)} rotation, every {layer.rotation_interval}
|
||
<%= case layer.rotation_type do %>
|
||
<% "daily" -> %>
|
||
{ngettext("day", "days", layer.rotation_interval)}
|
||
<% "weekly" -> %>
|
||
{ngettext("week", "weeks", layer.rotation_interval)}
|
||
<% _ -> %>
|
||
{ngettext("day", "days", layer.rotation_interval)}
|
||
<% end %>
|
||
· Handoff at {Calendar.strftime(layer.handoff_time, "%H:%M")}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-1 shrink-0">
|
||
<button
|
||
phx-click="move_layer"
|
||
phx-value-id={layer.id}
|
||
phx-value-direction="up"
|
||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:hover:text-gray-400"
|
||
disabled={idx == 0}
|
||
title={t("Move up")}
|
||
>
|
||
<.icon name="hero-arrow-up" class="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
phx-click="move_layer"
|
||
phx-value-id={layer.id}
|
||
phx-value-direction="down"
|
||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:hover:text-gray-400"
|
||
disabled={idx == layer_count - 1}
|
||
title={t("Move down")}
|
||
>
|
||
<.icon name="hero-arrow-down" class="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
phx-click="delete_layer"
|
||
phx-value-id={layer.id}
|
||
data-confirm={t("Are you sure you want to delete this layer?")}
|
||
class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors ml-1"
|
||
title={t("Delete")}
|
||
>
|
||
<.icon name="hero-trash" class="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<%!-- Members --%>
|
||
<div class="mb-2">
|
||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||
{t("Members")}
|
||
</p>
|
||
<%= if Enum.empty?(layer.members) do %>
|
||
<p class="text-xs text-gray-400 dark:text-gray-500 mb-2">
|
||
{t("No members yet")}
|
||
</p>
|
||
<% else %>
|
||
<div class="flex flex-wrap gap-2 mb-2">
|
||
<%= for member <- Enum.sort_by(layer.members, & &1.position) do %>
|
||
<span class="inline-flex items-center gap-1 rounded-full bg-gray-100 dark:bg-gray-800 pl-2.5 pr-1 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">
|
||
{display_name(member.user)}
|
||
<button
|
||
phx-click="remove_member"
|
||
phx-value-id={member.id}
|
||
class="ml-0.5 rounded-full p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||
>
|
||
<.icon name="hero-x-mark" class="h-3 w-3" />
|
||
</button>
|
||
</span>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%!-- Add member dropdown --%>
|
||
<% existing_user_ids = Enum.map(layer.members, & &1.user_id) %>
|
||
<% available_users = Enum.reject(@org_users, &(&1.id in existing_user_ids)) %>
|
||
<%= unless Enum.empty?(available_users) do %>
|
||
<select
|
||
phx-change="add_member"
|
||
name="user_id"
|
||
phx-value-layer_id={layer.id}
|
||
class="block w-full max-w-xs rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm py-1.5"
|
||
>
|
||
<option value="">{t("Select a user")}</option>
|
||
<%= for user <- available_users do %>
|
||
<option value={user.id}>{display_name(user)}</option>
|
||
<% end %>
|
||
</select>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%!-- Restrictions editor --%>
|
||
<div class="mt-3 pt-3 border-t border-gray-100 dark:border-white/5">
|
||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||
{t("Restrict on-call shifts")}
|
||
</p>
|
||
<form
|
||
phx-change="set_layer_restriction"
|
||
phx-submit="set_layer_restriction"
|
||
class="space-y-2"
|
||
>
|
||
<input type="hidden" name="layer_id" value={layer.id} />
|
||
<select
|
||
name="restriction_type"
|
||
class="block rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1"
|
||
>
|
||
<option value="" selected={is_nil(layer.restriction_type)}>
|
||
{t("Always on call (no restriction)")}
|
||
</option>
|
||
<option value="time_of_day" selected={layer.restriction_type == "time_of_day"}>
|
||
{t("Time of day")}
|
||
</option>
|
||
<option value="time_of_week" selected={layer.restriction_type == "time_of_week"}>
|
||
{t("Time of week")}
|
||
</option>
|
||
</select>
|
||
<%= if layer.restriction_type in ["time_of_day", "time_of_week"] do %>
|
||
<div class="flex items-center gap-2 text-xs text-gray-700 dark:text-gray-300">
|
||
<span>{t("From")}</span>
|
||
<input
|
||
type="time"
|
||
name="start_time"
|
||
value={layer.restrictions["start_time"] || "09:00"}
|
||
class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1"
|
||
/>
|
||
<span>{t("to")}</span>
|
||
<input
|
||
type="time"
|
||
name="end_time"
|
||
value={layer.restrictions["end_time"] || "17:00"}
|
||
class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1"
|
||
/>
|
||
</div>
|
||
<% end %>
|
||
<%= if layer.restriction_type == "time_of_week" do %>
|
||
<div class="flex flex-wrap gap-2 text-xs text-gray-700 dark:text-gray-300">
|
||
<% selected_days =
|
||
((layer.restrictions || %{})["days"] || [])
|
||
|> Enum.map(fn d ->
|
||
case d do
|
||
d when is_integer(d) -> d
|
||
d when is_binary(d) -> String.to_integer(d)
|
||
end
|
||
end) %>
|
||
<%= for {label, day} <- [{"Mon", 1}, {"Tue", 2}, {"Wed", 3}, {"Thu", 4}, {"Fri", 5}, {"Sat", 6}, {"Sun", 7}] do %>
|
||
<label class="inline-flex items-center gap-1">
|
||
<input
|
||
type="checkbox"
|
||
name={"days[#{day}]"}
|
||
value="true"
|
||
checked={day in selected_days}
|
||
class="rounded border-gray-300 dark:border-gray-600 dark:bg-gray-800"
|
||
/>
|
||
<span>{label}</span>
|
||
</label>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%!-- Overrides --%>
|
||
<div>
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{t("Overrides")}</h2>
|
||
<button
|
||
phx-click="toggle_add_override"
|
||
class="inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500"
|
||
>
|
||
<.icon name="hero-plus" class="h-4 w-4" />
|
||
{t("Add Override")}
|
||
</button>
|
||
</div>
|
||
|
||
<%= if @show_add_override do %>
|
||
<div class="mb-4 rounded-lg border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10 p-4">
|
||
<.form for={@override_form} phx-change="validate_override" phx-submit="save_override">
|
||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
|
||
<.input
|
||
field={@override_form[:user_id]}
|
||
type="select"
|
||
label={t("User")}
|
||
options={Enum.map(@org_users, &{display_name(&1), &1.id})}
|
||
prompt={t("Select user...")}
|
||
/>
|
||
<.input
|
||
field={@override_form[:start_time]}
|
||
type="datetime-local"
|
||
label={t("Start Time")}
|
||
/>
|
||
<.input
|
||
field={@override_form[:end_time]}
|
||
type="datetime-local"
|
||
label={t("End Time")}
|
||
/>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<.button type="submit" phx-disable-with={t("Saving...")}>
|
||
{t("Add Override")}
|
||
</.button>
|
||
<button
|
||
type="button"
|
||
phx-click="toggle_add_override"
|
||
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||
>
|
||
{t("Cancel")}
|
||
</button>
|
||
</div>
|
||
</.form>
|
||
</div>
|
||
<% end %>
|
||
|
||
<%= if Enum.empty?(@schedule.overrides) do %>
|
||
<div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-6 text-center">
|
||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||
{t("No overrides configured.")}
|
||
</p>
|
||
</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("User")}
|
||
</th>
|
||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||
{t("Start")}
|
||
</th>
|
||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||
{t("End")}
|
||
</th>
|
||
<th class="px-4 py-3"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-900">
|
||
<%= for override <- @schedule.overrides do %>
|
||
<tr>
|
||
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
||
{display_name(override.user)}
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
|
||
{Calendar.strftime(override.start_time, "%Y-%m-%d %H:%M")}
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
|
||
{Calendar.strftime(override.end_time, "%Y-%m-%d %H:%M")}
|
||
</td>
|
||
<td class="px-4 py-3 text-right">
|
||
<button
|
||
phx-click="delete_override"
|
||
phx-value-id={override.id}
|
||
data-confirm={t("Are you sure?")}
|
||
class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||
>
|
||
<.icon name="hero-trash" class="h-4 w-4" />
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<% end %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
</Layouts.authenticated>
|