mirror of
https://github.com/peter-tanner/money-manager.git
synced 2024-11-30 14:20:17 +08:00
TODO system added with notifications
This commit is contained in:
parent
33eb31f86e
commit
c331deb49e
|
@ -5,7 +5,15 @@ from django.contrib import admin
|
||||||
from util import next_payday, rgb_tuple_to_hex
|
from util import next_payday, rgb_tuple_to_hex
|
||||||
|
|
||||||
from .admin_base import AdminBase, DeletedListFilter, DeletableAdminForm
|
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 import forms
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from import_export import resources
|
from import_export import resources
|
||||||
|
@ -14,6 +22,49 @@ from admincharts.admin import AdminChartMixin
|
||||||
from admincharts.utils import months_between_dates
|
from admincharts.utils import months_between_dates
|
||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
from django.utils.html import format_html
|
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(
|
||||||
|
# "<button class='button' onclick=`doSomething({})`>+</button>",
|
||||||
|
# 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):
|
class LogsAdminForm(DeletableAdminForm):
|
||||||
|
|
54
expenses/migrations/0010_todo_historicaltodo.py
Normal file
54
expenses/migrations/0010_todo_historicaltodo.py
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -31,6 +31,16 @@ from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
# actions = [soft_delete_selected]
|
# 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):
|
class Log(DeletableModel):
|
||||||
# TODO: Use Markdown formatting!
|
# TODO: Use Markdown formatting!
|
||||||
title = models.CharField(max_length=256, blank=True)
|
title = models.CharField(max_length=256, blank=True)
|
||||||
|
|
122
expenses/static/js/todo.js
Normal file
122
expenses/static/js/todo.js
Normal file
|
@ -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);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user