diff --git a/CHANGELOG.md b/CHANGELOG.md index d52a5ce..fe89cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Add stats for dropped purchases, monthly playtimes * Allow deleting purchases * Add all-time stats +* Manage purchases ## Improved * mark refunded purchases red on game overview diff --git a/common/input.css b/common/input.css index 113775c..41edc14 100644 --- a/common/input.css +++ b/common/input.css @@ -120,14 +120,6 @@ textarea:disabled { @apply mx-1; } -th { - @apply text-right; -} - -th label { - @apply mr-4; -} - .basic-button-container { @apply flex space-x-2 justify-center; } @@ -170,4 +162,4 @@ th label { @apply inline-block truncate max-w-20char transition-all group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4; } -} */ \ No newline at end of file +} */ diff --git a/games/purchaseviews.py b/games/purchaseviews.py new file mode 100644 index 0000000..c740786 --- /dev/null +++ b/games/purchaseviews.py @@ -0,0 +1,83 @@ +from typing import Any + +from django.contrib.auth.decorators import login_required +from django.db.models.manager import BaseManager +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render +from django.template.loader import render_to_string +from django.urls import reverse + +from games.models import Purchase +from games.views import dateformat + + +@login_required +def list_purchases(request: HttpRequest) -> HttpResponse: + context: dict[Any, Any] = {} + purchases: BaseManager[Purchase] = Purchase.objects.all()[0:10] + context = { + "title": "Manage purchases", + "data": { + "columns": [ + "Name", + "Platform", + "Price", + "Currency", + "Infinite", + "Purchased", + "Refunded", + "Finished", + "Dropped", + "Created", + "Actions", + ], + "rows": [ + [ + purchase.edition.name, + purchase.platform, + purchase.price, + purchase.price_currency, + purchase.infinite, + purchase.date_purchased.strftime(dateformat), + ( + purchase.date_refunded.strftime(dateformat) + if purchase.date_refunded + else "-" + ), + ( + purchase.date_finished.strftime(dateformat) + if purchase.date_finished + else "-" + ), + ( + purchase.date_dropped.strftime(dateformat) + if purchase.date_dropped + else "-" + ), + purchase.created_at.strftime(dateformat), + render_to_string( + "components/button_group_sm.html", + { + "buttons": [ + { + "href": reverse( + "edit_purchase", args=[purchase.pk] + ), + "text": "Edit", + }, + { + "href": reverse( + "delete_purchase", args=[purchase.pk] + ), + "text": "Delete", + "color": "red", + }, + ] + }, + ), + ] + for purchase in purchases + ], + }, + } + return render(request, "list_purchases.html", context) diff --git a/games/static/base.css b/games/static/base.css index 7adf3d9..ee35cb5 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -1659,6 +1659,10 @@ input:checked + .toggle-bg { overflow: hidden; } +.overflow-x-auto { + overflow-x: auto; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -1717,6 +1721,10 @@ input:checked + .toggle-bg { border-width: 0px; } +.border-b { + border-bottom-width: 1px; +} + .border-blue-600 { --tw-border-opacity: 1; border-color: rgb(28 100 242 / var(--tw-border-opacity)); @@ -1762,6 +1770,11 @@ input:checked + .toggle-bg { background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .bg-gray-800 { --tw-bg-opacity: 1; background-color: rgb(31 41 55 / var(--tw-bg-opacity)); @@ -1822,6 +1835,11 @@ input:checked + .toggle-bg { padding-right: 1.25rem; } +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; @@ -1842,6 +1860,11 @@ input:checked + .toggle-bg { padding-bottom: 0.75rem; } +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + .pb-16 { padding-bottom: 4rem; } @@ -1866,6 +1889,10 @@ input:checked + .toggle-bg { padding-top: 2rem; } +.text-left { + text-align: left; +} + .text-center { text-align: center; } @@ -1943,6 +1970,10 @@ input:checked + .toggle-bg { font-weight: 600; } +.uppercase { + text-transform: uppercase; +} + .leading-6 { line-height: 1.5rem; } @@ -2256,13 +2287,13 @@ textarea:disabled:is(.dark *) { margin-right: 0.25rem; } -th { - text-align: right; -} +/* th { + @apply text-right; +} */ -th label { - margin-right: 1rem; -} +/* th label { + @apply mr-4; +} */ .basic-button-container { display: flex; @@ -2366,11 +2397,26 @@ th label { } } */ +.odd\:bg-white:nth-child(odd) { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.even\:bg-gray-50:nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .hover\:border-gray-300:hover { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.hover\:border-green-600:hover { + --tw-border-opacity: 1; + border-color: rgb(5 122 85 / var(--tw-border-opacity)); +} + .hover\:bg-blue-800:hover { --tw-bg-opacity: 1; background-color: rgb(30 66 159 / var(--tw-bg-opacity)); @@ -2386,6 +2432,16 @@ th label { background-color: rgb(156 163 175 / var(--tw-bg-opacity)); } +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.hover\:bg-green-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(14 159 110 / var(--tw-bg-opacity)); +} + .hover\:bg-green-700:hover { --tw-bg-opacity: 1; background-color: rgb(4 108 78 / var(--tw-bg-opacity)); @@ -2396,6 +2452,11 @@ th label { background-color: rgb(253 232 232 / var(--tw-bg-opacity)); } +.hover\:bg-red-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(240 82 82 / var(--tw-bg-opacity)); +} + .hover\:bg-violet-700:hover { --tw-bg-opacity: 1; background-color: rgb(109 40 217 / var(--tw-bg-opacity)); @@ -2426,6 +2487,11 @@ th label { color: rgb(17 24 39 / var(--tw-text-opacity)); } +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + .hover\:underline:hover { text-decoration-line: underline; } @@ -2476,6 +2542,11 @@ th label { --tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity)); } +.focus\:ring-green-700:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(4 108 78 / var(--tw-ring-opacity)); +} + .focus\:ring-violet-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity)); @@ -2654,6 +2725,26 @@ th label { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.odd\:dark\:bg-gray-900:is(.dark *):nth-child(odd) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.even\:dark\:bg-gray-800:is(.dark *):nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.dark\:hover\:border-green-700:hover:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(4 108 78 / var(--tw-border-opacity)); +} + +.dark\:hover\:border-red-700:hover:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(200 30 30 / var(--tw-border-opacity)); +} + .dark\:hover\:bg-blue-700:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(26 86 219 / var(--tw-bg-opacity)); @@ -2674,6 +2765,11 @@ th label { background-color: rgb(31 41 55 / var(--tw-bg-opacity)); } +.dark\:hover\:bg-green-600:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(5 122 85 / var(--tw-bg-opacity)); +} + .dark\:hover\:bg-red-700:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(200 30 30 / var(--tw-bg-opacity)); @@ -2704,6 +2800,11 @@ th label { --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); } +.dark\:focus\:ring-green-500:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:inline { display: inline; @@ -2721,6 +2822,10 @@ th label { max-width: 36rem; } + .sm\:rounded-lg { + border-radius: 0.5rem; + } + .sm\:px-4 { padding-left: 1rem; padding-right: 1rem; @@ -2792,4 +2897,46 @@ th label { .rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 1; -} \ No newline at end of file +} + +.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) { + text-align: right; +} + +.\[\&\:first-of-type_button\]\:rounded-s-lg:first-of-type button { + border-start-start-radius: 0.5rem; + border-end-start-radius: 0.5rem; +} + +.\[\&\:last-of-type_button\]\:rounded-e-lg:last-of-type button { + border-start-end-radius: 0.5rem; + border-end-end-radius: 0.5rem; +} + +.\[\&_td\:first-of-type\]\:whitespace-nowrap td:first-of-type { + white-space: nowrap; +} + +.\[\&_td\:first-of-type\]\:px-6 td:first-of-type { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.\[\&_td\:first-of-type\]\:py-4 td:first-of-type { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.\[\&_td\:first-of-type\]\:font-medium td:first-of-type { + font-weight: 500; +} + +.\[\&_td\:first-of-type\]\:text-gray-900 td:first-of-type { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.\[\&_td\:first-of-type\]\:dark\:text-white:is(.dark *) td:first-of-type { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} diff --git a/games/templates/components.yml b/games/templates/components.yml index 4a5d35c..a9528a4 100644 --- a/games/templates/components.yml +++ b/games/templates/components.yml @@ -1,3 +1,9 @@ components: gamelink: "components/game_link.html" - popover: "components/popover.html" \ No newline at end of file + popover: "components/popover.html" + table: "components/table.html" + table_row: "components/table_row.html" + table_td: "components/table_td.html" + simple_table: "components/simple_table.html" + button_group_sm: "components/button_group_sm.html" + button_group_button_sm: "components/button_group_button_sm.html" diff --git a/games/templates/components/button_group_button_sm.html b/games/templates/components/button_group_button_sm.html new file mode 100644 index 0000000..2febc2d --- /dev/null +++ b/games/templates/components/button_group_button_sm.html @@ -0,0 +1,20 @@ +{% var color=color|default:"gray" %} + + {% if color == "gray" %} + + {% elif color == "red" %} + + {% elif color == "green" %} + + {% endif %} + diff --git a/games/templates/components/button_group_sm.html b/games/templates/components/button_group_sm.html new file mode 100644 index 0000000..6bd902e --- /dev/null +++ b/games/templates/components/button_group_sm.html @@ -0,0 +1,5 @@ +
+ {% for button in buttons %} + {% button_group_button_sm href=button.href text=button.text color=button.color %} + {% endfor %} +
diff --git a/games/templates/components/simple_table.html b/games/templates/components/simple_table.html new file mode 100644 index 0000000..a13a93a --- /dev/null +++ b/games/templates/components/simple_table.html @@ -0,0 +1,14 @@ +
+ + + + {% for column in columns %}{% endfor %} + + + + {% for row in rows %} + {% table_row data=row %} + {% endfor %} + +
{{ column }}
+
diff --git a/games/templates/components/table.html b/games/templates/components/table.html new file mode 100644 index 0000000..964da22 --- /dev/null +++ b/games/templates/components/table.html @@ -0,0 +1,12 @@ +
+ + + + {% for column in columns %}{% endfor %} + + + + {{ children }} + +
{{ column }}
+
diff --git a/games/templates/components/table_row.html b/games/templates/components/table_row.html new file mode 100644 index 0000000..58072e9 --- /dev/null +++ b/games/templates/components/table_row.html @@ -0,0 +1,15 @@ +{% fragment as default_content %} +{% for td in data %} + {% if forloop.first %} + {{ td }} + {% else %} + {% #table_td %} + {{ td }} + {% /table_td %} + {% endif %} +{% endfor %} +{% endfragment %} + + {{ children|default:default_content }} + diff --git a/games/templates/components/table_td.html b/games/templates/components/table_td.html new file mode 100644 index 0000000..e4d7271 --- /dev/null +++ b/games/templates/components/table_td.html @@ -0,0 +1 @@ +{{ children }} diff --git a/games/templates/list_purchases.html b/games/templates/list_purchases.html new file mode 100644 index 0000000..8e3b2c5 --- /dev/null +++ b/games/templates/list_purchases.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load static %} +{% block title %} + {{ title }} +{% endblock title %} +{% block content %} + {% simple_table columns=data.columns rows=data.rows %} +{% endblock content %} diff --git a/games/urls.py b/games/urls.py index 43936fa..7bbb60f 100644 --- a/games/urls.py +++ b/games/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from games import views +from games import purchaseviews, views urlpatterns = [ path("", views.index, name="index"), @@ -25,6 +25,11 @@ urlpatterns = [ views.delete_purchase, name="delete_purchase", ), + path( + "purchase/list", + purchaseviews.list_purchases, + name="list_purchases", + ), path( "purchase/related-purchase-by-edition", views.related_purchase_by_edition, diff --git a/games/views.py b/games/views.py index 9ff9386..7a7cc0a 100644 --- a/games/views.py +++ b/games/views.py @@ -27,6 +27,9 @@ from .forms import ( ) from .models import Edition, Game, Platform, Purchase, Session +dateformat: str = "%d/%m/%Y" +datetimeformat: str = "%d/%m/%Y %H:%M" + def model_counts(request): return {