From 56b72f8038e1d453a907195b285a4aef49c5d213 Mon Sep 17 00:00:00 2001
From: Adam Piontek <adam@73k.us>
Date: Tue, 23 Mar 2021 11:54:49 -0400
Subject: [PATCH] CSV export implemented, a few other fixes

---
 assets/js/app.js                              |  1 +
 lib/shift73k/shifts.ex                        | 37 +++++++-
 .../shifts/templates/shift_template.ex        | 17 +---
 .../controllers/user_shifts_csv_controller.ex | 94 +++++++++++++++++++
 lib/shift73k_web/live/live_helpers.ex         |  8 +-
 .../live/shift_live/index.html.leex           |  9 +-
 lib/shift73k_web/router.ex                    |  3 +
 .../layout/navbar/_shifts_menu.html.eex       |  9 ++
 .../templates/user_shifts_csv/new.html.eex    | 75 +++++++++++++++
 .../views/user_shifts_csv_view.ex             | 13 +++
 10 files changed, 245 insertions(+), 21 deletions(-)
 create mode 100644 lib/shift73k_web/controllers/user_shifts_csv_controller.ex
 create mode 100644 lib/shift73k_web/templates/user_shifts_csv/new.html.eex
 create mode 100644 lib/shift73k_web/views/user_shifts_csv_view.ex

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
     </h2>
 
+
+
+
     <div class="row justify-content-start justify-content-sm-center">
       <div class="col-md-10 col-xl-10">
 
@@ -60,7 +63,11 @@
                         <span class="text-muted">(<%= shift.location %>)</span>
                       <% end %>
                     </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;">
                       <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"] %></span>
                     </div>
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 %>
     </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>
 
 </li>
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 @@
+<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>
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