Major redesign #73
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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;
|
||||
|
@ -2793,3 +2898,45 @@ th label {
|
|||
.rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 1;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
components:
|
||||
gamelink: "components/game_link.html"
|
||||
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"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{% var color=color|default:"gray" %}
|
||||
<a href="{{ href }}"
|
||||
class="[&:first-of-type_button]:rounded-s-lg [&:last-of-type_button]:rounded-e-lg">
|
||||
{% if color == "gray" %}
|
||||
<button type="button"
|
||||
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
|
||||
{{ text }}
|
||||
</button>
|
||||
{% elif color == "red" %}
|
||||
<button type="button"
|
||||
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-red-500 hover:text-white focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-red-700 dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
|
||||
{{ text }}
|
||||
</button>
|
||||
{% elif color == "green" %}
|
||||
<button type="button"
|
||||
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-green-500 hover:border-green-600 hover:text-white focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-green-700 dark:hover:bg-green-600 dark:focus:ring-green-500 dark:focus:text-white">
|
||||
{{ text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</a>
|
|
@ -0,0 +1,5 @@
|
|||
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||
{% for button in buttons %}
|
||||
{% button_group_button_sm href=button.href text=button.text color=button.color %}
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
{% table_row data=row %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ children }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
{% fragment as default_content %}
|
||||
{% for td in data %}
|
||||
{% if forloop.first %}
|
||||
<th scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{ td }}</th>
|
||||
{% else %}
|
||||
{% #table_td %}
|
||||
{{ td }}
|
||||
{% /table_td %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfragment %}
|
||||
<tr class="[&_td:first-of-type]:px-6 [&_td:first-of-type]:py-4 [&_td:first-of-type]:font-medium [&_td:first-of-type]:text-gray-900 [&_td:first-of-type]:whitespace-nowrap [&_td:first-of-type]:dark:text-white odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 border-b">
|
||||
{{ children|default:default_content }}
|
||||
</tr>
|
|
@ -0,0 +1 @@
|
|||
<td class="px-6 py-4">{{ children }}</td>
|
|
@ -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 %}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue