diff --git a/assets/js/app.js b/assets/js/app.js index 6d274010..5f83b83e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -55,6 +55,7 @@ import "../node_modules/bootstrap-icons/icons/eraser.svg"; import "../node_modules/bootstrap-icons/icons/save.svg"; import "../node_modules/bootstrap-icons/icons/asterisk.svg"; import "../node_modules/bootstrap-icons/icons/card-list.svg"; +import "../node_modules/bootstrap-icons/icons/file-earmark-spreadsheet.svg"; // webpack automatically bundles all modules in your // entry points. Those entry points can be configured diff --git a/lib/shift73k/shifts.ex b/lib/shift73k/shifts.ex index 8d9c363b..6e92438f 100644 --- a/lib/shift73k/shifts.ex +++ b/lib/shift73k/shifts.ex @@ -21,7 +21,7 @@ defmodule Shift73k.Shifts do Repo.all(Shift) end - defp query_shifts_by_user(user_id) do + def query_shifts_by_user(user_id) do from(s in Shift) |> where([s], s.user_id == ^user_id) end @@ -53,6 +53,25 @@ defmodule Shift73k.Shifts do |> Repo.all() end + def query_user_shift_dates(user_id) do + query_shifts_by_user(user_id) + |> select([s], s.date) + end + + def get_min_user_shift_date(user_id) do + query_user_shift_dates(user_id) + |> order_by([s], asc: s.date) + |> limit([s], 1) + |> Repo.one() + end + + def get_max_user_shift_date(user_id) do + query_user_shift_dates(user_id) + |> order_by([s], desc: s.date) + |> limit([s], 1) + |> Repo.one() + end + @doc """ Gets a single shift. @@ -146,4 +165,20 @@ defmodule Shift73k.Shifts do def change_shift(%Shift{} = shift, attrs \\ %{}) do Shift.changeset(shift, attrs) end + + ### + # UTILS + def shift_length(%{time_end: time_end, time_start: time_start}) do + time_end + |> Time.diff(time_start) + |> Integer.floor_div(60) + |> shift_length() + end + + def shift_length(len_min) when is_integer(len_min) and len_min >= 0, do: len_min + def shift_length(len_min) when is_integer(len_min) and len_min < 0, do: 1440 + len_min + + def shift_length(time_end, time_start) do + shift_length(%{time_end: time_end, time_start: time_start}) + end end diff --git a/lib/shift73k/shifts/templates/shift_template.ex b/lib/shift73k/shifts/templates/shift_template.ex index b44c64dc..7b1988bc 100644 --- a/lib/shift73k/shifts/templates/shift_template.ex +++ b/lib/shift73k/shifts/templates/shift_template.ex @@ -2,6 +2,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do use Ecto.Schema import Ecto.Changeset + alias Shift73k.Shifts alias Shift73k.Shifts.Templates.ShiftTemplate @app_vars Application.get_env(:shift73k, :app_global_vars, time_zone: "America/New_York") @@ -45,7 +46,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do |> validate_length(:subject, count: :codepoints, max: 280) |> validate_length(:location, count: :codepoints, max: 280) |> validate_change(:time_end, fn :time_end, time_end -> - shift_length = shift_length(time_end, time_start_from_attrs(attrs)) + shift_length = Shifts.shift_length(time_end, time_start_from_attrs(attrs)) cond do shift_length == 0 -> @@ -67,20 +68,6 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do defp time_start_from_attrs(%{time_start: time_start}), do: time_start defp time_start_from_attrs(_), do: nil - def shift_length(%ShiftTemplate{time_end: time_end, time_start: time_start}) do - time_end - |> Time.diff(time_start) - |> Integer.floor_div(60) - |> shift_length() - end - - def shift_length(len_min) when is_integer(len_min) and len_min >= 0, do: len_min - def shift_length(len_min) when is_integer(len_min) and len_min < 0, do: 1440 + len_min - - def shift_length(time_end, time_start) do - shift_length(%ShiftTemplate{time_end: time_end, time_start: time_start}) - end - # Get shift attrs from shift template def attrs(%ShiftTemplate{} = shift_template) do shift_template diff --git a/lib/shift73k_web/controllers/user_shifts_csv_controller.ex b/lib/shift73k_web/controllers/user_shifts_csv_controller.ex new file mode 100644 index 00000000..60783bfe --- /dev/null +++ b/lib/shift73k_web/controllers/user_shifts_csv_controller.ex @@ -0,0 +1,94 @@ +defmodule Shift73kWeb.UserShiftsCsvController do + use Shift73kWeb, :controller + + alias Shift73k.Shifts + alias Shift73k.Shifts.Shift + + def new(conn, _params) do + render(conn, "new.html", error_message: nil) + end + + def export(conn, %{"csv_export" => request_params}) do + IO.inspect(request_params, label: "csv request params :") + + case Map.get(request_params, "user_id") == conn.assigns.current_user.id do + true -> + export_csv(conn, request_params) + + false -> + conn + |> put_flash(:danger, "Unauthorized CSV export request") + |> redirect(to: "/") + |> halt() + end + end + + defp export_csv(conn, %{"date_min" => date_min, "date_max" => date_max}) do + date_range = Date.range(Date.from_iso8601!(date_min), Date.from_iso8601!(date_max)) + csv_content = build_csv_content(conn.assigns.current_user.id, date_range) + filename_dt = DateTime.utc_now() |> Calendar.strftime("%Y%m%dT%H%M%S") + filename = "#{conn.assigns.current_user.id}_#{filename_dt}.csv" + + conn + |> put_resp_content_type("text/csv") + |> put_resp_header("content-disposition", "attachment; filename=\"#{filename}\"") + |> send_resp(200, csv_content) + end + + def build_csv_content(user_id, date_range) do + [csv_headers() | csv_data(user_id, date_range)] + |> NimbleCSV.RFC4180.dump_to_iodata() + |> to_string() + |> IO.inspect() + end + + def csv_data(user_id, date_range) do + user_id + |> Shifts.list_shifts_by_user_in_date_range(date_range) + |> Enum.map(&csv_shift_row/1) + end + + def csv_headers do + [ + "Subject", + "Start Date", + "Start Time", + "End Date", + "End Time", + "All Day Event", + "Description", + "Location", + "Private" + ] + end + + defp csv_shift_row(%Shift{} = s) do + dt_start = DateTime.new!(s.date, s.time_start, s.time_zone) + shift_length_s = Shifts.shift_length(s) * 60 + dt_end = DateTime.add(dt_start, shift_length_s) + + [ + s.subject, + csv_date_string(dt_start), + csv_time_string(dt_start), + csv_date_string(dt_end), + csv_time_string(dt_end), + false, + s.description, + s.location, + false + ] + end + + defp csv_date_string(%DateTime{} = dt) do + dt + |> DateTime.to_date() + |> Calendar.strftime("%m/%d/%Y") + end + + defp csv_time_string(%DateTime{} = dt) do + dt + |> DateTime.to_time() + |> Calendar.strftime("%-I:%M %p") + end +end diff --git a/lib/shift73k_web/live/live_helpers.ex b/lib/shift73k_web/live/live_helpers.ex index 6ebe5bf4..ff98ac8d 100644 --- a/lib/shift73k_web/live/live_helpers.ex +++ b/lib/shift73k_web/live/live_helpers.ex @@ -5,7 +5,7 @@ defmodule Shift73kWeb.LiveHelpers do alias Shift73k.Accounts alias Shift73k.Accounts.User alias Shift73kWeb.UserAuth - alias Shift73k.Shifts.Templates.ShiftTemplate + alias Shift73k.Shifts @doc """ Performs the {:noreply, socket} for a given socket. @@ -74,9 +74,9 @@ defmodule Shift73kWeb.LiveHelpers do |> String.trim_trailing("m") end - def format_shift_length(%ShiftTemplate{} = shift_template) do - shift_template - |> ShiftTemplate.shift_length() + def format_shift_length(%{} = shift_or_template) do + shift_or_template + |> Shifts.shift_length() |> format_shift_length() end diff --git a/lib/shift73k_web/live/shift_live/index.html.leex b/lib/shift73k_web/live/shift_live/index.html.leex index 02f484db..344583c7 100644 --- a/lib/shift73k_web/live/shift_live/index.html.leex +++ b/lib/shift73k_web/live/shift_live/index.html.leex @@ -6,6 +6,9 @@ My Shifts + + +
@@ -60,7 +63,11 @@ (<%= shift.location %>) <% end %>
-
<%= shift.description %>
+ <%= if shift.description do %> +
+ <%= text_to_html shift.description %> +
+ <% end %>
<%= link "Delete", to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"] %>
diff --git a/lib/shift73k_web/router.ex b/lib/shift73k_web/router.ex index 115b8a3b..98af2bd8 100644 --- a/lib/shift73k_web/router.ex +++ b/lib/shift73k_web/router.ex @@ -98,6 +98,9 @@ defmodule Shift73kWeb.Router do live "/assign", ShiftAssignLive.Index, :index live "/shifts", ShiftLive.Index, :index + + get "/csv", UserShiftsCsvController, :new + post "/csv", UserShiftsCsvController, :export end # scope "/", Shift73kWeb do diff --git a/lib/shift73k_web/templates/layout/navbar/_shifts_menu.html.eex b/lib/shift73k_web/templates/layout/navbar/_shifts_menu.html.eex index 17c6870d..137f8140 100644 --- a/lib/shift73k_web/templates/layout/navbar/_shifts_menu.html.eex +++ b/lib/shift73k_web/templates/layout/navbar/_shifts_menu.html.eex @@ -26,6 +26,15 @@ <% end %> +
  • +<%# user_shifts_csv_path %> +
  • + <%= link nav_link_opts(@conn, to: Routes.user_shifts_csv_path(@conn, :new), class: "dropdown-item") do %> + <%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline me-1"] %> + CSV Export + <% end %> +
  • + diff --git a/lib/shift73k_web/templates/user_shifts_csv/new.html.eex b/lib/shift73k_web/templates/user_shifts_csv/new.html.eex new file mode 100644 index 00000000..e171fe26 --- /dev/null +++ b/lib/shift73k_web/templates/user_shifts_csv/new.html.eex @@ -0,0 +1,75 @@ +
    +
    + +

    + <%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline"] %> + CSV Export +

    +

    Select a date range for which to export a CSV of your scheduled shifts, or click "Export All" to export everything.

    + +
    +
    + + + <% min_date = min_user_shift_date(@current_user.id) %> + <% max_date = max_user_shift_date(@current_user.id) %> + <% today = Date.utc_today() %> + + <%= form_for :csv_export, Routes.user_shifts_csv_path(@conn, :export), fn csv_range -> %> + +
    + + <%= hidden_input csv_range, :user_id, value: @current_user.id %> + +
    + <%= label csv_range, :date_min, "From", class: "form-label" %> + <%= date_input csv_range, :date_min, + value: Date.beginning_of_month(today), + min: min_date, + max: max_date, + class: "form-control" + %> +
    + +
    + <%= label csv_range, :date_max, "To", class: "form-label" %> + <%= date_input csv_range, :date_max, + value: Date.end_of_month(today), + min: min_date, + max: max_date, + class: "form-control" + %> +
    + +
    + +
    +
    + <%= submit "Export for selected dates", class: "btn btn-primary" %> +
    +
    + + <% end %> + + + <%= form_for :csv_export, Routes.user_shifts_csv_path(@conn, :export), fn csv_all -> %> + + <%= hidden_input csv_all, :user_id, value: @current_user.id %> + <%= hidden_input csv_all, :date_min, value: min_date %> + <%= hidden_input csv_all, :date_max, value: max_date %> + + +
    +
    + <%= submit "Export all", class: "btn btn-outline-primary" %> +
    +
    + + <% end %> + +
    +
    + + +
    +
    diff --git a/lib/shift73k_web/views/user_shifts_csv_view.ex b/lib/shift73k_web/views/user_shifts_csv_view.ex new file mode 100644 index 00000000..67d19cb2 --- /dev/null +++ b/lib/shift73k_web/views/user_shifts_csv_view.ex @@ -0,0 +1,13 @@ +defmodule Shift73kWeb.UserShiftsCsvView do + use Shift73kWeb, :view + + alias Shift73k.Shifts + + def min_user_shift_date(user_id) do + Shifts.get_min_user_shift_date(user_id) |> Date.to_string() + end + + def max_user_shift_date(user_id) do + Shifts.get_max_user_shift_date(user_id) |> Date.to_string() + end +end