feat(finance): add finance dashboard and CRUD templates
Add new finance dashboard with key financial metrics and recent transaction overviews. Implement invoice and expense management templates with create, edit, and list views. **New Features** - **Finance Dashboard**: Shows total income, expenses, net profit, and pending income stats - **Recent Transactions**: Displays tables for recent invoices and expenses - **Invoice Management**: - Create/edit invoice form with client, project, amount, status - Invoice list view with filtering and actions - **Expense Tracking**: - Expense creation/editing form - Expense list with category filter and delete action **Enhancements & Refinements** - Responsive grid layouts for dashboard components - Status badges with color coding for invoices - HTMX integration for delete actions in list views - Consistent styling with borders, shadows, and hover states - Quick action buttons for creating new invoices/expenses - Dynamic form titles for create/edit modes </answer>
This commit is contained in:
5 files changed
+418
No files matched your search
@@ -0,0 +1,117 @@
|
||||
{% 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">Finance Dashboard</h1>
|
||||
<div class="flex space-x-4">
|
||||
<a href="{% url 'invoice_create' %}" class="btn btn-primary btn-sm">New Invoice</a>
|
||||
<a href="{% url 'expense_create' %}" class="btn btn-secondary btn-sm">New Expense</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
|
||||
<div class="stat-title text-body">Total Income</div>
|
||||
<div class="stat-value text-success">Rs.{{ total_income|floatformat:2 }}</div>
|
||||
<div class="stat-desc text-body-secondary">Paid Invoices</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
|
||||
<div class="stat-title text-body">Total Expenses</div>
|
||||
<div class="stat-value text-error">Rs.{{ total_expenses|floatformat:2 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
|
||||
<div class="stat-title text-body">Net Profit</div>
|
||||
<div class="stat-value {% if net_profit >= 0 %}text-success{% else %}text-error{% endif %}">Rs.{{
|
||||
net_profit|floatformat:2 }}</div>
|
||||
</div>
|
||||
|
||||
<div class="stat bg-neutral-secondary-soft rounded-lg shadow-sm border border-default">
|
||||
<div class="stat-title text-body">Pending Income</div>
|
||||
<div class="stat-value text-warning">Rs.{{ pending_income|floatformat:2 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Recent Invoices -->
|
||||
<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 Invoices</h2>
|
||||
<a href="{% url 'invoice_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>Number</th>
|
||||
<th>Client</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invoice in invoices %}
|
||||
<tr class="hover:bg-neutral-tertiary border-b border-default last:border-0">
|
||||
<td class="font-medium text-heading">#{{ invoice.invoice_number }}</td>
|
||||
<td class="text-body">{{ invoice.client.name }}</td>
|
||||
<td class="text-body">Rs.{{ invoice.amount|floatformat:2 }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if invoice.status == 'PAID' %}badge-success{% elif invoice.status == 'OVERDUE' %}badge-error{% else %}badge-warning{% endif %} badge-sm">
|
||||
{{ invoice.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-body-secondary py-4">No recent invoices</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Expenses -->
|
||||
<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 Expenses</h2>
|
||||
<a href="{% url 'expense_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>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expenses %}
|
||||
<tr class="hover:bg-neutral-tertiary border-b border-default last:border-0">
|
||||
<td class="font-medium text-heading">{{ expense.description }}</td>
|
||||
<td class="text-body">{{ expense.category }}</td>
|
||||
<td class="text-body">Rs.{{ expense.amount|floatformat:2 }}</td>
|
||||
<td class="text-body-secondary text-sm">{{ expense.date|date:"M d" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-body-secondary py-4">No recent expenses</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
{% 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 expense %}Edit Expense{% else %}Add Expense{% 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 %}
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<input type="text" name="description" placeholder="Expense description"
|
||||
class="input input-bordered w-full" value="{{ expense.description|default:'' }}" required />
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Category</span>
|
||||
</label>
|
||||
<input type="text" name="category" placeholder="e.g. Server Costs, Office Supplies"
|
||||
class="input input-bordered w-full" value="{{ expense.category|default:'' }}" />
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Amount</span>
|
||||
</label>
|
||||
<input type="number" step="0.01" name="amount" placeholder="0.00"
|
||||
class="input input-bordered w-full" value="{{ expense.amount|default:'' }}" required />
|
||||
</div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Date</span>
|
||||
</label>
|
||||
<input type="date" name="date" class="input input-bordered w-full"
|
||||
value="{{ expense.date|date:'Y-m-d' }}" required />
|
||||
</div>
|
||||
|
||||
<!-- Service (Optional) -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Service (Optional)</span>
|
||||
</label>
|
||||
<select name="service" class="select select-bordered w-full">
|
||||
<option value="" {% if not expense or not expense.service %}selected{% endif %}>None</option>
|
||||
{% for service in services %}
|
||||
<option value="{{ service.id }}" {% if expense and expense.service.id == service.id %}selected{% endif %}>
|
||||
{{ service.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end space-x-4 mt-8">
|
||||
<a href="{% url 'finance_dashboard' %}" class="btn btn-ghost">Cancel</a>
|
||||
<button type="submit" class="btn btn-secondary">{% if expense %}Update Expense{% else %}Add
|
||||
Expense{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% 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">Expenses</h1>
|
||||
<a href="{% url 'expense_create' %}" class="btn btn-secondary btn-sm">Add Expense</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>Category</th>
|
||||
<th>Amount</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expenses %}
|
||||
<tr class="hover:bg-neutral-tertiary border-b border-default last:border-0">
|
||||
<td class="font-medium text-heading">{{ expense.description }}</td>
|
||||
<td class="text-body">{{ expense.category }}</td>
|
||||
<td class="text-body">Rs.{{ expense.amount|floatformat:2 }}</td>
|
||||
<td class="text-body-secondary">{{ expense.date }}</td>
|
||||
<td>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{% url 'expense_edit' expense.pk %}" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<button hx-delete="{% url 'expense_delete' expense.pk %}"
|
||||
hx-confirm="Are you sure you want to delete this expense?"
|
||||
hx-target="closest tr" hx-swap="outerHTML"
|
||||
class="btn btn-ghost btn-xs text-error">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-body-secondary py-4">No expenses found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{% 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 invoice %}Edit Invoice{% else %}Create Invoice{% 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 %}
|
||||
|
||||
<!-- Client -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Client</span>
|
||||
</label>
|
||||
<select name="client" class="select select-bordered w-full" required>
|
||||
<option value="" disabled {% if not invoice %}selected{% endif %}>Select a client</option>
|
||||
{% for client in clients %}
|
||||
<option value="{{ client.id }}" {% if invoice and invoice.client.id == client.id %}selected{% endif %}>
|
||||
{{ client.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Project -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Project</span>
|
||||
</label>
|
||||
<select name="project" class="select select-bordered w-full" required>
|
||||
<option value="" disabled {% if not invoice %}selected{% endif %}>Select a project</option>
|
||||
{% for project in projects %}
|
||||
<option value="{{ project.id }}" {% if invoice and invoice.project.id == project.id %}selected{% endif %}>
|
||||
{{ project.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Amount</span>
|
||||
</label>
|
||||
<input type="number" step="0.01" name="amount" placeholder="0.00"
|
||||
class="input input-bordered w-full" value="{{ invoice.amount|default:'' }}" required />
|
||||
</div>
|
||||
|
||||
<!-- Due Date -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Due Date</span>
|
||||
</label>
|
||||
<input type="date" name="due_date" class="input input-bordered w-full"
|
||||
value="{{ invoice.due_date|date:'Y-m-d' }}" required />
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Status</span>
|
||||
</label>
|
||||
<select name="status" class="select select-bordered w-full" required>
|
||||
<option value="DRAFT" {% if invoice and invoice.status == 'DRAFT' %}selected{% endif %}>Draft
|
||||
</option>
|
||||
<option value="SENT" {% if invoice and invoice.status == 'SENT' %}selected{% endif %}>Sent
|
||||
</option>
|
||||
<option value="PAID" {% if invoice and invoice.status == 'PAID' %}selected{% endif %}>Paid
|
||||
</option>
|
||||
<option value="OVERDUE" {% if invoice and invoice.status == 'OVERDUE' %}selected{% endif %}>
|
||||
Overdue</option>
|
||||
<option value="CANCELLED" {% if invoice and invoice.status == 'CANCELLED' %}selected{% endif %}>
|
||||
Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end space-x-4 mt-8">
|
||||
<a href="{% url 'finance_dashboard' %}" class="btn btn-ghost">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">{% if invoice %}Update Invoice{% else %}Create
|
||||
Invoice{% endif %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% 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">Invoices</h1>
|
||||
<a href="{% url 'invoice_create' %}" class="btn btn-primary btn-sm">Create Invoice</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>Invoice #</th>
|
||||
<th>Client</th>
|
||||
<th>Project</th>
|
||||
<th>Amount</th>
|
||||
<th>Date Issued</th>
|
||||
<th>Due Date</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invoice in invoices %}
|
||||
<tr class="hover:bg-neutral-tertiary border-b border-default last:border-0">
|
||||
<td class="font-medium text-heading">#{{ invoice.invoice_number }}</td>
|
||||
<td class="text-body">{{ invoice.client.name }}</td>
|
||||
<td class="text-body">
|
||||
{% if invoice.project %}
|
||||
{{ invoice.project.name }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-body">Rs.{{ invoice.amount|floatformat:2 }}</td>
|
||||
<td class="text-body-secondary">{{ invoice.date_issued }}</td>
|
||||
<td class="text-body-secondary">{{ invoice.due_date }}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if invoice.status == 'PAID' %}badge-success{% elif invoice.status == 'OVERDUE' %}badge-error{% else %}badge-warning{% endif %} badge-sm">
|
||||
{{ invoice.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{% url 'invoice_edit' invoice.pk %}" class="btn btn-ghost btn-xs">Edit</a>
|
||||
<button hx-delete="{% url 'invoice_delete' invoice.pk %}"
|
||||
hx-confirm="Are you sure you want to delete this invoice?"
|
||||
hx-target="closest tr" hx-swap="outerHTML"
|
||||
class="btn btn-ghost btn-xs text-error">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-body-secondary py-4">No invoices found</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user