mirror of
https://codeberg.org/JasterV/spazio-solazzo.git
synced 2026-04-26 18:20:03 +00:00
refactor: make phone number optional
This commit is contained in:
parent
8dafc3e3fd
commit
d461c45ddc
13 changed files with 423 additions and 27 deletions
|
|
@ -83,7 +83,7 @@ defmodule SpazioSolazzo.Accounts.User do
|
|||
change AshAuthentication.Strategy.MagicLink.SignInChange
|
||||
|
||||
# Conditionally validate name and phone_number for new users
|
||||
change SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields
|
||||
change SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields
|
||||
|
||||
change {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenChange,
|
||||
strategy_name: :remember_me}
|
||||
|
|
@ -149,7 +149,7 @@ defmodule SpazioSolazzo.Accounts.User do
|
|||
end
|
||||
|
||||
attribute :phone_number, :string do
|
||||
allow_nil? false
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
defmodule SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields do
|
||||
defmodule SpazioSolazzo.Accounts.User.Changes.ParseRegistrationFields do
|
||||
@moduledoc """
|
||||
Conditionally validates that name and phone_number are present for new user registrations.
|
||||
For existing users (upserts), these fields are not required.
|
||||
|
|
@ -11,28 +11,53 @@ defmodule SpazioSolazzo.Accounts.User.Changes.ValidateRegistrationFields do
|
|||
|
||||
case SpazioSolazzo.Accounts.get_user_by_email(email, authorize?: false) do
|
||||
{:ok, %{phone_number: phone, name: name}} ->
|
||||
# User is already registered, we'll just set the same values it had
|
||||
changeset
|
||||
|> Ash.Changeset.force_change_attribute(:name, name)
|
||||
|> Ash.Changeset.force_change_attribute(:phone_number, phone)
|
||||
|
||||
_ ->
|
||||
# User is not yet registered, we'll parse & validate the new values
|
||||
name = Ash.Changeset.get_argument(changeset, :name)
|
||||
phone = Ash.Changeset.get_argument(changeset, :phone_number)
|
||||
|
||||
changeset
|
||||
|> validate_required_for_registration(:name, name)
|
||||
|> validate_required_for_registration(:phone_number, phone)
|
||||
|> parse_name(name)
|
||||
|> parse_phone_number(phone)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_required_for_registration(changeset, field, value) do
|
||||
if is_nil(value) || value == "" do
|
||||
Ash.Changeset.add_error(
|
||||
changeset,
|
||||
Ash.Error.Changes.Required.exception(field: field, type: :argument)
|
||||
)
|
||||
defp parse_name(changeset, nil) do
|
||||
Ash.Changeset.add_error(
|
||||
changeset,
|
||||
Ash.Error.Changes.Required.exception(field: :name, type: :argument)
|
||||
)
|
||||
end
|
||||
|
||||
defp parse_name(changeset, value) do
|
||||
value = String.trim(value)
|
||||
|
||||
if value == "" do
|
||||
parse_name(changeset, nil)
|
||||
else
|
||||
Ash.Changeset.change_attribute(changeset, field, value)
|
||||
Ash.Changeset.change_attribute(changeset, :name, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_phone_number(changeset, nil) do
|
||||
# The phone number is nullable, this is fine
|
||||
Ash.Changeset.change_attribute(changeset, :phone_number, nil)
|
||||
end
|
||||
|
||||
defp parse_phone_number(changeset, value) do
|
||||
value = String.trim(value)
|
||||
|
||||
if value == "" do
|
||||
# Instead of returning an error, we'll consider an empty phone number
|
||||
# as if the user didn't want to set one, which is valid.
|
||||
parse_name(changeset, nil)
|
||||
else
|
||||
Ash.Changeset.change_attribute(changeset, :phone_number, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
argument :date, :date, allow_nil?: false
|
||||
argument :customer_name, :string, allow_nil?: false
|
||||
argument :customer_email, :string, allow_nil?: false
|
||||
argument :customer_phone, :string, allow_nil?: false
|
||||
argument :customer_phone, :string, allow_nil?: true
|
||||
argument :customer_comment, :string, allow_nil?: true
|
||||
|
||||
change manage_relationship(:time_slot_template_id, :time_slot_template,
|
||||
|
|
@ -167,7 +167,7 @@ defmodule SpazioSolazzo.BookingSystem.Booking do
|
|||
attribute :customer_email, :string, allow_nil?: false
|
||||
attribute :start_time, :time, allow_nil?: false
|
||||
attribute :end_time, :time, allow_nil?: false
|
||||
attribute :customer_phone, :string, allow_nil?: false
|
||||
attribute :customer_phone, :string, allow_nil?: true
|
||||
attribute :customer_comment, :string, allow_nil?: true
|
||||
|
||||
attribute :state, :atom do
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
<div style="margin-bottom: 20px;">
|
||||
<p><strong>Customer:</strong> {@customer_name}</p>
|
||||
<p><strong>Email:</strong> <a href={"mailto:#{@customer_email}"}>{@customer_email}</a></p>
|
||||
<p><strong>Phone:</strong> <a href={"tel:#{@customer_phone}"}>{@customer_phone}</a></p>
|
||||
|
||||
<%= if @customer_phone && String.trim(@customer_phone) != "" do %>
|
||||
<p><strong>Phone:</strong> <a href={"tel:#{@customer_phone}"}>{@customer_phone}</a></p>
|
||||
<% else %>
|
||||
<p><strong>Phone:</strong> <a href="#">N/A</a></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.details_list>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<.detail_item label="Date">{@date}</.detail_item>
|
||||
<.detail_item label="Time">{@start_time} - {@end_time}</.detail_item>
|
||||
<.detail_item label="Email">{@customer_email}</.detail_item>
|
||||
<.detail_item label="Phone">{@customer_phone}</.detail_item>
|
||||
<.detail_item label="Phone">{@customer_phone || "N/A"}</.detail_item>
|
||||
<.detail_item label="Note">{@customer_comment || "N/A"}</.detail_item>
|
||||
</.details_list>
|
||||
|
||||
|
|
|
|||
|
|
@ -74,12 +74,17 @@ defmodule SpazioSolazzoWeb.AuthCallbackLive do
|
|||
) do
|
||||
%{token: token} = socket.assigns
|
||||
remember_me = Map.get(args, "remember_me") == "on"
|
||||
name = String.trim(name)
|
||||
phone_number = String.trim(phone_number)
|
||||
|
||||
{:noreply,
|
||||
redirect(socket,
|
||||
to:
|
||||
~p"/auth/magic/sign-in?token=#{token}&name=#{name}&phone_number=#{phone_number}&remember_me=#{remember_me}"
|
||||
)}
|
||||
url =
|
||||
if phone_number == "" do
|
||||
~p"/auth/magic/sign-in?token=#{token}&name=#{name}&remember_me=#{remember_me}"
|
||||
else
|
||||
~p"/auth/magic/sign-in?token=#{token}&name=#{name}&phone_number=#{phone_number}&remember_me=#{remember_me}"
|
||||
end
|
||||
|
||||
{:noreply, redirect(socket, to: url)}
|
||||
end
|
||||
|
||||
defp extract_email_from_token(token) do
|
||||
|
|
|
|||
|
|
@ -98,20 +98,21 @@
|
|||
for="phone_number"
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>
|
||||
Phone Number <span class="text-rose-500">*</span>
|
||||
Phone Number (Optional)
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="tel"
|
||||
name="phone_number"
|
||||
id="phone_number"
|
||||
required
|
||||
class="w-full pl-11 pr-4 py-3 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-shadow"
|
||||
placeholder="+39 123 456 7890"
|
||||
placeholder="+39"
|
||||
/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<.icon name="hero-phone" class="size-5 text-slate-400 dark:text-slate-500" />
|
||||
</div>
|
||||
<!-- TODO: Add a text here letting the user know that their phone number won't be used for any commercial purposes -->
|
||||
<!-- but only to contact them personally in case of any problems with a booking -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ defmodule SpazioSolazzoWeb.BookingFormLiveComponent do
|
|||
<.icon name="hero-phone" class="size-5 text-sky-600 dark:text-sky-400" />
|
||||
</div>
|
||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 truncate">
|
||||
{@current_user.phone_number}
|
||||
{@current_user.phone_number || "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,10 +26,14 @@ defmodule SpazioSolazzoWeb.ProfileLive do
|
|||
def handle_event("save_profile", %{"form" => form_params}, socket) do
|
||||
case Form.submit(socket.assigns.profile_form, params: form_params) do
|
||||
{:ok, updated_user} ->
|
||||
form =
|
||||
Accounts.form_to_update_profile(updated_user, actor: updated_user)
|
||||
|> to_form()
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:current_user, updated_user)
|
||||
|> assign(:profile_form, to_form(form_params))
|
||||
|> assign(:profile_form, form)
|
||||
|> put_flash(:info, "Profile updated successfully")}
|
||||
|
||||
{:error, form} ->
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
<.input
|
||||
field={@profile_form[:name]}
|
||||
type="text"
|
||||
label="Full Name"
|
||||
label="Full Name *"
|
||||
required
|
||||
placeholder="Enter your full name"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900/50 border-slate-200 dark:border-slate-700 rounded-xl px-4 py-3 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
defmodule SpazioSolazzo.Repo.Migrations.SetPhoneNumberAsNullable do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
modify :phone_number, :text, null: true
|
||||
end
|
||||
|
||||
alter table(:bookings) do
|
||||
modify :customer_phone, :text, null: true
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:bookings) do
|
||||
modify :customer_phone, :text, null: false
|
||||
end
|
||||
|
||||
alter table(:users) do
|
||||
modify :phone_number, :text, null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
244
priv/resource_snapshots/repo/bookings/20260115004442.json
Normal file
244
priv/resource_snapshots/repo/bookings/20260115004442.json
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_email",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "start_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "end_time",
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_phone",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "customer_comment",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"reserved\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "state",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_asset_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "assets"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "asset_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_time_slot_template_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "time_slot_templates"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "time_slot_template_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": true,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "bookings_user_id_fkey",
|
||||
"on_delete": "nilify",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "users"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "1480C13D76AD8CE079362CC851CF250063914A40A6CA48182E3D3B5D83CD174A",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "bookings"
|
||||
}
|
||||
82
priv/resource_snapshots/repo/users/20260115004442.json
Normal file
82
priv/resource_snapshots/repo/users/20260115004442.json
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "citext"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "phone_number",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "6D20D84C076B09FEAFF7A68C69930AF48936A628BBEE432695F0ECC02B0F4EFA",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "users_unique_email_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "email"
|
||||
}
|
||||
],
|
||||
"name": "unique_email",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.SpazioSolazzo.Repo",
|
||||
"schema": null,
|
||||
"table": "users"
|
||||
}
|
||||
Loading…
Reference in a new issue