mirror of
https://github.com/Lyxminxx/jast.git
synced 2026-10-11 12:51:51 +02:00
removed frontend, added ci
This commit is contained in:
1 parent
b6dbb34a89
commit
90ad2f05ce
11 files changed
+69
-752
No files matched your search
@@ -0,0 +1,34 @@
|
|||||||
|
name: CI/CD - Build Backend Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-backend-docker:
|
||||||
|
name: Build & Push Backend Container
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Backend Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/jast-backend:latest
|
||||||
+35
@@ -108,6 +108,13 @@ class TransactionIn(Schema):
|
|||||||
class SuccessResponse(Schema):
|
class SuccessResponse(Schema):
|
||||||
success: bool
|
success: bool
|
||||||
|
|
||||||
|
class UserUpdateIn(Schema):
|
||||||
|
username: str | None = None
|
||||||
|
email: str | None = None
|
||||||
|
first_name: str | None = None
|
||||||
|
last_name: str | None = None
|
||||||
|
password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
@api.post("/auth/register", response=TokenOut)
|
@api.post("/auth/register", response=TokenOut)
|
||||||
@@ -145,6 +152,34 @@ def login_view(request, payload: LoginIn):
|
|||||||
"user": user,
|
"user": user,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@api.put("/auth/me", response=UserOut, auth=auth)
|
||||||
|
def update_current_user(request, payload: UserUpdateIn):
|
||||||
|
user = request.auth
|
||||||
|
|
||||||
|
if payload.username and payload.username != user.username:
|
||||||
|
if User.objects.filter(username=payload.username).exclude(id=user.id).exists():
|
||||||
|
raise HttpError(400, "Username already taken")
|
||||||
|
user.username = payload.username
|
||||||
|
|
||||||
|
if payload.email is not None:
|
||||||
|
user.email = payload.email
|
||||||
|
if payload.first_name is not None:
|
||||||
|
user.first_name = payload.first_name
|
||||||
|
if payload.last_name is not None:
|
||||||
|
user.last_name = payload.last_name
|
||||||
|
if payload.password:
|
||||||
|
user.set_password(payload.password)
|
||||||
|
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@api.delete("/auth/me", response=SuccessResponse, auth=auth)
|
||||||
|
def delete_current_user(request):
|
||||||
|
user = request.auth
|
||||||
|
user.delete()
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
@api.post("/auth/refresh", response=RefreshOut)
|
@api.post("/auth/refresh", response=RefreshOut)
|
||||||
def refresh_token_view(request, payload: RefreshIn):
|
def refresh_token_view(request, payload: RefreshIn):
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from .models import Transaction
|
|
||||||
|
|
||||||
class TransactionForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Transaction
|
|
||||||
fields = ['title', 'amount', 'category', 'date']
|
|
||||||
|
|
||||||
|
|
||||||
class CustomUserCreationForm(UserCreationForm):
|
|
||||||
first_name = forms.CharField(max_length=30, required=True, label="First Name")
|
|
||||||
last_name = forms.CharField(max_length=30, required=True, label="Last Name")
|
|
||||||
|
|
||||||
class Meta(UserCreationForm.Meta):
|
|
||||||
model = User
|
|
||||||
fields = UserCreationForm.Meta.fields + ('first_name', 'last_name')
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
user = super().save(commit=False)
|
|
||||||
user.first_name = self.cleaned_data['first_name']
|
|
||||||
user.last_name = self.cleaned_data['last_name']
|
|
||||||
if commit:
|
|
||||||
user.save()
|
|
||||||
return user
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
:root {
|
|
||||||
/* Gruvbox Dark Palette */
|
|
||||||
--bg-main: #282828; /* bg0 */
|
|
||||||
--bg-card: #3c3836; /* bg1 */
|
|
||||||
--bg-card-hover: #504945; /* bg2 */
|
|
||||||
--border: #504945; /* bg2 */
|
|
||||||
--border-hover: #665c54; /* bg3 */
|
|
||||||
--border-focus: #fabd2f; /* Gruvbox Yellow */
|
|
||||||
|
|
||||||
--text-main: #ebdbb2; /* fg1 */
|
|
||||||
--text-heading: #fbf1c7; /* fg0 */
|
|
||||||
--text-muted: #a89984; /* fg4 */
|
|
||||||
|
|
||||||
--primary: #fabd2f; /* Gruvbox Yellow */
|
|
||||||
--primary-hover: #d79921; /* Gruvbox Dark Yellow */
|
|
||||||
--primary-text: #282828; /* Dark contrast text for yellow buttons */
|
|
||||||
|
|
||||||
--income: #b8bb26; /* Gruvbox Green */
|
|
||||||
--expense: #fb4934; /* Gruvbox Red */
|
|
||||||
--danger: #fb4934; /* Gruvbox Red */
|
|
||||||
--accent-blue: #83a598; /* Gruvbox Blue */
|
|
||||||
|
|
||||||
--radius-sm: 6px;
|
|
||||||
--radius-md: 10px;
|
|
||||||
--radius-lg: 16px;
|
|
||||||
--shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
max-width: 920px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 24px 20px 60px 20px;
|
|
||||||
color: var(--text-main);
|
|
||||||
background-color: var(--bg-main);
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headings & Links */
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: var(--text-heading);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navbar */
|
|
||||||
.navbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header Section & Balance Card */
|
|
||||||
.header-section {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
padding: 24px;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-section h1 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-text {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-text strong {
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--primary);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards & Auth Containers */
|
|
||||||
.card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
margin-bottom: 32px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container {
|
|
||||||
max-width: 420px;
|
|
||||||
margin: 60px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container h2 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-container p {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
margin-top: 10px;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: var(--bg-card-hover);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover td {
|
|
||||||
background-color: var(--bg-card-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.income {
|
|
||||||
color: var(--income);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expense {
|
|
||||||
color: var(--expense);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter Pills */
|
|
||||||
.filters {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters a {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--bg-card);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters a:hover {
|
|
||||||
border-color: var(--border-hover);
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters a.active {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-text);
|
|
||||||
border-color: var(--primary);
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 0 12px rgba(250, 189, 47, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chart Container */
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 400px;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 24px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
button, .btn {
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--primary-text);
|
|
||||||
padding: 9px 18px;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover, .btn:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-stats, .btn-nav {
|
|
||||||
background: var(--bg-card-hover);
|
|
||||||
color: var(--text-main);
|
|
||||||
border: 1px solid var(--border-hover);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-stats:hover, .btn-nav:hover {
|
|
||||||
background: var(--border-hover);
|
|
||||||
color: var(--text-heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout, .btn-delete {
|
|
||||||
background: rgba(251, 73, 52, 0.15);
|
|
||||||
color: var(--danger);
|
|
||||||
border: 1px solid rgba(251, 73, 52, 0.3);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-logout:hover, .btn-delete:hover {
|
|
||||||
background: var(--danger);
|
|
||||||
color: var(--bg-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-edit {
|
|
||||||
background: rgba(131, 165, 152, 0.15);
|
|
||||||
color: var(--accent-blue);
|
|
||||||
border: 1px solid rgba(131, 165, 152, 0.3);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-right: 6px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-edit:hover {
|
|
||||||
background: var(--accent-blue);
|
|
||||||
color: var(--bg-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel {
|
|
||||||
background: var(--bg-card-hover);
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 9px 18px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
border: 1px solid var(--border-hover);
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel:hover {
|
|
||||||
background: var(--border-hover);
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Django Form Styling */
|
|
||||||
form p {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input[type="text"],
|
|
||||||
form input[type="password"],
|
|
||||||
form input[type="email"],
|
|
||||||
form input[type="number"],
|
|
||||||
form input[type="date"],
|
|
||||||
form select {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 14px;
|
|
||||||
background-color: var(--bg-main);
|
|
||||||
color: var(--text-main);
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input:focus,
|
|
||||||
form select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--border-focus);
|
|
||||||
box-shadow: 0 0 0 3px rgba(250, 189, 47, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Django Form Password Requirements & Help Text */
|
|
||||||
.helptext, form span.helptext {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 6px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helptext ul {
|
|
||||||
margin: 6px 0 10px 0;
|
|
||||||
padding-left: 18px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helptext li {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validation Errors */
|
|
||||||
.errorlist {
|
|
||||||
color: var(--expense);
|
|
||||||
font-size: 13px;
|
|
||||||
list-style: none;
|
|
||||||
padding: 10px 14px;
|
|
||||||
margin: 6px 0 16px 0;
|
|
||||||
background: rgba(251, 73, 52, 0.1);
|
|
||||||
border: 1px solid rgba(251, 73, 52, 0.25);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorlist li {
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Edit Transaction - JAST</title>
|
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Editing: {{ transaction.title }}</h2>
|
|
||||||
<div class="card">
|
|
||||||
<form method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.as_p }}
|
|
||||||
<button type="submit">Save Changes</button>
|
|
||||||
<a href="{% url 'home' %}" class="cancel">Cancel</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>JAST</title>
|
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<nav class="navbar">
|
|
||||||
<a href="{% url 'home' %}" class="navbar-brand">JAST</a>
|
|
||||||
<div class="navbar-actions">
|
|
||||||
<a href="{% url 'stats' %}" class="btn btn-stats">View Statistics 📊</a>
|
|
||||||
<form action="{% url 'logout' %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to log out?');">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" class="btn btn-logout">Logout</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="header-section">
|
|
||||||
<h1>Hello, {% if user.first_name %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}!</h1>
|
|
||||||
<p class="balance-text">Total Balance: <strong>{{ total_balance|floatformat:2 }} kr</strong></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>Add Transaction</h3>
|
|
||||||
<form method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.as_p }}
|
|
||||||
<button type="submit">Save Transaction</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Category</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for t in transactions %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ t.date }}</td>
|
|
||||||
<td>{{ t.title }}</td>
|
|
||||||
<td>{{ t.category.name|default:"-" }}</td>
|
|
||||||
<td class="{% if t.is_income %}income{% else %}expense{% endif %}">
|
|
||||||
{{ t.amount }} kr
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'edit_transaction' t.pk %}" class="btn-edit">Edit</a>
|
|
||||||
<form action="{% url 'delete_transaction' t.pk %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this transaction?');">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" class="btn-delete">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr><td colspan="5">No transactions yet. Start spending!</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Login - JAST</title>
|
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="auth-container">
|
|
||||||
<h2>Login to JAST</h2>
|
|
||||||
<div class="card">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.as_p }}
|
|
||||||
<button type="submit">Login</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<p>Don't have an account? <a href="{% url 'register' %}">Register here</a></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Register - JAST</title>
|
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="auth-container">
|
|
||||||
<h2>Register for JAST</h2>
|
|
||||||
<div class="card">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form.as_p }}
|
|
||||||
<button type="submit">Sign Up</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<p>Already have an account? <a href="{% url 'login' %}">Log in here</a></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>JAST Statistics</title>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Top Navbar -->
|
|
||||||
<nav class="navbar">
|
|
||||||
<a href="{% url 'home' %}" class="navbar-brand">JAST</a>
|
|
||||||
<div class="navbar-actions">
|
|
||||||
<a href="{% url 'home' %}" class="btn btn-nav">← Back to Dashboard</a>
|
|
||||||
<form action="{% url 'logout' %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to log out?');">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="submit" class="btn btn-logout">Logout</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<h1>Spending Statistics</h1>
|
|
||||||
|
|
||||||
<!-- Time Period Filters -->
|
|
||||||
<div class="filters">
|
|
||||||
<a href="?filter=week" class="{% if current_filter == 'week' %}active{% endif %}">Last 7 Days</a>
|
|
||||||
<a href="?filter=month" class="{% if current_filter == 'month' %}active{% endif %}">Last 30 Days</a>
|
|
||||||
<a href="?filter=year" class="{% if current_filter == 'year' %}active{% endif %}">Last 365 Days</a>
|
|
||||||
<a href="?filter=all" class="{% if current_filter == 'all' %}active{% endif %}">All Time</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Chart Canvas -->
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="categoryChart"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const labels = JSON.parse('{{ labels|safe }}');
|
|
||||||
const data = JSON.parse('{{ data|safe }}');
|
|
||||||
|
|
||||||
const ctx = document.getElementById('categoryChart').getContext('2d');
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
ctx.font = "16px system-ui, sans-serif";
|
|
||||||
ctx.fillStyle = "#a89984"; // Gruvbox muted text
|
|
||||||
ctx.fillText("No expenses logged for this time period.", 10, 50);
|
|
||||||
} else {
|
|
||||||
new Chart(ctx, {
|
|
||||||
type: 'doughnut',
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
label: 'Amount Spent (kr)',
|
|
||||||
data: data,
|
|
||||||
backgroundColor: [
|
|
||||||
'#fabd2f', // Gruvbox Yellow
|
|
||||||
'#fe8019', // Gruvbox Orange
|
|
||||||
'#fb4934', // Gruvbox Red
|
|
||||||
'#b8bb26', // Gruvbox Green
|
|
||||||
'#83a598', // Gruvbox Blue
|
|
||||||
'#d3869b', // Gruvbox Purple
|
|
||||||
'#8ec07c' // Gruvbox Aqua
|
|
||||||
],
|
|
||||||
borderColor: '#3c3836', // Card background color for clean slice gaps
|
|
||||||
borderWidth: 2
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'right',
|
|
||||||
labels: {
|
|
||||||
color: '#ebdbb2', // Gruvbox primary text
|
|
||||||
font: {
|
|
||||||
family: 'system-ui, -apple-system, sans-serif',
|
|
||||||
size: 13
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +1,11 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
from core import views
|
|
||||||
from core.api import api # Import your Django Ninja API instance
|
from core.api import api # Import your Django Ninja API instance
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Admin Panel
|
# Admin Panel
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
|
||||||
# Web App Views
|
|
||||||
path('', views.home, name='home'),
|
|
||||||
path('edit/<int:pk>/', views.edit_transaction, name='edit_transaction'),
|
|
||||||
path('delete/<int:pk>/', views.delete_transaction, name='delete_transaction'),
|
|
||||||
path('stats/', views.stats, name='stats'),
|
|
||||||
|
|
||||||
# Auth Views
|
|
||||||
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
|
|
||||||
path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
|
|
||||||
path('register/', views.register, name='register'),
|
|
||||||
|
|
||||||
# Django Ninja API
|
# Django Ninja API
|
||||||
path('api/', api.urls),
|
path('api/', api.urls),
|
||||||
]
|
]
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
from django.shortcuts import render, redirect, get_object_or_404
|
|
||||||
from django.db.models import Sum
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.contrib.auth.forms import UserCreationForm
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from .models import Transaction
|
|
||||||
from .forms import TransactionForm, CustomUserCreationForm
|
|
||||||
import json
|
|
||||||
from datetime import timedelta
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
def register(request):
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = CustomUserCreationForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
user = form.save()
|
|
||||||
login(request, user)
|
|
||||||
return redirect('home')
|
|
||||||
else:
|
|
||||||
form = CustomUserCreationForm()
|
|
||||||
|
|
||||||
return render(request, 'registration/register.html', {'form': form})
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def home(request):
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = TransactionForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
transaction = form.save(commit=False)
|
|
||||||
transaction.user = request.user
|
|
||||||
transaction.save()
|
|
||||||
return redirect('home')
|
|
||||||
else:
|
|
||||||
form = TransactionForm()
|
|
||||||
|
|
||||||
transactions = Transaction.objects.filter(user=request.user)
|
|
||||||
total_balance = transactions.aggregate(Sum('amount'))['amount__sum'] or 0
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'form': form,
|
|
||||||
'transactions': transactions,
|
|
||||||
'total_balance': total_balance,
|
|
||||||
}
|
|
||||||
return render(request, 'home.html', context)
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def edit_transaction(request, pk):
|
|
||||||
transaction = get_object_or_404(Transaction, pk=pk, user=request.user)
|
|
||||||
if request.method == 'POST':
|
|
||||||
form = TransactionForm(request.POST, instance=transaction)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect('home')
|
|
||||||
else:
|
|
||||||
form = TransactionForm(instance=transaction)
|
|
||||||
return render(request, 'edit.html', {'form': form, 'transaction': transaction})
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def delete_transaction(request, pk):
|
|
||||||
transaction = get_object_or_404(Transaction, pk=pk, user=request.user)
|
|
||||||
if request.method == 'POST':
|
|
||||||
transaction.delete()
|
|
||||||
return redirect('home')
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def stats(request):
|
|
||||||
time_filter = request.GET.get('filter', 'all')
|
|
||||||
now = timezone.now().date()
|
|
||||||
|
|
||||||
expenses = Transaction.objects.filter(user=request.user, amount__lt=0)
|
|
||||||
|
|
||||||
if time_filter == 'week':
|
|
||||||
expenses = expenses.filter(date__gte=now - timedelta(days=7))
|
|
||||||
elif time_filter == 'month':
|
|
||||||
expenses = expenses.filter(date__gte=now - timedelta(days=30))
|
|
||||||
elif time_filter == 'year':
|
|
||||||
expenses = expenses.filter(date__gte=now - timedelta(days=365))
|
|
||||||
|
|
||||||
category_data = expenses.values('category__name').annotate(total=Sum('amount'))
|
|
||||||
|
|
||||||
labels = []
|
|
||||||
data = []
|
|
||||||
for item in category_data:
|
|
||||||
cat_name = item['category__name'] or 'Uncategorized'
|
|
||||||
labels.append(cat_name)
|
|
||||||
data.append(abs(float(item['total'])))
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'labels': json.dumps(labels),
|
|
||||||
'data': json.dumps(data),
|
|
||||||
'current_filter': time_filter,
|
|
||||||
}
|
|
||||||
return render(request, 'stats.html', context)
|
|
||||||
Reference in new issue
Block a user