diff --git a/lib/foedus/contracts.ex b/lib/foedus/contracts.ex
index a35cfba..7878e66 100644
--- a/lib/foedus/contracts.ex
+++ b/lib/foedus/contracts.ex
@@ -101,4 +101,100 @@ defmodule Foedus.Contracts do
def change_contract_template(%ContractTemplate{} = contract_template, attrs \\ %{}) do
ContractTemplate.changeset(contract_template, attrs)
end
+
+ alias Foedus.Contracts.Signer
+
+ @doc """
+ Returns the list of signers.
+
+ ## Examples
+
+ iex> list_signers()
+ [%Signer{}, ...]
+
+ """
+ def list_signers do
+ Repo.all(Signer)
+ end
+
+ @doc """
+ Gets a single signer.
+
+ Raises `Ecto.NoResultsError` if the Signer does not exist.
+
+ ## Examples
+
+ iex> get_signer!(123)
+ %Signer{}
+
+ iex> get_signer!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_signer!(id), do: Repo.get!(Signer, id)
+
+ @doc """
+ Creates a signer.
+
+ ## Examples
+
+ iex> create_signer(%{field: value})
+ {:ok, %Signer{}}
+
+ iex> create_signer(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_signer(attrs \\ %{}) do
+ %Signer{}
+ |> Signer.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a signer.
+
+ ## Examples
+
+ iex> update_signer(signer, %{field: new_value})
+ {:ok, %Signer{}}
+
+ iex> update_signer(signer, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_signer(%Signer{} = signer, attrs) do
+ signer
+ |> Signer.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a signer.
+
+ ## Examples
+
+ iex> delete_signer(signer)
+ {:ok, %Signer{}}
+
+ iex> delete_signer(signer)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_signer(%Signer{} = signer) do
+ Repo.delete(signer)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking signer changes.
+
+ ## Examples
+
+ iex> change_signer(signer)
+ %Ecto.Changeset{data: %Signer{}}
+
+ """
+ def change_signer(%Signer{} = signer, attrs \\ %{}) do
+ Signer.changeset(signer, attrs)
+ end
end
diff --git a/lib/foedus/contracts/signer.ex b/lib/foedus/contracts/signer.ex
new file mode 100644
index 0000000..38d3a04
--- /dev/null
+++ b/lib/foedus/contracts/signer.ex
@@ -0,0 +1,36 @@
+defmodule Foedus.Contracts.Signer do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ alias Foedus.Accounts.Company
+
+ @fields_required ~w(name lastname email role company_id)a
+ @fields_optional ~w(document birthdate status)a
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ schema "signers" do
+ field :name, :string
+ field :status, :boolean, default: true
+ field :role, :string
+ field :lastname, :string
+ field :email, :string
+ field :document, :string
+ field :birthdate, :date
+
+ belongs_to :company, Company
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(signer, attrs) do
+ signer
+ |> cast(attrs, @fields_required ++ @fields_optional)
+ |> validate_required(@fields_required)
+ |> validate_length(:name, min: 2, max: 100)
+ |> validate_length(:lastname, min: 2, max: 100)
+ |> validate_format(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/, message: "must be a valid email")
+ |> foreign_key_constraint(:company_id)
+ end
+end
diff --git a/lib/foedus_web/components/layouts.ex b/lib/foedus_web/components/layouts.ex
index 618b124..249a6bf 100644
--- a/lib/foedus_web/components/layouts.ex
+++ b/lib/foedus_web/components/layouts.ex
@@ -57,7 +57,7 @@ defmodule FoedusWeb.Layouts do
<.nav_link navigate={~p"/dashboard"}>Dashboard
<.nav_link navigate={~p"/contract_templates"}>Contract Templates
<.nav_link navigate={~p"/contractors"}>Contractors
- <.nav_link href="#">Relatórios
+ <.nav_link navigate={~p"/signers"}>Signers
<% end %>
"""
@@ -108,8 +108,8 @@ defmodule FoedusWeb.Layouts do
<.mobile_nav_link navigate={~p"/dashboard"}>Dashboard
<.mobile_nav_link navigate={~p"/contract_templates"}>Templates
- <.mobile_nav_link href="#">Contratos
- <.mobile_nav_link href="#">Relatórios
+ <.mobile_nav_link navigate={~p"/contractors"}>Contractors
+ <.mobile_nav_link navigate={~p"/signers"}>Signers
diff --git a/lib/foedus_web/components/ui/field_input.ex b/lib/foedus_web/components/ui/field_input.ex
new file mode 100644
index 0000000..61df156
--- /dev/null
+++ b/lib/foedus_web/components/ui/field_input.ex
@@ -0,0 +1,105 @@
+defmodule FoedusWeb.Components.UI.FieldInput do
+ use Phoenix.Component
+
+ attr :field, :any, required: true
+ attr :type, :string, default: "text"
+ attr :label, :string, required: true
+ attr :placeholder, :string, default: nil
+ attr :required, :boolean, default: false
+ attr :options, :list, default: []
+ attr :prompt, :string, default: nil
+ attr :class, :string, default: ""
+
+ def field_input(%{type: "select"} = assigns) do
+ ~H"""
+
+
+
+
+ {translate_errors(@field.errors)}
+
+
+ """
+ end
+
+ def field_input(%{type: "checkbox"} = assigns) do
+ ~H"""
+
+
+
+
+ """
+ end
+
+ def field_input(%{type: "textarea"} = assigns) do
+ ~H"""
+
+
+
+
+ {translate_errors(@field.errors)}
+
+
+ """
+ end
+
+ def field_input(assigns) do
+ ~H"""
+
+
+
+
+ {translate_errors(@field.errors)}
+
+
+ """
+ end
+
+ defp translate_errors(errors) when is_list(errors) do
+ Enum.map_join(errors, ", ", fn {msg, _opts} -> msg end)
+ end
+end
diff --git a/lib/foedus_web/components/ui/form_actions.ex b/lib/foedus_web/components/ui/form_actions.ex
new file mode 100644
index 0000000..100b712
--- /dev/null
+++ b/lib/foedus_web/components/ui/form_actions.ex
@@ -0,0 +1,50 @@
+defmodule FoedusWeb.Components.UI.FormActions do
+ use Phoenix.Component
+
+ import FoedusWeb.Components.UI.Button
+
+ @doc """
+ Renders form action buttons (Cancel and Submit).
+
+ ## Examples
+
+ <.form_actions
+ on_cancel={JS.patch(~p"/signers")}
+ submit_text="Create Signer"
+ />
+ """
+ attr :on_cancel, Phoenix.LiveView.JS, required: true
+ attr :submit_text, :string, required: true
+ attr :show_icon, :boolean, default: true
+
+ def form_actions(assigns) do
+ ~H"""
+
+ <.button
+ variant="secondary"
+ size="lg"
+ type="button"
+ phx-click={@on_cancel}
+ >
+ Cancel
+
+
+ <.button
+ variant="cta_primary"
+ size="lg"
+ type="submit"
+ >
+
+ {@submit_text}
+
+
+ """
+ end
+end
diff --git a/lib/foedus_web/components/ui/form_builder.ex b/lib/foedus_web/components/ui/form_builder.ex
new file mode 100644
index 0000000..47afb84
--- /dev/null
+++ b/lib/foedus_web/components/ui/form_builder.ex
@@ -0,0 +1,24 @@
+defmodule FoedusWeb.Components.UI.FormBuilder do
+ use Phoenix.Component
+
+ attr :for, :any, required: true
+ attr :id, :string, required: true
+ attr :action, :string, default: nil
+ attr :class, :string, default: ""
+ attr :rest, :global, include: ~w(phx-change phx-submit phx-target)
+ slot :inner_block, required: true
+
+ def form_builder(assigns) do
+ ~H"""
+ <.form
+ for={@for}
+ id={@id}
+ action={@action}
+ class={["space-y-8", @class]}
+ {@rest}
+ >
+ {render_slot(@inner_block)}
+
+ """
+ end
+end
diff --git a/lib/foedus_web/components/ui/form_header.ex b/lib/foedus_web/components/ui/form_header.ex
new file mode 100644
index 0000000..6f8dbcb
--- /dev/null
+++ b/lib/foedus_web/components/ui/form_header.ex
@@ -0,0 +1,61 @@
+defmodule FoedusWeb.Components.UI.FormHeader do
+ use Phoenix.Component
+ alias FoedusWeb.Components.UI.Icon
+
+ @doc """
+ Renders a gradient header with icon, title, subtitle and close button for forms.
+
+ ## Examples
+
+ <.form_header
+ title="New Signer"
+ subtitle="Add signers to your contracts"
+ icon="user"
+ on_close={JS.patch(~p"/signers")}
+ />
+ """
+ attr :title, :string, required: true
+ attr :subtitle, :string, default: nil
+ attr :icon, :string, required: true
+ attr :on_close, Phoenix.LiveView.JS, required: true
+ attr :gradient, :string, default: "from-indigo-600 to-purple-600"
+
+ def form_header(assigns) do
+ ~H"""
+
+
+
+
+
+
+
{@title}
+
+ {@subtitle}
+
+
+
+
+
+
+ """
+ end
+end
diff --git a/lib/foedus_web/components/ui/signer_details.ex b/lib/foedus_web/components/ui/signer_details.ex
new file mode 100644
index 0000000..dafaaf8
--- /dev/null
+++ b/lib/foedus_web/components/ui/signer_details.ex
@@ -0,0 +1,129 @@
+defmodule FoedusWeb.Components.UI.SignerDetails do
+ use Phoenix.Component
+ import FoedusWeb.Components.UI.Icon
+
+ attr :signer, :map, required: true
+
+ def signer_details(assigns) do
+ ~H"""
+
+
+
+ <.detail_field label="First Name" value={@signer.name} />
+ <.detail_field label="Last Name" value={@signer.lastname} />
+
+
+
+ <.detail_field_with_icon
+ label="Birth Date"
+ icon="hero-cake"
+ value={format_date(@signer.birthdate)}
+ />
+
+ <.detail_field_with_icon
+ label="Document"
+ icon="hero-identification"
+ value={format_document(@signer.document)}
+ />
+
+
+
+
+ {@signer.role}
+
+
+
+
+
+
+ {status_label(@signer.status)}
+
+
+
+
+
+ """
+ end
+
+ attr :label, :string, required: true
+ attr :value, :string, required: true
+
+ defp detail_field(assigns) do
+ ~H"""
+
+
+
{@value}
+
+ """
+ end
+
+ attr :label, :string, required: true
+ attr :icon, :string, required: true
+ attr :value, :string, required: true
+
+ defp detail_field_with_icon(assigns) do
+ ~H"""
+
+
+
+ <.icon name={@icon} class="w-4 h-4 text-gray-400" />
+
{@value}
+
+
+ """
+ end
+
+ defp status_label(true), do: "Active"
+ defp status_label(false), do: "Inactive"
+ defp status_label(_), do: "Inactive"
+
+ defp status_badge_class(true), do: "bg-green-100 text-green-700"
+ defp status_badge_class(false), do: "bg-gray-100 text-gray-700"
+ defp status_badge_class(_), do: "bg-gray-100 text-gray-700"
+
+ defp format_document(document) when is_binary(document) do
+ document
+ |> String.replace(~r/[^\d]/, "")
+ |> case do
+ <
> ->
+ "#{a}.#{b}.#{c}-#{d}"
+
+ <> ->
+ "#{a}.#{b}.#{c}/#{d}-#{e}"
+
+ doc ->
+ doc
+ end
+ end
+
+ defp format_document(nil), do: "N/A"
+
+ defp format_date(%Date{} = date), do: Calendar.strftime(date, "%d/%m/%Y")
+ defp format_date(nil), do: "N/A"
+ defp format_date(_), do: "N/A"
+end
diff --git a/lib/foedus_web/live/signer_live/form_component.ex b/lib/foedus_web/live/signer_live/form_component.ex
new file mode 100644
index 0000000..1f30029
--- /dev/null
+++ b/lib/foedus_web/live/signer_live/form_component.ex
@@ -0,0 +1,106 @@
+defmodule FoedusWeb.SignerLive.FormComponent do
+ use FoedusWeb, :live_component
+
+ import FoedusWeb.Components.UI.{
+ FormBuilder,
+ FieldInput,
+ FormActions,
+ FormHeader
+ }
+
+ alias Foedus.Contracts
+ alias Foedus.Contracts.Signer
+
+ def mount(_params, _session, socket) do
+ signers = Contracts.list_signers()
+ changeset = Contracts.change_signer(%Signer{status: true}, %{})
+
+ socket =
+ socket
+ |> assign(:form, to_form(changeset))
+ |> stream(:signers, signers)
+
+ {:ok, socket}
+ end
+
+ @impl true
+ def update(%{signer: signer} = assigns, socket) do
+ signer = if is_nil(signer.status), do: Map.put(signer, :status, true), else: signer
+ changeset = Contracts.change_signer(signer)
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:signer, signer)
+ |> assign(:form, to_form(changeset))}
+ end
+
+ @impl true
+ def handle_event("validate", %{"signer" => signer_params}, socket) do
+ signer_params = normalize_status_param(signer_params)
+
+ changeset =
+ socket.assigns.signer
+ |> Contracts.change_signer(signer_params)
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign(socket, :form, to_form(changeset))}
+ end
+
+ @impl true
+ def handle_event("save", %{"signer" => signer_params}, socket) do
+ company_id = socket.assigns.current_user.company_id
+ signer_params = normalize_status_param(signer_params)
+ save_signer(socket, socket.assigns.action, signer_params, company_id)
+ end
+
+ defp save_signer(socket, :new, signer_params, company_id) do
+ signer_params = Map.put(signer_params, "company_id", company_id)
+
+ case Contracts.create_signer(signer_params) do
+ {:ok, signer} ->
+ notify_parent({:saved, signer})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Signer created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, :form, to_form(changeset))}
+ end
+ end
+
+ defp save_signer(socket, :edit, signer_params, _company_id) do
+ case Contracts.update_signer(socket.assigns.signer, signer_params) do
+ {:ok, signer} ->
+ notify_parent({:saved, signer})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Signer updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, :form, to_form(changeset))}
+ end
+ end
+
+ defp normalize_status_param(%{"status" => "true"} = params) do
+ Map.put(params, "status", true)
+ end
+
+ defp normalize_status_param(%{"status" => "false"} = params) do
+ Map.put(params, "status", false)
+ end
+
+ defp normalize_status_param(%{"status" => val} = params) when is_boolean(val) do
+ params
+ end
+
+ defp normalize_status_param(params) do
+ Map.put(params, "status", false)
+ end
+
+ defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/lib/foedus_web/live/signer_live/form_component.html.heex b/lib/foedus_web/live/signer_live/form_component.html.heex
new file mode 100644
index 0000000..edd4a28
--- /dev/null
+++ b/lib/foedus_web/live/signer_live/form_component.html.heex
@@ -0,0 +1,43 @@
+
+ <.form_header
+ title="New Signer"
+ subtitle="Add signers to your contracts"
+ icon="user"
+ on_close={JS.patch(~p"/signers")}
+ />
+
+
+ <.form_builder
+ for={@form}
+ id="signer-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ class="space-y-6"
+ >
+
+ <.field_input field={@form[:name]} label="First Name" required />
+ <.field_input field={@form[:lastname]} label="Last Name" required />
+ <.field_input field={@form[:email]} type="email" label="Email Address" required />
+
+
+
+ <.field_input field={@form[:document]} label="Document Number" required />
+ <.field_input field={@form[:birthdate]} type="date" label="Birth Date" required />
+ <.field_input field={@form[:role]} type="select" label="Role" prompt="Select a role"
+ options={[{"Director", "director"}, {"Manager", "manager"}, {"Coordinator", "coordinator"}]}
+ required />
+
+
+ <.field_input field={@form[:status]} type="checkbox" label="Active" />
+
+
+ <.form_actions
+ on_cancel={JS.patch(~p"/signers")}
+ submit_text="Create Signer"
+ show_icon={true}
+ />
+
+
+
+
\ No newline at end of file
diff --git a/lib/foedus_web/live/signer_live/index.ex b/lib/foedus_web/live/signer_live/index.ex
new file mode 100644
index 0000000..0b0838c
--- /dev/null
+++ b/lib/foedus_web/live/signer_live/index.ex
@@ -0,0 +1,66 @@
+defmodule FoedusWeb.SignerLive.Index do
+ use FoedusWeb, :live_view
+
+ import FoedusWeb.Components.UI.Table
+
+ alias Foedus.Contracts
+ alias Foedus.Contracts.Signer
+ alias FoedusWeb.SignerLive.FormComponent
+
+ @impl true
+ def mount(_params, _session, socket) do
+ signers = Contracts.list_signers()
+
+ {:ok, stream(socket, :signers, signers)}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ signer = Contracts.get_signer!(id)
+ {:ok, _} = Contracts.delete_signer(signer)
+ socket = stream_delete(socket, :signers, signer)
+
+ {:noreply, put_flash(socket, :info, "Signer deleted successfully")}
+ end
+
+ @impl true
+ def handle_info({FormComponent, {:saved, signer}}, socket) do
+ socket = stream_insert(socket, :signers, signer, at: 0)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_info({FormComponent, {:updated, signer}}, socket) do
+ socket = stream_insert(socket, :signers, signer)
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_info({FormComponent, {:deleted, signer}}, socket) do
+ socket = stream_delete(socket, :signers, signer)
+ {:noreply, socket}
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Signer")
+ |> assign(:signer, %Signer{})
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Signer")
+ |> assign(:signer, Contracts.get_signer!(id))
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Signers")
+ |> assign(:signer, nil)
+ end
+end
diff --git a/lib/foedus_web/live/signer_live/index.html.heex b/lib/foedus_web/live/signer_live/index.html.heex
new file mode 100644
index 0000000..d598af0
--- /dev/null
+++ b/lib/foedus_web/live/signer_live/index.html.heex
@@ -0,0 +1,64 @@
+
+
+ <.header class="mb-8">
+
Signer
+ <:subtitle>
+
+ Manage your signers here.
+
+
+ <:actions>
+ <.link
+ patch={~p"/signers/new"}
+ phx-click={JS.push_focus()}
+ >
+ <.button class="bg-indigo-700 hover:bg-indigo-500 text-white font-medium px-6 py-3 rounded-lg shadow-sm transition-colors duration-200">
+ New Signer
+
+
+
+
+
+ <.data_table
+ id="signers-table"
+ rows={@streams.signers}
+ columns={[
+ %{field: :id, label: "ID"},
+ %{field: :name, label: "Name"},
+ %{field: :role, label: "Role"},
+ %{field: :status, label: "Status"},
+ %{field: :inserted_at, label: "Created at", formatter: :datetime}
+ ]}
+ actions={[:show, :edit, :delete]}
+ resource_path="/signers"
+ table_class="w-full border border-gray-300 rounded-lg shadow-sm overflow-hidden"
+ thead_class="bg-gradient-to-r from-indigo-500 to-indigo-600"
+ th_class="px-6 py-4 text-left text-sm font-semibold text-white uppercase tracking-wider"
+ tbody_class="bg-white divide-y divide-gray-200"
+ td_class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
+ striped={true}
+ hoverable={true}
+ empty_message="No signers found. Create your first signers!"
+ />
+
+ <.modal
+ :if={@live_action in [:new, :edit]}
+ id="signer-modal"
+ show
+ on_cancel={JS.patch(~p"/signers")}
+ class="!w-[98vw] !h-[95vh] !m-2"
+ >
+
+ <.live_component
+ module={FoedusWeb.SignerLive.FormComponent }
+ id={@signer.id || :new}
+ current_user={@current_user}
+ title={@page_title}
+ action={@live_action}
+ signer={@signer}
+ patch={~p"/signers"}
+ />
+
+
+
+
diff --git a/lib/foedus_web/live/signer_live/show.ex b/lib/foedus_web/live/signer_live/show.ex
new file mode 100644
index 0000000..26caf94
--- /dev/null
+++ b/lib/foedus_web/live/signer_live/show.ex
@@ -0,0 +1,58 @@
+defmodule FoedusWeb.SignerLive.Show do
+ use FoedusWeb, :live_view
+
+ import FoedusWeb.Components.UI.{
+ Breadcrumb,
+ MetadataCard,
+ Card,
+ ShowHeader,
+ SignerDetails,
+ Icon
+ }
+
+ alias Foedus.Contracts
+
+ @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(:signer, Contracts.get_signer!(id))}
+ end
+
+ defp page_title(:show), do: "Show Signer"
+ defp page_title(:edit), do: "Edit Signer"
+
+ defp full_name(signer) do
+ first = signer.name || ""
+ last = signer.lastname || ""
+ name = String.trim("#{first} #{last}")
+ if name == "", do: "Signer ##{signer.id}", else: name
+ end
+
+ defp format_document(document) when is_binary(document) do
+ document
+ |> String.replace(~r/[^\d]/, "")
+ |> case do
+ <> ->
+ "#{a}.#{b}.#{c}-#{d}"
+
+ <> ->
+ "#{a}.#{b}.#{c}/#{d}-#{e}"
+
+ doc ->
+ doc
+ end
+ end
+
+ defp format_document(nil), do: "N/A"
+
+ defp format_date(%Date{} = date), do: Calendar.strftime(date, "%d/%m/%Y")
+ defp format_date(nil), do: "N/A"
+ defp format_date(_), do: "N/A"
+end
diff --git a/lib/foedus_web/live/signer_live/show.html.heex b/lib/foedus_web/live/signer_live/show.html.heex
new file mode 100644
index 0000000..4f6d5dc
--- /dev/null
+++ b/lib/foedus_web/live/signer_live/show.html.heex
@@ -0,0 +1,83 @@
+
+
+ <.breadcrumb>
+ <.breadcrumb_link navigate={~p"/signers"}>
+ Signers
+
+ <.breadcrumb_separator />
+ <.breadcrumb_current>
+ {full_name(@signer)}
+
+
+
+ <.show_header
+ title={full_name(@signer)}
+ subtitle={@signer.email}
+ description="Signer details and management"
+ icon="hero-user-circle"
+ back_url={~p"/signers"}
+ edit_url={~p"/signers/#{@signer.id}/edit"}
+ />
+
+
+
+ <.card title="Signer Information" icon="hero-user" icon_color="blue" class="w-full">
+ <.signer_details signer={@signer} />
+
+
+ <.card title="Activity" icon="hero-clock" icon_color="green" class="w-full">
+
+
+
+ <.icon name="hero-document-text" class="w-12 h-12 mx-auto mb-3 text-gray-300" />
+
No recent activity
+
+
+
+
+
+
+
+ <.card title="Quick Actions" icon="hero-bolt" icon_color="yellow" class="w-full">
+
+
+ {if @signer.status, do: "Deactivate Signer", else: "Activate Signer"}
+
+
+
+
+ <.card title="Metadata" icon="hero-information-circle" icon_color="purple" class="w-full">
+
+
+ <.metadata_card>
+ <.date_info_item
+ icon="plus"
+ title="Created"
+ date={@signer.inserted_at}
+ color="green"
+ />
+ <.date_info_item
+ icon="refresh"
+ title="Last Updated"
+ date={@signer.updated_at}
+ color="blue"
+ />
+
+
+
+
+
+
+
+
diff --git a/lib/foedus_web/router.ex b/lib/foedus_web/router.ex
index 399d68f..b4ba86e 100644
--- a/lib/foedus_web/router.ex
+++ b/lib/foedus_web/router.ex
@@ -66,6 +66,11 @@ defmodule FoedusWeb.Router do
live "/contract_templates/new", ContractTemplateLive.Index, :new
live "/contract_templates/:id/edit", ContractTemplateLive.Index, :edit
+ live "/signers", SignerLive.Index, :index
+ live "/signers/new", SignerLive.Index, :new
+ live "/signers/:id/edit", SignerLive.Index, :edit
+ live "/signers/:id", SignerLive.Show, :show
+
live "/contract_templates/:id", ContractTemplateLive.Show, :show
end
end
diff --git a/priv/repo/migrations/20251014001804_create_signers.exs b/priv/repo/migrations/20251014001804_create_signers.exs
new file mode 100644
index 0000000..2281f1b
--- /dev/null
+++ b/priv/repo/migrations/20251014001804_create_signers.exs
@@ -0,0 +1,22 @@
+defmodule Foedus.Repo.Migrations.CreateSigners do
+ use Ecto.Migration
+
+ def change do
+ create table(:signers, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :name, :string, null: false
+ add :lastname, :string, null: false
+ add :email, :string, null: false
+ add :document, :string
+ add :role, :string
+ add :birthdate, :date
+ add :status, :boolean, default: false, null: false
+ add :company_id, references(:companies, on_delete: :nothing, type: :binary_id), null: false
+
+ timestamps(type: :utc_datetime)
+ end
+
+ create index(:signers, [:company_id])
+ create unique_index(:signers, [:email, :company_id])
+ end
+end
diff --git a/test/foedus/contracts_test.exs b/test/foedus/contracts_test.exs
index 12d6afa..ca8fcf4 100644
--- a/test/foedus/contracts_test.exs
+++ b/test/foedus/contracts_test.exs
@@ -67,4 +67,70 @@ defmodule Foedus.ContractsTest do
assert %Ecto.Changeset{} = Contracts.change_contract_template(contract_template)
end
end
+
+ describe "signers" do
+ alias Foedus.Contracts.Signer
+
+ import Foedus.ContractsFixtures
+
+ @invalid_attrs %{name: nil, status: nil, role: nil, lastname: nil, email: nil, document: nil, birthdate: nil}
+
+ test "list_signers/0 returns all signers" do
+ signer = signer_fixture()
+ assert Contracts.list_signers() == [signer]
+ end
+
+ test "get_signer!/1 returns the signer with given id" do
+ signer = signer_fixture()
+ assert Contracts.get_signer!(signer.id) == signer
+ end
+
+ test "create_signer/1 with valid data creates a signer" do
+ valid_attrs = %{name: "some name", status: true, role: "some role", lastname: "some lastname", email: "some email", document: "some document", birthdate: ~D[2025-10-13]}
+
+ assert {:ok, %Signer{} = signer} = Contracts.create_signer(valid_attrs)
+ assert signer.name == "some name"
+ assert signer.status == true
+ assert signer.role == "some role"
+ assert signer.lastname == "some lastname"
+ assert signer.email == "some email"
+ assert signer.document == "some document"
+ assert signer.birthdate == ~D[2025-10-13]
+ end
+
+ test "create_signer/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Contracts.create_signer(@invalid_attrs)
+ end
+
+ test "update_signer/2 with valid data updates the signer" do
+ signer = signer_fixture()
+ update_attrs = %{name: "some updated name", status: false, role: "some updated role", lastname: "some updated lastname", email: "some updated email", document: "some updated document", birthdate: ~D[2025-10-14]}
+
+ assert {:ok, %Signer{} = signer} = Contracts.update_signer(signer, update_attrs)
+ assert signer.name == "some updated name"
+ assert signer.status == false
+ assert signer.role == "some updated role"
+ assert signer.lastname == "some updated lastname"
+ assert signer.email == "some updated email"
+ assert signer.document == "some updated document"
+ assert signer.birthdate == ~D[2025-10-14]
+ end
+
+ test "update_signer/2 with invalid data returns error changeset" do
+ signer = signer_fixture()
+ assert {:error, %Ecto.Changeset{}} = Contracts.update_signer(signer, @invalid_attrs)
+ assert signer == Contracts.get_signer!(signer.id)
+ end
+
+ test "delete_signer/1 deletes the signer" do
+ signer = signer_fixture()
+ assert {:ok, %Signer{}} = Contracts.delete_signer(signer)
+ assert_raise Ecto.NoResultsError, fn -> Contracts.get_signer!(signer.id) end
+ end
+
+ test "change_signer/1 returns a signer changeset" do
+ signer = signer_fixture()
+ assert %Ecto.Changeset{} = Contracts.change_signer(signer)
+ end
+ end
end
diff --git a/test/support/factories/signer_factory.ex b/test/support/factories/signer_factory.ex
new file mode 100644
index 0000000..134e4ac
--- /dev/null
+++ b/test/support/factories/signer_factory.ex
@@ -0,0 +1,20 @@
+defmodule Foedus.SignerFactory do
+ alias Foedus.Contracts.Signer
+
+ defmacro __using__(_opts) do
+ quote do
+ def signer_factory do
+ %Signer{
+ role: Enum.random(["witness", "contractee"]),
+ name: Faker.Person.first_name(),
+ status: Enum.random([true, false]),
+ lastname: Faker.Person.last_name(),
+ document: Faker.format("###.###.###-##"),
+ birthdate: Faker.Date.date_of_birth(18..80),
+ email: Faker.Internet.email(),
+ company: build(:company)
+ }
+ end
+ end
+ end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index c6e1558..44d0f45 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -8,4 +8,5 @@ defmodule Foedus.Factory do
use Foedus.AddressFactory
use Foedus.CompanyFactory
use Foedus.PlatformAccessFactory
+ use Foedus.SignerFactory
end
diff --git a/test/support/fixtures/contracts_fixtures.ex b/test/support/fixtures/contracts_fixtures.ex
index 3a19484..dcf5122 100644
--- a/test/support/fixtures/contracts_fixtures.ex
+++ b/test/support/fixtures/contracts_fixtures.ex
@@ -18,4 +18,24 @@ defmodule Foedus.ContractsFixtures do
contract_template
end
+
+ @doc """
+ Generate a signer.
+ """
+ def signer_fixture(attrs \\ %{}) do
+ {:ok, signer} =
+ attrs
+ |> Enum.into(%{
+ birthdate: ~D[2025-10-13],
+ document: "some document",
+ email: "some email",
+ lastname: "some lastname",
+ name: "some name",
+ role: "some role",
+ status: true
+ })
+ |> Foedus.Contracts.create_signer()
+
+ signer
+ end
end