From 686db55e8bfa62dae72401701cb0b5b69207b58d Mon Sep 17 00:00:00 2001
From: Adam Piontek <adam@73k.us>
Date: Sun, 21 Mar 2021 10:47:53 -0400
Subject: [PATCH] improvements including removing Timex library

---
 config/config.exs                             |   2 +
 lib/shift73k/shifts.ex                        |  15 +-
 .../shifts/templates/shift_template.ex        |  13 +-
 lib/shift73k_web/live/live_helpers.ex         |  27 ++--
 .../delete_component.html.leex                |   2 +-
 .../live/shift_assign_live/index.ex           |  62 ++++----
 .../live/shift_assign_live/index.html.leex    |  10 +-
 lib/shift73k_web/live/shift_live/index.ex     |  36 +++--
 .../live/shift_live/index.html.leex           | 134 ++++++++++--------
 .../shift_template_live/delete_component.ex   |   1 -
 .../form_component.html.leex                  |   2 +-
 .../live/shift_template_live/index.ex         |   1 -
 .../live/user/settings/week_start.ex          |   8 +-
 .../live/user_management/index.ex             |   5 +-
 .../live/user_management/index.html.leex      |   4 +-
 mix.exs                                       |   3 +-
 mix.lock                                      |   1 +
 priv/repo/seeds.exs                           |   2 +-
 18 files changed, 179 insertions(+), 149 deletions(-)

diff --git a/config/config.exs b/config/config.exs
index 83f8b26b..545cb3c0 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -7,6 +7,8 @@
 # General application configuration
 use Mix.Config
 
+config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
+
 config :shift73k,
   ecto_repos: [Shift73k.Repo]
 
diff --git a/lib/shift73k/shifts.ex b/lib/shift73k/shifts.ex
index dcb55b5c..8d6253d7 100644
--- a/lib/shift73k/shifts.ex
+++ b/lib/shift73k/shifts.ex
@@ -26,8 +26,15 @@ defmodule Shift73k.Shifts do
     |> where([s], s.user_id == ^user_id)
   end
 
+  def list_shifts_by_user(user_id) do
+    user_id
+    |> query_shifts_by_user()
+    |> Repo.all()
+  end
+
   def list_shifts_by_user_in_date_range(user_id, %Date.Range{} = date_range) do
-    query_shifts_by_user(user_id)
+    user_id
+    |> query_shifts_by_user()
     |> where([s], s.date >= ^date_range.first)
     |> where([s], s.date <= ^date_range.last)
     |> order_by([s], [s.date, s.time_start])
@@ -35,12 +42,14 @@ defmodule Shift73k.Shifts do
   end
 
   defp query_shifts_by_user_from_list_of_dates(user_id, date_list) do
-    query_shifts_by_user(user_id)
+    user_id
+    |> query_shifts_by_user()
     |> where([s], s.date in ^date_list)
   end
 
   def list_shifts_by_user_from_list_of_dates(user_id, date_list) do
-    query_shifts_by_user_from_list_of_dates(user_id, date_list)
+    user_id
+    |> query_shifts_by_user_from_list_of_dates(date_list)
     |> Repo.all()
   end
 
diff --git a/lib/shift73k/shifts/templates/shift_template.ex b/lib/shift73k/shifts/templates/shift_template.ex
index ee749f65..bfa64d62 100644
--- a/lib/shift73k/shifts/templates/shift_template.ex
+++ b/lib/shift73k/shifts/templates/shift_template.ex
@@ -1,5 +1,4 @@
 defmodule Shift73k.Shifts.Templates.ShiftTemplate do
-  use Timex
   use Ecto.Schema
   import Ecto.Changeset
 
@@ -59,7 +58,7 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
           []
       end
     end)
-    |> validate_inclusion(:time_zone, Timex.timezones(),
+    |> validate_inclusion(:time_zone, Tzdata.zone_list(),
       message: "must be a valid IANA tz database time zone"
     )
   end
@@ -70,7 +69,8 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
 
   def shift_length(%ShiftTemplate{time_end: time_end, time_start: time_start}) do
     time_end
-    |> Timex.diff(time_start, :minute)
+    |> Time.diff(time_start)
+    |> Integer.floor_div(60)
     |> shift_length()
   end
 
@@ -79,11 +79,4 @@ defmodule Shift73k.Shifts.Templates.ShiftTemplate do
 
   def shift_length(time_end, time_start),
     do: shift_length(%ShiftTemplate{time_end: time_end, time_start: time_start})
-
-  def shift_length_h_m(%ShiftTemplate{time_end: _, time_start: _} = template) do
-    shift_length_seconds = shift_length(template)
-    h = shift_length_seconds |> Integer.floor_div(60)
-    m = shift_length_seconds |> rem(60)
-    {h, m}
-  end
 end
diff --git a/lib/shift73k_web/live/live_helpers.ex b/lib/shift73k_web/live/live_helpers.ex
index a0b9ba77..6ebe5bf4 100644
--- a/lib/shift73k_web/live/live_helpers.ex
+++ b/lib/shift73k_web/live/live_helpers.ex
@@ -68,16 +68,27 @@ defmodule Shift73kWeb.LiveHelpers do
     end)
   end
 
-  def format_shift_time(time), do: Timex.format!(time, "{h12}:{m}{am}")
+  def format_shift_time(time) do
+    time
+    |> Calendar.strftime("%-I:%M%P")
+    |> String.trim_trailing("m")
+  end
 
-  def format_shift_length(shift_template) do
+  def format_shift_length(%ShiftTemplate{} = shift_template) do
     shift_template
     |> ShiftTemplate.shift_length()
-    |> Timex.Duration.from_minutes()
-    |> Timex.format_duration()
-    |> String.replace("PT", "")
-    |> String.replace("H", "h ")
-    |> String.replace("M", "m")
-    |> String.trim()
+    |> format_shift_length()
+  end
+
+  def format_shift_length(minutes) when is_integer(minutes) do
+    h = Integer.floor_div(minutes, 60)
+    m = rem(minutes, 60)
+
+    cond do
+      h > 0 && m > 0 -> "#{h}h #{m}m"
+      h > 0 -> "#{h}h"
+      m > 0 -> "#{m}m"
+      true -> "0m"
+    end
   end
 end
diff --git a/lib/shift73k_web/live/shift_assign_live/delete_component.html.leex b/lib/shift73k_web/live/shift_assign_live/delete_component.html.leex
index f159bcdf..81aa79a6 100644
--- a/lib/shift73k_web/live/shift_assign_live/delete_component.html.leex
+++ b/lib/shift73k_web/live/shift_assign_live/delete_component.html.leex
@@ -7,7 +7,7 @@
     <% months = Map.keys(data) %>
     <dd>
       <%= for {m, i} <- Enum.with_index(months, 1) do %>
-        <%= data |> Map.get(m) |> hd() |> Timex.format!("{Mshort}") %>:
+        <%= data |> Map.get(m) |> hd() |> Calendar.strftime("%b") %>:
         <% days = Map.get(data, m) %>
         <%= for {d, i} <- Enum.with_index(days, 1) do %>
           <%= d.day %><%= if i < length(days) do %>,<% end %>
diff --git a/lib/shift73k_web/live/shift_assign_live/index.ex b/lib/shift73k_web/live/shift_assign_live/index.ex
index 38c2db9c..0b5ccb0b 100644
--- a/lib/shift73k_web/live/shift_assign_live/index.ex
+++ b/lib/shift73k_web/live/shift_assign_live/index.ex
@@ -1,10 +1,8 @@
 defmodule Shift73kWeb.ShiftAssignLive.Index do
   use Shift73kWeb, :live_view
-  use Timex
 
   alias Ecto.Multi
   alias Shift73k.Repo
-  alias Shift73k.EctoEnums.WeekdayEnum
   alias Shift73k.Shifts
   alias Shift73k.Shifts.Shift
   alias Shift73k.Shifts.Templates
@@ -27,8 +25,6 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
 
   @impl true
   def handle_params(_params, _url, socket) do
-    user = socket.assigns.current_user
-
     socket
     |> init_shift_templates()
     |> init_shift_template()
@@ -36,8 +32,8 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
     |> assign_shift_length()
     |> assign_shift_template_changeset()
     |> assign_modal_close_handlers()
-    |> assign(:day_names, day_names(user.week_start_at))
-    |> init_today(Timex.today())
+    |> init_today(Date.utc_today())
+    |> init_day_names()
     |> update_calendar()
     |> live_noreply()
   end
@@ -61,7 +57,11 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
   end
 
   defp init_today(socket, today) do
-    assign(socket, current_date: today, cursor_date: today)
+    assign(socket, current_date: today, cursor_date: cursor_date(today))
+  end
+
+  defp cursor_date(%Date{} = date) do
+    date |> Date.beginning_of_month() |> Date.add(4)
   end
 
   defp assign_shift_template_changeset(%{assigns: %{shift_template: shift}} = socket) do
@@ -105,27 +105,26 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
     {label, template.id}
   end
 
-  defp rotate_week(week_start_at) do
-    {a, b} = Enum.split_while(WeekdayEnum.__enum_map__(), fn {k, _v} -> k != week_start_at end)
-    b ++ a
-  end
+  defp init_day_names(%{assigns: %{current_user: user, current_date: today}} = socket) do
+    week_start = Date.beginning_of_week(today, user.week_start_at)
 
-  defp day_names(week_start_at) do
-    week_start_at
-    |> rotate_week()
-    |> Keyword.values()
-    |> Enum.map(&Timex.day_shortname/1)
+    day_names =
+      week_start
+      |> Date.range(Date.add(week_start, 6))
+      |> Enum.map(&Calendar.strftime(&1, "%a"))
+
+    assign(socket, :day_names, day_names)
   end
 
   defp date_range(cursor_date, week_start_at) do
     last =
       cursor_date
-      |> Timex.end_of_month()
-      |> Timex.end_of_week(week_start_at)
+      |> Date.end_of_month()
+      |> Date.end_of_week(week_start_at)
 
     cursor_date
-    |> Timex.beginning_of_month()
-    |> Timex.beginning_of_week(week_start_at)
+    |> Date.beginning_of_month()
+    |> Date.beginning_of_week(week_start_at)
     |> Date.range(last)
   end
 
@@ -134,26 +133,20 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
     assign(socket, :date_range, date_range)
   end
 
-  defp week_rows(%Date.Range{} = date_range) do
-    Interval.new(from: date_range.first, until: date_range.last, right_open: false)
-    |> Stream.map(&NaiveDateTime.to_date(&1))
-    |> Enum.chunk_every(7)
-  end
-
   defp assign_week_rows(%{assigns: %{date_range: date_range}} = socket) do
-    assign(socket, :week_rows, week_rows(date_range))
+    assign(socket, :week_rows, Enum.chunk_every(date_range, 7))
   end
 
   def day_color(day, current_date, cursor_date, selected_days) do
     cond do
       Enum.member?(selected_days, Date.to_string(day)) ->
         cond do
-          Timex.compare(day, current_date, :days) == 0 -> "bg-triangle-info text-white"
+          Date.compare(day, current_date) == :eq -> "bg-triangle-info text-white"
           day.month != cursor_date.month -> "bg-triangle-light text-gray"
           true -> "bg-triangle-white"
         end
 
-      Timex.compare(day, current_date, :days) == 0 ->
+      Date.compare(day, current_date) == :eq ->
         "bg-info text-white"
 
       day.month != cursor_date.month ->
@@ -221,15 +214,16 @@ defmodule Shift73kWeb.ShiftAssignLive.Index do
   end
 
   @impl true
-  def handle_event("month-nav", %{"month" => direction}, socket) do
+  def handle_event("month-nav", %{"month" => nav}, socket) do
     new_cursor =
       cond do
-        direction == "now" ->
-          Timex.today()
+        nav == "now" ->
+          Date.utc_today()
 
         true ->
-          months = (direction == "prev" && -1) || 1
-          Timex.shift(socket.assigns.cursor_date, months: months)
+          socket.assigns.cursor_date
+          |> Date.add((nav == "prev" && -30) || 30)
+          |> cursor_date()
       end
 
     socket
diff --git a/lib/shift73k_web/live/shift_assign_live/index.html.leex b/lib/shift73k_web/live/shift_assign_live/index.html.leex
index 81b7e550..a1559fcc 100644
--- a/lib/shift73k_web/live/shift_assign_live/index.html.leex
+++ b/lib/shift73k_web/live/shift_assign_live/index.html.leex
@@ -118,7 +118,7 @@
                     list: "tz_list"
                   %>
                   <datalist id="tz_list">
-                    <%= for tz_name <- Timex.timezones() do %>
+                    <%= for tz_name <- Tzdata.zone_list() do %>
                       <option value="<%= tz_name %>"></option>
                     <% end %>
                     end
@@ -171,10 +171,10 @@
 <%# month navigation %>
 <div class="d-flex justify-content-between align-items-end mt-4">
   <h3 class="text-muted mb-0">
-    <%= Timex.format!(@cursor_date, "{Mfull} {YYYY}") %>
+    <%= Calendar.strftime(@cursor_date, "%B %Y") %>
   </h3>
   <div>
-    <button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Timex.today(), :month), do: "disabled" %>>
+    <button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: "disabled" %>>
       <%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
       <span class="d-none d-sm-inline">Today</span>
     </button>
@@ -206,7 +206,7 @@
       <%= for day <- week do %>
         <td class="<%= day_color(day, @current_date, @cursor_date, @selected_days) %>" phx-click="select-day" phx-value-day="<%= day %>">
 
-          <%= Timex.format!(day, "{0D}") %><%= if day.month != @cursor_date.month, do: "-#{Timex.format!(day, "{Mshort}")}" %>
+          <%= Calendar.strftime(day, "%d") %><%= if day.month != @cursor_date.month, do: "-#{Calendar.strftime(day, "%b")}" %>
 
           <% day_shifts = Enum.filter(@known_shifts, fn s -> s.date == day end) %>
           <% shifts_to_show = shifts_to_show(day_shifts) %>
@@ -214,7 +214,7 @@
           <%= for shift <- shifts_to_show do %>
             <span class="badge bg-primary text-start d-block">
               <span>
-              <%= shift.time_start |> Timex.format!("{h12}:{m}{am}") |> String.trim_trailing("m") %>
+              <%= format_shift_time(shift.time_start) %>
               <%= shift.subject %>
               </span>
             </span>
diff --git a/lib/shift73k_web/live/shift_live/index.ex b/lib/shift73k_web/live/shift_live/index.ex
index 7e7c6513..7346cd62 100644
--- a/lib/shift73k_web/live/shift_live/index.ex
+++ b/lib/shift73k_web/live/shift_live/index.ex
@@ -1,6 +1,5 @@
 defmodule Shift73kWeb.ShiftLive.Index do
   use Shift73kWeb, :live_view
-  use Timex
 
   alias Shift73k.Shifts
   alias Shift73k.Shifts.Shift
@@ -21,9 +20,8 @@ defmodule Shift73kWeb.ShiftLive.Index do
 
     if Roles.can?(current_user, shift, live_action) do
       socket
-      |> init_today(Timex.today())
-      |> assign_date_range()
-      |> assign_known_shifts()
+      |> init_today(Date.utc_today())
+      |> update_agenda()
       |> assign(:delete_shift, nil)
       |> apply_action(socket.assigns.live_action, params)
       |> live_noreply()
@@ -49,7 +47,11 @@ defmodule Shift73kWeb.ShiftLive.Index do
   defp shift_from_params(_params), do: %Shift{}
 
   defp init_today(socket, today) do
-    assign(socket, current_date: today, cursor_date: today)
+    assign(socket, current_date: today, cursor_date: cursor_date(today))
+  end
+
+  defp cursor_date(%Date{} = date) do
+    date |> Date.beginning_of_month() |> Date.add(4)
   end
 
   defp assign_date_range(%{assigns: %{cursor_date: cursor_date}} = socket) do
@@ -58,8 +60,8 @@ defmodule Shift73kWeb.ShiftLive.Index do
 
   defp date_range(cursor_date) do
     cursor_date
-    |> Timex.beginning_of_month()
-    |> Date.range(Timex.end_of_month(cursor_date))
+    |> Date.beginning_of_month()
+    |> Date.range(Date.end_of_month(cursor_date))
   end
 
   defp assign_known_shifts(socket) do
@@ -68,6 +70,12 @@ defmodule Shift73kWeb.ShiftLive.Index do
     assign(socket, :shifts, shifts)
   end
 
+  defp update_agenda(socket) do
+    socket
+    |> assign_date_range()
+    |> assign_known_shifts()
+  end
+
   @impl true
   def handle_event("delete", %{"id" => id}, socket) do
     shift = Shifts.get_shift!(id)
@@ -77,21 +85,21 @@ defmodule Shift73kWeb.ShiftLive.Index do
   end
 
   @impl true
-  def handle_event("month-nav", %{"month" => direction}, socket) do
+  def handle_event("month-nav", %{"month" => nav}, socket) do
     new_cursor =
       cond do
-        direction == "now" ->
-          Timex.today()
+        nav == "now" ->
+          Date.utc_today()
 
         true ->
-          months = (direction == "prev" && -1) || 1
-          Timex.shift(socket.assigns.cursor_date, months: months)
+          socket.assigns.cursor_date
+          |> Date.add((nav == "prev" && -30) || 30)
+          |> cursor_date()
       end
 
     socket
     |> assign(:cursor_date, new_cursor)
-    |> assign_date_range()
-    |> assign_known_shifts()
+    |> update_agenda()
     |> live_noreply()
   end
 end
diff --git a/lib/shift73k_web/live/shift_live/index.html.leex b/lib/shift73k_web/live/shift_live/index.html.leex
index afb3a992..02f484db 100644
--- a/lib/shift73k_web/live/shift_live/index.html.leex
+++ b/lib/shift73k_web/live/shift_live/index.html.leex
@@ -1,76 +1,84 @@
 <div class="row justify-content-start justify-content-sm-center">
   <div class="col-md-10 col-xl-10">
 
-<h2 class="mb-3 mb-sm-0">
-  <%= icon_div @socket, "bi-card-list", [class: "icon baseline"] %>
-  My Shifts
-</h2>
+    <h2 class="mb-3 mb-sm-0">
+      <%= icon_div @socket, "bi-card-list", [class: "icon baseline"] %>
+      My Shifts
+    </h2>
 
-<%# month navigation %>
-<div class="d-flex justify-content-between align-items-end mt-4">
-  <h3 class="text-muted mb-0">
-    <%= Timex.format!(@cursor_date, "{Mfull} {YYYY}") %>
-  </h3>
-  <div>
-    <button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Timex.today(), :month), do: "disabled" %>>
-      <%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
-      <span class="d-none d-sm-inline">Today</span>
-    </button>
-    <button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
-      <%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
-      <span class="d-none d-sm-inline">Prev</span>
-    </button>
-    <button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
-      <span class="d-none d-sm-inline">Next</span>
-      <%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
-    </button>
-  </div>
-</div>
+    <div class="row justify-content-start justify-content-sm-center">
+      <div class="col-md-10 col-xl-10">
 
 
-<dl>
-<%= for day <- Enum.to_list(@date_range) do %>
-  <%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
-  <div class="border-top mt-4 mb-4"></div>
-  <% end %>
-  <dt>
-    <%= Timex.format!(day, "{WDfull}, {Mshort} {D}") %>
-  </dt>
-  <% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
-  <%= if !Enum.empty?(day_shifts) do %>
-    <dd id="day-<%= day.day %>">
-      <%= for shift <- day_shifts do %>
-        <div class="row gx-2" id="shift-<%= shift.id %>">
-          <div class="col-4 col-md-3 col-lg-2 text-end">
-            <div>
-              <%= format_shift_time(shift.time_start) |> String.trim_trailing("m") %>
-              &mdash;
-              <%= format_shift_time(shift.time_end) |> String.trim_trailing("m") %>
-            </div>
-            <div style="font-size: smaller;"><%= shift.time_zone %></div>
-          </div>
-          <div class="col-8 col-md-9 col-lg-10">
-            <div>
-              <%= shift.subject %>
-              <%= if shift.location do %>
-                <span class="text-muted">(<%= shift.location %>)</span>
-              <% end %>
-            </div>
-            <div style="font-size: smaller;"><%= shift.description %></div>
-            <div style="font-size: smaller;">
-              <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"] %></span>
-            </div>
+        <%# month navigation %>
+        <div class="d-flex justify-content-between align-items-end my-4">
+          <h3 class="text-muted mb-0">
+            <%= Calendar.strftime(@cursor_date, "%B %Y") %>
+          </h3>
+          <div>
+            <button type="button" phx-click="month-nav" phx-value-month="now" class="btn btn-info text-white" <%= if Map.get(@cursor_date, :month) == Map.get(Date.utc_today(), :month), do: "disabled" %>>
+              <%= icon_div @socket, "bi-asterisk", [class: "icon baseline"] %>
+              <span class="d-none d-sm-inline">Today</span>
+            </button>
+            <button type="button" phx-click="month-nav" phx-value-month="prev" class="btn btn-primary">
+              <%= icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
+              <span class="d-none d-sm-inline">Prev</span>
+            </button>
+            <button type="button" phx-click="month-nav" phx-value-month="next" class="btn btn-primary">
+              <span class="d-none d-sm-inline">Next</span>
+              <%= icon_div @socket, "bi-chevron-right", [class: "icon baseline", style: "margin-left:0.125rem;"] %>
+            </button>
           </div>
         </div>
-      <% end %>
 
-    </dd>
 
-  <% else %>
-    <dd><em>Nothing scheduled</em></dd>
-  <% end %>
-<% end %>
-</dl>
+        <dl>
+        <%= for day <- Enum.to_list(@date_range) do %>
+          <%= if Date.day_of_week(day, @current_user.week_start_at) == 1 do %>
+          <div class="border-top mt-4 mb-4"></div>
+          <% end %>
+          <dt>
+            <%= Calendar.strftime(day, "%A, %b %-d") %>
+          </dt>
+          <% day_shifts = Enum.filter(@shifts, fn s -> s.date == day end) %>
+          <%= if !Enum.empty?(day_shifts) do %>
+            <dd id="day-<%= day.day %>">
+              <%= for shift <- day_shifts do %>
+                <div class="row gx-2" id="shift-<%= shift.id %>">
+                  <div class="col-4 col-md-3 col-xl-2 text-end">
+                    <div>
+                      <%= format_shift_time(shift.time_start) %>
+                      &mdash;
+                      <%= format_shift_time(shift.time_end) %>
+                    </div>
+                    <div style="font-size: smaller;"><%= shift.time_zone %></div>
+                  </div>
+                  <div class="col-8 col-md-9 col-xl-10">
+                    <div>
+                      <%= shift.subject %>
+                      <%= if shift.location do %>
+                        <span class="text-muted">(<%= shift.location %>)</span>
+                      <% end %>
+                    </div>
+                    <div style="font-size: smaller;"><%= shift.description %></div>
+                    <div style="font-size: smaller;">
+                      <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: shift.id, data: [confirm: "Are you sure?"] %></span>
+                    </div>
+                  </div>
+                </div>
+              <% end %>
+
+            </dd>
+
+          <% else %>
+            <dd><em>Nothing scheduled</em></dd>
+          <% end %>
+        <% end %>
+        </dl>
+
+
+      </div>
+    </div>
 
 
   </div>
diff --git a/lib/shift73k_web/live/shift_template_live/delete_component.ex b/lib/shift73k_web/live/shift_template_live/delete_component.ex
index 44209db5..953aa213 100644
--- a/lib/shift73k_web/live/shift_template_live/delete_component.ex
+++ b/lib/shift73k_web/live/shift_template_live/delete_component.ex
@@ -1,6 +1,5 @@
 defmodule Shift73kWeb.ShiftTemplateLive.DeleteComponent do
   use Shift73kWeb, :live_component
-  use Timex
 
   alias Shift73k.Shifts.Templates
 
diff --git a/lib/shift73k_web/live/shift_template_live/form_component.html.leex b/lib/shift73k_web/live/shift_template_live/form_component.html.leex
index 77de5232..8ef4d9f7 100644
--- a/lib/shift73k_web/live/shift_template_live/form_component.html.leex
+++ b/lib/shift73k_web/live/shift_template_live/form_component.html.leex
@@ -90,7 +90,7 @@
           list: "tz_list"
         %>
         <datalist id="tz_list">
-          <%= for tz_name <- Timex.timezones() do %>
+          <%= for tz_name <- Tzdata.zone_list() do %>
             <option value="<%= tz_name %>"></option>
           <% end %>
           end
diff --git a/lib/shift73k_web/live/shift_template_live/index.ex b/lib/shift73k_web/live/shift_template_live/index.ex
index a1287922..aafc8332 100644
--- a/lib/shift73k_web/live/shift_template_live/index.ex
+++ b/lib/shift73k_web/live/shift_template_live/index.ex
@@ -1,6 +1,5 @@
 defmodule Shift73kWeb.ShiftTemplateLive.Index do
   use Shift73kWeb, :live_view
-  use Timex
 
   alias Shift73k.Accounts
   alias Shift73k.Shifts.Templates
diff --git a/lib/shift73k_web/live/user/settings/week_start.ex b/lib/shift73k_web/live/user/settings/week_start.ex
index d6c3eaf2..7759cd74 100644
--- a/lib/shift73k_web/live/user/settings/week_start.ex
+++ b/lib/shift73k_web/live/user/settings/week_start.ex
@@ -14,7 +14,13 @@ defmodule Shift73kWeb.UserLive.Settings.WeekStart do
   end
 
   def week_start_options do
-    WeekdayEnum.__enum_map__() |> Enum.map(fn {d, n} -> {Timex.day_name(n), d} end)
+    {week_start_at, _} = WeekdayEnum.__enum_map__() |> hd()
+    week_start = Date.beginning_of_week(Date.utc_today(), week_start_at)
+
+    week_start
+    |> Date.range(Date.add(week_start, 6))
+    |> Enum.map(&Calendar.strftime(&1, "%A"))
+    |> Enum.zip(Keyword.keys(WeekdayEnum.__enum_map__()))
   end
 
   @impl true
diff --git a/lib/shift73k_web/live/user_management/index.ex b/lib/shift73k_web/live/user_management/index.ex
index 64ba2041..ae7f8037 100644
--- a/lib/shift73k_web/live/user_management/index.ex
+++ b/lib/shift73k_web/live/user_management/index.ex
@@ -1,6 +1,5 @@
 defmodule Shift73kWeb.UserManagementLive.Index do
   use Shift73kWeb, :live_view
-  use Timex
 
   import Ecto.Query
   import Shift73k.Util.Dt
@@ -200,7 +199,7 @@ defmodule Shift73kWeb.UserManagementLive.Index do
 
   def dt_out(ndt) do
     ndt
-    |> Timex.to_datetime(app_time_zone())
-    |> Timex.format!("{YYYY} {Mshort} {D}, {h12}:{m} {AM}")
+    |> DateTime.from_naive!(app_time_zone())
+    |> Calendar.strftime("%Y %b %-d, %-I:%M %p")
   end
 end
diff --git a/lib/shift73k_web/live/user_management/index.html.leex b/lib/shift73k_web/live/user_management/index.html.leex
index 9412d876..f1c65b18 100644
--- a/lib/shift73k_web/live/user_management/index.html.leex
+++ b/lib/shift73k_web/live/user_management/index.html.leex
@@ -92,7 +92,7 @@
             </dt>
             <dd class="d-inline d-sm-block col-auto">
               <span class="visually-hidden"><%= user.confirmed_at && "Yes" || "No" %></span>
-              <input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %>>
+              <input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
             </dd>
           </dl>
 
@@ -174,7 +174,7 @@
             <td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
             <td class="align-middle">
               <span class="visually-hidden"><%= user.confirmed_at && "Confirmed" || "Not confirmed" %></span>
-              <input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %>>
+              <input type="checkbox" class="form-check-input" aria-hidden="true" <%= user.confirmed_at && "checked" || "" %> disabled>
             </td>
             <td class="align-middle text-end text-nowrap">
 
diff --git a/mix.exs b/mix.exs
index 6b83d8a8..17c3a0b2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -52,7 +52,8 @@ defmodule Shift73k.MixProject do
       {:bamboo, "~> 2.0"},
       {:bamboo_smtp, "~> 4.0"},
       {:scrivener_ecto, "~> 2.0"},
-      {:timex, "~> 3.6"}
+      {:tzdata, "~> 1.1"},
+      {:nimble_csv, "~> 1.0"}
     ]
   end
 
diff --git a/mix.lock b/mix.lock
index c7f93411..a97b0243 100644
--- a/mix.lock
+++ b/mix.lock
@@ -27,6 +27,7 @@
   "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
   "mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
   "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+  "nimble_csv": {:hex, :nimble_csv, "1.1.0", "b1dba4a86be9e03065c9de829050468e591f569100332db949e7ce71be0afc25", [:mix], [], "hexpm", "e986755bc302832cac429be6deda0fc9d82d3c82b47abefb68b3c17c9d949a3f"},
   "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
   "phoenix": {:hex, :phoenix, "1.5.8", "71cfa7a9bb9a37af4df98939790642f210e35f696b935ca6d9d9c55a884621a4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "35ded0a32f4836168c7ab6c33b88822eccd201bcd9492125a9bea4c54332d955"},
   "phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs
index deacba92..916f876e 100644
--- a/priv/repo/seeds.exs
+++ b/priv/repo/seeds.exs
@@ -110,7 +110,7 @@ for user <- Accounts.list_users() do
         subject: e["subject"],
         description: e["description"],
         location: e["location"],
-        time_zone: Timex.timezones() |> Enum.random(),
+        time_zone: Tzdata.zone_list() |> Enum.random(),
         time_start: time_start,
         time_end: time_end,
         user_id: user.id,