CSV export implemented, a few other fixes

This commit is contained in:
Adam Piontek 2021-03-23 11:54:49 -04:00
parent 3a6e3e8eed
commit 56b72f8038
10 changed files with 245 additions and 21 deletions

View file

@ -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/save.svg";
import "../node_modules/bootstrap-icons/icons/asterisk.svg"; import "../node_modules/bootstrap-icons/icons/asterisk.svg";
import "../node_modules/bootstrap-icons/icons/card-list.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 // webpack automatically bundles all modules in your
// entry points. Those entry points can be configured // entry points. Those entry points can be configured

View file

@ -21,7 +21,7 @@ defmodule Shift73k.Shifts do
Repo.all(Shift) Repo.all(Shift)
end end
defp query_shifts_by_user(user_id) do def query_shifts_by_user(user_id) do
from(s in Shift) from(s in Shift)
|> where([s], s.user_id == ^user_id) |> where([s], s.user_id == ^user_id)
end end
@ -53,6 +53,25 @@ defmodule Shift73k.Shifts do
|> Repo.all() |> Repo.all()
end 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 """ @doc """
Gets a single shift. Gets a single shift.
@ -146,4 +165,20 @@ defmodule Shift73k.Shifts do
def change_shift(%Shift{} = shift, attrs \\ %{}) do def change_shift(%Shift{} = shift, attrs \\ %{}) do
Shift.changeset(shift, attrs) Shift.changeset(shift, attrs)
end 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 end

View file

@ -2,6 +2,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Shift73k.Shifts
alias Shift73k.Shifts.Templates.ShiftTemplate alias Shift73k.Shifts.Templates.ShiftTemplate
@app_vars Application.get_env(:shift73k, :app_global_vars, time_zone: "America/New_York") @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(:subject, count: :codepoints, max: 280)
|> validate_length(:location, count: :codepoints, max: 280) |> validate_length(:location, count: :codepoints, max: 280)
|> validate_change(:time_end, fn :time_end, time_end -> |> 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 cond do
shift_length == 0 -> 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(%{time_start: time_start}), do: time_start
defp time_start_from_attrs(_), do: nil 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 # Get shift attrs from shift template
def attrs(%ShiftTemplate{} = shift_template) do def attrs(%ShiftTemplate{} = shift_template) do
shift_template shift_template

View file

@ -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

View file

@ -5,7 +5,7 @@ defmodule Shift73kWeb.LiveHelpers do
alias Shift73k.Accounts alias Shift73k.Accounts
alias Shift73k.Accounts.User alias Shift73k.Accounts.User
alias Shift73kWeb.UserAuth alias Shift73kWeb.UserAuth
alias Shift73k.Shifts.Templates.ShiftTemplate alias Shift73k.Shifts
@doc """ @doc """
Performs the {:noreply, socket} for a given socket. Performs the {:noreply, socket} for a given socket.
@ -74,9 +74,9 @@ defmodule Shift73kWeb.LiveHelpers do
|> String.trim_trailing("m") |> String.trim_trailing("m")
end end
def format_shift_length(%ShiftTemplate{} = shift_template) do def format_shift_length(%{} = shift_or_template) do
shift_template shift_or_template
|> ShiftTemplate.shift_length() |> Shifts.shift_length()
|> format_shift_length() |> format_shift_length()
end end

View file

@ -6,6 +6,9 @@
My Shifts My Shifts
</h2> </h2>
<div class="row justify-content-start justify-content-sm-center"> <div class="row justify-content-start justify-content-sm-center">
<div class="col-md-10 col-xl-10"> <div class="col-md-10 col-xl-10">
@ -60,7 +63,11 @@
<span class="text-muted">(<%= shift.location %>)</span> <span class="text-muted">(<%= shift.location %>)</span>
<% end %> <% end %>
</div> </div>
<div style="font-size: smaller;"><%= shift.description %></div> <%= if shift.description do %>
<div style="font-size: smaller;">
<%= text_to_html shift.description %>
</div>
<% end %>
<div style="font-size: smaller;"> <div style="font-size: smaller;">
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"] %></span> <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"] %></span>
</div> </div>

View file

@ -98,6 +98,9 @@ defmodule Shift73kWeb.Router do
live "/assign", ShiftAssignLive.Index, :index live "/assign", ShiftAssignLive.Index, :index
live "/shifts", ShiftLive.Index, :index live "/shifts", ShiftLive.Index, :index
get "/csv", UserShiftsCsvController, :new
post "/csv", UserShiftsCsvController, :export
end end
# scope "/", Shift73kWeb do # scope "/", Shift73kWeb do

View file

@ -26,6 +26,15 @@
<% end %> <% end %>
</li> </li>
<li><hr class="dropdown-divider"></li>
<%# user_shifts_csv_path %>
<li>
<%= 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 %>
</li>
</ul> </ul>
</li> </li>

View file

@ -0,0 +1,75 @@
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-xl-8">
<h2>
<%= icon_div @conn, "bi-file-earmark-spreadsheet", [class: "icon baseline"] %>
CSV Export
</h2>
<p class="lead">Select a date range for which to export a CSV of your scheduled shifts, or click "Export All" to export everything.</p>
<div class="row justify-content-center">
<div class="col-12 col-sm-9 col-md-8 col-lg-6 col-xxl-5">
<% 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 -> %>
<div class="row gx-2 gx-sm-3 mb-3">
<%= hidden_input csv_range, :user_id, value: @current_user.id %>
<div class="col-6">
<%= 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"
%>
</div>
<div class="col-6">
<%= 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"
%>
</div>
</div>
<div class="row gx-2 gx-sm-3 mb-3">
<div class="col text-end">
<%= submit "Export for selected dates", class: "btn btn-primary" %>
</div>
</div>
<% 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 %>
<div class="row gx-2 gx-sm-3 mb-3">
<div class="col text-end">
<%= submit "Export all", class: "btn btn-outline-primary" %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>

View file

@ -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