Packages

phoenix_kit

1.7.170
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web live settings users.html.heex
Raw

lib/phoenix_kit_web/live/settings/users.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("User Settings")}
page_subtitle={gettext("Manage user registration, defaults, and custom fields")}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex flex-col mx-auto px-4 py-6">
<%!-- Settings Form --%>
<div class="space-y-6">
<%!-- User Configuration Settings Card --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl mb-6">{gettext("User Configuration")}</h2>
<.form
:let={_form}
for={@changeset}
id="user_settings_form"
phx-submit="save_settings"
phx-change="validate_settings"
class="space-y-6"
>
<%!-- Hidden fields for required settings from General page --%>
<%!-- Use @saved_settings to preserve database values, not @settings which may have incorrect form state --%>
<%!-- General Settings --%>
<input
type="hidden"
name="settings[project_title]"
value={@saved_settings["project_title"]}
/>
<input
type="hidden"
name="settings[site_url]"
value={@saved_settings["site_url"]}
/>
<input
type="hidden"
name="settings[week_start_day]"
value={@saved_settings["week_start_day"]}
/>
<input
type="hidden"
name="settings[time_zone]"
value={@saved_settings["time_zone"]}
/>
<input
type="hidden"
name="settings[date_format]"
value={@saved_settings["date_format"]}
/>
<input
type="hidden"
name="settings[time_format]"
value={@saved_settings["time_format"]}
/>
<%!-- Registration Control Setting --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("Registration Settings")}
</span>
</label>
<div class="flex items-center gap-3 mb-3">
<input
name="settings[allow_registration]"
type="hidden"
value="false"
/>
<input
name="settings[allow_registration]"
type="checkbox"
value="true"
checked={@settings["allow_registration"] == "true"}
class="checkbox checkbox-primary"
/>
<span class="label-text">{gettext("Anyone can register")}</span>
</div>
<div class="flex items-center gap-3 mb-3">
<input
name="settings[track_registration_geolocation]"
type="hidden"
value="false"
/>
<input
name="settings[track_registration_geolocation]"
type="checkbox"
value="true"
checked={@settings["track_registration_geolocation"] == "true"}
class="checkbox checkbox-primary"
/>
<span class="label-text">{gettext("Track user registration location")}</span>
</div>
<div class="flex items-center gap-3">
<input
name="settings[registration_show_username]"
type="hidden"
value="false"
/>
<input
name="settings[registration_show_username]"
type="checkbox"
value="true"
checked={@settings["registration_show_username"] == "true"}
class="checkbox checkbox-primary"
/>
<span class="label-text">{gettext("Show username field on registration")}</span>
</div>
<%!-- Organization Accounts --%>
<div class="flex items-center gap-3 mt-3">
<input
name="settings[enable_organization_accounts]"
type="hidden"
value="false"
/>
<input
name="settings[enable_organization_accounts]"
type="checkbox"
value="true"
checked={@settings["enable_organization_accounts"] == "true"}
class="checkbox checkbox-primary"
/>
<span class="label-text">{gettext("Enable organization accounts")}</span>
</div>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
<%= if @settings["allow_registration"] != @saved_settings["allow_registration"] or @settings["track_registration_geolocation"] != @saved_settings["track_registration_geolocation"] do %>
<span class="text-warning">
{gettext("Registration:")} {if @settings["allow_registration"] == "true",
do: gettext("Enabled"),
else: gettext("Disabled")} | {gettext("Geolocation:")} {if @settings[
"track_registration_geolocation"
] == "true",
do:
gettext(
"Enabled"
),
else:
gettext(
"Disabled"
)}
</span>
<span class="text-base-content/40">
| {gettext("Saved:")} {if @saved_settings["allow_registration"] == "true",
do: gettext("Enabled"),
else: gettext("Disabled")} | {if @saved_settings[
"track_registration_geolocation"
] ==
"true",
do: gettext("Enabled"),
else: gettext("Disabled")}
</span>
<% else %>
{gettext("Registration:")} {if @settings["allow_registration"] == "true",
do: gettext("Enabled"),
else: gettext("Disabled")} | {gettext("Geolocation:")} {if @settings[
"track_registration_geolocation"
] == "true",
do:
gettext(
"Enabled"
),
else:
gettext(
"Disabled"
)}
<% end %>
</span>
</div>
<%!-- Privacy Notice for Geolocation --%>
<%= if @settings["track_registration_geolocation"] == "true" do %>
<div class="alert alert-info text-sm mt-2 max-w-prose">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{gettext("Privacy Notice: IP Geolocation Data")}
</div>
<div class="text-xs">
{gettext(
"When enabled, registration IP addresses and approximate location data (country, region, city) will be collected for analytics."
)}
{gettext(
"Uses free APIs: IP-API.com (45 requests/minute) with ipapi.co (1000 requests/day) fallback. Defaults to OFF for privacy."
)}
</div>
</div>
</div>
<% end %>
</div>
<%!-- New User Default Role + Status --%>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("New User Default Role")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Role assigned to new user registrations")}
</span>
</label>
<label class="select w-full focus:select-primary">
<select name="settings[new_user_default_role]">
<%= for {label, value} <- @setting_options["new_user_default_role"] do %>
<option value={value} selected={value == @settings["new_user_default_role"]}>
{label}
</option>
<% end %>
</select>
</label>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
<%= if @settings["new_user_default_role"] != @saved_settings["new_user_default_role"] do %>
<span class="text-warning">
{gettext("Selected:")} {get_option_label(
@settings["new_user_default_role"],
@setting_options["new_user_default_role"]
)}
</span>
<span class="text-base-content/40">
| {gettext("Saved:")} {get_option_label(
@saved_settings["new_user_default_role"],
@setting_options["new_user_default_role"]
)}
</span>
<% else %>
{gettext("Saved:")} {get_option_label(
@settings["new_user_default_role"],
@setting_options["new_user_default_role"]
)}
<% end %>
</span>
</div>
<%!-- Security Warning for Admin Role --%>
<%= if @settings["new_user_default_role"] == "Admin" do %>
<div class="alert alert-warning text-sm mt-2 max-w-prose">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{gettext("Security Notice: Medium Risk")}
</div>
<div class="text-xs">
{gettext(
"All new users will receive administrative privileges and access to the admin panel."
)}
{gettext("Consider \"User\" for most installations to maintain security.")}
</div>
</div>
</div>
<% end %>
</div>
<%!-- New User Default Status --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("New User Default Status")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Active status for new user registrations")}
</span>
</label>
<label class="select w-full focus:select-primary">
<select name="settings[new_user_default_status]">
<%= for {label, value} <- @setting_options["new_user_default_status"] do %>
<option
value={value}
selected={value == @settings["new_user_default_status"]}
>
{label}
</option>
<% end %>
</select>
</label>
<div class="label">
<span class="label-text-alt text-xs text-base-content/50">
<%= if @settings["new_user_default_status"] != @saved_settings["new_user_default_status"] do %>
<span class="text-warning">
{gettext("Selected:")} {get_option_label(
@settings["new_user_default_status"],
@setting_options["new_user_default_status"]
)}
</span>
<span class="text-base-content/40">
| {gettext("Saved:")} {get_option_label(
@saved_settings["new_user_default_status"],
@setting_options["new_user_default_status"]
)}
</span>
<% else %>
{gettext("Saved:")} {get_option_label(
@settings["new_user_default_status"],
@setting_options["new_user_default_status"]
)}
<% end %>
</span>
</div>
<%!-- Security Warning for Inactive Status --%>
<%= if @settings["new_user_default_status"] == "false" do %>
<div class="alert alert-warning text-sm mt-2 max-w-prose">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{gettext("Security Notice: Inactive Default")}
</div>
<div class="text-xs">
{gettext(
"New users will be created as inactive and will not be able to log in until an admin activates their account."
)}
{gettext(
"The first user (Owner) will always be active regardless of this setting."
)}
</div>
</div>
</div>
<% end %>
</div>
</div>
<%!-- Divider for Custom Fields --%>
<div class="divider text-base-content/60 mt-6">{gettext("Custom User Fields")}</div>
<%!-- Custom Fields Section --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("Manage Custom Fields")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Define additional profile fields for users")}
</span>
</label>
<%= if Enum.empty?(@field_definitions) do %>
<%!-- Empty State --%>
<div class="text-center py-8 border border-dashed border-base-300 rounded-lg">
<div class="text-4xl mb-4 opacity-50">📋</div>
<p class="text-base-content/60 mb-4">
{gettext("No custom fields defined yet")}
</p>
<button
type="button"
class="btn btn-primary btn-sm"
phx-click="show_add_field_form"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Add First Field")}
</button>
</div>
<% else %>
<%!-- Table Display --%>
<.table_default
id="user-custom-fields-table"
variant="zebra"
size="sm"
toggleable
items={Enum.sort_by(@field_definitions, & &1["position"])}
card_title={fn field -> field["label"] end}
card_fields={
fn field ->
[
%{label: gettext("Key:"), value: field["key"]},
%{label: gettext("Type"), value: field["type"]},
%{
label: gettext("Required"),
value: if(field["required"], do: gettext("Yes"), else: gettext("No"))
},
%{
label: gettext("Status"),
value:
if(field["enabled"],
do: gettext("Enabled"),
else: gettext("Disabled")
)
},
%{
label: gettext("User Access"),
value:
if(Map.get(field, "user_accessible", true),
do: gettext("User Editable"),
else: gettext("Admin Only")
)
}
]
end
}
>
<:toolbar_title>
<span class="text-sm text-base-content/60">
{length(@field_definitions)} {gettext("Custom Fields")}
</span>
</:toolbar_title>
<:toolbar_actions>
<button
type="button"
class="btn btn-primary btn-sm"
phx-click="show_add_field_form"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Add Custom Field")}
</button>
</:toolbar_actions>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("Field")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Type")}</.table_default_header_cell>
<.table_default_header_cell>
{gettext("Required")}
</.table_default_header_cell>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
<.table_default_header_cell class="whitespace-nowrap">
{gettext("User Access")}
</.table_default_header_cell>
<.table_default_header_cell>
{gettext("Actions")}
</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<.table_default_row :for={
field <- Enum.sort_by(@field_definitions, & &1["position"])
}>
<.table_default_cell>
<div class="font-semibold">{field["label"]}</div>
<div class="text-xs text-base-content/60">
{gettext("Key:")}
<code class="bg-base-300 px-1 rounded">{field["key"]}</code>
</div>
</.table_default_cell>
<.table_default_cell>
<span class="badge badge-sm h-auto">{field["type"]}</span>
</.table_default_cell>
<.table_default_cell>
<%= if field["required"] do %>
<span class="badge badge-sm badge-warning">{gettext("Yes")}</span>
<% else %>
<span class="badge badge-sm badge-ghost">{gettext("No")}</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<%= if field["enabled"] do %>
<span class="badge badge-sm badge-success">{gettext("Enabled")}</span>
<% else %>
<span class="badge badge-sm badge-ghost">{gettext("Disabled")}</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<%= if Map.get(field, "user_accessible", true) do %>
<span class="badge badge-sm badge-success whitespace-nowrap">
{gettext("User Editable")}
</span>
<% else %>
<span class="badge badge-sm badge-warning whitespace-nowrap">
{gettext("Admin Only")}
</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<div class="flex gap-1">
<button
type="button"
class="btn btn-xs btn-ghost tooltip tooltip-bottom"
data-tip={
if field["enabled"],
do: gettext("Disable"),
else: gettext("Enable")
}
phx-click="toggle_field_enabled"
phx-value-key={field["key"]}
>
<%= if field["enabled"] do %>
<.icon name="hero-eye" class="w-3 h-3" />
<% else %>
<.icon name="hero-eye-slash" class="w-3 h-3" />
<% end %>
</button>
<button
type="button"
class="btn btn-xs btn-ghost tooltip tooltip-bottom"
data-tip={gettext("Edit")}
phx-click="show_edit_field_form"
phx-value-key={field["key"]}
>
<.icon name="hero-pencil" class="w-3 h-3" />
</button>
<button
type="button"
class="btn btn-xs btn-ghost text-error tooltip tooltip-bottom"
data-tip={gettext("Delete")}
phx-click="delete_field"
phx-value-key={field["key"]}
data-confirm={gettext("Are you sure you want to delete this field?")}
>
<.icon name="hero-trash" class="w-3 h-3" />
</button>
</div>
</.table_default_cell>
</.table_default_row>
</.table_default_body>
<:card_actions :let={field}>
<button
type="button"
class="btn btn-sm btn-ghost gap-1"
phx-click="toggle_field_enabled"
phx-value-key={field["key"]}
>
<%= if field["enabled"] do %>
<.icon name="hero-eye" class="w-4 h-4" /> {gettext("Disable")}
<% else %>
<.icon name="hero-eye-slash" class="w-4 h-4" /> {gettext("Enable")}
<% end %>
</button>
<button
type="button"
class="btn btn-sm btn-ghost gap-1"
phx-click="show_edit_field_form"
phx-value-key={field["key"]}
>
<.icon name="hero-pencil" class="w-4 h-4" /> {gettext("Edit")}
</button>
<button
type="button"
class="btn btn-sm btn-ghost text-error gap-1"
phx-click="delete_field"
phx-value-key={field["key"]}
data-confirm={gettext("Are you sure you want to delete this field?")}
>
<.icon name="hero-trash" class="w-4 h-4" /> {gettext("Delete")}
</button>
</:card_actions>
</.table_default>
<% end %>
</div>
<%!-- Action Buttons --%>
<div class="flex gap-3 pt-4">
<button
type="submit"
class={[
"btn btn-primary flex-1",
if(@saving, do: "loading", else: "")
]}
disabled={@saving}
>
<%= if @saving do %>
{gettext("Saving...")}
<% else %>
{gettext("Save User Settings")}
<% end %>
</button>
</div>
</.form>
<%!-- Field Form Modal (outside main form) --%>
<%= if @show_field_form do %>
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4">
{if @editing_field,
do: gettext("Edit Custom Field"),
else: gettext("Add Custom Field")}
</h3>
<.form :let={_f} for={%{}} phx-submit="save_field" class="space-y-4">
<%!-- Field Key --%>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Field Key")} *</span>
</label>
<input
type="text"
name="field[key]"
value={if @editing_field, do: @editing_field["key"], else: ""}
class="input input-bordered"
placeholder="phone_number"
required
disabled={!!@editing_field}
/>
<div class="label">
<span class="label-text-alt">
{gettext("Lowercase letters, numbers, underscores only")}
</span>
</div>
</div>
<%!-- Field Label --%>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Label")} *</span>
</label>
<input
type="text"
name="field[label]"
value={if @editing_field, do: @editing_field["label"], else: ""}
class="input input-bordered"
placeholder={gettext("Phone Number")}
required
/>
</div>
<%!-- Field Type --%>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Type")} *</span>
</label>
<label class="select">
<select
name="field[type]"
required
phx-change="field_type_changed"
>
<%= for type <- ~w(text textarea number boolean date email url uuid select radio checkbox) do %>
<option
value={type}
selected={@field_form_type == type}
>
{String.capitalize(type)}
</option>
<% end %>
</select>
</label>
</div>
<%!-- Required Checkbox --%>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="field[required]"
value="true"
checked={@editing_field && @editing_field["required"]}
class="checkbox checkbox-primary"
/>
<span class="label-text">{gettext("Required field")}</span>
</label>
</div>
<%!-- User Accessible Checkbox --%>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input
type="hidden"
name="field[user_accessible]"
value="false"
/>
<input
type="checkbox"
name="field[user_accessible]"
value="true"
checked={
if @editing_field,
do: Map.get(@editing_field, "user_accessible", true),
else: true
}
class="checkbox checkbox-primary"
/>
<span class="label-text">{gettext("User can edit this field")}</span>
</label>
<div class="label">
<span class="label-text-alt">
{gettext(
"When checked, users can view and edit this field on their settings page. When unchecked, only admins can edit it."
)}
</span>
</div>
</div>
<%!-- Options for Select/Radio/Checkbox types --%>
<%= if @field_form_type in ~w(select radio checkbox) do %>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Options")} *</span>
<span class="label-text-alt">
{length(@field_form_options)} {gettext("option(s)")}
</span>
</label>
<%!-- Current options list --%>
<%= if @field_form_options != [] do %>
<div class="space-y-2 mb-3">
<%= for {option, index} <- Enum.with_index(@field_form_options) do %>
<div class="flex items-center gap-2">
<input type="hidden" name="field[options][]" value={option} />
<span class="badge badge-lg flex-1 justify-start">{option}</span>
<button
type="button"
class="btn btn-xs btn-ghost text-error"
phx-click="remove_field_option"
phx-value-index={index}
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</div>
<% end %>
</div>
<% end %>
<%!-- Add option input --%>
<div class="flex gap-2">
<input
type="text"
value={@new_option_value}
class="input input-bordered input-sm flex-1"
placeholder={gettext("Enter option value")}
phx-keyup="update_new_option"
id="new_option_input"
/>
<button
type="button"
class="btn btn-sm btn-outline"
phx-click="add_field_option"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Add")}
</button>
</div>
<div class="label">
<span class="label-text-alt">
{gettext("Type an option and press Enter or click Add")}
</span>
</div>
<%= if @field_form_options == [] do %>
<div class="text-sm text-warning mt-1">
<.icon name="hero-exclamation-triangle" class="w-4 h-4 inline" />
{gettext("At least one option is required for %{type} fields",
type: String.capitalize(@field_form_type)
)}
</div>
<% end %>
</div>
<% end %>
<%!-- Modal Actions --%>
<div class="modal-action">
<button type="submit" class="btn btn-primary">
{if @editing_field, do: gettext("Update Field"), else: gettext("Add Field")}
</button>
<button type="button" class="btn" phx-click="hide_field_form">
{gettext("Cancel")}
</button>
</div>
</.form>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>