Skip to content

feat: certificate style builder and improve design #571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
6 changes: 6 additions & 0 deletions lib/atomic/activities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,12 @@ defmodule Atomic.Activities do
Repo.all(Enrollment)
end

def list_enrollments(opts) when is_list(opts) do
Enrollment
|> apply_filters(opts)
|> Repo.all()
end

@doc """
Gets a single enrollment.

Expand Down
27 changes: 27 additions & 0 deletions lib/atomic/organizations/certificate.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule Atomic.Certificate do

Check warning on line 1 in lib/atomic/organizations/certificate.ex

View workflow job for this annotation

GitHub Actions / OTP 27.x / Elixir 1.17.x

invalid association `organization` in schema Atomic.Certificate: associated schema Atomic.Organization does not exist
use Atomic.Schema

alias Atomic.Organization

@required_fields ~w(background title content background_color title_color content_color organization_color organization_id)a

schema "certificates" do
field :title, :integer
field :background, :boolean, default: false
field :content, :string
field :background_color, :string
field :title_color, :string
field :content_color, :string
field :organization_color, :string
belongs_to :organization, Organization

timestamps()
end

@doc false
def changeset(certificate, attrs) do
certificate
|> cast(attrs, @required_fields)
|> validate_required(@required_fields)
end
end
5 changes: 4 additions & 1 deletion lib/atomic/organizations/organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ defmodule Atomic.Organizations.Organization do
alias Atomic.Location
alias Atomic.Organizations.{Announcement, Department, Membership, Partner}
alias Atomic.Uploaders
alias Atomic.Certificate

@required_fields ~w(name long_name description)a
@optional_fields ~w()a
@optional_fields ~w(certificate_template_id)a

@derive {
Flop.Schema,
Expand Down Expand Up @@ -45,6 +46,8 @@ defmodule Atomic.Organizations.Organization do

many_to_many :users, User, join_through: Membership

belongs_to :certificate_template, Certificate

timestamps()
end

Expand Down
43 changes: 38 additions & 5 deletions lib/atomic/quantum/certificate_delivery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ defmodule Atomic.Quantum.CertificateDelivery do

# It uses `wkhtmltopdf` to build it from an HTML template, which
# is rendered beforehand.
defp generate_certificate(
%Enrollment{} = enrollment,
%Activity{} = activity,
%Organization{} = organization
) do
def generate_certificate(
%Enrollment{} = enrollment,
%Activity{} = activity,
%Organization{} = organization
) do
# Create the string corresponding to the HTML to convert
# to a PDF
Phoenix.View.render_to_string(AtomicWeb.PDFView, "activity_certificate.html",
Expand All @@ -96,6 +96,39 @@ defmodule Atomic.Quantum.CertificateDelivery do
)
end

def generate_certificate(
%Enrollment{} = enrollment,
%Activity{} = activity,
%Organization{} = organization,
certificate_options \\ %{}
) do
# Create the string corresponding to the HTML to convert
# to a PDF
Phoenix.View.render_to_string(AtomicWeb.PDFView, "activity_certificate.html",
enrollment: enrollment,
activity: activity,
organization: organization,
certificate_options: certificate_options
)
|> PdfGenerator.generate(
delete_temporary: true,
page_size: "A4",
filename: "certificate_#{enrollment.id}",
shell_params: [
"--margin-top",
"0",
"--margin-left",
"0",
"--margin-right",
"0",
"--margin-bottom",
"0",
"-O",
"landscape"
]
)
end

# Builds the query to determine the activities to consider for certificate
# delivery.

Expand Down
141 changes: 141 additions & 0 deletions lib/atomic_web/live/organization_live/certificate_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
defmodule AtomicWeb.OrganizationLive.CertificateLive.Index do
use AtomicWeb, :live_view

alias Atomic.Activities
alias Atomic.Organizations
alias Atomic.Certificate
import AtomicWeb.Components.Forms

@impl true
def mount(_params, _session, socket) do
certificate_options = %{
background: true,
title: 62,
content: "Para os devidos efeitos, certifica-se que participou na atividade",
background_color: "#ffffff",
title_color: "#fb923c",
content_color: "#000000",
organization_color: "#000000"
}

{:ok, assign(socket, certificate_options: certificate_options)}
end

@impl true
def handle_params(%{"organization_id" => organization_id} = params, _url, socket) do
activities = list_activities(organization_id)
organization = Organizations.get_organization!(organization_id)
IO.inspect(organization_id, label: "Organization_id")

default_options = %{
background: true,
title: 62,
content: "Para os devidos efeitos, certifica-se que participou na atividade",
background_color: "#ffffff",
title_color: "#fb923c",
content_color: "#000000",
organization_color: "#000000"
}

certificate_options = default_options

changeset = Certificate.changeset(%Certificate{}, %{
organization_id: organization_id,
background: default_options.background,
title: default_options.title,
content: default_options.content,
background_color: default_options.background_color,
title_color: default_options.title_color,
content_color: default_options.content_color,
organization_color: default_options.organization_color
})

certificate = %Certificate{}

{:noreply,
socket
|> assign(:page_title, gettext("Certificate"))
|> assign(:current_page, :certificate)
|> assign(:changeset, changeset)
|> assign(:certificate, certificate)
|> assign(:activities, activities)
|> assign(:organization, organization)
|> assign(:certificate_options, certificate_options)
|> assign(:params, params)}
end

@impl true
def handle_event("validate", %{"certificate" => certificate_params}, socket) do
certificate_options = extract_certificate_options(certificate_params)

changeset =
(socket.assigns.certificate || %Certificate{})
|> Certificate.changeset(Map.put(certificate_params, "organization_id", socket.assigns.organization.id))
|> Map.put(:action, :validate)

{:noreply,
socket
|> assign(:changeset, changeset)
|> assign(:certificate_options, certificate_options)}
end

@impl true
def handle_event("save", _params, socket) do
organization_id = socket.assigns.organization.id
certificate_params = %{
"organization_id" => organization_id,
"background" => socket.assigns.certificate_options.background,
"title" => socket.assigns.certificate_options.title,
"content" => socket.assigns.certificate_options.content,
"background_color" => socket.assigns.certificate_options.background_color,
"title_color" => socket.assigns.certificate_options.title_color,
"content_color" => socket.assigns.certificate_options.content_color,
"organization_color" => socket.assigns.certificate_options.organization_color
}

case %Certificate{} |> Certificate.changeset(certificate_params) |> Atomic.Repo.insert() do
{:ok, certificate} ->
case Organizations.update_organization(socket.assigns.organization, %{certificate_template_id: socket.assigns.certificate.id}) |> IO.inspect() do
{:ok, _organization} ->
{:noreply, socket |> put_flash(:info, "Certificate saved and organization updated successfully!")}

{:error, %Ecto.Changeset{} = changeset} ->
IO.inspect(changeset.errors, label: "Organization update errors")
{:noreply, socket |> put_flash(:error, "Certificate saved, but failed to update organization")}
end

{:error, %Ecto.Changeset{} = changeset} ->
IO.inspect(changeset.errors, label: "Certificate errors")
{:noreply, socket |> assign(:changeset, changeset) |> put_flash(:error, "Error saving certificate")}
end
end


defp list_activities(organization_id) do
case Activities.list_activities_by_organization_id(organization_id) do
{:ok, {activities, _meta}} -> activities
{:error, _flop} -> []
end
end

defp extract_certificate_options(params) do
%{
background: Map.get(params, "background") == "true",
title: parse_integer(Map.get(params, "title"), 62),
content: Map.get(params, "content") || "Para os devidos efeitos, certifica-se que participou na atividade",
background_color: Map.get(params, "background_color") || "#ffffff",
title_color: Map.get(params, "title_color") || "#fb923c",
content_color: Map.get(params, "content_color") || "#000000",
organization_color: Map.get(params, "organization_color") || "#000000"
}
end

defp parse_integer(value, default) when is_binary(value) do
case Integer.parse(value) do
{int, _} -> int
:error -> default
end
end

defp parse_integer(_, default), do: default
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<.page title="Certificate">
<:actions>
<.button size={:md} color={:white} icon="hero-cube" type="button" phx-click="save">
{gettext("Save Certificate")}
</.button>
</:actions>

<.form :let={f} for={@changeset} id="certificate-form" phx-change="validate" class="flex flex-row items-start gap-8">
<div class="w-64 flex-shrink-0">
<h3 class="mb-4 font-bold">Display Elements</h3>
<.field field={f[:background]} type="switch" label="Show Background" required class="mb-4 w-full" value={@certificate_options.background} />
<.field field={f[:title]} type="number" label="Title Size" required class="mb-4 w-full" value={@certificate_options.title} />
<.field field={f[:content]} type="textarea" label="Content text" required class="mb-4 w-full" value={@certificate_options.content} />

<h3 class="mt-8 mb-4 font-bold">Color Options</h3>
<.field field={f[:background_color]} type="color" label="Background Color" required class="mb-4 w-full" value={@certificate_options.background_color} />
<.field field={f[:title_color]} type="color" label="Title Color" required class="mb-4 w-full" value={@certificate_options.title_color} />
<.field field={f[:content_color]} type="color" label="Content Color" required class="mb-4 w-full" value={@certificate_options.content_color} />
<.field field={f[:organization_color]} type="color" label="Organization Color" required class="mb-4 w-full" value={@certificate_options.organization_color} />

</div>

<div class="flex-grow">
<div class="origin-top-left scale-50 md:scale-75">
{Phoenix.View.render(AtomicWeb.CertificateView, "main.html", Map.put(assigns, :certificate_options, @certificate_options) |> Map.put(:organization, @organization))}
</div>
</div>
</.form>
</.page>
7 changes: 6 additions & 1 deletion lib/atomic_web/live/organization_live/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
phx-click="unfollow"
@click.away="open = false"
@click="open = false"
class="absolute left-0 z-10 mt-2 -mr-1 w-72 origin-top-left divide-y divide-zinc-200 overflow-hidden rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:left-0 sm:left-auto"
class="absolute left-0 z-10 mt-2 -mr-1 w-72 origin-top-left divide-y divide-zinc-200 overflow-hidden rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:left-auto"
tabindex="-1"
role="listbox"
aria-labelledby="listbox-label"
Expand Down Expand Up @@ -81,6 +81,11 @@
<.icon name="hero-trash-solid" class="mr-3 h-5 w-5 text-zinc-400" /> Delete
</div>
<% end %>
<.link patch={~p"/organizations/#{@organization}/certificate"} class="button">
<div type="button" class="inline-flex w-fit justify-center rounded-md border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-700 shadow-sm hover:bg-zinc-50" id="sort-menu-button" aria-expanded="false" aria-haspopup="true">
<.icon name="hero-pencil-square-solid" class="mr-3 h-5 w-5 text-zinc-400" /> Edit Certificate
</div>
</.link>
<% end %>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions lib/atomic_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ defmodule AtomicWeb.Router do

scope "/organizations/:organization_id" do
live "/edit", OrganizationLive.Edit, :edit
live "/certificate", OrganizationLive.CertificateLive.Index, :index

scope "/activities" do
pipe_through :confirm_activity_association
Expand Down
Loading
Loading