feat(finance): add expense management endpoints

This update introduces new endpoints for expense management in the finance API, complementing the existing invoice functionality. The changes include new URL routes for listing and creating expenses, along with minor formatting improvements to the URL configuration file.

**New Features:**
- Added `/expenses/` endpoint for listing expenses
- Added `/expenses/create/` endpoint for creating new expenses

**Enhancements & Refinements:**
- Removed trailing whitespace and blank lines in `urls.py` for improved code consistency
- Reorganized imports to place Django imports first, followed by local imports
```
This commit is contained in:
sankalp committed 2025-12-18 21:16:30 +05:45
1 parent 0be952becd
commit 3643e34304
6 files changed
+374 -2

No files matched your search

+1 -2
View File
@@ -1,15 +1,14 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.finance_dashboard, name="finance_dashboard"),
# Invoices
path("invoices/", views.invoice_list, name="invoice_list"),
path("invoices/create/", views.invoice_create, name="invoice_create"),
path("invoices/<int:pk>/edit/", views.invoice_edit, name="invoice_edit"),
path("invoices/<int:pk>/delete/", views.invoice_delete, name="invoice_delete"),
# Expenses
path("expenses/", views.expense_list, name="expense_list"),
path("expenses/create/", views.expense_create, name="expense_create"),
+115
View File
@@ -0,0 +1,115 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-20 min-h-screen flex flex-col pb-10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-heading">Productivity Dashboard</h1>
<div class="flex space-x-4">
<a href="{% url 'note_create' %}" class="btn btn-primary btn-sm">New Note</a>
<a href="{% url 'timeentry_create' %}" class="btn btn-secondary btn-sm">Log Time</a>
</div>
</div>
<!-- Top Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
<div class="stat-title text-body">Recent Notes</div>
<div class="stat-value text-heading">{{ recent_notes|length }}</div>
<div class="stat-desc text-body-secondary">Last {{ recent_notes|length }} notes</div>
</div>
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
<div class="stat-title text-body">Recent Time Entries</div>
<div class="stat-value text-heading">{{ recent_time_entries|length }}</div>
<div class="stat-desc text-body-secondary">Tracked sessions</div>
</div>
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
<div class="stat-title text-body">Tracked Hours (Recent)</div>
<div class="stat-value text-success">{{ total_tracked_hours|floatformat:2 }}</div>
<div class="stat-desc text-body-secondary">From recent entries</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Recent Notes -->
<div class="bg-neutral-secondary-soft rounded-lg shadow-sm border border-default p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-heading">Recent Notes</h2>
<a href="{% url 'note_list' %}" class="btn btn-ghost btn-xs">View All</a>
</div>
<div class="space-y-4">
{% for note in recent_notes %}
<div class="border border-default rounded-lg p-4 bg-base-100">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-semibold text-heading">{{ note.title }}</h3>
<p class="mt-1 text-sm text-body-secondary">
{{ note.created_at|date:"M d, Y H:i" }}
</p>
</div>
<a href="{% url 'note_edit' note.pk %}" class="btn btn-ghost btn-xs">Edit</a>
</div>
<p class="mt-2 text-body line-clamp-3 whitespace-pre-line">
{{ note.content }}
</p>
</div>
{% empty %}
<p class="text-body-secondary text-center py-4">No notes yet.</p>
{% endfor %}
</div>
</div>
<!-- Recent Time Entries -->
<div class="bg-neutral-secondary-soft rounded-lg shadow-sm border border-default p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-heading">Recent Time Entries</h2>
<a href="{% url 'timeentry_list' %}" class="btn btn-ghost btn-xs">View All</a>
</div>
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr class="text-body border-b border-default">
<th>Description</th>
<th>Project</th>
<th>Task</th>
<th>Start</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{% for entry in recent_time_entries %}
<tr class="hover:bg-neutral-tertiary border-b border-default last:border-0">
<td class="font-medium text-heading">{{ entry.description }}</td>
<td class="text-body">
{% if entry.project %}{{ entry.project.name }}{% else %}-{% endif %}
</td>
<td class="text-body">
{% if entry.task %}{{ entry.task.title }}{% else %}-{% endif %}
</td>
<td class="text-body-secondary text-sm">
{{ entry.start_time|date:"M d, Y H:i" }}
</td>
<td class="text-body-secondary text-sm">
{% if entry.duration %}{{ entry.duration }}{% else %}-{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-body-secondary py-4">No time entries yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+47
View File
@@ -0,0 +1,47 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-20 min-h-screen flex flex-col pb-10">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div class="mb-8">
<h1 class="text-3xl font-bold text-heading">
{% if note %}Edit Note{% else %}New Note{% endif %}
</h1>
</div>
<div class="bg-neutral-secondary-soft rounded-lg shadow-sm border border-default p-6">
<form method="POST" class="space-y-6">
{% csrf_token %}
<div class="form-control w-full">
<label class="label">
<span class="label-text">Title</span>
</label>
<input type="text" name="title" class="input input-bordered w-full" required
value="{{ note.title|default:'' }}" />
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Content</span>
</label>
<textarea name="content" rows="8" class="textarea textarea-bordered w-full"
required>{{ note.content|default:'' }}</textarea>
</div>
<div class="flex justify-end space-x-4 mt-8">
<a href="{% url 'note_list' %}" class="btn btn-ghost">Cancel</a>
<button type="submit" class="btn btn-primary">
{% if note %}Update Note{% else %}Create Note{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+51
View File
@@ -0,0 +1,51 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-20 min-h-screen flex flex-col pb-10">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-heading">Notes</h1>
<a href="{% url 'note_create' %}" class="btn btn-primary btn-sm">New Note</a>
</div>
<div class="bg-neutral-secondary-soft rounded-lg shadow-sm border border-default p-6">
{% if notes %}
<div class="space-y-4">
{% for note in notes %}
<div class="border border-default rounded-lg p-4 bg-base-100">
<div class="flex justify-between items-start">
<div>
<h2 class="text-lg font-semibold text-heading">{{ note.title }}</h2>
<p class="mt-2 text-body whitespace-pre-line">{{ note.content }}</p>
</div>
<div class="flex flex-col items-end space-y-2">
<span class="text-xs text-body-secondary">
{{ note.updated_at|date:"M d, Y H:i" }}
</span>
<div class="flex space-x-2">
<a href="{% url 'note_edit' note.pk %}" class="btn btn-ghost btn-xs">Edit</a>
<form method="post" action="{% url 'note_delete' note.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-ghost btn-xs text-error"
onclick="return confirm('Delete this note?');">
Delete
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-body-secondary text-center py-8">No notes yet. Create your first note!</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,87 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-20 min-h-screen flex flex-col pb-10">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div class="mb-8">
<h1 class="text-3xl font-bold text-heading">
{% if time_entry %}Edit Time Entry{% else %}Log Time{% endif %}
</h1>
</div>
<div class="bg-neutral-secondary-soft rounded-lg shadow-sm border border-default p-6">
<form method="POST" class="space-y-6">
{% csrf_token %}
<div class="form-control w-full">
<label class="label">
<span class="label-text">Project (optional)</span>
</label>
<select name="project" class="select select-bordered w-full">
<option value="">None</option>
{% for project in projects %}
<option value="{{ project.id }}"
{% if time_entry and time_entry.project and time_entry.project.id == project.id %}selected{% endif %}>
{{ project.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Task (optional)</span>
</label>
<select name="task" class="select select-bordered w-full">
<option value="">None</option>
{% for task in tasks %}
<option value="{{ task.id }}"
{% if time_entry and time_entry.task and time_entry.task.id == task.id %}selected{% endif %}>
{{ task.title }}
</option>
{% endfor %}
</select>
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Description</span>
</label>
<input type="text" name="description" class="input input-bordered w-full" required
value="{{ time_entry.description|default:'' }}" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Start Time</span>
</label>
<input type="datetime-local" name="start_time" class="input input-bordered w-full" required
value="{% if time_entry and time_entry.start_time %}{{ time_entry.start_time|date:'Y-m-d\\TH:i' }}{% endif %}" />
</div>
<div class="form-control w-full">
<label class="label">
<span class="label-text">End Time (optional)</span>
</label>
<input type="datetime-local" name="end_time" class="input input-bordered w-full"
value="{% if time_entry and time_entry.end_time %}{{ time_entry.end_time|date:'Y-m-d\\TH:i' }}{% endif %}" />
</div>
</div>
<div class="flex justify-end space-x-4 mt-8">
<a href="{% url 'timeentry_list' %}" class="btn btn-ghost">Cancel</a>
<button type="submit" class="btn btn-primary">
{% if time_entry %}Update Entry{% else %}Save Entry{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,73 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-20 min-h-screen flex flex-col pb-10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-heading">Time Entries</h1>
<a href="{% url 'timeentry_create' %}" class="btn btn-primary btn-sm">Log Time</a>
</div>
<div class="bg-neutral-secondary-soft rounded-lg shadow-sm border border-default p-6">
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr class="text-body border-b border-default">
<th>Description</th>
<th>Project</th>
<th>Task</th>
<th>Start</th>
<th>End</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in time_entries %}
<tr class="hover:bg-neutral-tertiary border-b border-default last:border-0">
<td class="font-medium text-heading">{{ entry.description }}</td>
<td class="text-body">
{% if entry.project %}{{ entry.project.name }}{% else %}-{% endif %}
</td>
<td class="text-body">
{% if entry.task %}{{ entry.task.title }}{% else %}-{% endif %}
</td>
<td class="text-body-secondary text-sm">
{{ entry.start_time|date:"M d, Y H:i" }}
</td>
<td class="text-body-secondary text-sm">
{% if entry.end_time %}{{ entry.end_time|date:"M d, Y H:i" }}{% else %}In Progress{% endif %}
</td>
<td class="text-body-secondary text-sm">
{% if entry.duration %}{{ entry.duration }}{% else %}-{% endif %}
</td>
<td>
<div class="flex space-x-2">
<a href="{% url 'timeentry_edit' entry.pk %}" class="btn btn-ghost btn-xs">Edit</a>
<form method="post" action="{% url 'timeentry_delete' entry.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-ghost btn-xs text-error"
onclick="return confirm('Delete this time entry?');">
Delete
</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center text-body-secondary py-4">No time entries yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}