diff --git a/assets/css/components/button.css b/assets/css/components/button.css
index 52123f2ba..afe6bc3ab 100644
--- a/assets/css/components/button.css
+++ b/assets/css/components/button.css
@@ -1,7 +1,7 @@
/* Buttons */
.atomic-button {
- @apply inline-flex items-center justify-center font-medium transition duration-150 ease-in-out border rounded-md focus:outline-none;
+ @apply inline-flex items-center justify-center font-medium transition duration-150 ease-in-out border rounded-md select-none focus:outline-none;
}
/* Buttons - sizes */
diff --git a/lib/atomic/accounts/user.ex b/lib/atomic/accounts/user.ex
index ca67ec77a..248dd3775 100644
--- a/lib/atomic/accounts/user.ex
+++ b/lib/atomic/accounts/user.ex
@@ -6,7 +6,7 @@ defmodule Atomic.Accounts.User do
alias Atomic.Accounts.Course
alias Atomic.Activities.ActivityEnrollment
- alias Atomic.Organizations.{Membership, Organization}
+ alias Atomic.Organizations.{Collaborator, Membership, Organization}
alias Atomic.Uploaders.ProfilePicture
@required_fields ~w(email password)a
@@ -23,14 +23,17 @@ defmodule Atomic.Accounts.User do
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
-
field :phone_number, :string
field :profile_picture, ProfilePicture.Type
field :role, Ecto.Enum, values: @roles, default: :student
+
belongs_to :course, Course
belongs_to :current_organization, Organization
has_many :activity_enrollments, ActivityEnrollment
+ has_many :memberships, Membership
+ has_many :collaborators, Collaborator
+
many_to_many :organizations, Organization, join_through: Membership
timestamps()
diff --git a/lib/atomic/departments.ex b/lib/atomic/departments.ex
index facb25a1a..1abf710d0 100644
--- a/lib/atomic/departments.ex
+++ b/lib/atomic/departments.ex
@@ -4,7 +4,10 @@ defmodule Atomic.Departments do
"""
use Atomic.Context
+ alias Atomic.Accounts.User
alias Atomic.Organizations.{Collaborator, Department}
+ alias AtomicWeb.DepartmentEmails
+ alias AtomicWeb.Router.Helpers
@doc """
Returns the list of departments.
@@ -87,10 +90,11 @@ defmodule Atomic.Departments do
{:error, %Ecto.Changeset{}}
"""
- def create_department(attrs \\ %{}) do
+ def create_department(attrs \\ %{}, after_save \\ &{:ok, &1}) do
%Department{}
|> Department.changeset(attrs)
|> Repo.insert()
+ |> after_save(after_save)
end
@doc """
@@ -105,10 +109,11 @@ defmodule Atomic.Departments do
{:error, %Ecto.Changeset{}}
"""
- def update_department(%Department{} = department, attrs) do
+ def update_department(%Department{} = department, attrs, after_save \\ &{:ok, &1}) do
department
|> Department.changeset(attrs)
|> Repo.update()
+ |> after_save(after_save)
end
@doc """
@@ -127,6 +132,42 @@ defmodule Atomic.Departments do
Repo.delete(department)
end
+ @doc """
+ Archives a department.
+
+ ## Examples
+
+ iex> archive_department(department)
+ {:ok, %Department{}}
+
+ iex> archive_department(department)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def archive_department(%Department{} = department) do
+ department
+ |> Department.changeset(%{archived: true})
+ |> Repo.update()
+ end
+
+ @doc """
+ Unarchives a department.
+
+ ## Examples
+
+ iex> unarchive_department(department)
+ {:ok, %Department{}}
+
+ iex> unarchive_department(department)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def unarchive_department(%Department{} = department) do
+ department
+ |> Department.changeset(%{archived: false})
+ |> Repo.update()
+ end
+
@doc """
Returns an `%Ecto.Changeset{}` for tracking department changes.
@@ -180,7 +221,11 @@ defmodule Atomic.Departments do
** (Ecto.NoResultsError)
"""
- def get_collaborator!(id), do: Repo.get!(Collaborator, id)
+ def get_collaborator!(id, opts \\ []) do
+ Collaborator
+ |> apply_filters(opts)
+ |> Repo.get!(id)
+ end
@doc """
Gets a single collaborator.
@@ -251,6 +296,24 @@ defmodule Atomic.Departments do
|> Repo.update()
end
+ @doc """
+ Accepts a collaborator.
+
+ ## Examples
+
+ iex> accept_collaborator(collaborator)
+ {:ok, %Collaborator{}}
+
+ iex> accept_collaborator(collaborator)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def accept_collaborator(%Collaborator{} = collaborator) do
+ collaborator
+ |> Collaborator.changeset(%{accepted: true, accepted_at: NaiveDateTime.utc_now()})
+ |> Repo.update()
+ end
+
@doc """
Deletes a collaborator.
@@ -280,6 +343,21 @@ defmodule Atomic.Departments do
Collaborator.changeset(collaborator, attrs)
end
+ @doc """
+ Returns a paginated list of collaborators.
+
+ ## Examples
+
+ iex> list_display_collaborators()
+ [%Collaborator{}, ...]
+
+ """
+ def list_display_collaborators(%{} = flop, opts \\ []) do
+ Collaborator
+ |> apply_filters(opts)
+ |> Flop.validate_and_run(flop, for: Collaborator)
+ end
+
@doc """
Returns the list of collaborators belonging to a department.
@@ -295,4 +373,114 @@ defmodule Atomic.Departments do
|> where([c], c.department_id == ^id)
|> Repo.all()
end
+
+ @doc """
+ Updates a department banner.
+
+ ## Examples
+
+ iex> update_department_banner(department, %{field: new_value})
+ {:ok, %Department{}}
+
+ iex> update_department_banner(department, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_department_banner(%Department{} = department, attrs) do
+ department
+ |> Department.banner_changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Get all admins of an organization that are collaborators of a department.
+
+ ## Examples
+
+ iex> get_admin_collaborators(department)
+ [%User{}, ...]
+
+ """
+ def get_admin_collaborators(%Department{} = department) do
+ User
+ |> join(:inner, [u], c in assoc(u, :collaborators))
+ |> where([u, c], c.department_id == ^department.id and c.accepted == true)
+ |> join(:inner, [u, c], m in assoc(u, :memberships))
+ |> where(
+ [u, c, m],
+ m.organization_id == ^department.organization_id and m.role in [:admin, :owner]
+ )
+ |> Repo.all()
+ end
+
+ @doc """
+ Request collaborator access and send email.
+
+ ## Examples
+
+ iex> request_collaborator_access(user, department)
+ {:ok, %Collaborator{}}
+
+ iex> request_collaborator_access(user, department)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def request_collaborator_access(%User{} = user, %Department{} = department) do
+ case create_collaborator(%{department_id: department.id, user_id: user.id}) do
+ {:ok, %Collaborator{} = collaborator} ->
+ DepartmentEmails.send_collaborator_request_email(
+ collaborator |> Repo.preload(:user),
+ department,
+ Helpers.department_show_path(
+ AtomicWeb.Endpoint,
+ :edit_collaborator,
+ department.organization_id,
+ department,
+ collaborator,
+ tab: "collaborators"
+ ),
+ to: get_admin_collaborators(department) |> Enum.map(& &1.email)
+ )
+
+ {:ok, collaborator}
+
+ error ->
+ error
+ end
+ end
+
+ @doc """
+ Accept collaborator request and send email.
+
+ ## Examples
+
+ iex> accept_collaborator_request(collaborator)
+ {:ok, %Collaborator{}}
+
+ iex> accept_collaborator_request(collaborator)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def accept_collaborator_request(%Collaborator{} = collaborator) do
+ collaborator
+ |> Repo.preload(department: [:organization])
+ |> accept_collaborator()
+ |> case do
+ {:ok, collaborator} ->
+ DepartmentEmails.send_collaborator_accepted_email(
+ collaborator,
+ collaborator.department,
+ Helpers.department_show_path(
+ AtomicWeb.Endpoint,
+ :show,
+ collaborator.department.organization,
+ collaborator.department
+ ),
+ to: collaborator.user.email
+ )
+
+ error ->
+ error
+ end
+ end
end
diff --git a/lib/atomic/organizations/collaborator.ex b/lib/atomic/organizations/collaborator.ex
index 5872afe29..704b23ee1 100644
--- a/lib/atomic/organizations/collaborator.ex
+++ b/lib/atomic/organizations/collaborator.ex
@@ -7,14 +7,29 @@ defmodule Atomic.Organizations.Collaborator do
alias Atomic.Accounts.User
alias Atomic.Organizations.Department
- @required_fields ~w(user_id department_id)a
- @optional_fields ~w(accepted)a
+ @required_fields ~w(user_id department_id accepted)a
+ @optional_fields ~w(accepted_at)a
+
+ @derive {
+ Flop.Schema,
+ default_limit: 7,
+ filterable: [:accepted],
+ sortable: [:collaborator_name, :inserted_at, :updated_at],
+ default_order: %{
+ order_by: [:inserted_at],
+ order_directions: [:desc]
+ },
+ join_fields: [
+ collaborator_name: [binding: :user, field: :name, path: [:user, :name]]
+ ]
+ }
schema "collaborators" do
belongs_to :user, User
belongs_to :department, Department
field :accepted, :boolean, default: false
+ field :accepted_at, :naive_datetime
timestamps()
end
diff --git a/lib/atomic/organizations/department.ex b/lib/atomic/organizations/department.ex
index 8b89a4c5e..831e7d012 100644
--- a/lib/atomic/organizations/department.ex
+++ b/lib/atomic/organizations/department.ex
@@ -6,11 +6,14 @@ defmodule Atomic.Organizations.Department do
alias Atomic.Organizations.Organization
@required_fields ~w(name organization_id)a
- @optional_fields ~w(description)a
+ @optional_fields ~w(description collaborator_applications archived)a
schema "departments" do
field :name, :string
field :description, :string
+ field :banner, Atomic.Uploaders.Banner.Type
+ field :collaborator_applications, :boolean, default: false
+ field :archived, :boolean, default: false
belongs_to :organization, Organization, on_replace: :delete_if_exists
@@ -22,4 +25,9 @@ defmodule Atomic.Organizations.Department do
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end
+
+ def banner_changeset(department, attrs) do
+ department
+ |> cast_attachments(attrs, [:banner])
+ end
end
diff --git a/lib/atomic/uploaders/banner.ex b/lib/atomic/uploaders/banner.ex
new file mode 100644
index 000000000..b642d1eb1
--- /dev/null
+++ b/lib/atomic/uploaders/banner.ex
@@ -0,0 +1,12 @@
+defmodule Atomic.Uploaders.Banner do
+ @moduledoc """
+ Uploader for department banners.
+ """
+ use Atomic.Uploader
+
+ alias Atomic.Organizations.Department
+
+ def storage_dir(_version, {_file, %Department{} = scope}) do
+ "uploads/atomic/departments/#{scope.id}/banner"
+ end
+end
diff --git a/lib/atomic_web.ex b/lib/atomic_web.ex
index f8fae7d86..53f6c7367 100644
--- a/lib/atomic_web.ex
+++ b/lib/atomic_web.ex
@@ -92,7 +92,6 @@ defmodule AtomicWeb do
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
import Phoenix.LiveView.Helpers
import Phoenix.Component
- import AtomicWeb.LiveHelpers
# Import commonly used components
unquote(components())
diff --git a/lib/atomic_web/components/avatar.ex b/lib/atomic_web/components/avatar.ex
index 7331b1821..9dacd363a 100644
--- a/lib/atomic_web/components/avatar.ex
+++ b/lib/atomic_web/components/avatar.ex
@@ -6,6 +6,10 @@ defmodule AtomicWeb.Components.Avatar do
attr :name, :string, required: true, doc: "The name of the entity associated with the avatar."
+ attr :auto_generate_initials, :boolean,
+ default: true,
+ doc: "Whether to automatically generate the initials from the name."
+
attr :type, :atom,
values: [:user, :organization, :company],
default: :user,
@@ -42,7 +46,11 @@ defmodule AtomicWeb.Components.Avatar do
<%= if @src do %>
<% else %>
- <%= extract_initials(@name) %>
+ <%= if @auto_generate_initials do %>
+ <%= extract_initials(@name) %>
+ <% else %>
+ <%= @name %>
+ <% end %>
<% end %>
"""
diff --git a/lib/atomic_web/components/button.ex b/lib/atomic_web/components/button.ex
index f6b909cbe..39350908a 100644
--- a/lib/atomic_web/components/button.ex
+++ b/lib/atomic_web/components/button.ex
@@ -58,10 +58,10 @@ defmodule AtomicWeb.Components.Button do
attr :rest, :global,
include:
- ~w(csrf_token disabled download form href hreflang method name navigate patch referrerpolicy rel replace target type value),
+ ~w(csrf_token disabled download form href hreflang method name navigate patch referrerpolicy rel replace target type value autofocus tabindex),
doc: "Arbitrary HTML or phx attributes."
- slot :inner_block, required: true, doc: "Slot for the body content of the page."
+ slot :inner_block, required: true, doc: "Slot for the content of the button."
def button(assigns) do
assigns
diff --git a/lib/atomic_web/components/forms.ex b/lib/atomic_web/components/forms.ex
index ea357107d..2b224f0f9 100644
--- a/lib/atomic_web/components/forms.ex
+++ b/lib/atomic_web/components/forms.ex
@@ -153,9 +153,7 @@ defmodule AtomicWeb.Components.Forms do
<%= @label %>
-
+
<.field_error :for={msg <- @errors}><%= msg %>
<.field_help_text help_text={@help_text} />
diff --git a/lib/atomic_web/components/gradient.ex b/lib/atomic_web/components/gradient.ex
new file mode 100644
index 000000000..bb0cb1e42
--- /dev/null
+++ b/lib/atomic_web/components/gradient.ex
@@ -0,0 +1,54 @@
+defmodule AtomicWeb.Components.Gradient do
+ @moduledoc """
+ Generates a random gradient background or a predictable gradient background based on a seed that can be of any data type.
+ """
+ use Phoenix.Component
+
+ # List of gradients
+ @colors [
+ {"#000046", "#1CB5E0"},
+ {"#007991", "#78ffd6"},
+ {"#30E8BF", "#FF8235"},
+ {"#C33764", "#1D2671"},
+ {"#34e89e", "#0f3443"},
+ {"#44A08D", "#093637"},
+ {"#DCE35B", "#45B649"},
+ {"#c0c0aa", "#1cefff"},
+ {"#ee0979", "#ff6a00"}
+ ]
+
+ attr :class, :string, default: "", doc: "Additional classes to apply to the component."
+ attr :seed, :any, required: false, doc: "For predictable gradients."
+
+ def gradient(assigns) do
+ {gradient_color_a, gradient_color_b} =
+ if Map.has_key?(assigns, :seed) do
+ generate_color(assigns.seed)
+ else
+ generate_color()
+ end
+
+ assigns
+ |> assign(:gradient_color_a, gradient_color_a)
+ |> assign(:gradient_color_b, gradient_color_b)
+ |> render_gradient()
+ end
+
+ defp render_gradient(assigns) do
+ ~H"""
+
+ """
+ end
+
+ defp generate_color(seed) when is_binary(seed) do
+ # Convert the argument into an integer
+ index = :erlang.phash2(seed, length(@colors))
+
+ # Return the chosen color
+ Enum.at(@colors, index)
+ end
+
+ defp generate_color do
+ Enum.random(@colors)
+ end
+end
diff --git a/lib/atomic_web/components/image_uploader.ex b/lib/atomic_web/components/image_uploader.ex
index e843dd0a2..740220120 100644
--- a/lib/atomic_web/components/image_uploader.ex
+++ b/lib/atomic_web/components/image_uploader.ex
@@ -13,15 +13,12 @@ defmodule AtomicWeb.Components.ImageUploader do
def render(assigns) do
~H"""
@@ -65,7 +65,6 @@ defmodule AtomicWeb.Components.Modal do
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
- |> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
diff --git a/lib/atomic_web/components/table.ex b/lib/atomic_web/components/table.ex
index 9c21ca8c8..d928d0210 100644
--- a/lib/atomic_web/components/table.ex
+++ b/lib/atomic_web/components/table.ex
@@ -29,7 +29,7 @@ defmodule AtomicWeb.Components.Table do
<%= for item <- @items do %>
<%= for col <- @col do %>
-
+
<%= render_slot(col, item) %>
<% end %>
@@ -48,7 +48,7 @@ defmodule AtomicWeb.Components.Table do
assign(assigns, :direction, order_direction(assigns.meta.flop.order_directions, index))
~H"""
-
+
<%= if is_sortable?(@field, @meta.schema) && is_filterable?(@field, @meta.schema) && should_filter(@field, @filter) do %>
<.link patch={build_sorting_query(@field, @meta)} class="mr-2 w-full">
diff --git a/lib/atomic_web/emails/department_emails.ex b/lib/atomic_web/emails/department_emails.ex
new file mode 100644
index 000000000..7bbf4eb10
--- /dev/null
+++ b/lib/atomic_web/emails/department_emails.ex
@@ -0,0 +1,63 @@
+defmodule AtomicWeb.DepartmentEmails do
+ @moduledoc """
+ A module to build department related emails.
+ """
+ use Phoenix.Swoosh, view: AtomicWeb.EmailView
+
+ alias Atomic.Mailer
+
+ @doc """
+ Sends an email to the collaborator when their application to join the department is accepted.
+
+ ## Examples
+
+ iex> send_collaborator_request_email(collaborator, department, department_show_url, to: email)
+ {:ok, email}
+
+ iex> send_collaborator_request_email(collaborator, department, department_show_url, to: email)
+ {:error, reason}
+ """
+ def send_collaborator_accepted_email(collaborator, department, department_show_url, to: email) do
+ base_email(to: email)
+ |> subject("[Atomic] You are now a collaborator of #{department.name}")
+ |> assign(:collaborator, collaborator)
+ |> assign(:department, department)
+ |> assign(:department_url, department_show_url)
+ |> render_body("collaborator_accepted.html")
+ |> Mailer.deliver()
+ end
+
+ @doc """
+ Sends an email to the department admins when a new collaborator requests to join the department.
+
+ ## Examples
+
+ iex> send_collaborator_request_email(collaborator, department, collaborator_review_url,
+ ...> to: emails
+ ...> )
+ {:ok, email}
+
+ iex> send_collaborator_request_email(collaborator, department, collaborator_review_url,
+ ...> to: emails
+ ...> )
+ {:error, reason}
+ """
+ def send_collaborator_request_email(collaborator, department, collaborator_review_url,
+ to: emails
+ ) do
+ base_email(to: emails)
+ |> subject("[Atomic] New collaborator request for #{department.name}")
+ |> assign(:collaborator, collaborator)
+ |> assign(:department, department)
+ |> assign(:collaborator_review_url, collaborator_review_url)
+ |> render_body("collaborator_request.html")
+ |> Mailer.deliver()
+ end
+
+ defp base_email(to: email) do
+ new()
+ |> from({"Atomic", "noreply@atomic.cesium.pt"})
+ |> to(email)
+ |> reply_to("caos@cesium.di.uminho.pt")
+ end
+end
diff --git a/lib/atomic_web/live/collaborator_live/form_component.ex b/lib/atomic_web/live/collaborator_live/form_component.ex
new file mode 100644
index 000000000..09082e9a9
--- /dev/null
+++ b/lib/atomic_web/live/collaborator_live/form_component.ex
@@ -0,0 +1,212 @@
+defmodule AtomicWeb.CollaboratorLive.FormComponent do
+ use AtomicWeb, :live_component
+
+ import AtomicWeb.Components.{Avatar, Badge}
+
+ alias Atomic.Departments
+ alias Phoenix.LiveView.JS
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
Collaborator
+
+ <%= if !@collaborator.accepted do %>
+
+ <%= gettext("%{user_name} has requested to be a collaborator of %{department_name}.", user_name: extract_first_name(@collaborator.user.name), department_name: @department.name) %>
+
+ """
+ end
+
+ @impl true
+ def update(%{collaborator: collaborator} = assigns, socket) do
+ changeset = Departments.change_collaborator(collaborator)
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:action_modal, nil)
+ |> assign(:changeset, changeset)}
+ end
+
+ defp deny_collaborator_request(socket) do
+ case Departments.delete_collaborator(socket.assigns.collaborator) do
+ {:ok, _} ->
+ notify_result(socket, :success, gettext("Collaborator request denied."))
+
+ _ ->
+ notify_result(
+ socket,
+ :error,
+ gettext("Could not deny the collaborator request. Please try again later.")
+ )
+ end
+ end
+
+ defp delete_collaborator(socket) do
+ case Departments.delete_collaborator(socket.assigns.collaborator) do
+ {:ok, _} ->
+ notify_result(socket, :success, gettext("Collaborator removed successfully."))
+
+ _ ->
+ notify_result(
+ socket,
+ :error,
+ gettext("Could not delete the collaborator. Please try again later.")
+ )
+ end
+ end
+
+ defp accept_collaborator_request(socket) do
+ case Departments.accept_collaborator_request(socket.assigns.collaborator) do
+ {:ok, _} ->
+ notify_result(socket, :success, gettext("Collaborator accepted successfully."))
+
+ _ ->
+ notify_result(
+ socket,
+ :error,
+ gettext("Could not accept the collaborator. Please try again later.")
+ )
+ end
+ end
+
+ defp notify_result(socket, status, message) do
+ send(self(), {:change_collaborator, %{status: status, message: message}})
+ {:noreply, assign(socket, :action_modal, nil)}
+ end
+
+ @impl true
+ def handle_event("confirm", _, socket) do
+ case socket.assigns.action_modal do
+ :confirm_request -> accept_collaborator_request(socket)
+ :deny_request -> deny_collaborator_request(socket)
+ :delete_collaborator -> delete_collaborator(socket)
+ end
+ end
+
+ @impl true
+ def handle_event("clear-action", _, socket) do
+ {:noreply,
+ socket
+ |> assign(:action_modal, nil)}
+ end
+
+ @impl true
+ def handle_event("allow", _, socket) do
+ {:noreply,
+ socket
+ |> assign(:action_modal, :confirm_request)}
+ end
+
+ @impl true
+ def handle_event("deny", _, socket) do
+ {:noreply,
+ socket
+ |> assign(:action_modal, :deny_request)}
+ end
+
+ @impl true
+ def handle_event("delete", _, socket) do
+ {:noreply,
+ socket
+ |> assign(:action_modal, :delete_collaborator)}
+ end
+
+ defp display_action_goal_confirm_title(action) do
+ case action do
+ :confirm_request ->
+ gettext("Are you sure you want to accept this request?")
+
+ :deny_request ->
+ gettext("Are you sure you want to deny this request?")
+
+ :delete_collaborator ->
+ gettext("Are you sure you want to remove this person from the department?")
+ end
+ end
+
+ defp display_action_goal_confirm_description(action, department) do
+ case action do
+ :confirm_request ->
+ gettext("If you change your mind you can always remove this person later.")
+
+ :deny_request ->
+ gettext("If you deny this request, this person will not get access to the department.")
+
+ :delete_collaborator ->
+ gettext(
+ "If you remove this person, they will no longer have access to %{department_name}.",
+ department_name: department.name
+ )
+ end
+ end
+end
diff --git a/lib/atomic_web/live/department_live/components/department_card.ex b/lib/atomic_web/live/department_live/components/department_card.ex
new file mode 100644
index 000000000..f67f711f7
--- /dev/null
+++ b/lib/atomic_web/live/department_live/components/department_card.ex
@@ -0,0 +1,43 @@
+defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do
+ @moduledoc false
+ use AtomicWeb, :component
+
+ import AtomicWeb.Components.{Avatar, Badge, Gradient}
+
+ attr :department, :map, required: true, doc: "The department to display."
+ attr :collaborators, :list, required: true, doc: "The list of collaborators in the department."
+
+ def department_card(assigns) do
+ ~H"""
+
+
+ <%= if @department.banner do %>
+
+ <% else %>
+ <.gradient seed={@department.id} class="rounded-t-lg" />
+ <% end %>
+
+ <.field type="text" help_text={gettext("The name of the department")} field={f[:name]} placeholder="Name" required />
+ <.field type="textarea" help_text={gettext("A brief description of the department")} field={f[:description]} placeholder="Description" />
+ <.field type="checkbox" help_text={gettext("Allow any user to apply to be a collaborator in this department")} field={f[:collaborator_applications]} />
+
This email has been automatically generated. Please, do not reply.
+
+
+
diff --git a/lib/atomic_web/views/helpers.ex b/lib/atomic_web/views/helpers.ex
index 318725c37..75bdce693 100644
--- a/lib/atomic_web/views/helpers.ex
+++ b/lib/atomic_web/views/helpers.ex
@@ -225,6 +225,31 @@ defmodule AtomicWeb.Helpers do
end
end
+ @doc """
+ Return the first name of a name.
+
+ ## Examples
+
+ iex> extract_first_name("John Doe")
+ "John"
+
+ iex> extract_first_name("John")
+ "John"
+
+ iex> extract_first_name(nil)
+ ""
+
+ """
+ def extract_first_name(nil), do: ""
+
+ def extract_first_name(name) do
+ name
+ |> String.split(" ")
+ |> Enum.filter(&String.match?(String.slice(&1, 0, 1), ~r/^\p{L}$/u))
+ |> Enum.map(&String.capitalize/1)
+ |> hd()
+ end
+
@doc """
Slices a string if it is longer than the given length
diff --git a/priv/repo/migrations/20221000000000_create_departments.exs b/priv/repo/migrations/20221000000000_create_departments.exs
index 14a22628a..641873234 100644
--- a/priv/repo/migrations/20221000000000_create_departments.exs
+++ b/priv/repo/migrations/20221000000000_create_departments.exs
@@ -6,6 +6,9 @@ defmodule Atomic.Repo.Migrations.CreateDepartments do
add :id, :binary_id, primary_key: true
add :name, :string, null: false
add :description, :text
+ add :banner, :string
+ add :collaborator_applications, :boolean, default: false, null: false
+ add :archived, :boolean, default: false, null: false
add :organization_id, references(:organizations, on_delete: :delete_all, type: :binary_id),
null: false
diff --git a/priv/repo/migrations/20230880102641_create_collaborators.exs b/priv/repo/migrations/20230880102641_create_collaborators.exs
index 2b0410d0b..e6446ccb5 100644
--- a/priv/repo/migrations/20230880102641_create_collaborators.exs
+++ b/priv/repo/migrations/20230880102641_create_collaborators.exs
@@ -5,10 +5,11 @@ defmodule Atomic.Repo.Migrations.CreateCollaborators do
create table(:collaborators, primary_key: false) do
add :id, :binary_id, primary_key: true
add :accepted, :boolean, default: false
+ add :accepted_at, :naive_datetime
add :user_id, references(:users, on_delete: :nothing, type: :binary_id), null: false
- add :department_id, references(:departments, on_delete: :nothing, type: :binary_id),
+ add :department_id, references(:departments, on_delete: :delete_all, type: :binary_id),
null: false
timestamps()
diff --git a/priv/repo/seeds/departments.exs b/priv/repo/seeds/departments.exs
index faa11ee42..cbab532b6 100644
--- a/priv/repo/seeds/departments.exs
+++ b/priv/repo/seeds/departments.exs
@@ -6,23 +6,28 @@ defmodule Atomic.Repo.Seeds.Departments do
alias Atomic.Organizations.{Department, Organization}
alias Atomic.Repo
- @department_names [
- "CAOS",
- "Marketing e Conteúdo",
- "Relações Externas e Parcerias",
- "Pedagógico",
- "Recreativo",
- "Financeiro",
- "Administrativo",
- "Comunicação",
- "Tecnologia",
- "Design"
+ @departments [
+ {"CAOS",
+ "O CAOS (Centro de Apoio ao Open-Source) é o departamento responsável por todo o desenvolvimento de software associado ao CeSIUM, quer sejam as plataformas que servem diretamente o núcleo e os seus eventos, ou as plataformas úteis para os alunos. Durante o processo, procura expandir o conhecimento dos seus colaboradores, para além daquilo que aprendem no curso. 💻"},
+ {"Marketing e Conteúdo",
+ "O Departamento de Marketing e Conteúdo é responsável por promover todas as atividades efetuadas pelo núcleo, manter toda a identidade visual do CeSIUM e efetuar a comunicação através das nossas redes sociais. 💡"},
+ {"Recreativo",
+ "O Departamento Recreativo é o responsável por organizar atividades de cariz cultural e/ou lúdico, com o intuito de promover a relação entre os vários estudantes, professores e funcionários. 🤪"},
+ {"Pedagógico",
+ "O Departamento Pedagógico é o responsável por promover a relação entre a Direção de Curso e os alunos do LEI/MEI/MIEI e, ainda, por organizar atividades que complementam a formação académica dos mesmos. ✏️"},
+ {"Relações Externas e Merch",
+ "O Departamento de Relações Externas e Merch é responsável por todo o merchandising do núcleo e por manter e fechar parceriais com diversas entidades e negócios, das quais os sócios do CeSIUM, assim como o núcleo, possam tirar proveito. 🤝"},
+ {"Financeiro",
+ "O Departamento Financeiro é o responsável por toda a gestão financeira do núcleo, desde a organização de contas, ao controlo de despesas e receitas. 💰"},
+ {"Administrativo",
+ "O Departamento Administrativo é o responsável por toda a gestão administrativa do núcleo, desde a organização de documentos, ao controlo de processos e procedimentos. 📝"}
]
def run do
case Repo.all(Department) do
[] ->
seed_departments()
+ seed_collaborators()
_ ->
Mix.shell().error("Found departments, aborting seeding departments.")
@@ -33,12 +38,40 @@ defmodule Atomic.Repo.Seeds.Departments do
organizations = Repo.all(Organization)
for organization <- organizations do
- for i <- 0..Enum.random(4..(length(@department_names) - 1)) do
- %{
- name: Enum.at(@department_names, i),
- organization_id: organization.id
- }
- |> Departments.create_department()
+ for i <- 0..Enum.random(4..(length(@departments) - 1)) do
+ case @departments |> Enum.at(i) do
+ {name, description} ->
+ %{
+ name: name,
+ description: description,
+ organization_id: organization.id,
+ collaborator_applications: Enum.random([true, false])
+ }
+ |> Departments.create_department()
+ end
+ end
+ end
+ end
+
+ def seed_collaborators do
+ for department <- Repo.all(Department) do
+ for user <- Repo.all(Atomic.Accounts.User) do
+ is_accepted = Enum.random([true, false])
+
+ if Enum.random(0..6) == 1 do
+ %{
+ department_id: department.id,
+ user_id: user.id,
+ accepted: is_accepted,
+ accepted_at:
+ if is_accepted do
+ NaiveDateTime.utc_now()
+ else
+ nil
+ end
+ }
+ |> Departments.create_collaborator()
+ end
end
end
end
diff --git a/storybook/components/gradient.story.exs b/storybook/components/gradient.story.exs
new file mode 100644
index 000000000..df2e86958
--- /dev/null
+++ b/storybook/components/gradient.story.exs
@@ -0,0 +1,29 @@
+defmodule AtomicWeb.Storybook.Components.Gradient do
+ use PhoenixStorybook.Story, :component
+
+ alias AtomicWeb.Components.Gradient
+
+ def function, do: &Gradient.gradient/1
+
+ def template do
+ """
+