refactor: Remove deprecated LiveViews

- Remove CustomFieldValueLive (Index, Form, Show)
- Remove ContributionTypeLive.Index
- Remove ContributionPeriodLive.Show
- Remove corresponding routes from router
- Remove references in CustomFieldValueLive.Index
This commit is contained in:
Moritz 2026-01-20 15:38:09 +01:00 committed by Simon
parent d72bb8869f
commit 4154296b54
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
7 changed files with 0 additions and 1086 deletions

View file

@ -1,345 +0,0 @@
defmodule MvWeb.ContributionPeriodLive.Show do
@moduledoc """
Mock-up LiveView for Member Contribution Periods (Admin/Treasurer View).
This is a preview-only page that displays the planned UI for viewing
and managing contribution periods for a specific member.
It shows static mock data and is not functional.
## Planned Features (Future Implementation)
- Display all contribution periods for a member
- Show period dates, interval, amount, and status
- Quick status change (paid/unpaid/suspended)
- Bulk marking of multiple periods
- Notes per period
## Note
This page is intentionally non-functional and serves as a UI mockup
for the upcoming Membership Contributions feature.
"""
use MvWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, gettext("Member Contributions"))
|> assign(:member, mock_member())
|> assign(:periods, mock_periods())
|> assign(:selected_periods, MapSet.new())}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.mockup_warning />
<.header>
{gettext("Contributions for %{name}", name: MvWeb.Helpers.MemberHelpers.display_name(@member))}
<:subtitle>
{gettext("Contribution type")}:
<span class="font-semibold">{@member.contribution_type}</span>
· {gettext("Member since")}: <span class="font-mono">{@member.joined_at}</span>
</:subtitle>
<:actions>
<.link navigate={~p"/membership_fee_settings"} class="btn btn-ghost btn-sm">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back to Settings")}
</.link>
</:actions>
</.header>
<%!-- Member Info Card --%>
<div class="mb-6 shadow card bg-base-100">
<div class="card-body">
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
<div>
<span class="text-sm text-base-content/60">{gettext("Email")}</span>
<p class="font-medium">{@member.email}</p>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Contribution Start")}</span>
<p class="font-mono">{@member.contribution_start}</p>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Total Contributions")}</span>
<p class="font-semibold">{length(@periods)}</p>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Open Contributions")}</span>
<p class="font-semibold text-error">
{Enum.count(@periods, &(&1.status == :unpaid))}
</p>
</div>
</div>
</div>
</div>
<%!-- Contribution Type Change --%>
<div class="mb-6 card bg-base-200">
<div class="py-4 card-body">
<div class="flex flex-wrap items-center gap-4">
<span class="font-semibold">{gettext("Change Contribution Type")}:</span>
<select class="w-64 select select-bordered select-sm" disabled>
<option selected>{@member.contribution_type} (60,00 , {gettext("Yearly")})</option>
<option>{gettext("Reduced")} (30,00 , {gettext("Yearly")})</option>
<option>{gettext("Honorary")} (0,00 , {gettext("Yearly")})</option>
</select>
<span
class="text-sm text-base-content/60 cursor-help tooltip tooltip-bottom"
data-tip={
gettext(
"Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
)
}
>
<.icon name="hero-question-mark-circle" class="inline size-4" />
{gettext("Why are not all contribution types shown?")}
</span>
</div>
</div>
</div>
<%!-- Bulk Actions --%>
<div class="flex flex-wrap items-center gap-4 mb-4">
<span class="text-sm text-base-content/60">
{ngettext(
"%{count} period selected",
"%{count} periods selected",
MapSet.size(@selected_periods),
count: MapSet.size(@selected_periods)
)}
</span>
<button class="btn btn-sm btn-success" disabled>
<.icon name="hero-check" class="size-4" />
{gettext("Mark as Paid")}
</button>
<button class="btn btn-sm btn-ghost" disabled>
<.icon name="hero-minus-circle" class="size-4" />
{gettext("Mark as Suspended")}
</button>
<button class="btn btn-sm btn-ghost" disabled>
<.icon name="hero-x-circle" class="size-4" />
{gettext("Mark as Unpaid")}
</button>
</div>
<%!-- Periods Table --%>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>
<input type="checkbox" class="checkbox checkbox-sm" disabled />
</th>
<th>{gettext("Time Period")}</th>
<th>{gettext("Interval")}</th>
<th>{gettext("Amount")}</th>
<th>{gettext("Status")}</th>
<th>{gettext("Notes")}</th>
<th>{gettext("Actions")}</th>
</tr>
</thead>
<tbody>
<tr :for={period <- @periods} class={period_row_class(period.status)}>
<td>
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={MapSet.member?(@selected_periods, period.id)}
disabled
/>
</td>
<td>
<div class="font-mono">
{period.period_start} {period.period_end}
</div>
<div :if={period.is_current} class="mt-1 badge badge-info badge-sm">
{gettext("Current")}
</div>
</td>
<td>
<span class="badge badge-outline badge-sm">{format_interval(period.interval)}</span>
</td>
<td>
<span class="font-mono">{format_currency(period.amount)}</span>
</td>
<td>
<.status_badge status={period.status} />
</td>
<td>
<span :if={period.notes} class="text-sm italic text-base-content/60">
{period.notes}
</span>
<span :if={!period.notes} class="text-base-content/30"></span>
</td>
<td class="w-0 font-semibold whitespace-nowrap">
<div class="flex gap-4">
<.link
href="#"
class={[
"cursor-not-allowed",
if(period.status == :paid, do: "invisible", else: "opacity-50")
]}
>
{gettext("Paid")}
</.link>
<.link
href="#"
class={[
"cursor-not-allowed",
if(period.status == :suspended, do: "invisible", else: "opacity-50")
]}
>
{gettext("Suspend")}
</.link>
<.link
href="#"
class={[
"cursor-not-allowed",
if(period.status != :paid, do: "invisible", else: "opacity-50")
]}
>
{gettext("Reopen")}
</.link>
<.link href="#" class="opacity-50 cursor-not-allowed">
{gettext("Note")}
</.link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</Layouts.app>
"""
end
# Mock-up warning banner component - subtle orange style
defp mockup_warning(assigns) do
~H"""
<div class="flex items-center gap-3 px-4 py-3 mb-6 border rounded-lg border-warning text-warning bg-base-100">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<div>
<span class="font-semibold">{gettext("Preview Mockup")}</span>
<span class="ml-2 text-sm text-base-content/70">
{gettext("This page is not functional and only displays the planned features.")}
</span>
</div>
</div>
"""
end
# Status badge component
attr :status, :atom, required: true
defp status_badge(%{status: :paid} = assigns) do
~H"""
<span class="gap-1 badge badge-success">
<.icon name="hero-check-circle-mini" class="size-3" />
{gettext("Paid")}
</span>
"""
end
defp status_badge(%{status: :unpaid} = assigns) do
~H"""
<span class="gap-1 badge badge-error">
<.icon name="hero-x-circle-mini" class="size-3" />
{gettext("Unpaid")}
</span>
"""
end
defp status_badge(%{status: :suspended} = assigns) do
~H"""
<span class="gap-1 badge badge-neutral">
<.icon name="hero-pause-circle-mini" class="size-3" />
{gettext("Suspended")}
</span>
"""
end
defp period_row_class(:unpaid), do: "bg-error/5"
defp period_row_class(:suspended), do: "bg-base-200/50"
defp period_row_class(_), do: ""
# Mock member data
defp mock_member do
%{
id: "123",
first_name: "Maria",
last_name: "Weber",
email: "maria.weber@example.de",
contribution_type: gettext("Regular"),
joined_at: "15.03.2021",
contribution_start: "01.01.2021"
}
end
# Mock periods data
defp mock_periods do
[
%{
id: "p1",
period_start: "01.01.2025",
period_end: "31.12.2025",
interval: :yearly,
amount: Decimal.new("60.00"),
status: :unpaid,
notes: nil,
is_current: true
},
%{
id: "p2",
period_start: "01.01.2024",
period_end: "31.12.2024",
interval: :yearly,
amount: Decimal.new("60.00"),
status: :paid,
notes: gettext("Paid via bank transfer"),
is_current: false
},
%{
id: "p3",
period_start: "01.01.2023",
period_end: "31.12.2023",
interval: :yearly,
amount: Decimal.new("50.00"),
status: :paid,
notes: nil,
is_current: false
},
%{
id: "p4",
period_start: "01.01.2022",
period_end: "31.12.2022",
interval: :yearly,
amount: Decimal.new("50.00"),
status: :paid,
notes: nil,
is_current: false
},
%{
id: "p5",
period_start: "01.01.2021",
period_end: "31.12.2021",
interval: :yearly,
amount: Decimal.new("50.00"),
status: :suspended,
notes: gettext("Joining year - reduced to 0"),
is_current: false
}
]
end
defp format_currency(%Decimal{} = amount) do
"#{Decimal.to_string(amount)}"
end
defp format_interval(:monthly), do: gettext("Monthly")
defp format_interval(:quarterly), do: gettext("Quarterly")
defp format_interval(:half_yearly), do: gettext("Half-yearly")
defp format_interval(:yearly), do: gettext("Yearly")
end

View file

@ -1,205 +0,0 @@
defmodule MvWeb.ContributionTypeLive.Index do
@moduledoc """
Mock-up LiveView for Contribution Types Management (Admin).
This is a preview-only page that displays the planned UI for managing
contribution types. It shows static mock data and is not functional.
## Planned Features (Future Implementation)
- List all contribution types
- Display: Name, Amount, Interval, Member count
- Create new contribution types
- Edit existing contribution types (name, amount, description - NOT interval)
- Delete contribution types (if no members assigned)
## Note
This page is intentionally non-functional and serves as a UI mockup
for the upcoming Membership Contributions feature.
"""
use MvWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, gettext("Contribution Types"))
|> assign(:contribution_types, mock_contribution_types())}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.mockup_warning />
<.header>
{gettext("Contribution Types")}
<:subtitle>
{gettext("Manage contribution types for membership fees.")}
</:subtitle>
<:actions>
<button class="btn btn-primary" disabled>
<.icon name="hero-plus" /> {gettext("New Contribution Type")}
</button>
</:actions>
</.header>
<.table id="contribution_types" rows={@contribution_types} row_id={fn ct -> "ct-#{ct.id}" end}>
<:col :let={ct} label={gettext("Name")}>
<span class="font-medium">{ct.name}</span>
<p :if={ct.description} class="text-sm text-base-content/60">{ct.description}</p>
</:col>
<:col :let={ct} label={gettext("Amount")}>
<span class="font-mono">{format_currency(ct.amount)}</span>
</:col>
<:col :let={ct} label={gettext("Interval")}>
<span class="badge badge-outline">{format_interval(ct.interval)}</span>
</:col>
<:col :let={ct} label={gettext("Members")}>
<span class="badge badge-ghost">{ct.member_count}</span>
</:col>
<:action :let={_ct}>
<button class="btn btn-ghost btn-xs" disabled title={gettext("Edit")}>
<.icon name="hero-pencil" class="size-4" />
</button>
</:action>
<:action :let={ct}>
<button
class="btn btn-ghost btn-xs text-error"
disabled
title={
if ct.member_count > 0,
do: gettext("Cannot delete - members assigned"),
else: gettext("Delete")
}
>
<.icon name="hero-trash" class="size-4" />
</button>
</:action>
</.table>
<.info_card />
</Layouts.app>
"""
end
# Mock-up warning banner component - subtle orange style
defp mockup_warning(assigns) do
~H"""
<div class="border border-warning text-warning bg-base-100 rounded-lg px-4 py-3 mb-6 flex items-center gap-3">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<div>
<span class="font-semibold">{gettext("Preview Mockup")}</span>
<span class="text-sm text-base-content/70 ml-2">
{gettext("This page is not functional and only displays the planned features.")}
</span>
</div>
</div>
"""
end
# Info card explaining the contribution type concept
defp info_card(assigns) do
~H"""
<div class="card bg-base-200 mt-6">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-information-circle" class="size-5" />
{gettext("About Contribution Types")}
</h2>
<div class="prose prose-sm max-w-none">
<p>
{gettext(
"Contribution types define different membership fee structures. Each type has a fixed cycle (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
)}
</p>
<ul>
<li>
<strong>{gettext("Name & Amount")}</strong>
- {gettext("Can be changed at any time. Amount changes affect future periods only.")}
</li>
<li>
<strong>{gettext("Interval")}</strong>
- {gettext(
"Fixed after creation. Members can only switch between types with the same interval."
)}
</li>
<li>
<strong>{gettext("Deletion")}</strong>
- {gettext("Only possible if no members are assigned to this type.")}
</li>
</ul>
</div>
</div>
</div>
"""
end
# Mock data for demonstration
defp mock_contribution_types do
[
%{
id: "1",
name: gettext("Regular"),
description: gettext("Standard membership fee for regular members"),
amount: Decimal.new("60.00"),
interval: :yearly,
member_count: 45
},
%{
id: "2",
name: gettext("Reduced"),
description: gettext("Reduced fee for unemployed, pensioners, or low income"),
amount: Decimal.new("30.00"),
interval: :yearly,
member_count: 12
},
%{
id: "3",
name: gettext("Student"),
description: gettext("Monthly fee for students and trainees"),
amount: Decimal.new("5.00"),
interval: :monthly,
member_count: 8
},
%{
id: "4",
name: gettext("Family"),
description: gettext("Quarterly fee for family memberships"),
amount: Decimal.new("25.00"),
interval: :quarterly,
member_count: 15
},
%{
id: "5",
name: gettext("Supporting Member"),
description: gettext("Half-yearly contribution for supporting members"),
amount: Decimal.new("100.00"),
interval: :half_yearly,
member_count: 3
},
%{
id: "6",
name: gettext("Honorary"),
description: gettext("No fee for honorary members"),
amount: Decimal.new("0.00"),
interval: :yearly,
member_count: 2
}
]
end
defp format_currency(%Decimal{} = amount) do
"#{Decimal.to_string(amount)}"
end
defp format_interval(:monthly), do: gettext("Monthly")
defp format_interval(:quarterly), do: gettext("Quarterly")
defp format_interval(:half_yearly), do: gettext("Half-yearly")
defp format_interval(:yearly), do: gettext("Yearly")
end

View file

@ -1,300 +0,0 @@
defmodule MvWeb.CustomFieldValueLive.Form do
@moduledoc """
LiveView form for creating and editing custom field values.
## Features
- Create new custom field values with member and type selection
- Edit existing custom field values
- Value input adapts to custom field type (string, integer, boolean, date, email)
- Real-time validation
## Form Fields
**Required:**
- member - Select which member owns this custom field value
- custom_field - Select the type (defines value type)
- value - The actual value (input type depends on custom field type)
## Value Types
The form dynamically renders appropriate inputs based on custom field type:
- String: text input
- Integer: number input
- Boolean: checkbox
- Date: date picker
- Email: email input with validation
## Events
- `validate` - Real-time form validation
- `save` - Submit form (create or update custom field value)
## Note
Custom field values are typically managed through the member edit form,
not through this standalone form.
"""
use MvWeb, :live_view
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@page_title}
<:subtitle>
{gettext("Use this form to manage Custom Field Value records in your database.")}
</:subtitle>
</.header>
<.form for={@form} id="custom_field_value-form" phx-change="validate" phx-submit="save">
<!-- Custom Field Selection -->
<.input
field={@form[:custom_field_id]}
type="select"
label={gettext("Custom field")}
options={custom_field_options(@custom_fields)}
prompt={gettext("Choose a custom field")}
/>
<!-- Member Selection -->
<.input
field={@form[:member_id]}
type="select"
label={gettext("Member")}
options={member_options(@members)}
prompt={gettext("Choose a member")}
/>
<!-- Value Input - handles Union type -->
<%= if @selected_custom_field do %>
<.union_value_input form={@form} custom_field={@selected_custom_field} />
<% else %>
<div class="text-sm text-gray-600">
{gettext("Please select a custom field first")}
</div>
<% end %>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Custom Field Value")}
</.button>
<.button navigate={return_path(@return_to, @custom_field_value)}>{gettext("Cancel")}</.button>
</.form>
</Layouts.app>
"""
end
# Helper function for Union-Value Input
defp union_value_input(assigns) do
# Extract the current value from the CustomFieldValue
current_value = extract_current_value(assigns.form.data, assigns.custom_field.value_type)
assigns = assign(assigns, :current_value, current_value)
~H"""
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">
{gettext("Value")}
</label>
<%= case @custom_field.value_type do %>
<% :string -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="text" label="" value={@current_value} />
<input type="hidden" name={value_form[:_union_type].name} value="string" />
</.inputs_for>
<% :integer -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="number" label="" value={@current_value} />
<input type="hidden" name={value_form[:_union_type].name} value="integer" />
</.inputs_for>
<% :boolean -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="checkbox" label="" checked={@current_value} />
<input type="hidden" name={value_form[:_union_type].name} value="boolean" />
</.inputs_for>
<% :date -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input
field={value_form[:value]}
type="date"
label=""
value={format_date_value(@current_value)}
/>
<input type="hidden" name={value_form[:_union_type].name} value="date" />
</.inputs_for>
<% :email -> %>
<.inputs_for :let={value_form} field={@form[:value]}>
<.input field={value_form[:value]} type="email" label="" value={@current_value} />
<input type="hidden" name={value_form[:_union_type].name} value="email" />
</.inputs_for>
<% _ -> %>
<div class="text-sm text-red-600">
{gettext("Unsupported value type: %{type}", type: @custom_field.value_type)}
</div>
<% end %>
</div>
"""
end
# Helper function to extract the current value from the CustomFieldValue
defp extract_current_value(
%Mv.Membership.CustomFieldValue{value: %Ash.Union{value: value}},
_value_type
) do
value
end
defp extract_current_value(_data, _value_type) do
nil
end
# Helper function to format Date values for HTML input
defp format_date_value(%Date{} = date) do
Date.to_iso8601(date)
end
defp format_date_value(nil), do: ""
defp format_date_value(date) when is_binary(date) do
case Date.from_iso8601(date) do
{:ok, parsed_date} -> Date.to_iso8601(parsed_date)
_ -> ""
end
end
defp format_date_value(_), do: ""
@impl true
def mount(params, _session, socket) do
custom_field_value =
case params["id"] do
nil -> nil
id -> Ash.get!(Mv.Membership.CustomFieldValue, id) |> Ash.load!([:custom_field])
end
action = if is_nil(custom_field_value), do: "New", else: "Edit"
page_title = action <> " " <> "Custom field value"
# Load all CustomFields and Members for the selection fields
actor = current_actor(socket)
custom_fields = Ash.read!(Mv.Membership.CustomField, actor: actor)
members = Ash.read!(Mv.Membership.Member, actor: actor)
{:ok,
socket
|> assign(:return_to, return_to(params["return_to"]))
|> assign(custom_field_value: custom_field_value)
|> assign(:page_title, page_title)
|> assign(:custom_fields, custom_fields)
|> assign(:members, members)
|> assign(:selected_custom_field, custom_field_value && custom_field_value.custom_field)
|> assign_form()}
end
defp return_to("show"), do: "show"
defp return_to(_), do: "index"
@impl true
def handle_event("validate", %{"custom_field_value" => custom_field_value_params}, socket) do
# Find the selected CustomField
selected_custom_field =
case custom_field_value_params["custom_field_id"] do
"" -> nil
nil -> nil
id -> Enum.find(socket.assigns.custom_fields, &(&1.id == id))
end
# Set the Union type based on the selected CustomField
updated_params =
if selected_custom_field do
union_type = to_string(selected_custom_field.value_type)
put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else
custom_field_value_params
end
{:noreply,
socket
|> assign(:selected_custom_field, selected_custom_field)
|> assign(form: AshPhoenix.Form.validate(socket.assigns.form, updated_params))}
end
def handle_event("save", %{"custom_field_value" => custom_field_value_params}, socket) do
# Set the Union type based on the selected CustomField
updated_params =
if socket.assigns.selected_custom_field do
union_type = to_string(socket.assigns.selected_custom_field.value_type)
put_in(custom_field_value_params, ["value", "_union_type"], union_type)
else
custom_field_value_params
end
actor = current_actor(socket)
case submit_form(socket.assigns.form, updated_params, actor) do
{:ok, custom_field_value} ->
notify_parent({:saved, custom_field_value})
action =
case socket.assigns.form.source.type do
:create -> gettext("create")
:update -> gettext("update")
other -> to_string(other)
end
socket =
socket
|> put_flash(
:info,
gettext("Custom field value %{action} successfully", action: action)
)
|> push_navigate(to: return_path(socket.assigns.return_to, custom_field_value))
{:noreply, socket}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
defp assign_form(%{assigns: %{custom_field_value: custom_field_value}} = socket) do
form =
if custom_field_value do
# Determine the Union type based on the custom_field
union_type = custom_field_value.custom_field && custom_field_value.custom_field.value_type
params =
if union_type do
%{"value" => %{"_union_type" => to_string(union_type)}}
else
%{}
end
AshPhoenix.Form.for_update(custom_field_value, :update,
as: "custom_field_value",
params: params
)
else
AshPhoenix.Form.for_create(Mv.Membership.CustomFieldValue, :create,
as: "custom_field_value"
)
end
assign(socket, form: to_form(form))
end
defp return_path("index", _custom_field_value), do: ~p"/custom_field_values"
defp return_path("show", custom_field_value),
do: ~p"/custom_field_values/#{custom_field_value.id}"
# Helper functions for selection options
defp custom_field_options(custom_fields) do
Enum.map(custom_fields, &{&1.name, &1.id})
end
defp member_options(members) do
Enum.map(members, &{MvWeb.Helpers.MemberHelpers.display_name(&1), &1.id})
end
end

View file

@ -1,157 +0,0 @@
defmodule MvWeb.CustomFieldValueLive.Index do
@moduledoc """
LiveView for displaying and managing custom field values.
## Features
- List all custom field values with their values and types
- Show which member each custom field value belongs to
- Display custom field information
- Navigate to custom field value details and edit forms
- Delete custom field values
## Relationships
Each custom field value is linked to:
- A member (the custom field value owner)
- A custom field (defining value type and behavior)
## Events
- `delete` - Remove a custom field value from the database
## Note
Custom field values are typically managed through the member edit form.
This view provides a global overview of all custom field values.
"""
use MvWeb, :live_view
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
import MvWeb.LiveHelpers, only: [current_actor: 1]
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Listing Custom field values
<:actions>
<.button variant="primary" navigate={~p"/custom_field_values/new"}>
<.icon name="hero-plus" /> New Custom field value
</.button>
</:actions>
</.header>
<.table
id="custom_field_values"
rows={@streams.custom_field_values}
row_click={
fn {_id, custom_field_value} ->
JS.navigate(~p"/custom_field_values/#{custom_field_value}")
end
}
>
<:col :let={{_id, custom_field_value}} label="Id">{custom_field_value.id}</:col>
<:action :let={{_id, custom_field_value}}>
<div class="sr-only">
<.link navigate={~p"/custom_field_values/#{custom_field_value}"}>Show</.link>
</div>
<.link navigate={~p"/custom_field_values/#{custom_field_value}/edit"}>Edit</.link>
</:action>
<:action :let={{id, custom_field_value}}>
<.link
phx-click={JS.push("delete", value: %{id: custom_field_value.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
actor = current_actor(socket)
# Early return if no actor (prevents exceptions in unauthenticated tests)
if is_nil(actor) do
{:ok,
socket
|> assign(:page_title, "Listing Custom field values")
|> stream(:custom_field_values, [])}
else
case Ash.read(Mv.Membership.CustomFieldValue, actor: actor) do
{:ok, custom_field_values} ->
{:ok,
socket
|> assign(:page_title, "Listing Custom field values")
|> stream(:custom_field_values, custom_field_values)}
{:error, %Ash.Error.Forbidden{}} ->
{:ok,
socket
|> assign(:page_title, "Listing Custom field values")
|> stream(:custom_field_values, [])
|> put_flash(:error, gettext("You do not have permission to view custom field values"))}
{:error, error} ->
{:ok,
socket
|> assign(:page_title, "Listing Custom field values")
|> stream(:custom_field_values, [])
|> put_flash(:error, format_error(error))}
end
end
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
actor = MvWeb.LiveHelpers.current_actor(socket)
case Ash.get(Mv.Membership.CustomFieldValue, id, actor: actor) do
{:ok, custom_field_value} ->
case Ash.destroy(custom_field_value, actor: actor) do
:ok ->
{:noreply,
socket
|> stream_delete(:custom_field_values, custom_field_value)
|> put_flash(:info, gettext("Custom field value deleted successfully"))}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to delete this custom field value")
)}
{:error, error} ->
{:noreply, put_flash(socket, :error, format_error(error))}
end
{:error, %Ash.Error.Query.NotFound{}} ->
{:noreply, put_flash(socket, :error, gettext("Custom field value not found"))}
{:error, %Ash.Error.Forbidden{} = _error} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to access this custom field value")
)}
{:error, error} ->
{:noreply, put_flash(socket, :error, format_error(error))}
end
end
defp format_error(%Ash.Error.Invalid{errors: errors}) do
Enum.map_join(errors, ", ", fn %{message: message} -> message end)
end
defp format_error(error) do
inspect(error)
end
end

View file

@ -1,67 +0,0 @@
defmodule MvWeb.CustomFieldValueLive.Show do
@moduledoc """
LiveView for displaying a single custom field value's details.
## Features
- Display custom field value and type
- Show linked member
- Show custom field definition
- Navigate to edit form
- Return to custom field value list
## Displayed Information
- Custom field value (formatted based on type)
- Custom field name and description
- Member information (who owns this custom field value)
- Custom field value metadata (ID, timestamps if added)
## Navigation
- Back to custom field value list
- Edit custom field value
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
Data field value {@custom_field_value.id}
<:subtitle>This is a custom_field_value record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/custom_field_values"}>
<.icon name="hero-arrow-left" />
</.button>
<.button
variant="primary"
navigate={~p"/custom_field_values/#{@custom_field_value}/edit?return_to=show"}
>
<.icon name="hero-pencil-square" /> Edit Custom field value
</.button>
</:actions>
</.header>
<.list>
<:item title="Id">{@custom_field_value.id}</:item>
</.list>
</Layouts.app>
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
|> assign(:page_title, page_title(socket.assigns.live_action))
|> assign(:custom_field_value, Ash.get!(Mv.Membership.CustomFieldValue, id))}
end
defp page_title(:show), do: "Show data field value"
defp page_title(:edit), do: "Edit data field value"
end

View file

@ -58,12 +58,6 @@ defmodule MvWeb.Router do
live "/members/:id", MemberLive.Show, :show
live "/members/:id/show/edit", MemberLive.Show, :edit
live "/custom_field_values", CustomFieldValueLive.Index, :index
live "/custom_field_values/new", CustomFieldValueLive.Form, :new
live "/custom_field_values/:id/edit", CustomFieldValueLive.Form, :edit
live "/custom_field_values/:id", CustomFieldValueLive.Show, :show
live "/custom_field_values/:id/show/edit", CustomFieldValueLive.Show, :edit
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Form, :new
live "/users/:id/edit", UserLive.Form, :edit
@ -80,10 +74,6 @@ defmodule MvWeb.Router do
live "/membership_fee_types/new", MembershipFeeTypeLive.Form, :new
live "/membership_fee_types/:id/edit", MembershipFeeTypeLive.Form, :edit
# Contribution Management (Mock-ups)
live "/contribution_types", ContributionTypeLive.Index, :index
live "/contributions/member/:id", ContributionPeriodLive.Show, :show
# Role Management (Admin only)
live "/admin/roles", RoleLive.Index, :index
live "/admin/roles/new", RoleLive.Form, :new

View file

@ -146,8 +146,6 @@ defmodule MvWeb.ProfileNavigationTest do
"/",
"/members",
"/members/new",
"/custom_field_values",
"/custom_field_values/new",
"/users",
"/users/new"
]