defmodule PhoenixKit.Modules.Entities.Web.DataView do @moduledoc """ LiveView for viewing entity data records. Displays data with public form fields separated from other fields. Uses FormBuilder with disabled fields for the form section. """ use PhoenixKitWeb, :live_view on_mount PhoenixKit.Modules.Entities.Web.Hooks alias PhoenixKit.Modules.Entities alias PhoenixKit.Modules.Entities.EntityData alias PhoenixKit.Modules.Entities.FormBuilder alias PhoenixKit.Settings @impl true def mount(%{"entity_slug" => entity_slug, "id" => id} = params, _session, socket) do locale = params["locale"] || socket.assigns[:current_locale] entity = Entities.get_entity_by_name(entity_slug) data_record = EntityData.get!(id) mount_data_view(socket, entity, data_record, locale) end def mount(%{"entity_id" => entity_id, "id" => id} = params, _session, socket) do locale = params["locale"] || socket.assigns[:current_locale] entity = Entities.get_entity!(entity_id) data_record = EntityData.get!(id) mount_data_view(socket, entity, data_record, locale) end defp mount_data_view(socket, entity, data_record, locale) do project_title = Settings.get_project_title() # Get public form configuration settings = entity.settings || %{} public_form_enabled = Map.get(settings, "public_form_enabled", false) public_form_fields = Map.get(settings, "public_form_fields", []) # Check if this record was submitted via public form is_public_submission = public_submission?(data_record.metadata) # Get all field definitions fields_definition = entity.fields_definition || [] # Separate fields into form fields and other fields # Show form fields separately if: # 1. Public form is currently enabled, OR # 2. This record was submitted via public form (even if form is now disabled) {form_fields, other_fields} = if public_form_enabled || is_public_submission do Enum.split_with(fields_definition, fn field -> field["key"] in public_form_fields end) else # If public form not enabled and not a public submission, all fields go to "other" {[], fields_definition} end # Get data values data = data_record.data || %{} # Create changeset for FormBuilder (readonly display) changeset = EntityData.change(data_record) # Create a modified entity with only form fields for FormBuilder form_entity = %{entity | fields_definition: form_fields} # Create a modified entity with only other fields for display other_entity = %{entity | fields_definition: other_fields} socket = socket |> assign(:current_locale, locale) |> assign(:page_title, gettext("View Data")) |> assign(:project_title, project_title) |> assign(:entity, entity) |> assign(:form_entity, form_entity) |> assign(:other_entity, other_entity) |> assign(:data_record, data_record) |> assign(:changeset, changeset) |> assign(:data, data) |> assign(:public_form_enabled, public_form_enabled) |> assign(:form_fields, form_fields) |> assign(:other_fields, other_fields) |> assign(:public_form_title, Map.get(settings, "public_form_title", "")) |> assign(:public_form_description, Map.get(settings, "public_form_description", "")) |> assign(:metadata, data_record.metadata || %{}) |> assign(:is_public_submission, public_submission?(data_record.metadata)) {:ok, socket} end defp public_submission?(nil), do: false defp public_submission?(metadata), do: Map.get(metadata, "source") == "public_form" @impl true def render(assigns) do ~H"""
<%!-- Header Section --%>
<%!-- Back Button --%> <.link navigate={PhoenixKit.Utils.Routes.path("/admin/entities/#{@entity.name}/data")} class="btn btn-outline btn-primary btn-sm absolute left-0 top-0 -mb-12" > <.icon name="hero-arrow-left" class="w-4 h-4 mr-2" /> {gettext("Back to %{entity} Data", entity: @entity.display_name)} <%!-- Title Section --%>

{@data_record.title}

{gettext("Viewing %{entity} record", entity: @entity.display_name)}

<%= if @data_record.slug do %>

<.icon name="hero-link" class="w-4 h-4 inline" /> {@data_record.slug}

<% end %>
<%!-- Record Metadata --%>

<.icon name="hero-information-circle" class="w-5 h-5" /> {gettext("Record Information")}

{gettext("Status")}
{@data_record.status}
{gettext("Created")}
{PhoenixKit.Utils.Date.format_datetime_with_user_format(@data_record.date_created)}
{gettext("Updated")}
{PhoenixKit.Utils.Date.format_datetime_with_user_format(@data_record.date_updated)}
<%= if (@public_form_enabled || @is_public_submission) && length(@form_fields) > 0 do %> <%!-- Public Form Fields Section - Using FormBuilder with disabled inputs --%>

<.icon name="hero-document-text" class="w-5 h-5" /> <%= if @public_form_title != "" do %> {@public_form_title} <% else %> {gettext("Form Submission")} <% end %>

<%= if @public_form_description != "" do %>

{@public_form_description}

<% end %> <%!-- Use FormBuilder with disabled fields --%> {FormBuilder.build_fields(@form_entity, @changeset, wrapper_class: "mb-4", disabled: true )}
<% end %> <%= if @is_public_submission do %> <%!-- Security Warnings Section (if any) --%> <%= if @metadata["security_warnings"] && length(@metadata["security_warnings"]) > 0 do %>
<.icon name="hero-exclamation-triangle" class="w-6 h-6" />

{gettext("Security Flags")}

<%= for warning <- @metadata["security_warnings"] do %>
{security_warning_label(warning["type"])} {security_action_label(warning["action"])}
<% end %>
<% end %> <%!-- Submission Metadata Section --%>

<.icon name="hero-globe-alt" class="w-5 h-5" /> {gettext("Submission Details")}

<%= if @metadata["ip_address"] do %>
<.icon name="hero-signal" class="w-5 h-5 text-base-content/60" />
{gettext("IP Address")}
{@metadata["ip_address"]}
<% end %> <%= if @metadata["browser"] do %>
<.icon name="hero-window" class="w-5 h-5 text-base-content/60" />
{gettext("Browser")}
{@metadata["browser"]}
<% end %> <%= if @metadata["os"] do %>
<.icon name="hero-computer-desktop" class="w-5 h-5 text-base-content/60" />
{gettext("Operating System")}
{@metadata["os"]}
<% end %> <%= if @metadata["device"] do %>
<.icon name={device_icon(@metadata["device"])} class="w-5 h-5 text-base-content/60" />
{gettext("Device")}
{@metadata["device"]}
<% end %> <%= if @metadata["submitted_at"] do %>
<.icon name="hero-clock" class="w-5 h-5 text-base-content/60" />
{gettext("Submitted At")}
{format_submitted_at(@metadata["submitted_at"])}
<% end %> <%= if @metadata["time_to_submit_seconds"] do %>
<.icon name="hero-stopwatch" class="w-5 h-5 text-base-content/60" />
{gettext("Time to Submit")}
{format_duration(@metadata["time_to_submit_seconds"])}
<% end %> <%= if @metadata["referer"] do %>
<.icon name="hero-arrow-top-right-on-square" class="w-5 h-5 text-base-content/60" />
{gettext("Referrer")}
{@metadata["referer"]}
<% end %>
<%= if @metadata["user_agent"] do %>
{gettext("Full User Agent")}
{@metadata["user_agent"]}
<% end %>
<% end %> <%= if length(@other_fields) > 0 do %> <%!-- Other Fields Section - Using FormBuilder with disabled inputs --%>

<.icon name="hero-squares-2x2" class="w-5 h-5" /> <%= if (@public_form_enabled || @is_public_submission) && length(@form_fields) > 0 do %> {gettext("Additional Data")} <% else %> {gettext("Data Fields")} <% end %>

<%!-- Use FormBuilder with disabled fields --%> {FormBuilder.build_fields(@other_entity, @changeset, wrapper_class: "mb-4", disabled: true )}
<% end %> <%= if map_size(@data) == 0 && length(@form_fields) == 0 && length(@other_fields) == 0 do %> <%!-- Empty State --%>
📄

{gettext("No data fields have been filled in yet.")}

<% end %> <%!-- Actions --%>
<.link navigate={PhoenixKit.Utils.Routes.path("/admin/entities/#{@entity.name}/data")} class="btn btn-outline" > {gettext("Back")} <.link navigate={ PhoenixKit.Utils.Routes.path( "/admin/entities/#{@entity.name}/data/#{@data_record.uuid}/edit" ) } class="btn btn-primary" > <.icon name="hero-pencil" class="w-4 h-4 mr-2" /> {gettext("Edit %{entity}", entity: @entity.display_name)}
""" end defp status_badge_class("published"), do: "badge-success" defp status_badge_class("draft"), do: "badge-warning" defp status_badge_class("archived"), do: "badge-ghost" defp status_badge_class(_), do: "badge-ghost" defp device_icon("mobile"), do: "hero-device-phone-mobile" defp device_icon("tablet"), do: "hero-device-tablet" defp device_icon(_), do: "hero-computer-desktop" defp format_submitted_at(iso_string) when is_binary(iso_string) do case DateTime.from_iso8601(iso_string) do {:ok, datetime, _offset} -> PhoenixKit.Utils.Date.format_datetime_with_user_format(datetime) _ -> iso_string end end defp format_submitted_at(_), do: "-" defp format_duration(seconds) when is_integer(seconds) do cond do seconds < 60 -> ngettext("%{count} second", "%{count} seconds", seconds, count: seconds) seconds < 3600 -> minutes = div(seconds, 60) ngettext("%{count} minute", "%{count} minutes", minutes, count: minutes) true -> hours = div(seconds, 3600) minutes = div(rem(seconds, 3600), 60) if minutes > 0 do "#{ngettext("%{count} hour", "%{count} hours", hours, count: hours)}, #{ngettext("%{count} minute", "%{count} minutes", minutes, count: minutes)}" else ngettext("%{count} hour", "%{count} hours", hours, count: hours) end end end defp format_duration(_), do: "-" defp security_warning_label("honeypot"), do: gettext("Honeypot triggered") defp security_warning_label("too_fast"), do: gettext("Submitted too fast") defp security_warning_label("rate_limited"), do: gettext("Rate limited") defp security_warning_label(type), do: type defp security_action_label("save_suspicious"), do: gettext("Marked as suspicious") defp security_action_label("save_log"), do: gettext("Logged warning") defp security_action_label(action), do: action end