Packages

phoenix_kit

1.7.199
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"]}
/>
<.section_header icon="hero-user-plus" title={gettext("Registration")} />
<%!-- Registration Control Setting --%>
<div class="form-control w-full">
<.checkbox
name="settings[allow_registration]"
checked={@settings["allow_registration"] == "true"}
label={gettext("Anyone can register")}
wrapper_class="mb-3"
/>
<.checkbox
name="settings[track_registration_geolocation]"
checked={@settings["track_registration_geolocation"] == "true"}
label={gettext("Track user registration location")}
wrapper_class="mb-3"
/>
<.checkbox
name="settings[registration_show_username]"
checked={@settings["registration_show_username"] == "true"}
label={gettext("Show username field on registration")}
/>
<%!-- Organization Accounts --%>
<.checkbox
name="settings[enable_organization_accounts]"
checked={@settings["enable_organization_accounts"] == "true"}
label={gettext("Enable organization accounts")}
wrapper_class="mt-3"
/>
<.unsaved_hint dirty={
Enum.any?(
[
"allow_registration",
"track_registration_geolocation",
"registration_show_username",
"enable_organization_accounts"
],
fn key -> @settings[key] != @saved_settings[key] end
)
} />
<%!-- 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>
<.section_header icon="hero-user-circle" title={gettext("New User Defaults")} />
<%!-- 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>
<.unsaved_hint
dirty={
@settings["new_user_default_role"] != @saved_settings["new_user_default_role"]
}
saved={
get_option_label(
@saved_settings["new_user_default_role"],
@setting_options["new_user_default_role"]
)
}
/>
<%!-- 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>
<.unsaved_hint
dirty={
@settings["new_user_default_status"] !=
@saved_settings["new_user_default_status"]
}
saved={
get_option_label(
@saved_settings["new_user_default_status"],
@setting_options["new_user_default_status"]
)
}
/>
<%!-- 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>
<.section_header icon="hero-list-bullet" title={gettext("Custom User Fields")} />
<%!-- Custom Fields Section --%>
<div class="form-control w-full">
<label class="label">
<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">
<.icon
name="hero-clipboard-document-list"
class="w-10 h-10 mx-auto mb-4 text-base-content/30"
/>
<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">
<.checkbox
name="field[required]"
checked={@editing_field && @editing_field["required"]}
label={gettext("Required field")}
/>
</div>
<%!-- User Accessible Checkbox --%>
<div class="form-control">
<.checkbox
name="field[user_accessible]"
checked={
if @editing_field,
do: Map.get(@editing_field, "user_accessible", true),
else: true
}
label={gettext("User can edit this field")}
>
<:description>
{gettext(
"When checked, users can view and edit this field on their settings page. When unchecked, only admins can edit it."
)}
</:description>
</.checkbox>
</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>