towerops/lib/towerops_web/live/org/settings_live.html.heex

1161 lines
50 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="pb-5">
<.breadcrumb items={[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Settings"}
]} />
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Organization Settings")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Manage organization defaults, integrations, and configuration")}
</p>
</div>
<header class="border-b border-gray-200 dark:border-white/5">
<nav class="flex overflow-x-auto py-4">
<ul
role="list"
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-gray-500 sm:px-6 lg:px-8 dark:text-gray-400"
>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=general"}
class={
if @active_tab == "general", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("General")}
</.link>
</li>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=snmp"}
class={if @active_tab == "snmp", do: "text-indigo-600 dark:text-indigo-400", else: ""}
>
{t("SNMP")}
</.link>
</li>
<%= if @current_scope.user.is_superuser do %>
<%= if false do %>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=mikrotik"}
class={
if @active_tab == "mikrotik",
do: "text-indigo-600 dark:text-indigo-400",
else: ""
}
>
{t("MikroTik")}
</.link>
</li>
<% end %>
<% end %>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=agents"}
class={
if @active_tab == "agents", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("Agents")}
</.link>
</li>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=members"}
class={
if @active_tab == "members", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("Members")}
</.link>
</li>
<li>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations"}
class=""
>
{t("Integrations")}
</.link>
</li>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=billing"}
class={
if @active_tab == "billing", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("Billing")}
</.link>
</li>
</ul>
</nav>
</header>
<%= if @active_tab not in ["integrations", "members"] do %>
<.form
for={@form}
id="organization-form"
phx-change="validate"
phx-submit="save"
data-1p-ignore
data-form-type="other"
>
<div class="divide-y divide-gray-200 dark:divide-white/10">
<%= if @active_tab == "general" do %>
<!-- Organization Name Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Organization Name")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Update the name of your organization.")}
</p>
</div>
<div class="md:col-span-2">
<.input
field={@form[:name]}
type="text"
label={t("Organization Name")}
required
/>
</div>
</div>
<%= if @user_orgs_count > 1 do %>
<!-- Default Organization Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Default Organization")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Set this organization as your default. When you log in, you'll be directed to your default organization."
)}
</p>
</div>
<div class="md:col-span-2">
<%= if @membership.is_default do %>
<div class="rounded-md bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4">
<div class="flex items-center">
<.icon
name="hero-check-circle"
class="h-5 w-5 text-green-600 dark:text-green-400"
/>
<p class="ml-3 text-sm text-green-800 dark:text-green-200">
{t("This is your default organization")}
</p>
</div>
</div>
<% else %>
<button
id="toggle-default-org"
type="button"
phx-click="toggle_default_org"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400"
>
<.icon name="hero-star" class="h-4 w-4 inline" /> Set as Default Organization
</button>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Click to make this your default organization")}
</p>
<% end %>
</div>
</div>
<% end %>
<!-- Use Sites Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Site Organization")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Enable site organization to group devices into physical locations (offices, datacenters, etc.)."
)}
</p>
</div>
<div class="md:col-span-2">
<.input
field={@form[:use_sites]}
type="checkbox"
label={t("Use sites to organize devices")}
/>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t(
"When enabled, you can organize devices into sites. When disabled, all devices belong directly to the organization."
)}
</p>
<%= if @organization.use_sites && @form[:use_sites].value == false do %>
<div class="mt-4 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<div class="flex">
<div class="shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-amber-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
{t("Warning: Disabling Sites")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{t(
"All devices will be removed from their current sites and assigned directly to the organization."
)}
{t(
"Site assignments will be lost, but devices will remain in the organization."
)}
</p>
</div>
</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Alert Routing Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-gray-200 dark:border-white/5">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Alert Routing")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Choose how alerts are routed for on-call notifications.")}
</p>
</div>
<div class="md:col-span-2">
<fieldset>
<legend class="sr-only">{t("Alert routing method")}</legend>
<div class="space-y-3">
<% routing_value = to_string(@form[:alert_routing].value || "builtin") %>
<!-- Built-in option -->
<label class={[
"relative flex cursor-pointer rounded-lg border p-4 transition-colors",
if(routing_value == "builtin",
do:
"border-blue-500 bg-blue-50/50 ring-1 ring-blue-500 dark:bg-blue-900/10 dark:border-blue-400",
else:
"border-gray-200 hover:border-gray-300 dark:border-white/10 dark:hover:border-white/20"
)
]}>
<input
type="radio"
name="organization[alert_routing]"
value="builtin"
checked={routing_value == "builtin"}
class="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600 dark:border-gray-600 dark:bg-gray-800"
/>
<div class="ml-3">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
<.icon name="hero-bell-alert" class="h-4 w-4 inline -mt-0.5" />
{t("TowerOps Built-in")}
</span>
<span class="block text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{t(
"Use TowerOps schedules and escalation policies to notify on-call staff."
)}
</span>
</div>
</label>
<!-- PagerDuty option -->
<label class={[
"relative flex cursor-pointer rounded-lg border p-4 transition-colors",
if(routing_value == "pagerduty",
do:
"border-blue-500 bg-blue-50/50 ring-1 ring-blue-500 dark:bg-blue-900/10 dark:border-blue-400",
else:
"border-gray-200 hover:border-gray-300 dark:border-white/10 dark:hover:border-white/20"
)
]}>
<input
type="radio"
name="organization[alert_routing]"
value="pagerduty"
checked={routing_value == "pagerduty"}
class="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600 dark:border-gray-600 dark:bg-gray-800"
/>
<div class="ml-3">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
<.icon
name="hero-arrow-top-right-on-square"
class="h-4 w-4 inline -mt-0.5"
/>
{t("PagerDuty")}
</span>
<span class="block text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{t(
"Route alerts to PagerDuty for incident management and on-call notifications."
)}
</span>
<%= if routing_value == "pagerduty" && !@integrations["pagerduty"] do %>
<div class="mt-2 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-2.5">
<div class="flex items-center gap-2">
<.icon
name="hero-exclamation-triangle"
class="h-4 w-4 text-amber-500 shrink-0"
/>
<span class="text-xs text-amber-800 dark:text-amber-200">
{t("PagerDuty integration not configured yet.")}
<.link
navigate={
~p"/orgs/#{@organization.slug}/settings?tab=integrations"
}
class="font-semibold underline"
>
{t("Set it up →")}
</.link>
</span>
</div>
</div>
<% end %>
</div>
</label>
<!-- Both option -->
<label class={[
"relative flex cursor-pointer rounded-lg border p-4 transition-colors",
if(routing_value == "both",
do:
"border-blue-500 bg-blue-50/50 ring-1 ring-blue-500 dark:bg-blue-900/10 dark:border-blue-400",
else:
"border-gray-200 hover:border-gray-300 dark:border-white/10 dark:hover:border-white/20"
)
]}>
<input
type="radio"
name="organization[alert_routing]"
value="both"
checked={routing_value == "both"}
class="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600 dark:border-gray-600 dark:bg-gray-800"
/>
<div class="ml-3">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
<.icon name="hero-arrows-right-left" class="h-4 w-4 inline -mt-0.5" />
{t("Both")}
</span>
<span class="block text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{t(
"Send alerts to both PagerDuty and built-in escalation. Useful during migration."
)}
</span>
</div>
</label>
</div>
</fieldset>
</div>
</div>
<% end %>
<%= if @active_tab == "snmp" do %>
<!-- SNMP Configuration Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level."
)}
</p>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Hierarchy: Device > Site > Organization
</p>
</div>
<div class="md:col-span-2">
<.input
field={@form[:snmp_version]}
type="select"
label={t("SNMP Version")}
prompt="Select SNMP version"
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
/>
<!-- v1/v2c Community String -->
<%= if @form[:snmp_version].value in ["1", "2c"] do %>
<.input
field={@form[:snmp_community]}
type="text"
label={t("SNMP Community String")}
placeholder="e.g., public"
/>
<% end %>
<!-- v3 Credentials -->
<%= if @form[:snmp_version].value == "3" do %>
<.input
field={@form[:snmpv3_security_level]}
type="select"
label={t("Security Level")}
prompt="Select security level"
options={[
{"No Auth, No Priv", "noAuthNoPriv"},
{"Auth, No Priv", "authNoPriv"},
{"Auth, Priv", "authPriv"}
]}
/>
<.input
field={@form[:snmpv3_username]}
type="text"
label={t("Username")}
placeholder="e.g., snmpuser"
/>
<!-- Auth fields (shown for authNoPriv and authPriv) -->
<%= if @form[:snmpv3_security_level].value in ["authNoPriv", "authPriv"] do %>
<.input
field={@form[:snmpv3_auth_protocol]}
type="select"
label={t("Auth Protocol")}
prompt="Select protocol"
options={[
{"SHA-256 (recommended)", "SHA-256"},
{"SHA-512", "SHA-512"},
{"SHA-384", "SHA-384"},
{"SHA-224", "SHA-224"},
{"SHA (SHA-1)", "SHA"},
{"MD5", "MD5"}
]}
/>
<.input
field={@form[:snmpv3_auth_password]}
type="password"
label={t("Auth Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
<% end %>
<!-- Priv fields (shown only for authPriv) -->
<%= if @form[:snmpv3_security_level].value == "authPriv" do %>
<.input
field={@form[:snmpv3_priv_protocol]}
type="select"
label={t("Privacy Protocol")}
prompt="Select protocol"
options={[
{"AES-128 (recommended)", "AES"},
{"AES-256", "AES-256"},
{"AES-192", "AES-192"},
{"AES-256-C", "AES-256-C"},
{"DES (legacy)", "DES"}
]}
/>
<.input
field={@form[:snmpv3_priv_password]}
type="password"
label={t("Privacy Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
<% end %>
<% end %>
<.input
field={@form[:snmp_port]}
type="number"
label={t("SNMP Port")}
placeholder="161"
/>
<%= if @organization.snmp_community do %>
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t(
"This will override SNMP settings for ALL devices across all sites in this organization."
)}
</p>
<button
id="apply-snmp-to-all"
type="button"
phx-click="apply_snmp_to_all"
data-confirm={
t(
"This will replace SNMP settings for ALL devices in this organization. Are you sure?"
)
}
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
{t("Apply SNMP Config to All Devices")}
</button>
</div>
<% end %>
</div>
</div>
<% end %>
<%= if false && @active_tab == "mikrotik" && @current_scope.user.is_superuser do %>
<!-- MikroTik API Configuration Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Set default MikroTik RouterOS API credentials for all devices in this organization. Only applies to devices detected as MikroTik."
)}
</p>
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
</p>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Hierarchy: Device > Site > Organization
</p>
</div>
<div class="md:col-span-2">
<.input
field={@form[:mikrotik_enabled]}
type="checkbox"
label={t("Enable MikroTik API")}
/>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
<.input
field={@form[:mikrotik_username]}
type="text"
label={t("Username")}
placeholder="e.g., admin"
autocomplete="off"
/>
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
<.input
field={@form[:mikrotik_password]}
type="password"
label={t("Password")}
placeholder={t("RouterOS API password")}
autocomplete="off"
/>
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
<.input
field={@form[:mikrotik_port]}
type="number"
label={t("API Port")}
placeholder="8729"
autocomplete="off"
/>
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
<.input
field={@form[:mikrotik_use_ssl]}
type="checkbox"
label={t("Use SSL (API-SSL)")}
/>
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value != false, do: "hidden"}"}>
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>{t("Critical Security Warning:")}</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted over the network. This setting is"
)}
<strong>blocked for devices using cloud pollers</strong>
and should only be used with local agents on trusted networks.
</p>
</div>
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value == false, do: "hidden"}"}>
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<p class="text-sm text-yellow-700 dark:text-yellow-300">
<strong>{t("Security Warning:")}</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible."
)}
</p>
</div>
</div>
</div>
</div>
<% end %>
<%= if @active_tab == "agents" do %>
<!-- Default Agent Section -->
<%= if @available_agents != [] do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Select a default agent for SNMP polling. This will be used for all devices in this organization unless overridden at the site or device level."
)}
</p>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Hierarchy: Device > Site > Organization
</p>
</div>
<div class="md:col-span-2">
<.input
field={@form[:default_agent_token_id]}
type="select"
label={t("Default Remote Agent")}
prompt="No default agent - cloud polling"
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
<div class="rounded-md bg-gray-50 dark:bg-gray-800/50 p-4">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("Current Device Assignment:")}
</p>
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
<.icon name="hero-server" class="h-4 w-4" />
<span>
<strong>{@assignment_breakdown.direct}</strong> device-level override
</span>
</div>
<div class="flex items-center gap-2">
<.icon name="hero-building-office" class="h-4 w-4" />
<span>
<strong>{@assignment_breakdown.site}</strong> inherited from site
</span>
</div>
<div class="flex items-center gap-2">
<.icon name="hero-building-office-2" class="h-4 w-4" />
<span>
<strong>{@assignment_breakdown.organization}</strong>
inheriting organization default
</span>
</div>
<div class="flex items-center gap-2">
<.icon name="hero-cloud" class="h-4 w-4" />
<span>
<strong>{@assignment_breakdown.cloud}</strong> cloud polling (no agent)
</span>
</div>
</div>
</div>
<%= if @organization.default_agent_token_id do %>
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t(
"This will assign the default agent to ALL devices across all sites in this organization."
)}
</p>
<button
id="apply-agent-to-all"
type="button"
phx-click="apply_agent_to_all"
data-confirm={
t(
"This will replace agent assignments for ALL devices in this organization. Are you sure?"
)
}
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
{t("Apply Default Agent to All Devices")}
</button>
</div>
<% end %>
</div>
</div>
<% else %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("No agents configured yet.")}
<.link
navigate={~p"/agents"}
class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t("Create an agent")}
</.link>
to enable remote polling.
</p>
</div>
<div class="md:col-span-2">
<!-- Empty space for layout consistency -->
</div>
</div>
<% end %>
<% end %>
</div>
<!-- Save Button at Bottom (not shown on integrations tab) -->
<div class="flex items-center justify-end gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-6 lg:px-8 dark:border-white/10">
<.link
navigate={~p"/dashboard"}
class="text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Cancel")}
</.link>
<button
type="submit"
phx-disable-with={t("Saving...")}
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
{t("Save Settings")}
</button>
</div>
</.form>
<% end %>
<%= if @active_tab == "members" do %>
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Invite form (owner/admin only) -->
<%= if @membership.role in [:owner, :admin] do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Invite a Team Member")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Send an invitation email to add someone to this organization.")}
</p>
</div>
<div class="md:col-span-2">
<.form for={@invite_form} id="invite-form" phx-submit="send_invitation">
<div class="grid gap-4 sm:grid-cols-[1fr_10rem_auto] sm:items-end">
<.input
field={@invite_form[:email]}
type="email"
label={t("Email address")}
placeholder="colleague@example.com"
required
/>
<.input
field={@invite_form[:role]}
type="select"
label={t("Role")}
options={[
{"Admin", "admin"},
{"Executive", "executive"},
{"Technician", "technician"},
{"Viewer", "viewer"}
]}
/>
<div>
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white invisible">
&nbsp;
</label>
<button
type="submit"
phx-disable-with={t("Sending...")}
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 h-[42px]"
>
{t("Send Invite")}
</button>
</div>
</div>
</.form>
</div>
</div>
<% end %>
<!-- Pending invitations -->
<%= if @pending_invitations != [] do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Pending Invitations")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Invitations that haven't been accepted yet.")}
</p>
</div>
<div class="md:col-span-2">
<div class="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-white/5">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Email")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Role")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Sent")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Expires")}
</th>
<%= if @membership.role in [:owner, :admin] do %>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-transparent">
<tr :for={invitation <- @pending_invitations}>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
{invitation.email}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
{invitation.role}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<.timestamp
datetime={invitation.inserted_at}
timezone={@timezone}
format="relative"
/>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<.timestamp
datetime={invitation.expires_at}
timezone={@timezone}
format="date"
/>
</td>
<%= if @membership.role in [:owner, :admin] do %>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
<button
type="button"
phx-click="cancel_invitation"
phx-value-id={invitation.id}
data-confirm={t("Cancel this invitation?")}
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
{t("Cancel")}
</button>
</td>
<% end %>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<% end %>
<!-- Current members -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Members")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("People who have access to this organization.")}
</p>
</div>
<div class="md:col-span-2">
<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-white/5">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("User")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Role")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Joined")}
</th>
<%= if @membership.role in [:owner, :admin] do %>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-transparent">
<tr :for={member <- @members}>
<td class="whitespace-nowrap px-4 py-3">
<div>
<div class="text-sm font-medium text-gray-900 dark:text-white">
{member.user.email}
</div>
</div>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
<span class={[
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
case member.role do
:owner ->
"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
:admin ->
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
:executive ->
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
role when role in [:technician, :member] ->
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
:viewer ->
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400"
end
]}>
{member.role}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<.timestamp
datetime={member.inserted_at}
timezone={@timezone}
format="absolute"
/>
</td>
<%= if @membership.role in [:owner, :admin] do %>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
<%= if member.role != :owner do %>
<div class="flex items-center justify-end gap-3">
<form phx-change="change_role" phx-value-user-id={member.user_id}>
<select
name="role"
class="rounded-md border-gray-300 py-1 pl-2 pr-8 text-xs dark:border-white/10 dark:bg-white/5 dark:text-white"
>
<option value="admin" selected={member.role == :admin}>
{t("Admin")}
</option>
<option value="executive" selected={member.role == :executive}>
{t("Executive")}
</option>
<option
value="technician"
selected={member.role in [:technician, :member]}
>
{t("Technician")}
</option>
<option value="viewer" selected={member.role == :viewer}>
{t("Viewer")}
</option>
</select>
</form>
<.button
type="button"
phx-click="remove_member"
phx-value-user-id={member.user_id}
data-confirm={t("Remove this member from the organization?")}
variant="danger"
>
{t("Remove")}
</.button>
</div>
<% end %>
</td>
<% end %>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<% end %>
<%= if @active_tab == "integrations" do %>
<div class="mt-8 flex items-center justify-center">
<div class="text-center rounded-xl border border-gray-200 bg-white p-10 shadow-sm dark:border-white/10 dark:bg-white/[0.03]">
<.icon name="hero-arrow-top-right-on-square" class="mx-auto h-10 w-10 text-indigo-400" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("Integrations have moved to their own page")}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Manage all your integrations from the dedicated integrations page.")}
</p>
<div class="mt-6">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations"}
class="rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Go to Integrations")}
</.link>
</div>
</div>
</div>
<% end %>
<%= if @active_tab == "billing" do %>
<div class="space-y-6 px-4 py-6 sm:p-6 lg:p-8">
<div class="border rounded-lg p-6 dark:border-white/10">
<h2 class="text-xl font-semibold mb-4 dark:text-white">{t("Subscription & Billing")}</h2>
<dl class="space-y-3">
<div>
<dt class="text-sm text-gray-600 dark:text-gray-400">{t("Current Plan")}</dt>
<dd class="text-lg font-semibold capitalize dark:text-white">
{@organization.subscription_plan || "free"}
</dd>
</div>
<div>
<dt class="text-sm text-gray-600 dark:text-gray-400">{t("Device Usage")}</dt>
<dd class="text-lg dark:text-white">
{@current_devices} {t("devices")}
<%= if @device_limit != :unlimited do %>
<span class="text-gray-500 dark:text-gray-400">
/ {@device_limit} {t("limit")}
</span>
<% end %>
</dd>
</div>
<%= if @organization.subscription_plan == "paid" do %>
<div>
<dt class="text-sm text-gray-600 dark:text-gray-400">
{t("Estimated Monthly Cost")}
</dt>
<dd class="text-2xl font-bold text-green-600 dark:text-green-400">
${@estimated_cost.cost_usd}
</dd>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{@estimated_cost.billable} {t("billable devices")} × ${@estimated_cost.price_per_device}
</p>
</div>
<%= if @organization.subscription_status == "active" do %>
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800/50 rounded p-3">
<p class="text-sm text-green-800 dark:text-green-300">
✓ {t("Subscription Active")}
</p>
<%= if @organization.subscription_current_period_end do %>
<p class="text-xs text-green-600 dark:text-green-400 mt-1">
{t("Next billing")}: {Calendar.strftime(
@organization.subscription_current_period_end,
"%B %d, %Y"
)}
</p>
<% end %>
</div>
<% end %>
<%= if @organization.subscription_status == "past_due" do %>
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800/50 rounded p-3">
<p class="text-sm text-yellow-800 dark:text-yellow-300">
⚠ {t("Payment Past Due")}
</p>
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-1">
{t("Please update your payment method to avoid service interruption.")}
</p>
</div>
<% end %>
<% else %>
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50 rounded p-3">
<p class="text-sm text-blue-800 dark:text-blue-300">
{t("Free Plan - First %{count} devices included",
count: @estimated_cost.free_included
)}
</p>
<p class="text-xs text-blue-600 dark:text-blue-400 mt-1">
{t("Upgrade to monitor unlimited devices at $%{price}/device/month",
price: @estimated_cost.price_per_device
)}
</p>
</div>
<% end %>
</dl>
<div class="mt-6 flex gap-3">
<%= if @organization.subscription_plan == "free" do %>
<.button
phx-click="upgrade_to_paid"
class="bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700"
>
{t("Upgrade to Paid Plan")}
</.button>
<% else %>
<.button
phx-click="manage_billing"
class="bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
>
{t("Manage Billing")}
</.button>
<% end %>
</div>
</div>
<%= if @organization.subscription_plan == "paid" do %>
<div class="border rounded-lg p-6 dark:border-white/10">
<h3 class="text-lg font-semibold mb-3 dark:text-white">{t("Billing Information")}</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">{t("Subscription Status")}</dt>
<dd class="font-medium capitalize dark:text-white">
{@organization.subscription_status}
</dd>
</div>
<%= if @organization.subscription_current_period_start do %>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">{t("Current Period Start")}</dt>
<dd class="font-medium dark:text-white">
{Calendar.strftime(@organization.subscription_current_period_start, "%B %d, %Y")}
</dd>
</div>
<% end %>
<%= if @organization.subscription_current_period_end do %>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">{t("Current Period End")}</dt>
<dd class="font-medium dark:text-white">
{Calendar.strftime(@organization.subscription_current_period_end, "%B %d, %Y")}
</dd>
</div>
<% end %>
<%= if @organization.payment_method_status do %>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">{t("Payment Method")}</dt>
<dd class={[
"font-medium capitalize",
if(@organization.payment_method_status == "valid",
do: "text-green-600 dark:text-green-400",
else: "text-yellow-600 dark:text-yellow-400"
)
]}>
{String.replace(@organization.payment_method_status, "_", " ")}
</dd>
</div>
<% end %>
</dl>
</div>
<% end %>
</div>
<% end %>
</Layouts.authenticated>