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
134 lines
5.2 KiB
Text
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>
|