Replace the green accent with a warm orange/amber palette and switch to a dark-first design. Add a fixed sidebar for desktop navigation and a bottom tab bar for mobile, replacing the top navbar. Cards now use glass-morphism with left accent bars, buttons use orange gradients, and decorative glow effects add depth. All 8 page templates updated, both light and dark modes tested across desktop and mobile viewports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
269 lines
12 KiB
HTML
269 lines
12 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{% block title %}Create Receipt | FoxFitt{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- === CREATE EXPENSE RECEIPT ===
|
|
Single-page form for recording business expenses.
|
|
- Dynamic line items (add/remove rows with JavaScript)
|
|
- Live VAT calculation (Included / Excluded / None)
|
|
- On submit: saves to database + emails HTML + PDF to Spark Receipt -->
|
|
|
|
<div class="container py-4">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-10 col-xl-8">
|
|
|
|
<!-- Page header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1 class="page-title"><i class="fas fa-receipt me-2" style="color: var(--accent);"></i>Create Expense Receipt</h1>
|
|
<a href="{% url 'home' %}" class="btn btn-outline-secondary btn-sm">
|
|
<i class="fas fa-arrow-left fa-sm me-1"></i> Back
|
|
</a>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body p-4">
|
|
<form method="post" id="receipt-form">
|
|
{% csrf_token %}
|
|
|
|
<!-- === RECEIPT HEADER FIELDS === -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Date</label>
|
|
{{ form.date }}
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Vendor Name</label>
|
|
{{ form.vendor_name }}
|
|
<small style="color: var(--text-tertiary);">
|
|
<i class="fas fa-info-circle"></i> Will appear as the main title on the receipt.
|
|
</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">Payment Method</label>
|
|
{{ form.payment_method }}
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold">Description</label>
|
|
{{ form.description }}
|
|
</div>
|
|
</div>
|
|
|
|
<hr style="border-color: var(--border-default);">
|
|
|
|
<!-- === LINE ITEMS === -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3 mt-4">
|
|
<h5 class="fw-bold m-0">Items</h5>
|
|
<button type="button" id="add-item" class="btn btn-sm btn-outline-secondary rounded-pill">
|
|
<i class="fas fa-plus me-1"></i> Add Line
|
|
</button>
|
|
</div>
|
|
|
|
{{ items.management_form }}
|
|
|
|
<div id="items-container">
|
|
{% for item_form in items %}
|
|
<div class="item-row row g-2 align-items-center mb-2">
|
|
{{ item_form.id }}
|
|
<div class="col-12 col-md-7">
|
|
{{ item_form.product_name }}
|
|
</div>
|
|
<div class="col-10 col-md-4">
|
|
<div class="input-group">
|
|
<span class="input-group-text">R</span>
|
|
{{ item_form.amount }}
|
|
</div>
|
|
</div>
|
|
<div class="col-2 col-md-1 text-center">
|
|
{% if items.can_delete %}
|
|
<div class="form-check d-none">
|
|
{{ item_form.DELETE }}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<hr style="border-color: var(--border-default);">
|
|
|
|
<!-- === VAT + TOTALS === -->
|
|
<div class="row mt-4">
|
|
<!-- VAT radio buttons -->
|
|
<div class="col-md-6 mb-3 mb-md-0">
|
|
<label class="form-label d-block fw-semibold mb-2">VAT Configuration (15%)</label>
|
|
<div class="p-3 rounded" style="background: var(--bg-inset); border: 1px solid var(--border-default);">
|
|
{% for radio in form.vat_type %}
|
|
<div class="form-check mb-2">
|
|
{{ radio.tag }}
|
|
<label class="form-check-label" for="{{ radio.id_for_label }}">
|
|
{{ radio.choice_label }}
|
|
</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live totals -->
|
|
<div class="col-md-6">
|
|
<label class="form-label d-block fw-semibold mb-2">Receipt Totals</label>
|
|
<div class="p-3 rounded" style="background: var(--bg-inset); border: 1px solid var(--border-default);">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span style="color: var(--text-secondary);">Subtotal (Excl. VAT):</span>
|
|
<span class="fw-bold">R <span id="display-subtotal">0.00</span></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span style="color: var(--text-secondary);">VAT (15%):</span>
|
|
<span class="fw-bold">R <span id="display-vat">0.00</span></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between pt-2 mt-2" style="border-top: 1px solid var(--border-default);">
|
|
<span class="h5 mb-0 fw-bold">Total:</span>
|
|
<span class="h5 mb-0" style="color: var(--accent); font-family: 'Poppins', sans-serif;">
|
|
R <span id="display-total">0.00</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<div class="text-end mt-4">
|
|
<button type="submit" class="btn btn-accent btn-lg">
|
|
<i class="fas fa-paper-plane me-2"></i> Create & Send Receipt
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- === JavaScript: Dynamic line items + live VAT calculation === -->
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
var itemsContainer = document.getElementById('items-container');
|
|
var addItemBtn = document.getElementById('add-item');
|
|
var totalForms = document.querySelector('#id_line_items-TOTAL_FORMS');
|
|
var displaySubtotal = document.getElementById('display-subtotal');
|
|
var displayVat = document.getElementById('display-vat');
|
|
var displayTotal = document.getElementById('display-total');
|
|
var vatRadios = document.querySelectorAll('input[name="vat_type"]');
|
|
|
|
// === ADD NEW LINE ITEM ===
|
|
addItemBtn.addEventListener('click', function() {
|
|
var formIdx = parseInt(totalForms.value);
|
|
var row = document.createElement('div');
|
|
row.className = 'item-row row g-2 align-items-center mb-2';
|
|
|
|
var hiddenId = document.createElement('input');
|
|
hiddenId.type = 'hidden';
|
|
hiddenId.name = 'line_items-' + formIdx + '-id';
|
|
hiddenId.id = 'id_line_items-' + formIdx + '-id';
|
|
row.appendChild(hiddenId);
|
|
|
|
var prodCol = document.createElement('div');
|
|
prodCol.className = 'col-12 col-md-7';
|
|
var prodInput = document.createElement('input');
|
|
prodInput.type = 'text';
|
|
prodInput.name = 'line_items-' + formIdx + '-product_name';
|
|
prodInput.className = 'form-control';
|
|
prodInput.placeholder = 'Item Name';
|
|
prodInput.id = 'id_line_items-' + formIdx + '-product_name';
|
|
prodCol.appendChild(prodInput);
|
|
row.appendChild(prodCol);
|
|
|
|
var amtCol = document.createElement('div');
|
|
amtCol.className = 'col-10 col-md-4';
|
|
var inputGroup = document.createElement('div');
|
|
inputGroup.className = 'input-group';
|
|
var prefix = document.createElement('span');
|
|
prefix.className = 'input-group-text';
|
|
prefix.textContent = 'R';
|
|
var amtInput = document.createElement('input');
|
|
amtInput.type = 'number';
|
|
amtInput.name = 'line_items-' + formIdx + '-amount';
|
|
amtInput.className = 'form-control item-amount';
|
|
amtInput.step = '0.01';
|
|
amtInput.placeholder = '0.00';
|
|
amtInput.id = 'id_line_items-' + formIdx + '-amount';
|
|
inputGroup.appendChild(prefix);
|
|
inputGroup.appendChild(amtInput);
|
|
amtCol.appendChild(inputGroup);
|
|
row.appendChild(amtCol);
|
|
|
|
var delCol = document.createElement('div');
|
|
delCol.className = 'col-2 col-md-1 text-center';
|
|
var delBtn = document.createElement('button');
|
|
delBtn.type = 'button';
|
|
delBtn.className = 'btn btn-outline-danger btn-sm delete-row rounded-circle';
|
|
delBtn.title = 'Remove';
|
|
var delIcon = document.createElement('i');
|
|
delIcon.className = 'fas fa-times';
|
|
delBtn.appendChild(delIcon);
|
|
delCol.appendChild(delBtn);
|
|
row.appendChild(delCol);
|
|
|
|
itemsContainer.appendChild(row);
|
|
totalForms.value = formIdx + 1;
|
|
updateCalculations();
|
|
});
|
|
|
|
// === DELETE LINE ITEM ===
|
|
itemsContainer.addEventListener('click', function(e) {
|
|
var deleteBtn = e.target.closest('.delete-row');
|
|
if (!deleteBtn) return;
|
|
var row = deleteBtn.closest('.item-row');
|
|
var deleteCheckbox = row.querySelector('input[name$="-DELETE"]');
|
|
if (deleteCheckbox) {
|
|
deleteCheckbox.checked = true;
|
|
row.classList.add('d-none', 'deleted');
|
|
} else {
|
|
row.remove();
|
|
}
|
|
updateCalculations();
|
|
});
|
|
|
|
// === LIVE AMOUNT CHANGES ===
|
|
itemsContainer.addEventListener('input', function(e) {
|
|
if (e.target.classList.contains('item-amount')) updateCalculations();
|
|
});
|
|
|
|
vatRadios.forEach(function(radio) {
|
|
radio.addEventListener('change', updateCalculations);
|
|
});
|
|
|
|
// === VAT CALCULATION ===
|
|
function updateCalculations() {
|
|
var sum = 0;
|
|
document.querySelectorAll('.item-row:not(.deleted) .item-amount').forEach(function(input) {
|
|
sum += parseFloat(input.value) || 0;
|
|
});
|
|
|
|
var vatType = 'None';
|
|
vatRadios.forEach(function(r) { if (r.checked) vatType = r.value; });
|
|
|
|
var subtotal = 0, vat = 0, total = 0;
|
|
if (vatType === 'Included') {
|
|
total = sum; subtotal = total / 1.15; vat = total - subtotal;
|
|
} else if (vatType === 'Excluded') {
|
|
subtotal = sum; vat = subtotal * 0.15; total = subtotal + vat;
|
|
} else {
|
|
subtotal = sum; total = sum;
|
|
}
|
|
|
|
displaySubtotal.textContent = subtotal.toFixed(2);
|
|
displayVat.textContent = vat.toFixed(2);
|
|
displayTotal.textContent = total.toFixed(2);
|
|
}
|
|
|
|
updateCalculations();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|