CSV export implemented, a few other fixes
This commit is contained in:
parent
3a6e3e8eed
commit
56b72f8038
10 changed files with 245 additions and 21 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
94
lib/shift73k_web/controllers/user_shifts_csv_controller.ex
Normal file
94
lib/shift73k_web/controllers/user_shifts_csv_controller.ex
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
75
lib/shift73k_web/templates/user_shifts_csv/new.html.eex
Normal file
75
lib/shift73k_web/templates/user_shifts_csv/new.html.eex
Normal 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>
|
13
lib/shift73k_web/views/user_shifts_csv_view.ex
Normal file
13
lib/shift73k_web/views/user_shifts_csv_view.ex
Normal 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
|
Loading…
Reference in a new issue