Home

mint @main - refs - log -
-
https://git.jolheiser.com/mint.git
Budget
tree log patch
clean up and separate code Signed-off-by: jolheiser <git@jolheiser.com>
Signature
-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgBTEvCQk6VqUAdN2RuH6bj1dNkY oOpbPWj+jw4ua1B1cAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQHITBx1i0sujmrIKoZLIwJR6rco4qTXPEKxE9e2uF5RG2brDRNeKI2kw98326rXrh1 mJ8Z2mVSQj3OZa/j5BzwQ= -----END SSH SIGNATURE-----
jolheiser <git@jolheiser.com>
1 month ago
6 changed files, 272 additions(+), 225 deletions(-)
budget/templates/budget/index.htmlbudget/views.pymint/settings.pymint/urls.pystatic/css/mint.cssstatic/js/mint.js
M budget/templates/budget/index.html -> budget/templates/budget/index.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
diff --git a/budget/templates/budget/index.html b/budget/templates/budget/index.html
index 079818016b8729aa463be58e4e2f8504d9ab9599..4bcd3916a6f27ec3f9f1c4a7fe5004348642400b 100644
--- a/budget/templates/budget/index.html
+++ b/budget/templates/budget/index.html
@@ -1,232 +1,20 @@
+{% load static %}
 <!DOCTYPE html>
 <html lang='en'>
 
 <head>
-  <meta charset='utf-8'>
-  <script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.18/index.global.min.js'></script>
-  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
-  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
-  <script>
-    const MINT_DEBUG = "{{ debug }}" === "True";
-    let calendar;
-
-    document.addEventListener('DOMContentLoaded', function () {
-      const calendarEl = document.getElementById('calendar');
-      calendar = new FullCalendar.Calendar(calendarEl, {
-        initialView: 'dayGridMonth',
-        dayMaxEventRows: true,
-        customButtons: {
-          admin: {
-            text: "admin",
-            hint: "Django Admin",
-            click: () => {
-              location.href = "{% url 'admin:index' %}";
-            }
-          },
-          copy: {
-            text: "copy",
-            hint: "Copy budget month-to-month",
-            click: () => {
-              Swal.fire({
-                title: "Are you sure?",
-                html: "This will copy <strong>all</strong> budget items from last month.",
-                icon: "warning",
-                allowOutsideClick: false,
-                showDenyButton: true,
-                showCloseButton: true,
-                confirmButtonText: "Yes",
-                reverseButtons: true,
-                showLoaderOnConfirm: true,
-                preConfirm: () => {
-                  fetch("/copy/", {
-                    method: "POST",
-                    headers: {"X-CSRFToken": '{{ csrf_token }}'},
-                    body: JSON.stringify({"month": calendar.getDate().getMonth() + 1}),
-                  }).then(() => calendar.refetchEvents());
-                }
-              });
-            }
-          }
-        },
-        headerToolbar: {
-          left: 'prev,next today copy {% if user.is_staff %}admin{% endif %}',
-          center: 'title',
-          right: 'dayGridWeek,dayGridMonth,dayGridYear'
-        },
-        selectable: true,
-        select: (info) => {
-          if (MINT_DEBUG) console.log(info);
-          const events = calendar.getEvents().filter((event) => {
-            const start = event.start;
-            const end = event.end || event.start;
-            return start < info.end && end >= info.start
-          });
-          if (MINT_DEBUG) console.log(events);
-          const sum = events.filter((event) => !event.extendedProps.income).reduce((acc, event) => acc + event.extendedProps.amount, 0);
-          toast(`\$${sum / 100}`, 'info')
-        },
-        eventStartEditable: true,
-        eventDrop: (info) => {
-          const event = info.event;
-          const props = event.extendedProps;
-          fetch("/transaction/", {
-            method: "PATCH",
-            headers: {"X-CSRFToken": '{{ csrf_token }}'},
-            body: JSON.stringify({
-              id: event.id,
-              start: event.start,
-              extendedProps: {
-                title: props.title,
-                amount: props.amount,
-                recurrence: props.recurrence,
-                week: props.week,
-                income: props.income
-              }
-            })
-          }).then(() => calendar.refetchEvents());
-        },
-        eventClick: (info) => {
-          const event = info.event;
-          const props = event.extendedProps;
-          input(event.start, event.id, props.title, props.amount / 100, props.recurrence, props.week, props.income);
-        },
-        navLinks: true,
-        navLinkDayClick: (date) => input(date),
-        events: "/events/",
-      });
-      calendar.render();
-    });
-
-    function input(time, id = '', title = 'New Transaction', amount = 0, recurrence = "M", week = 1, income = false) {
-      const weekDay = time.getDay();
-      const monthDay = time.getDate();
-      title = title.replace(/"/g, "&quot;")
-      Swal.fire({
-        title: `<input type="text" class="font-semibold" name="title" value="${title}"/>`,
-        html: `
-          <div class="grid grid-cols-1 gap-x-4 gap-y-3 items-center text-left">
-            <input type="hidden" name="id" value="${id}"/>
-            <input type="hidden" name="date" value="${time.toISOString()}"/>
-            <label class="font-semibold">Amount: $<input class="border rounded w-30 mx-px p-1" type="number" name="amount" value="${amount}"/></label>
-            <label class="font-semibold">Recurring? <input class="border rounded w-4 h-4" type="checkbox" name="recurring" ${recurrence !== "" ? "checked" : ""}/></label>
-            <div id="recurringOptions" class="${recurrence !== "" ? "" : "hidden"}">
-              <label><input type="radio" name="recurrence" ${recurrence === "M" ? "checked" : ""} value="M"/> Monthly on the ${ordinal(monthDay)}</label>
-              <br/>
-              <label><input type="radio" name="recurrence" ${recurrence === "W" ? "checked" : ""} value="W"/> Every <input name="week" class="border rounded w-15 p-1" type="number" min="1" max="4" value="${week}"/> weeks on ${dayOfWeek(weekDay)}</label>
-            </div>
-            <label class="font-semibold">Income? <input class="border rounded w-4 h-4" type="checkbox" name="income" ${income ? "checked" : ""}/></label>
-          </div>
-        `,
-        allowOutsideClick: false,
-        showCloseButton: true,
-        reverseButtons: true,
-        confirmButtonText: 'Save',
-        showDenyButton: id !== '',
-        denyButtonText: 'Delete',
-        customClass: {
-          confirmButton: 'ml-[15rem]!'
-        },
-        didRender: () => {
-          const $recurring = document.querySelector("[name='recurring']");
-          const $opts = document.getElementById("recurringOptions");
-          $recurring.addEventListener('change', () => {
-            $recurring.checked ? $opts.classList.remove('hidden') : $opts.classList.add('hidden');
-          });
-        },
-        preDeny: () => {
-          const $popup = Swal.getPopup();
-          return {
-            id: $popup.querySelector("[name='id']").value
-          };
-        },
-        preConfirm: () => {
-          const $popup = Swal.getPopup();
-          return {
-            id: $popup.querySelector("[name='id']").value,
-            date: new Date($popup.querySelector("[name='date']").value),
-            title: $popup.querySelector("[name='title']").value,
-            amount: parseFloat($popup.querySelector("[name='amount']").value),
-            recurrence: $popup.querySelector("[name='recurring']").checked ? $popup.querySelector("[name='recurrence']:checked").value : '',
-            week: $popup.querySelector("[name='week']").value,
-            income: $popup.querySelector("[name='income']").checked
-          };
-        },
-      }).then((result) => {
-        const value = result.value;
-        if (result.isDenied) {
-          fetch("/transaction/", {
-            method: "DELETE",
-            headers: {"X-CSRFToken": '{{ csrf_token }}'},
-            body: JSON.stringify(value)
-          }).then(() => calendar.refetchEvents());
-          return
-        }
-        if (!result.isConfirmed) return;
-        if (MINT_DEBUG) console.log(value);
-        const event = {
-          id: value.id,
-          title: `${value.title} (\$${value.amount})`,
-          start: new Date(value.date),
-          allDay: true,
-          extendedProps: {
-            title: value.title,
-            amount: value.amount * 100,
-            recurrence: value.recurrence,
-            week: value.week,
-            income: value.income
-          }
-        };
-        if (event.id !== "") {
-          fetch("/transaction/", {
-            method: "PATCH",
-            headers: {"X-CSRFToken": '{{ csrf_token }}'},
-            body: JSON.stringify(event)
-          }).then(() => calendar.refetchEvents());
-          if (MINT_DEBUG) console.log('Event Modified:', event);
-          return;
-        }
-        fetch("/transaction/", {
-          method: "POST",
-          headers: {"X-CSRFToken": '{{ csrf_token }}'},
-          body: JSON.stringify(event)
-        }).then(() => calendar.refetchEvents());
-        if (MINT_DEBUG) console.log('Event Added:', event);
-      });
-    }
-
-    function toast(message, level = "error", duration = 5) {
-      const Toast = Swal.mixin({
-        position: "top-end",
-        showConfirmButton: false,
-        timer: duration * 1000,
-        timerProgressBar: true,
-        didOpen: (toast) => {
-          toast.addEventListener('click', () => Swal.close());
-          toast.onmouseenter = Swal.stopTimer;
-          toast.onmouseleave = Swal.resumeTimer;
-        }
-      });
-      Toast.fire({
-        icon: level,
-        title: message
-      });
-    }
-
-    function ordinal(i = 0) {
-      const suffixes = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"];
-      i %= 100;
-      if (suffixes.includes(i)) return `${i}th`;
-      return `${i}${suffixes[i % 10]}`;
-    }
-
-    function dayOfWeek(i = 0) {
-      return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][i];
-    }
-  </script>
+    <meta charset='utf-8'>
+    <script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.18/index.global.min.js'></script>
+    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
+    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
+    {{ json_ctx|json_script:"ctx-data" }}
+    <script>const ctxData = JSON.parse(document.getElementById("ctx-data").textContent);</script>
+    <script src="{% static 'js/mint.js' %}"></script>
+    <link href="{% static 'css/mint.css' %}" rel="stylesheet"/>
 </head>
 
 <body>
-  <div id='calendar' class='max-w-[90vw] max-h-[90vh] m-[40px_auto]'></div>
+<div id='calendar' class='max-w-[90vw] max-h-[90vh] m-[40px_auto]'></div>
 </body>
 
 </html>
\ No newline at end of file
M budget/views.py -> budget/views.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
diff --git a/budget/views.py b/budget/views.py
index 5315950bfc6d0f7e93c476940891e0cd3ba881ed..567b95c0743eefde7782bb8dded67443a38bf530 100644
--- a/budget/views.py
+++ b/budget/views.py
@@ -1,5 +1,7 @@
+from django.conf import settings
 from django.shortcuts import render
 from django.contrib.auth.mixins import LoginRequiredMixin
+from django.urls import reverse
 from django.views import View
 from django.http import HttpRequest, JsonResponse, HttpResponse
 from datetime import date, datetime, timedelta
@@ -18,7 +20,13 @@
 
 class IndexView(LoginRequiredMixin, View):
     def get(self, request: HttpRequest) -> HttpResponse:
-        return render(request, "budget/index.html")
+        return render(request, "budget/index.html", {
+            "json_ctx": {
+                "debug": settings.DEBUG,
+                "adminIndex": reverse("admin:index"),
+                "showAdmin": request.user.is_staff,
+            }
+        })
 
 
 class EventsView(LoginRequiredMixin, View):
M mint/settings.py -> mint/settings.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff --git a/mint/settings.py b/mint/settings.py
index ae48cdb188e0ab93d2ccf62afec7302f6d082af5..69fdc4ef6cb498d9d5cced5a9a333b5d95419e8f 100644
--- a/mint/settings.py
+++ b/mint/settings.py
@@ -11,7 +11,7 @@ SECRET_KEY = env.str("SECRET_KEY")
 
 DEBUG = env.bool("DEBUG", False)
 
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ["localhost", "dev.jolheiser.com"]
 
 INSTALLED_APPS = [
     "django.contrib.admin",
@@ -73,6 +73,9 @@
 USE_TZ = True
 
 STATIC_URL = "static/"
+STATICFILES_DIRS = [
+    BASE_DIR / "static",
+]
 
 DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
 
M mint/urls.py -> mint/urls.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/mint/urls.py b/mint/urls.py
index e11f05a4192066bb9c2d5df3d92fe1ad140c8498..09744c736b23f5c2b659a663c485341520ca8d2b 100644
--- a/mint/urls.py
+++ b/mint/urls.py
@@ -1,5 +1,6 @@
 
 from django.conf import settings
+from django.conf.urls.static import static
 from django.contrib import admin
 from django.urls import path, include
 from budget.views import IndexView, EventsView, TransactionView, CopyView
@@ -12,7 +13,7 @@     path("copy/", CopyView.as_view(), name="copy"),
 
     path("admin/", admin.site.urls),
     path("oidc/", include("mozilla_django_oidc.urls")),
-]
+] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
 
 if settings.DEBUG:
     try:
I static/css/mint.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
diff --git a/static/css/mint.css b/static/css/mint.css
new file mode 100644
index 0000000000000000000000000000000000000000..48a17b5bf3b3d5a98846c48b0983145f72a76175
--- /dev/null
+++ b/static/css/mint.css
@@ -0,0 +1,10 @@
+@media screen {
+
+    .fc-view-harness {
+        height: 90vh !important;
+    }
+
+    .fc-scrollgrid-sync-table {
+        height: 84vh !important;
+    }
+}
\ No newline at end of file
I static/js/mint.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
diff --git a/static/js/mint.js b/static/js/mint.js
new file mode 100644
index 0000000000000000000000000000000000000000..66f8b5434011a2785f328be054488898270a5d40
--- /dev/null
+++ b/static/js/mint.js
@@ -0,0 +1,237 @@
+let calendar;
+const csrftoken = getCookie('csrftoken');
+
+document.addEventListener('DOMContentLoaded', function () {
+  const calendarEl = document.getElementById('calendar');
+  calendar = new FullCalendar.Calendar(calendarEl, {
+    initialView: 'dayGridMonth',
+    dayMaxEventRows: true,
+    selectable: true,
+    selectLongPressDelay: 500,
+    eventStartEditable: true,
+    navLinks: true,
+    events: "/events/",
+    customButtons: {
+      admin: {
+        text: "admin",
+        hint: "Django Admin",
+        click: () => location.href = ctxData.adminIndex,
+      },
+      copy: {
+        text: "copy",
+        hint: "Copy budget month-to-month",
+        click: () => copy(),
+      }
+    },
+    headerToolbar: {
+      left: `prev,next today copy${ctxData.showAdmin ? " admin" : ""}`,
+      center: 'title',
+      right: 'dayGridWeek,dayGridMonth,dayGridYear'
+    },
+    navLinkDayClick: (date) => input(date),
+    select: (info) => select(info),
+    eventDrop: (info) => eventDrop(info),
+    eventClick: (info) => eventClick(info),
+  });
+  calendar.render();
+});
+
+function copy() {
+  Swal.fire({
+    title: "Are you sure?",
+    html: "This will copy <strong>all</strong> budget items from last month.",
+    icon: "warning",
+    allowOutsideClick: false,
+    showDenyButton: true,
+    showCloseButton: true,
+    confirmButtonText: "Yes",
+    reverseButtons: true,
+    showLoaderOnConfirm: true,
+    preConfirm: () => {
+      fetch("/copy/", {
+        method: "POST",
+        headers: {"X-CSRFToken": csrftoken},
+        body: JSON.stringify({"month": calendar.getDate().getMonth() + 1}),
+      }).then(() => calendar.refetchEvents());
+    }
+  });
+}
+
+function select(info) {
+  if (ctxData.debug) console.log(info);
+  const events = calendar.getEvents().filter((event) => {
+    const start = event.start;
+    const end = event.end || event.start;
+    return start < info.end && end >= info.start
+  });
+  if (ctxData.debug) console.log(events);
+  const sum = events.filter((event) => !event.extendedProps.income).reduce((acc, event) => acc + event.extendedProps.amount, 0);
+  toast(`\$${sum / 100}`, 'info')
+}
+
+function eventDrop(info) {
+  const event = info.event;
+  const props = event.extendedProps;
+  fetch("/transaction/", {
+    method: "PATCH",
+    headers: {"X-CSRFToken": csrftoken},
+    body: JSON.stringify({
+      id: event.id,
+      start: event.start,
+      extendedProps: {
+        title: props.title,
+        amount: props.amount,
+        recurrence: props.recurrence,
+        week: props.week,
+        income: props.income
+      }
+    })
+  }).then(() => calendar.refetchEvents());
+}
+
+function eventClick(info) {
+  const event = info.event;
+  const props = event.extendedProps;
+  input(event.start, event.id, props.title, props.amount / 100, props.recurrence, props.week, props.income);
+}
+
+function input(time, id = '', title = 'New Transaction', amount = 0, recurrence = "M", week = 1, income = false) {
+  const weekDay = time.getDay();
+  const monthDay = time.getDate();
+  title = title.replace(/"/g, "&quot;")
+  Swal.fire({
+    title: `<input type="text" class="font-semibold" name="title" value="${title}"/>`,
+    html: `
+          <div class="grid grid-cols-1 gap-x-4 gap-y-3 items-center text-left">
+            <input type="hidden" name="id" value="${id}"/>
+            <input type="hidden" name="date" value="${time.toISOString()}"/>
+            <label class="font-semibold">Amount: $<input class="border rounded w-30 mx-px p-1" type="number" name="amount" value="${amount}"/></label>
+            <label class="font-semibold">Recurring? <input class="border rounded w-4 h-4" type="checkbox" name="recurring" ${recurrence !== "" ? "checked" : ""}/></label>
+            <div id="recurringOptions" class="${recurrence !== "" ? "" : "hidden"}">
+              <label><input type="radio" name="recurrence" ${recurrence === "M" ? "checked" : ""} value="M"/> Monthly on the ${ordinal(monthDay)}</label>
+              <br/>
+              <label><input type="radio" name="recurrence" ${recurrence === "W" ? "checked" : ""} value="W"/> Every <input name="week" class="border rounded w-15 p-1" type="number" min="1" max="4" value="${week}"/> weeks on ${dayOfWeek(weekDay)}</label>
+            </div>
+            <label class="font-semibold">Income? <input class="border rounded w-4 h-4" type="checkbox" name="income" ${income ? "checked" : ""}/></label>
+          </div>
+        `,
+    allowOutsideClick: false,
+    showCloseButton: true,
+    reverseButtons: true,
+    confirmButtonText: 'Save',
+    showDenyButton: id !== '',
+    denyButtonText: 'Delete',
+    customClass: {
+      confirmButton: 'ml-[15rem]!'
+    },
+    didRender: () => {
+      const $recurring = document.querySelector("[name='recurring']");
+      const $opts = document.getElementById("recurringOptions");
+      $recurring.addEventListener('change', () => {
+        $recurring.checked ? $opts.classList.remove('hidden') : $opts.classList.add('hidden');
+      });
+    },
+    preDeny: () => {
+      const $popup = Swal.getPopup();
+      return {
+        id: $popup.querySelector("[name='id']").value
+      };
+    },
+    preConfirm: () => {
+      const $popup = Swal.getPopup();
+      return {
+        id: $popup.querySelector("[name='id']").value,
+        date: new Date($popup.querySelector("[name='date']").value),
+        title: $popup.querySelector("[name='title']").value,
+        amount: parseFloat($popup.querySelector("[name='amount']").value),
+        recurrence: $popup.querySelector("[name='recurring']").checked ? $popup.querySelector("[name='recurrence']:checked").value : '',
+        week: $popup.querySelector("[name='week']").value,
+        income: $popup.querySelector("[name='income']").checked
+      };
+    },
+  }).then((result) => {
+    const value = result.value;
+    if (result.isDenied) {
+      fetch("/transaction/", {
+        method: "DELETE",
+        headers: {"X-CSRFToken": csrftoken},
+        body: JSON.stringify(value)
+      }).then(() => calendar.refetchEvents());
+      return
+    }
+    if (!result.isConfirmed) return;
+    if (ctxData.debug) console.log(value);
+    const event = {
+      id: value.id,
+      title: `${value.title} (\$${value.amount})`,
+      start: new Date(value.date),
+      allDay: true,
+      extendedProps: {
+        title: value.title,
+        amount: value.amount * 100,
+        recurrence: value.recurrence,
+        week: value.week,
+        income: value.income
+      }
+    };
+    if (event.id !== "") {
+      fetch("/transaction/", {
+        method: "PATCH",
+        headers: {"X-CSRFToken": csrftoken},
+        body: JSON.stringify(event)
+      }).then(() => calendar.refetchEvents());
+      if (ctxData.debug) console.log('Event Modified:', event);
+      return;
+    }
+    fetch("/transaction/", {
+      method: "POST",
+      headers: {"X-CSRFToken": csrftoken},
+      body: JSON.stringify(event)
+    }).then(() => calendar.refetchEvents());
+    if (ctxData.debug) console.log('Event Added:', event);
+  });
+}
+
+function toast(message, level = "error", duration = 5) {
+  const Toast = Swal.mixin({
+    position: "top-end",
+    showConfirmButton: false,
+    timer: duration * 1000,
+    timerProgressBar: true,
+    didOpen: (toast) => {
+      toast.addEventListener('click', () => Swal.close());
+      toast.onmouseenter = Swal.stopTimer;
+      toast.onmouseleave = Swal.resumeTimer;
+    }
+  });
+  Toast.fire({
+    icon: level,
+    title: message
+  });
+}
+
+function ordinal(i = 0) {
+  const suffixes = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"];
+  i %= 100;
+  if ([11, 12, 13].includes(i)) return `${i}th`;
+  return `${i}${suffixes[i % 10]}`;
+}
+
+function dayOfWeek(i = 0) {
+  return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][i];
+}
+
+function getCookie(name) {
+  let cookieValue = null;
+  if (document.cookie && document.cookie !== '') {
+    const cookies = document.cookie.split(';');
+    for (let i = 0; i < cookies.length; i++) {
+      const cookie = cookies[i].trim();
+      if (cookie.substring(0, name.length + 1) === (name + '=')) {
+        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+        break;
+      }
+    }
+  }
+  return cookieValue;
+}
\ No newline at end of file