202 lines
8.9 KiB
Text
202 lines
8.9 KiB
Text
<Layouts.authenticated
|
||
flash={@flash}
|
||
current_scope={@current_scope}
|
||
active_page="sites"
|
||
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
||
>
|
||
<.header>
|
||
{@page_title}
|
||
<:subtitle>{t("Manage your site locations and hierarchy")}</:subtitle>
|
||
<:actions>
|
||
<.link
|
||
navigate={~p"/sites-map"}
|
||
class="btn text-sm !py-1 !px-3 border border-gray-300 bg-transparent dark:border-gray-600 gap-1.5 mr-2"
|
||
>
|
||
<.icon name="hero-map" class="h-4 w-4" /> Map View
|
||
</.link>
|
||
<.button navigate={~p"/sites/new"} variant="primary">
|
||
<.icon name="hero-plus" class="h-5 w-5" /> New Site
|
||
</.button>
|
||
</:actions>
|
||
</.header>
|
||
|
||
<%= if @sites == [] do %>
|
||
<div class="flex items-center justify-center py-16">
|
||
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
|
||
<div class="items-center text-center">
|
||
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
|
||
<.icon name="hero-map-pin-solid" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
|
||
</div>
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{t("No Sites Yet")}</h3>
|
||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||
{t("Create your first site to organize devices by tower location.")}
|
||
</p>
|
||
<div class="mt-4 flex gap-2">
|
||
<.button navigate={~p"/sites/new"} variant="primary">
|
||
<.icon name="hero-plus" class="h-4 w-4" /> Add Site
|
||
</.button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% else %>
|
||
<%!-- Sites table --%>
|
||
<div class="mt-6 overflow-x-auto 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/80">
|
||
<tr>
|
||
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Site")}
|
||
</th>
|
||
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Devices")}
|
||
</th>
|
||
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Down")}
|
||
</th>
|
||
<th
|
||
class="hidden sm:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||
title={t("Quality of Experience — Preseem network quality score (0–100)")}
|
||
>
|
||
{t("QoE")}
|
||
</th>
|
||
<th class="hidden sm:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Subscribers")}
|
||
</th>
|
||
<th class="hidden md:table-cell px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Location")}
|
||
</th>
|
||
<th class="hidden lg:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Coordinates")}
|
||
</th>
|
||
<th class="hidden lg:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Last Check")}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100 bg-white dark:divide-white/5 dark:bg-gray-800/50">
|
||
<tr :for={site <- @sites} class="hover:bg-gray-50 dark:hover:bg-white/5">
|
||
<td class="px-4 py-2.5">
|
||
<.link
|
||
navigate={~p"/sites/#{site.id}"}
|
||
class="flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
|
||
>
|
||
<span class={[
|
||
"h-3 w-3 rounded-full flex-shrink-0",
|
||
health_dot(@summary_map[site.id] && @summary_map[site.id].site_health)
|
||
]}>
|
||
</span>
|
||
{site.name}
|
||
</.link>
|
||
<p :if={site.parent_site} class="text-xs text-gray-500 dark:text-gray-400 ml-4">
|
||
↳ {site.parent_site.name}
|
||
</p>
|
||
</td>
|
||
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
|
||
{if @summary_map[site.id], do: @summary_map[site.id].device_count, else: 0}
|
||
</td>
|
||
<td class="px-4 py-2.5 text-right text-sm">
|
||
<%= if @summary_map[site.id] && @summary_map[site.id].devices_down > 0 do %>
|
||
<span class="text-red-600 dark:text-red-400">
|
||
{@summary_map[site.id].devices_down}
|
||
</span>
|
||
<% else %>
|
||
<span class="text-gray-400 dark:text-gray-500">0</span>
|
||
<% end %>
|
||
</td>
|
||
<td class="hidden sm:table-cell px-4 py-2.5 text-right text-sm">
|
||
<%= if @summary_map[site.id] && @summary_map[site.id].avg_qoe do %>
|
||
<span class={qoe_color(@summary_map[site.id].avg_qoe)}>
|
||
{format_qoe(@summary_map[site.id].avg_qoe)}
|
||
</span>
|
||
<% else %>
|
||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||
<% end %>
|
||
</td>
|
||
<td class="hidden sm:table-cell px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
|
||
<%= if @summary_map[site.id] && @summary_map[site.id].subscribers do %>
|
||
{@summary_map[site.id].subscribers}
|
||
<% else %>
|
||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||
<% end %>
|
||
</td>
|
||
<td class="hidden md:table-cell px-4 py-2.5 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">
|
||
{site.location || "—"}
|
||
</td>
|
||
<td class="hidden lg:table-cell px-4 py-2.5 text-right">
|
||
<%= if site.latitude && site.longitude do %>
|
||
<span class="text-xs font-mono text-gray-400 dark:text-gray-500">
|
||
{Float.round(site.latitude, 4)}, {Float.round(site.longitude, 4)}
|
||
</span>
|
||
<% else %>
|
||
<span class="text-gray-400 dark:text-gray-500 text-sm">—</span>
|
||
<% end %>
|
||
</td>
|
||
<td class="hidden lg:table-cell px-4 py-2.5 text-right text-xs text-gray-500 dark:text-gray-400">
|
||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<%!-- Insights --%>
|
||
<%= if @insights != [] do %>
|
||
<div class="mt-8">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
{t("Insights")}
|
||
</h2>
|
||
<.link
|
||
navigate={~p"/insights"}
|
||
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
||
>
|
||
View all →
|
||
</.link>
|
||
</div>
|
||
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
|
||
<div
|
||
:for={insight <- @insights}
|
||
id={"insight-#{insight.id}"}
|
||
class="flex items-start justify-between gap-3 px-4 py-3 bg-white dark:bg-gray-800/50 hover:bg-gray-50 dark:hover:bg-white/5"
|
||
>
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex items-center gap-1.5">
|
||
<span class={[
|
||
"inline-flex rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
||
urgency_classes(insight.urgency)
|
||
]}>
|
||
{insight.urgency}
|
||
</span>
|
||
<span class={[
|
||
"inline-flex rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
||
source_classes(insight.source)
|
||
]}>
|
||
{insight.source}
|
||
</span>
|
||
</div>
|
||
<p class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
||
{insight.title}
|
||
</p>
|
||
<p
|
||
:if={insight.description}
|
||
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-1"
|
||
>
|
||
{insight.description}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
phx-click="dismiss_insight"
|
||
phx-value-id={insight.id}
|
||
class="flex-shrink-0 rounded p-1 text-gray-300 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-400"
|
||
title="Dismiss"
|
||
>
|
||
<.icon name="hero-x-mark" class="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<% end %>
|
||
<% end %>
|
||
</Layouts.authenticated>
|