From c331deb49ec9c7578d7c45f1774f814f1b87970c Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 8 Oct 2023 19:17:20 +0800 Subject: [PATCH] TODO system added with notifications --- expenses/admin.py | 53 +++++++- .../migrations/0010_todo_historicaltodo.py | 54 ++++++++ expenses/models.py | 10 ++ expenses/static/js/todo.js | 122 ++++++++++++++++++ 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 expenses/migrations/0010_todo_historicaltodo.py create mode 100644 expenses/static/js/todo.js diff --git a/expenses/admin.py b/expenses/admin.py index 9a1ece6..e57d4d1 100644 --- a/expenses/admin.py +++ b/expenses/admin.py @@ -5,7 +5,15 @@ from django.contrib import admin from util import next_payday, rgb_tuple_to_hex from .admin_base import AdminBase, DeletedListFilter, DeletableAdminForm -from .models import Log, Expense, ExpenseCategory, Timesheet, TimesheetRate, Vendor +from .models import ( + Log, + Expense, + ExpenseCategory, + Timesheet, + TimesheetRate, + Todo, + Vendor, +) from django import forms from django.utils import timezone from import_export import resources @@ -14,6 +22,49 @@ from admincharts.admin import AdminChartMixin from admincharts.utils import months_between_dates from djmoney.contrib.exchange.models import convert_money from django.utils.html import format_html +from django.db.models import F + + +class TodosAdminForm(DeletableAdminForm): + class Meta: + model = Todo + fields = "__all__" + + +@admin.register(Todo) +class TodosAdmin(AdminBase): + class Media: + js = ("js/todo.js",) + + # TODO: add stats bars for resolved/completed reports + list_display = ( + "title", + "due_date", + "priority", + # "bump_priority", + ) + search_fields = ( + "title", + "due_date", + "priority", + ) + ordering = ( + F("due_date").asc(nulls_last=True), + "-priority", + ) + form = TodosAdminForm + + # def bump_priority(self, obj): + # return format_html( + # "", + # obj.id, + # obj.id, + # ) + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + extra_context = {"title": "SHIT TODO"} + return super().changelist_view(request, extra_context=extra_context) class LogsAdminForm(DeletableAdminForm): diff --git a/expenses/migrations/0010_todo_historicaltodo.py b/expenses/migrations/0010_todo_historicaltodo.py new file mode 100644 index 0000000..997370e --- /dev/null +++ b/expenses/migrations/0010_todo_historicaltodo.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.5 on 2023-10-08 10:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('expenses', '0009_historicallog_goodness_value_log_goodness_value_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Todo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('title', models.CharField(blank=True, max_length=256)), + ('description', models.TextField(blank=True)), + ('due_date', models.DateTimeField(blank=True, null=True)), + ('priority', models.IntegerField(default=0)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='HistoricalTodo', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('deleted', models.BooleanField(default=False)), + ('title', models.CharField(blank=True, max_length=256)), + ('description', models.TextField(blank=True)), + ('due_date', models.DateTimeField(blank=True, null=True)), + ('priority', models.IntegerField(default=0)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical todo', + 'verbose_name_plural': 'historical todos', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/expenses/models.py b/expenses/models.py index 4314c3e..07cc0ba 100644 --- a/expenses/models.py +++ b/expenses/models.py @@ -31,6 +31,16 @@ from django.core.validators import MaxValueValidator, MinValueValidator # actions = [soft_delete_selected] +class Todo(DeletableModel): + title = models.CharField(max_length=256, blank=True) + description = models.TextField(blank=True) + due_date = models.DateTimeField(null=True, blank=True) + priority = models.IntegerField(default=0) + + def __str__(self): + return f"{self.title} [{self.due_date}]" + + class Log(DeletableModel): # TODO: Use Markdown formatting! title = models.CharField(max_length=256, blank=True) diff --git a/expenses/static/js/todo.js b/expenses/static/js/todo.js new file mode 100644 index 0000000..fc92920 --- /dev/null +++ b/expenses/static/js/todo.js @@ -0,0 +1,122 @@ +const createNotification = (title, body) => { + // Check if the browser supports the Web Notifications API + if ("Notification" in window) { + // Request permission to show notifications + Notification.requestPermission().then(function (permission) { + if (permission === "granted") { + // Create a notification + var notification = new Notification(title, { + body: body, + }); + + // You can add event listeners to handle user interactions with the notification + notification.onclick = function () { + console.log("Notification clicked"); + // Add your code to handle the click event here + }; + + notification.onclose = function () { + console.log("Notification closed"); + // Add your code to handle the notification close event here + }; + } else { + console.alert("Notification permission denied"); + } + }); + } else { + console.alert("Web Notifications API not supported in this browser"); + } +}; + +function parseDateString(dateString) { + // Remove "p.m." and split the date string + const parts = dateString.replace("p.m.", "").split(/[\s,]+/); + + // Map month abbreviation to month number + const months = { + "Jan.": 0, + "Feb.": 1, + "Mar.": 2, + "Apr.": 3, + "May.": 4, + "Jun.": 5, + "Jul.": 6, + "Aug.": 7, + "Sep.": 8, + "Oct.": 9, + "Nov.": 10, + "Dec.": 11, + }; + + // Extract date components + const month = months[parts[0]]; + const day = parseInt(parts[1], 10); + const year = parseInt(parts[2], 10); + const hour = parseInt(parts[3].split(":")[0], 10); + const minute = parseInt(parts[3].split(":")[1], 10); + + // Create the Date object + const dateObject = new Date(year, month, day, hour, minute); + + return dateObject; +} + +const alerted = new Set(); + +document.addEventListener("DOMContentLoaded", function () { + // NOTE: + // This is to stop me from accidentally clicking away from the TODO list and disabling notifications! + // You can still use the top links to navigate away + const navSidebar = document.getElementById("nav-sidebar"); + const toggleNavSidebar = document.getElementById("toggle-nav-sidebar"); + navSidebar ? navSidebar.parentNode.removeChild(navSidebar) : undefined; + toggleNavSidebar + ? toggleNavSidebar.parentNode.removeChild(toggleNavSidebar) + : undefined; + + function checkDueDates() { + const rows = document.querySelectorAll("#result_list tbody tr"); + const currentDate = new Date(); + + rows.forEach(function (row) { + const titleCell = row.querySelector("th.field-title a"); // Select the due_date cell + const title = titleCell ? titleCell.textContent : "??"; + const dueDateCell = row.querySelector("td.field-due_date"); // Select the due_date cell + const dueDateValue = dueDateCell.textContent.trim(); // Get the due date value + + // Check if the due date cell contains a valid date + if (dueDateValue !== "-") { + var dueDate = parseDateString(dueDateValue); + + // Calculate the difference in days between the due date and current date + var timeDifference = dueDate - currentDate; + var daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + + // Check if the due date is in 7 days + if ( + daysDifference <= 7 && + daysDifference >= 0 && + !alerted.has(title + dueDate) + ) { + alerted.add(title + dueDate); + // alert( + // `Task '${ + // row.querySelector("th.field-title a").textContent + // }' is due in ${daysDifference} days.` + // ); + createNotification( + `Task '${ + row.querySelector("th.field-title a").textContent + }' is due in ${daysDifference} days.`, + "Finish this shit already!" + ); + } + } + }); + } + const resetAlerted = () => alerted.clear(); + checkDueDates(); + // Run the checkDueDates function every 5 seconds (adjust the interval as needed) + setInterval(checkDueDates, 5000); + setInterval(resetAlerted, 6 * 60 * 60 * 1000); +});