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" %}
+
+ {{ text }}
+
+ {% elif color == "red" %}
+
+ {{ text }}
+
+ {% elif color == "green" %}
+
+ {{ text }}
+
+ {% 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 %}{{ column }} {% endfor %}
+
+
+
+ {% for row in rows %}
+ {% table_row data=row %}
+ {% endfor %}
+
+
+
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 %}{{ column }} {% endfor %}
+
+
+
+ {{ children }}
+
+
+
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 {