mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-14 00:32:46 +02:00
feat: add calendar view
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarViewConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "apps.calendar_view"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("calendar/", views.index, name="calendar_index"),
|
||||||
|
path(
|
||||||
|
"calendar/<int:month>/<int:year>/list/",
|
||||||
|
views.calendar_list,
|
||||||
|
name="calendar_list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"calendar/<int:day>/<int:month>/<int:year>/transactions/",
|
||||||
|
views.calendar_transactions_list,
|
||||||
|
name="calendar_transactions_list",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"calendar/<int:month>/<int:year>/",
|
||||||
|
views.calendar,
|
||||||
|
name="calendar",
|
||||||
|
),
|
||||||
|
# path(
|
||||||
|
# "calendar/available_dates/",
|
||||||
|
# views.month_year_picker,
|
||||||
|
# name="available_dates",
|
||||||
|
# ),
|
||||||
|
]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from datetime import datetime, date
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
# def get_transactions_by_day(year, month):
|
||||||
|
# # Get all transactions for the month
|
||||||
|
# transactions = Transaction.objects.filter(
|
||||||
|
# date__year=year, date__month=month
|
||||||
|
# ).order_by("date")
|
||||||
|
#
|
||||||
|
# # Create a dictionary with all days of the month
|
||||||
|
# all_days = {
|
||||||
|
# day: {"day": day, "date": date(year, month, day), "transactions": []}
|
||||||
|
# for day in range(1, calendar.monthrange(year, month)[1] + 1)
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Group transactions by day
|
||||||
|
# for transaction in transactions:
|
||||||
|
# day = transaction.date.day
|
||||||
|
# all_days[day]["transactions"].append(transaction)
|
||||||
|
#
|
||||||
|
# # Convert to list and sort by day
|
||||||
|
# result = list(all_days.values())
|
||||||
|
#
|
||||||
|
# return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_transactions_by_day(year, month):
|
||||||
|
# Configure calendar to start on Monday
|
||||||
|
calendar.setfirstweekday(calendar.MONDAY)
|
||||||
|
|
||||||
|
# Get the first and last day of the month
|
||||||
|
first_day = date(year, month, 1)
|
||||||
|
|
||||||
|
# Get all transactions for the month
|
||||||
|
transactions = Transaction.objects.filter(
|
||||||
|
date__year=year, date__month=month
|
||||||
|
).order_by("date")
|
||||||
|
|
||||||
|
# Calculate padding days needed
|
||||||
|
padding_days = first_day.weekday() # Monday is 0, Sunday is 6
|
||||||
|
|
||||||
|
# Create padding days as empty dicts
|
||||||
|
padding_dates = [{}] * padding_days
|
||||||
|
|
||||||
|
# Create current month days
|
||||||
|
current_month_dates = [
|
||||||
|
{"day": day, "date": date(year, month, day), "transactions": []}
|
||||||
|
for day in range(1, calendar.monthrange(year, month)[1] + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Group transactions by day
|
||||||
|
for transaction in transactions:
|
||||||
|
day = transaction.date.day
|
||||||
|
for day_data in current_month_dates:
|
||||||
|
if day_data["day"] == day:
|
||||||
|
day_data["transactions"].append(transaction)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Combine padding and current month dates
|
||||||
|
result = padding_dates + current_month_dates
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
|
from apps.calendar_view.utils.calendar import get_transactions_by_day
|
||||||
|
from apps.transactions.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def index(request):
|
||||||
|
now = timezone.localdate(timezone.now())
|
||||||
|
|
||||||
|
return redirect(to="calendar", month=now.month, year=now.year)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def calendar(request, month: int, year: int):
|
||||||
|
if month < 1 or month > 12:
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
|
raise Http404("Month is out of range")
|
||||||
|
|
||||||
|
next_month = 1 if month == 12 else month + 1
|
||||||
|
next_year = year + 1 if next_month == 1 and month == 12 else year
|
||||||
|
|
||||||
|
previous_month = 12 if month == 1 else month - 1
|
||||||
|
previous_year = year - 1 if previous_month == 12 and month == 1 else year
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"calendar_view/pages/calendar.html",
|
||||||
|
context={
|
||||||
|
"month": month,
|
||||||
|
"year": year,
|
||||||
|
"next_month": next_month,
|
||||||
|
"next_year": next_year,
|
||||||
|
"previous_month": previous_month,
|
||||||
|
"previous_year": previous_year,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def calendar_list(request, month: int, year: int):
|
||||||
|
today = timezone.localdate(timezone.now())
|
||||||
|
dates = get_transactions_by_day(month=month, year=year)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"calendar_view/fragments/list.html",
|
||||||
|
context={
|
||||||
|
"month": month,
|
||||||
|
"year": year,
|
||||||
|
"today": today,
|
||||||
|
"dates": dates,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def calendar_transactions_list(request, day: int, month: int, year: int):
|
||||||
|
date = datetime.date(year=year, month=month, day=day)
|
||||||
|
transactions = Transaction.objects.filter(date=date)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"calendar_view/fragments/list_transactions.html",
|
||||||
|
context={
|
||||||
|
"date": date,
|
||||||
|
"transactions": transactions,
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{% load natural %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="tw-hidden lg:tw-grid lg:tw-grid-cols-7 tw-gap-4 lg:tw-gap-0">
|
||||||
|
<div class="border-start border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'MON' %}
|
||||||
|
</div>
|
||||||
|
<div class="border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'TUE' %}
|
||||||
|
</div>
|
||||||
|
<div class="border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'WED' %}
|
||||||
|
</div>
|
||||||
|
<div class="border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'THU' %}
|
||||||
|
</div>
|
||||||
|
<div class="border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'FRI' %}
|
||||||
|
</div>
|
||||||
|
<div class="border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'SAT' %}
|
||||||
|
</div>
|
||||||
|
<div class="border-end border-top border-bottom p-2 text-center">
|
||||||
|
{% translate 'SUN' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-grid tw-grid-cols-1 tw-grid-rows-1 lg:tw-grid-cols-7 lg:tw-grid-rows-6 tw-gap-4 lg:tw-gap-0">
|
||||||
|
{% for date in dates %}
|
||||||
|
{% if date %}
|
||||||
|
<div class="card h-100 hover:tw-bg-zinc-900 rounded-0{% if not date.transactions %} !tw-hidden lg:!tw-flex{% endif %}{% if today == date.date %} tw-border-yellow-300 border-primary{% endif %} " role="button"
|
||||||
|
hx-get="{% url 'calendar_transactions_list' day=date.date.day month=date.date.month year=date.date.year %}"
|
||||||
|
hx-target="#persistent-generic-offcanvas-left">
|
||||||
|
<div class="card-header border-0 bg-transparent text-end tw-flex justify-content-between p-2 !tw-text-gray-400 w-100">
|
||||||
|
<div class="lg:tw-hidden text-start w-100">{{ date.date|date:"l"|lower }}</div>
|
||||||
|
<div class="text-end w-100">{{ date.day }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
{% for transaction in date.transactions %}
|
||||||
|
{% if transaction.is_paid %}
|
||||||
|
{% if transaction.type == "IN" and not transaction.account.is_asset %}
|
||||||
|
<i class="fa-solid fa-circle-check tw-text-green-400"></i>
|
||||||
|
{% elif transaction.type == "IN" and transaction.account.is_asset %}
|
||||||
|
<i class="fa-solid fa-circle-check tw-text-green-300"></i>
|
||||||
|
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
|
||||||
|
<i class="fa-solid fa-circle-check tw-text-red-400"></i>
|
||||||
|
{% elif transaction.type == "EX" and transaction.account.is_asset %}
|
||||||
|
<i class="fa-solid fa-circle-check tw-text-red-300"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if transaction.type == "IN" and not transaction.account.is_asset %}
|
||||||
|
<i class="fa-regular fa-circle tw-text-green-400"></i>
|
||||||
|
{% elif transaction.type == "IN" and transaction.account.is_asset %}
|
||||||
|
<i class="fa-regular fa-circle tw-text-green-300"></i>
|
||||||
|
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
|
||||||
|
<i class="fa-regular fa-circle tw-text-red-400"></i>
|
||||||
|
{% elif transaction.type == "EX" and transaction.account.is_asset %}
|
||||||
|
<i class="fa-regular fa-circle tw-text-green-300"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="!tw-hidden lg:!tw-block card h-100 rounded-0"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends 'extends/offcanvas.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Transactions on' %} {{ date|date:"SHORT_DATE_FORMAT" }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div hx-get="{% url 'calendar_transactions_list' day=date.day month=date.month year=date.year %}" hx-trigger="updated from:window" hx-vals='{"disable_selection": true}' hx-target="closest .offcanvas" class="show-loading">
|
||||||
|
{% for transaction in transactions %}
|
||||||
|
<c-transaction.item
|
||||||
|
:transaction="transaction"
|
||||||
|
:disable-selection="True"></c-transaction.item>
|
||||||
|
{% empty %}
|
||||||
|
<c-msg.empty
|
||||||
|
title="{% translate 'No transactions on this date' %}"></c-msg.empty>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load month_name %}
|
||||||
|
{% load static %}
|
||||||
|
{% load webpack_loader %}
|
||||||
|
|
||||||
|
{% block title %}{% translate 'Monthly Overview' %} :: {{ month|month_name }}/{{ year }}{% endblock %}
|
||||||
|
|
||||||
|
{% block body_hyperscript %}
|
||||||
|
on keyup[code is 'ArrowLeft' and target.nodeName is 'BODY'] from body trigger 'previous_month' end
|
||||||
|
on keyup[code is 'ArrowRight' and target.nodeName is 'BODY'] from body trigger 'next_month' end
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container px-md-3 py-3 column-gap-5">
|
||||||
|
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
|
||||||
|
{# Date picker#}
|
||||||
|
<div class="col-12 col-xl-4 flex-row align-items-center d-flex">
|
||||||
|
<div class="tw-text-base h-100 align-items-center d-flex">
|
||||||
|
<a role="button"
|
||||||
|
class="pe-4 py-2"
|
||||||
|
hx-boost="true"
|
||||||
|
hx-trigger="click, previous_month from:window"
|
||||||
|
href="{% url 'calendar' month=previous_month year=previous_year %}"><i
|
||||||
|
class="fa-solid fa-chevron-left"></i></a>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"
|
||||||
|
hx-get="{% url 'month_year_picker' %}"
|
||||||
|
hx-target="#generic-offcanvas-left"
|
||||||
|
hx-trigger="click, date_picker from:window"
|
||||||
|
hx-vals='{"month": {{ month }}, "year": {{ year }}, "for": "calendar", "field": "date"}' role="button">
|
||||||
|
{{ month|month_name }} {{ year }}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
|
||||||
|
<a role="button"
|
||||||
|
class="ps-3 py-2"
|
||||||
|
hx-boost="true"
|
||||||
|
hx-trigger="click, next_month from:window"
|
||||||
|
href="{% url 'calendar' month=next_month year=next_year %}">
|
||||||
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Action buttons#}
|
||||||
|
<div class="col-12 col-xl-8">
|
||||||
|
<div class="d-grid gap-2 d-xl-flex justify-content-xl-end">
|
||||||
|
<button class="btn btn-sm btn-outline-success"
|
||||||
|
hx-get="{% url 'transaction_add' %}"
|
||||||
|
hx-target="#generic-offcanvas"
|
||||||
|
hx-trigger="click, add_income from:window"
|
||||||
|
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "IN"}'>
|
||||||
|
<i class="fa-solid fa-arrow-right-to-bracket me-2"></i>
|
||||||
|
{% translate "Income" %}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger"
|
||||||
|
hx-get="{% url 'transaction_add' %}"
|
||||||
|
hx-target="#generic-offcanvas"
|
||||||
|
hx-trigger="click, add_expense from:window"
|
||||||
|
hx-vals='{"year": {{ year }}, "month": {{ month }}, "type": "EX"}'>
|
||||||
|
<i class="fa-solid fa-arrow-right-from-bracket me-2"></i>
|
||||||
|
{% translate "Expense" %}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-warning"
|
||||||
|
hx-get="{% url 'installment_plan_add' %}"
|
||||||
|
hx-trigger="click, installment from:window"
|
||||||
|
hx-target="#generic-offcanvas">
|
||||||
|
<i class="fa-solid fa-divide me-2"></i>
|
||||||
|
{% translate "Installment" %}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-warning"
|
||||||
|
hx-get="{% url 'recurring_transaction_add' %}"
|
||||||
|
hx-trigger="click, balance from:window"
|
||||||
|
hx-target="#generic-offcanvas">
|
||||||
|
<i class="fa-solid fa-repeat me-2"></i>
|
||||||
|
{% translate "Recurring" %}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-info"
|
||||||
|
hx-get="{% url 'transactions_transfer' %}"
|
||||||
|
hx-target="#generic-offcanvas"
|
||||||
|
hx-trigger="click, add_transfer from:window"
|
||||||
|
hx-vals='{"year": {{ year }}, "month": {{ month }}}'>
|
||||||
|
<i class="fa-solid fa-money-bill-transfer me-2"></i>
|
||||||
|
{% translate "Transfer" %}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-info"
|
||||||
|
hx-get="{% url 'account_reconciliation' %}"
|
||||||
|
hx-trigger="click, balance from:window"
|
||||||
|
hx-target="#generic-offcanvas">
|
||||||
|
<i class="fa-solid fa-scale-balanced me-2"></i>
|
||||||
|
{% translate "Balance" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="show-loading" hx-get="{% url 'calendar_list' month=month year=year %}" hx-trigger="load, updated from:window, selective_update from:window"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user