towerops/lib/towerops_web/live/admin/org_live/index.html.heex
Graham McIntire 9aec87636b
feat: per-organization billing override admin UI
Allow superadmins to override free device limits and per-device pricing
on a per-organization basis. Overrides cascade through subscription
limits, billing calculations, and user-facing settings display.

- Add custom_free_device_limit and custom_price_per_device to organizations
- Add billing_override_changeset with validation
- Update SubscriptionLimits.effective_device_limit to respect overrides
- Update Billing to use effective free count and price per device
- Add Admin.update_billing_overrides with audit logging
- Add override editing UI to /admin/organizations
- Update org settings page to show effective limits/pricing
2026-03-06 13:02:05 -06:00

134 lines
5.2 KiB
Text

<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Organizations")}</h1>
<p class="text-gray-600 dark:text-gray-400">{t_admin("Manage organizations")}</p>
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.table id="organizations" rows={@organizations}>
<:col :let={org} label={t("Name")}>{org.name}</:col>
<:col :let={org} label={t("Slug")}>{org.slug}</:col>
<:col :let={org} label={t("Plan")}>
<span class={[
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
if(org.subscription_plan == "paid",
do: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
else: "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"
)
]}>
{org.subscription_plan}
</span>
</:col>
<:col :let={org} label={t("Devices")}>
{org.device_count}
</:col>
<:col :let={org} label={t("Overrides")}>
<%= if org.custom_free_device_limit || org.custom_price_per_device do %>
<div class="flex flex-wrap gap-1">
<%= if org.custom_free_device_limit do %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
{org.custom_free_device_limit} {t("free")}
</span>
<% end %>
<%= if org.custom_price_per_device do %>
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/30 dark:text-purple-400">
${org.custom_price_per_device}/{t("device")}
</span>
<% end %>
</div>
<% else %>
<span class="text-gray-400 dark:text-gray-500 text-xs">{t("Default")}</span>
<% end %>
</:col>
<:col :let={org} label={t("Members")}>{length(org.memberships)}</:col>
<:col :let={org} label={t("Created")}>
{ToweropsWeb.TimeHelpers.format_date(org.inserted_at, @timezone)}
</:col>
<:col :let={org} label="">
<div class="flex items-center gap-3">
<button
id={"edit-overrides-#{org.id}"}
phx-click="edit_overrides"
phx-value-id={org.id}
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
{t_admin("Edit")}
</button>
<button
phx-click="delete_org"
phx-value-id={org.id}
data-confirm={
t(
"Are you sure? This will delete all sites, device, and data for this organization."
)
}
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
>
{t("Delete")}
</button>
</div>
</:col>
</.table>
</div>
<%!-- Billing Override Edit Panel --%>
<%= if @editing_org do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{t_admin("Billing Overrides")} — {@editing_org.name}
</h2>
<button
id="cancel-overrides-btn"
phx-click="cancel_overrides"
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
{t("Cancel")}
</button>
</div>
<.form
for={@override_form}
id="billing-override-form"
phx-change="validate_overrides"
phx-submit="save_overrides"
>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<.input
field={@override_form[:custom_free_device_limit]}
type="number"
label={t_admin("Custom Free Device Limit")}
placeholder="10 (default)"
min="1"
max="9999"
/>
<.input
field={@override_form[:custom_price_per_device]}
type="number"
label={t_admin("Custom Price Per Device")}
placeholder="1.00 (default)"
step="0.01"
min="0"
max="999.99"
/>
</div>
<div class="mt-4 flex items-center gap-3">
<.button type="submit" phx-disable-with={t("Saving...")}>
{t("Save")}
</.button>
<button
id="clear-overrides-btn"
type="button"
phx-click="clear_overrides"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
>
{t_admin("Reset to Defaults")}
</button>
</div>
</.form>
</div>
<% end %>
</div>
</Layouts.admin>