- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat - Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast - ~11,600 color replacements across ~99 files - Update tests for new color class names
312 lines
12 KiB
Text
312 lines
12 KiB
Text
<Layouts.admin flash={@flash} timezone={@timezone}>
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
|
|
{t("All Organizations")}
|
|
</h1>
|
|
<p class="text-cool-steel-600 dark:text-cool-steel-400">
|
|
{t_admin("Manage organizations")}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Global Billing Defaults Card -->
|
|
<div
|
|
class="rounded-lg border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-800"
|
|
data-test="global-defaults-card"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-slate-900 dark:text-white">
|
|
Global Billing Defaults
|
|
</h3>
|
|
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
|
Default pricing for all organizations (can be overridden per-org)
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
phx-click="edit_global_defaults"
|
|
data-test="edit-global-defaults"
|
|
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
|
|
>
|
|
Edit Defaults
|
|
</button>
|
|
</div>
|
|
|
|
<dl class="mt-6 grid grid-cols-2 gap-6">
|
|
<div>
|
|
<dt class="text-sm font-medium text-slate-500 dark:text-slate-400">Free Devices</dt>
|
|
<dd
|
|
class="mt-1 text-2xl font-semibold text-slate-900 dark:text-white"
|
|
data-test="default-free-devices"
|
|
>
|
|
{@default_free_devices}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-sm font-medium text-slate-500 dark:text-slate-400">
|
|
Price Per Device
|
|
</dt>
|
|
<dd
|
|
class="mt-1 text-2xl font-semibold text-slate-900 dark:text-white"
|
|
data-test="default-price"
|
|
>
|
|
${@default_price}/mo
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-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-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-700 dark:text-cool-steel-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-cerulean-100 px-2 py-0.5 text-xs font-medium text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-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-cool-steel-400 dark:text-cool-steel-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}
|
|
variant="secondary"
|
|
>
|
|
{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."
|
|
)
|
|
}
|
|
variant="danger"
|
|
>
|
|
{t("Delete")}
|
|
</.button>
|
|
</div>
|
|
</:col>
|
|
</.table>
|
|
</div>
|
|
|
|
<%!-- Billing Override Edit Panel --%>
|
|
<%= if @editing_org do %>
|
|
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-lg font-semibold text-cool-steel-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-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-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-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
|
|
>
|
|
{t_admin("Reset to Defaults")}
|
|
</button>
|
|
</div>
|
|
</.form>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- Edit Global Defaults Modal --%>
|
|
<%= if @editing_global do %>
|
|
<div
|
|
class="fixed inset-0 z-50 overflow-y-auto"
|
|
data-test="global-defaults-modal"
|
|
>
|
|
<div class="flex min-h-screen items-center justify-center p-4">
|
|
<div
|
|
class="fixed inset-0 bg-slate-500 bg-opacity-75"
|
|
phx-click="cancel_global_edit"
|
|
>
|
|
</div>
|
|
|
|
<div class="relative w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-slate-800">
|
|
<h3 class="text-lg font-medium text-slate-900 dark:text-white">
|
|
Edit Global Billing Defaults
|
|
</h3>
|
|
|
|
<.form
|
|
for={@global_form}
|
|
phx-change="validate_global_pricing"
|
|
phx-submit="save_global_pricing"
|
|
data-test="global-defaults-form"
|
|
class="mt-6 space-y-4"
|
|
>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
Free Devices
|
|
</label>
|
|
<input
|
|
type="number"
|
|
name="global_pricing[default_free_devices]"
|
|
value={@global_form.data["default_free_devices"]}
|
|
class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 dark:border-slate-600 dark:bg-slate-700"
|
|
/>
|
|
<%= if error = @global_form.errors[:default_free_devices] do %>
|
|
<p
|
|
id="global_pricing_default_free_devices-error"
|
|
class="mt-1 text-sm text-sweet-salmon-600"
|
|
>
|
|
{elem(error, 0)}
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
Price Per Device (USD/month)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
name="global_pricing[default_price_per_device]"
|
|
value={@global_form.data["default_price_per_device"]}
|
|
class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 dark:border-slate-600 dark:bg-slate-700"
|
|
/>
|
|
<%= if error = @global_form.errors[:default_price_per_device] do %>
|
|
<p
|
|
id="global_pricing_default_price_per_device-error"
|
|
class="mt-1 text-sm text-sweet-salmon-600"
|
|
>
|
|
{elem(error, 0)}
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
phx-click="cancel_global_edit"
|
|
data-test="cancel-global-edit"
|
|
class="rounded-md px-3 py-2 text-sm font-semibold text-slate-900 hover:bg-slate-100 dark:text-white dark:hover:bg-slate-700"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
|
|
>
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</.form>
|
|
|
|
<%!-- Confirmation Dialog --%>
|
|
<%= if @show_confirm do %>
|
|
<div
|
|
class="absolute inset-0 flex items-center justify-center bg-slate-900/50"
|
|
data-test="confirm-pricing-change"
|
|
>
|
|
<div class="w-full max-w-sm rounded-lg bg-white p-6 shadow-xl dark:bg-slate-800">
|
|
<h4 class="text-lg font-medium text-slate-900 dark:text-white">
|
|
Confirm Price Change
|
|
</h4>
|
|
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
|
|
This will update {@active_sub_count} active subscriptions to the new price.
|
|
Changes take effect at the next billing cycle.
|
|
</p>
|
|
<div class="mt-6 flex justify-end gap-3">
|
|
<button
|
|
type="button"
|
|
phx-click="cancel_global_edit"
|
|
class="rounded-md px-3 py-2 text-sm font-semibold text-slate-900 hover:bg-slate-100 dark:text-white dark:hover:bg-slate-700"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
phx-click="confirm_pricing_update"
|
|
data-test="confirm-pricing-update"
|
|
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
|
|
>
|
|
Confirm Update
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</Layouts.admin>
|