diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 93e18b4..a85bf69 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -256,7 +256,7 @@ defmodule MvWeb.MemberLive.Show do id={"membership-fees-#{@member.id}"} member={@member} current_user={@current_user} - vereinfacht_debug_response={@vereinfacht_debug_response} + vereinfacht_receipts={@vereinfacht_receipts} /> <% end %> @@ -268,7 +268,7 @@ defmodule MvWeb.MemberLive.Show do {:ok, socket |> assign(:active_tab, :contact) - |> assign(:vereinfacht_debug_response, nil)} + |> assign(:vereinfacht_receipts, nil)} end @impl true @@ -320,14 +320,14 @@ defmodule MvWeb.MemberLive.Show do {:noreply, assign(socket, :active_tab, :membership_fees)} end - def handle_event("load_vereinfacht_debug", %{"contact_id" => contact_id}, socket) do + def handle_event("load_vereinfacht_receipts", %{"contact_id" => contact_id}, socket) do response = - case Mv.Vereinfacht.Client.get_contact(contact_id) do - {:ok, body} -> {:ok, body} + case Mv.Vereinfacht.Client.get_contact_with_receipts(contact_id) do + {:ok, receipts} -> {:ok, receipts} {:error, reason} -> {:error, reason} end - {:noreply, assign(socket, :vereinfacht_debug_response, response)} + {:noreply, assign(socket, :vereinfacht_receipts, response)} end # Flash set in LiveComponent is not shown in parent layout; child sends this to display flash diff --git a/lib/mv_web/live/member_live/show/membership_fees_component.ex b/lib/mv_web/live/member_live/show/membership_fees_component.ex index 02c9d66..946f249 100644 --- a/lib/mv_web/live/member_live/show/membership_fees_component.ex +++ b/lib/mv_web/live/member_live/show/membership_fees_component.ex @@ -54,42 +54,67 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do <%= if Mv.Config.vereinfacht_configured?() do %> <%= if @member.vereinfacht_contact_id do %>
- -
- - {gettext("Contact ID: %{id}", id: @member.vereinfacht_contact_id)} - +
<.link :if={Mv.Config.vereinfacht_contact_view_url(@member.vereinfacht_contact_id)} href={Mv.Config.vereinfacht_contact_view_url(@member.vereinfacht_contact_id)} target="_blank" rel="noopener noreferrer" - class="link link-accent underline inline-flex items-center gap-1" + class="link link-accent underline inline-flex items-center gap-1 w-fit" > {gettext("View contact in Vereinfacht")} <.icon name="hero-arrow-top-right-on-square" class="inline-block size-4" /> -
- {gettext("Debug:")} +
- <%= if @vereinfacht_debug_response do %> + <%= if @vereinfacht_receipts do %>
-
<%= format_vereinfacht_debug_response(@vereinfacht_debug_response) %>
+ <%= if match?({:ok, _}, @vereinfacht_receipts) do %> + <% {_, receipts} = @vereinfacht_receipts %> + <%= if receipts == [] do %> +

{gettext("No receipts")}

+ <% else %> + <% cols = receipt_display_columns(receipts) %> + + + + <%= for {_key, translated_label} <- cols do %> + + <% end %> + + + + <%= for r <- receipts do %> + + <%= for {col_key, _header_key} <- cols do %> + + <% end %> + + <% end %> + +
{translated_label}
{format_receipt_cell(col_key, r[col_key])}
+ <% end %> + <% else %> + <% {:error, reason} = @vereinfacht_receipts %> +

+ {gettext("Error loading receipts: %{reason}", + reason: format_vereinfacht_error(reason) + )} +

+ <% end %>
<% end %>
@@ -499,7 +524,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do |> assign_new(:create_cycle_date, fn -> nil end) |> assign_new(:create_cycle_error, fn -> nil end) |> assign_new(:regenerating, fn -> false end) - |> assign_new(:vereinfacht_debug_response, fn -> nil end)} + |> assign_new(:vereinfacht_receipts, fn -> nil end)} end @impl true @@ -1057,23 +1082,138 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do defp format_create_cycle_period(_date, _interval), do: "" - defp format_vereinfacht_debug_response({:ok, body}) when is_map(body) do - Jason.encode!(body, pretty: true) + defp format_vereinfacht_error({:http, status, detail}) when is_binary(detail), + do: "HTTP #{status} – #{detail}" + + defp format_vereinfacht_error({:http, status, _}), do: "HTTP #{status}" + defp format_vereinfacht_error(reason), do: inspect(reason) + + # Ordered receipt columns: {api_key, gettext key for header}. Only columns present in data are shown. + @receipt_column_spec [ + {:amount, "Amount"}, + {:bookingDate, "Booking date"}, + {:createdAt, "Created at"}, + {:receiptType, "Receipt type"}, + {:referenceNumber, "Reference number"}, + {:status, "Status"}, + {:updatedAt, "Updated at"} + ] + + defp receipt_display_columns(receipts) when is_list(receipts) do + keys_in_data = receipts |> Enum.flat_map(&Map.keys/1) |> MapSet.new() + + Enum.filter(@receipt_column_spec, fn {key, _} -> MapSet.member?(keys_in_data, key) end) + |> Enum.map(fn {key, msgid} -> {key, Gettext.gettext(MvWeb.Gettext, msgid)} end) end - defp format_vereinfacht_debug_response({:error, {:http, status, detail}}) - when is_binary(detail) do - "Error: HTTP #{status} – #{detail}" + defp format_receipt_cell(:amount, nil), do: "—" + + defp format_receipt_cell(:amount, val) when is_number(val) do + case Decimal.cast(val) do + {:ok, d} -> MembershipFeeHelpers.format_currency(d) + _ -> to_string(val) + end end - defp format_vereinfacht_debug_response({:error, {:http, status, _}}) do - "Error: HTTP #{status}" + defp format_receipt_cell(:amount, val) when is_binary(val) do + case Decimal.parse(val) do + {d, _} -> MembershipFeeHelpers.format_currency(d) + :error -> val + end end - defp format_vereinfacht_debug_response({:error, reason}) do - "Error: " <> inspect(reason) + defp format_receipt_cell(:amount, val), do: to_string(val) + + defp format_receipt_cell(:status, nil), do: "—" + + defp format_receipt_cell(:status, val) when is_binary(val) do + translate_receipt_status(val) end + defp format_receipt_cell(:status, val), do: translate_receipt_status(to_string(val)) + + defp format_receipt_cell(:receiptType, nil), do: "—" + + defp format_receipt_cell(:receiptType, val) when is_binary(val) do + translate_receipt_type(val) + end + + defp format_receipt_cell(:receiptType, val), do: translate_receipt_type(to_string(val)) + + defp format_receipt_cell(col_key, nil) when col_key in [:bookingDate, :createdAt, :updatedAt], + do: "—" + + defp format_receipt_cell(col_key, val) when col_key in [:bookingDate, :createdAt, :updatedAt] do + format_receipt_date(val) + end + + defp format_receipt_cell(_col_key, val) when is_binary(val), do: val + defp format_receipt_cell(_col_key, val) when is_number(val), do: to_string(val) + + defp format_receipt_cell(_col_key, val) when is_boolean(val), + do: if(val, do: gettext("Yes"), else: gettext("No")) + + defp format_receipt_cell(_col_key, %Date{} = d), do: format_receipt_date_short(d) + defp format_receipt_cell(_col_key, val) when is_map(val) or is_list(val), do: Jason.encode!(val) + defp format_receipt_cell(_col_key, val), do: to_string(val) + + defp format_receipt_date(%Date{} = d), do: format_receipt_date_short(d) + + defp format_receipt_date(val) when is_binary(val) do + case parse_receipt_date(val) do + {:ok, d} -> format_receipt_date_short(d) + _ -> val + end + end + + defp format_receipt_date(val), do: to_string(val) + + # Parses ISO date or datetime string to Date (uses first 10 chars for datetime strings) + defp parse_receipt_date(val) when is_binary(val) do + date_str = if String.length(val) >= 10, do: String.slice(val, 0, 10), else: val + Date.from_iso8601(date_str) + end + + # Format as "12. Dez. 2025" (day. abbreviated month. year) with translated month + defp format_receipt_date_short(%Date{day: day, month: month, year: year}) do + "#{day}. #{receipt_month_abbr(month)} #{year}" + end + + defp receipt_month_abbr(1), do: gettext("Jan.") + defp receipt_month_abbr(2), do: gettext("Feb.") + defp receipt_month_abbr(3), do: gettext("Mar.") + defp receipt_month_abbr(4), do: gettext("Apr.") + defp receipt_month_abbr(5), do: gettext("May") + defp receipt_month_abbr(6), do: gettext("Jun.") + defp receipt_month_abbr(7), do: gettext("Jul.") + defp receipt_month_abbr(8), do: gettext("Aug.") + defp receipt_month_abbr(9), do: gettext("Sep.") + defp receipt_month_abbr(10), do: gettext("Oct.") + defp receipt_month_abbr(11), do: gettext("Nov.") + defp receipt_month_abbr(12), do: gettext("Dec.") + defp receipt_month_abbr(_), do: "" + + # Translate API status values for display (extend as API returns more values) + defp translate_receipt_status("paid"), do: gettext("Paid") + defp translate_receipt_status("unpaid"), do: gettext("Unpaid") + defp translate_receipt_status("suspended"), do: gettext("Suspended") + defp translate_receipt_status("open"), do: gettext("Open") + defp translate_receipt_status("cancelled"), do: gettext("Cancelled") + defp translate_receipt_status("draft"), do: gettext("Draft") + defp translate_receipt_status("incompleted"), do: gettext("Incompleted") + defp translate_receipt_status("completed"), do: gettext("Completed") + defp translate_receipt_status("empty"), do: "—" + defp translate_receipt_status(other), do: other + + # Translate API receipt type values (extend as API returns more values) + defp translate_receipt_type("invoice"), do: gettext("Invoice") + defp translate_receipt_type("receipt"), do: gettext("Receipt") + defp translate_receipt_type("credit_note"), do: gettext("Credit note") + defp translate_receipt_type("credit"), do: gettext("Credit") + defp translate_receipt_type("expense"), do: gettext("Expense") + defp translate_receipt_type("income"), do: gettext("Income") + defp translate_receipt_type(other), do: other + # Helper component for section box attr :title, :string, required: true slot :inner_block, required: true