feat: pay_type UI (form/list/detail), Salary modal+entry, badge, clean Salary payslip

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-15 21:14:27 +02:00
parent d6f12e7dd1
commit 862766f9b5
11 changed files with 266 additions and 17 deletions

View File

@ -255,7 +255,7 @@ class WorkerForm(forms.ModelForm):
class Meta:
model = Worker
fields = [
'name', 'id_number', 'phone_number', 'monthly_salary',
'name', 'id_number', 'phone_number', 'monthly_salary', 'pay_type',
'tax_number', 'uif_number', 'bank_name', 'bank_account_number',
'employment_date', 'active', 'notes',
'shoe_size', 'overall_top_size', 'pants_size', 'tshirt_size',
@ -267,6 +267,10 @@ class WorkerForm(forms.ModelForm):
'id_number': forms.TextInput(attrs={'class': 'form-control'}),
'phone_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '+27...'}),
'monthly_salary': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
# pay_type: 'daily' = normal field worker (paid per logged work day);
# 'fixed' = manager / salaried staff (paid a monthly Salary
# adjustment, never logged on attendance).
'pay_type': forms.Select(attrs={'class': 'form-select'}),
# Banking & Tax
'tax_number': forms.TextInput(attrs={'class': 'form-control'}),
'uif_number': forms.TextInput(attrs={'class': 'form-control'}),

View File

@ -29,7 +29,7 @@
<div class="header">
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
<div class="beneficiary-name">{{ record.worker.name }}</div>
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% elif is_salary %}Salary Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
</div>
<!-- Beneficiary details -->
@ -58,6 +58,11 @@
<td>Loan Payment: {{ loan_description }}</td>
<td style="text-align: right;">R {{ loan_amount|floatformat:2 }}</td>
</tr>
{% elif is_salary %}
<tr>
<td>Salary: {{ salary_description }}</td>
<td style="text-align: right;">R {{ salary_amount|floatformat:2 }}</td>
</tr>
{% else %}
<!-- Base pay line -->
<tr>

View File

@ -41,6 +41,13 @@
<button type="button" class="btn-action-soft btn-action-add" data-bs-toggle="modal" data-bs-target="#addAdjustmentModal">
<i class="fas fa-plus"></i> Add Adjustment
</button>
{# Pay Salary — opens the SAME Add-Adjustment modal pre-set to #}
{# type=Salary (project field + Pay-Immediately checkbox shown). #}
{# Discoverable shortcut for paying a manager / salaried worker. #}
<button type="button" class="btn-action-soft btn-action-add" id="paySalaryBtn"
title="Pay a manager / salaried worker their monthly salary">
<i class="fas fa-user-tie"></i> Pay Salary
</button>
<button type="button" class="btn-action-soft btn-action-price" data-bs-toggle="modal" data-bs-target="#priceOvertimeModal">
<i class="fas fa-clock"></i> Price Overtime
</button>
@ -1099,10 +1106,10 @@
<textarea name="description" class="form-control" rows="2" placeholder="Reason for this adjustment..."></textarea>
</div>
{# Pay Immediately — only shown for New Loan type #}
{# When checked, the loan is paid to the worker right away and #}
{# a payslip is emailed to Spark. When unchecked, the loan sits #}
{# in pending payments and gets included in the next pay cycle. #}
{# Pay Immediately — shown for New Loan AND Salary types. #}
{# When checked, the amount is paid to the worker right away #}
{# and a payslip is emailed to Spark. When unchecked, it sits #}
{# in pending payments and is included in the next pay cycle. #}
<div class="col-12" id="addAdjPayImmediatelyGroup" style="display: none;">
<div class="form-check">
<input type="checkbox" class="form-check-input" name="pay_immediately"
@ -1928,9 +1935,13 @@ document.addEventListener('DOMContentLoaded', function() {
addAdjProject.value = ''; // Clear selection when hidden
}
}
// "Pay Immediately" checkbox — only shown for New Loan type
// "Pay Immediately" checkbox — shown for New Loan AND Salary.
// Salary mirrors New Loan: ticked = pay the manager now + email
// payslip; unticked = leave it pending for the next pay cycle.
if (addAdjPayImmediatelyGroup) {
addAdjPayImmediatelyGroup.style.display = (addAdjType.value === 'New Loan') ? '' : 'none';
var payNowTypes = ['New Loan', 'Salary'];
addAdjPayImmediatelyGroup.style.display =
(payNowTypes.indexOf(addAdjType.value) !== -1) ? '' : 'none';
}
}
if (addAdjType) {
@ -2020,8 +2031,35 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// When the modal is opened from the HEADER button (not quick-adjust),
// clear any pre-selected workers and project from a previous quick-adjust.
// === PAY SALARY BUTTON ===
// Opens the SAME Add-Adjustment modal pre-set to type=Salary, with a
// clean worker/project slate. Sets _paySalaryOpen so the modal's
// show.bs.modal reset handler (which normally forces type=Bonus)
// leaves our Salary selection alone. Mirrors the _quickAdjustOpen
// pattern. Distinct id 'paySalaryBtn' — no duplicate-id collision.
var _paySalaryOpen = false;
var paySalaryBtn = document.getElementById('paySalaryBtn');
if (paySalaryBtn) {
paySalaryBtn.addEventListener('click', function() {
_paySalaryOpen = true;
// Clean slate: no pre-selected workers, no pre-selected project
addAdjWorkerCheckboxes.forEach(function(cb) { cb.checked = false; });
updateWorkerCount();
if (addAdjProject) addAdjProject.value = '';
// Pre-set Salary and trigger field visibility (shows the
// project field + the Pay-Immediately checkbox).
if (addAdjType) {
addAdjType.value = 'Salary';
toggleProjectField();
}
var modal = new bootstrap.Modal(document.getElementById('addAdjustmentModal'));
modal.show();
});
}
// When the modal is opened from the HEADER button (not quick-adjust,
// not Pay-Salary), clear any pre-selected workers/project and reset
// the type to Bonus.
var addAdjModal = document.getElementById('addAdjustmentModal');
if (addAdjModal) {
addAdjModal.addEventListener('show.bs.modal', function() {
@ -2029,6 +2067,10 @@ document.addEventListener('DOMContentLoaded', function() {
_quickAdjustOpen = false;
return; // Quick-adjust already set the values
}
if (_paySalaryOpen) {
_paySalaryOpen = false;
return; // Pay-Salary already set type=Salary
}
// Reset: uncheck all workers, clear project, reset type
addAdjWorkerCheckboxes.forEach(function(cb) { cb.checked = false; });
updateWorkerCount();

View File

@ -28,12 +28,12 @@
<div class="stat-label mb-1">Payment To Beneficiary:</div>
<h2 class="fw-bold mb-0 text-uppercase">{{ record.worker.name }}</h2>
<p class="mb-0" style="color: var(--text-tertiary); font-size: 0.85rem;">
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}
{% if is_advance %}Advance{% elif is_loan %}Loan{% elif is_salary %}Salary{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}
</p>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<h3 class="fw-bold text-uppercase" style="color: var(--text-secondary);">
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip
{% if is_advance %}Advance{% elif is_loan %}Loan{% elif is_salary %}Salary{% endif %} Payslip
</h3>
<div class="fw-bold">{{ record.date|date:"F j, Y" }}</div>
<div style="color: var(--text-tertiary); font-size: 0.85rem;">Payer: Fox Fitt</div>
@ -127,6 +127,42 @@
</div>
</div>
{% elif is_salary %}
{# === SALARY PAYMENT DETAIL === #}
{# Manager / fixed-salary monthly pay. Clean single-line #}
{# layout — no empty work-log table, no R 0.00 base-pay row. #}
<div class="stat-label mb-3">Salary Details</div>
<div class="table-responsive mb-4">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Description</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ salary_adj.date|date:"M d, Y" }}</td>
<td><span class="badge-type-salary">SALARY</span></td>
<td>{{ salary_adj.description|default:"Monthly salary" }}</td>
<td class="text-end fw-bold" style="color: var(--color-success);">R {{ salary_adj.amount|floatformat:2 }}</td>
</tr>
</tbody>
</table>
</div>
<div class="row justify-content-end mt-4">
<div class="col-md-5">
<table class="table table-sm border-0">
<tr style="border-top: 2px solid var(--text-primary);">
<td class="text-end border-0 fw-bold fs-5">Salary Amount:</td>
<td class="text-end border-0 fw-bold fs-5">R {{ salary_adj.amount|floatformat:2 }}</td>
</tr>
</table>
</div>
</div>
{% else %}
<!-- === WORK LOG TABLE === -->
<div class="stat-label mb-3">Work Log Details (Attendance)</div>

View File

@ -107,7 +107,7 @@
<div class="header">
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
<div class="beneficiary-name">{{ record.worker.name }}</div>
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% elif is_salary %}Salary Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
</div>
<!-- Beneficiary details box -->
@ -138,6 +138,12 @@
<td>Loan Payment: {{ loan_description }}</td>
<td class="text-right">R {{ loan_amount|floatformat:2 }}</td>
</tr>
{% elif is_salary %}
<!-- Salary — single line item (manager / fixed monthly pay) -->
<tr>
<td>Salary: {{ salary_description }}</td>
<td class="text-right">R {{ salary_amount|floatformat:2 }}</td>
</tr>
{% else %}
<!-- Base Pay — number of days worked × day rate -->
<tr>

View File

@ -62,6 +62,16 @@
<div class="card-header"><h6 class="m-0 fw-bold">Personal & Pay</h6></div>
<div class="card-body">
<dl class="row mb-0" style="font-size: 0.9rem;">
<dt class="col-sm-5">Pay Type</dt>
<dd class="col-sm-7">
{# Manager/salaried staff are paid a monthly Salary, #}
{# not per logged work day. #}
{% if worker.is_salaried %}
<span class="badge-type-salary">Manager / Salaried</span>
{% else %}
<span class="badge" style="background: rgba(148, 163, 184, 0.15); color: var(--text-secondary);">Daily</span>
{% endif %}
</dd>
<dt class="col-sm-5">Monthly Salary</dt><dd class="col-sm-7 fw-semibold">R {{ worker.monthly_salary|money }}</dd>
<dt class="col-sm-5">Daily Rate</dt> <dd class="col-sm-7">R {{ worker.daily_rate|money }}</dd>
<dt class="col-sm-5">Employment Date</dt><dd class="col-sm-7">{{ worker.employment_date|date:"d M Y" }}</dd>

View File

@ -65,6 +65,19 @@
{{ form.monthly_salary }}
{% if form.monthly_salary.errors %}<div class="invalid-feedback d-block">{{ form.monthly_salary.errors|first }}</div>{% endif %}
</div>
{# Pay Type — Daily-rated field worker vs Fixed-salary manager. #}
{# A "Fixed salary" worker is paid a monthly Salary adjustment #}
{# and is never logged on attendance / absences. #}
<div class="col-md-4">
<label class="form-label fw-semibold">
Pay Type
<i class="fas fa-info-circle text-muted ms-1" style="font-size: 0.8em;"
data-bs-toggle="tooltip"
title="Daily-rated = normal field worker (paid per logged work day). Fixed salary = manager / salaried staff (paid a monthly Salary, never logged on attendance)."></i>
</label>
{{ form.pay_type }}
{% if form.pay_type.errors %}<div class="invalid-feedback d-block">{{ form.pay_type.errors|first }}</div>{% endif %}
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Employment Date</label>
{{ form.employment_date }}

View File

@ -75,6 +75,7 @@
<tr>
<th>Name</th>
<th>ID Number</th>
<th>Type</th>
<th>Phone</th>
<th class="text-end">Salary</th>
<th class="text-end">Days Worked</th>
@ -91,6 +92,15 @@
</a>
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">{{ w.id_number }}</td>
<td>
{# Pay-type indicator — managers/salaried staff are #}
{# paid a monthly Salary, not per logged work day. #}
{% if w.is_salaried %}
<span class="badge-type-salary">Manager / Salaried</span>
{% else %}
<span class="badge" style="background: rgba(148, 163, 184, 0.15); color: var(--text-secondary);">Daily</span>
{% endif %}
</td>
<td style="color: var(--text-secondary); font-size: 0.85rem;">{{ w.phone_number|default:'—' }}</td>
<td class="text-end fw-semibold">R {{ w.monthly_salary|money }}</td>
<td class="text-end">{{ w.days_worked }}</td>

View File

@ -3710,3 +3710,100 @@ class ManagerSalariedPayReportTests(TestCase):
self.assertIsNotNone(sal.payroll_record)
self.assertEqual(sal.payroll_record, ded.payroll_record) # same PayrollRecord
self.assertEqual(sal.payroll_record.amount_paid, Decimal('39000.00')) # 40000 - 1000
# =============================================================================
# === TASK 7 — MANAGER / SALARIED PAY: UI SURFACE + CLEAN SALARY PAYSLIP ===
# Server-side, light (no browser). Proves:
# - WorkerForm exposes the pay_type field (Part A) and the worker-create
# page renders it (Part B).
# - The friendly worker list labels a fixed-salary worker as a
# "Manager / Salaried" type (Part C).
# - An immediate Salary payment's payslip uses the clean single-line
# layout — NO "0 days worked" / "Base Pay Subtotal" / "No work logs"
# generic-branch artefacts (Part F, absorbed from Task 5 review #2).
# =============================================================================
class ManagerSalariedPayUITests(TestCase):
def setUp(self):
self.admin = User.objects.create_user('msu_admin', password='x', is_staff=True)
self.client.force_login(self.admin)
def test_workerform_has_pay_type(self):
from core.forms import WorkerForm
self.assertIn('pay_type', WorkerForm().fields)
def test_worker_new_page_renders_pay_type(self):
# URL name 'worker_new' → /workers/new/ (core/urls.py).
resp = self.client.get(reverse('worker_new'))
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'pay_type')
def test_worker_list_shows_manager_label_for_fixed(self):
Worker.objects.create(
name='UI Mgr', id_number='MSU-M', monthly_salary=Decimal('40000'),
pay_type='fixed')
# worker_list status filter param confirmed in CLAUDE.md routes table.
resp = self.client.get(reverse('worker_list') + '?status=all')
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'Manager')
def test_salary_immediate_payslip_has_no_zero_days_line(self):
# Absorbed Task-5 review #2: an immediate Salary payment's payslip
# must use the clean single-line layout — NOT the generic
# work-log/base-pay branch (which for log_count==0 renders an
# empty "Work Log Details" table with "No work logs in this
# period." + a "Base Pay Subtotal" of R 0.00). The PDF/email
# templates phrase this as "Base Pay (0 days worked)".
proj = Project.objects.create(name='UI Sal Proj')
mgr = Worker.objects.create(
name='UI Sal Mgr', id_number='MSU-S', monthly_salary=Decimal('40000'),
pay_type='fixed')
self.client.post(reverse('add_adjustment'), {
'workers': [mgr.id], 'type': 'Salary', 'amount': '40000.00',
'description': 'Salary - May 2026',
'date': _date.today().strftime('%Y-%m-%d'),
'project': proj.id, 'pay_immediately': '1',
})
pr = PayrollRecord.objects.get(worker=mgr)
resp = self.client.get(reverse('payslip_detail', args=[pr.id]))
self.assertEqual(resp.status_code, 200)
body = resp.content.decode()
# The clean Salary layout shows the Salary detail; it must NOT
# fall through to the empty generic work-log/base-pay branch.
self.assertIn('Salary', body)
self.assertNotIn('No work logs in this period.', body)
self.assertNotIn('Base Pay Subtotal', body)
def test_salary_immediate_payslip_pdf_context_is_clean(self):
# Directly prove the PDF/email context flag: a lone Salary
# adjustment with log_count==0 must NOT render the generic
# "Base Pay (N days worked)" line in payslip_pdf.html.
from django.template.loader import render_to_string
proj = Project.objects.create(name='UI Sal Proj2')
mgr = Worker.objects.create(
name='UI Sal Mgr2', id_number='MSU-S2', monthly_salary=Decimal('40000'),
pay_type='fixed')
self.client.post(reverse('add_adjustment'), {
'workers': [mgr.id], 'type': 'Salary', 'amount': '40000.00',
'description': 'Salary - May 2026',
'date': _date.today().strftime('%Y-%m-%d'),
'project': proj.id, 'pay_immediately': '1',
})
pr = PayrollRecord.objects.get(worker=mgr)
adjs = list(pr.adjustments.all())
self.assertEqual(len(adjs), 1)
self.assertEqual(adjs[0].type, 'Salary')
sal = adjs[0]
ctx = {
'record': pr, 'logs_count': 0, 'logs_amount': Decimal('0.00'),
'adjustments': pr.adjustments.all(),
'deductive_types': ['Deduction', 'Loan Repayment', 'Advance Repayment'],
'is_advance': False, 'advance_amount': None, 'advance_description': '',
'is_loan': False, 'loan_amount': None, 'loan_description': '',
'is_salary': True, 'salary_amount': sal.amount,
'salary_description': sal.description,
}
html = render_to_string('core/pdf/payslip_pdf.html', ctx)
self.assertNotIn('days worked', html) # generic Base Pay line absent
self.assertIn('Salary', html)

View File

@ -3878,10 +3878,12 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
total_amount = payroll_record.amount_paid
# === DETECT STANDALONE PAYMENT (no work logs, single adjustment) ===
# Advance-only or Loan-only payments use a cleaner payslip layout
# showing just the amount instead of "0 days worked + adjustment".
# Advance-only, Loan-only or Salary-only payments use a cleaner
# payslip layout showing just the amount instead of the generic
# "Base Pay (0 days worked) — R 0.00" line followed by the adjustment.
advance_adj = None
loan_adj = None
salary_adj = None
if log_count == 0:
adjs_list = list(payroll_record.adjustments.all())
if len(adjs_list) == 1:
@ -3889,13 +3891,18 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
advance_adj = adjs_list[0]
elif adjs_list[0].type == 'New Loan':
loan_adj = adjs_list[0]
elif adjs_list[0].type == 'Salary':
salary_adj = adjs_list[0]
is_advance = advance_adj is not None
is_loan = loan_adj is not None
is_salary = salary_adj is not None
if is_advance:
subject = f"Advance Payslip for {worker.name} - {payroll_record.date}"
elif is_loan:
subject = f"Loan Payslip for {worker.name} - {payroll_record.date}"
elif is_salary:
subject = f"Salary Payslip for {worker.name} - {payroll_record.date}"
else:
subject = f"Payslip for {worker.name} - {payroll_record.date}"
@ -3912,6 +3919,9 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
'is_loan': is_loan,
'loan_amount': loan_adj.amount if loan_adj else None,
'loan_description': loan_adj.description if loan_adj else '',
'is_salary': is_salary,
'salary_amount': salary_adj.amount if salary_adj else None,
'salary_description': salary_adj.description if salary_adj else '',
}
# 1. Render HTML email body
@ -5172,15 +5182,20 @@ def payslip_detail(request, pk):
adjustments_net = record.amount_paid - base_pay
# === DETECT STANDALONE PAYMENT (no work logs, single adjustment) ===
# Advance-only or Loan-only payments use a cleaner layout.
# Advance-only, Loan-only or Salary-only payments use a cleaner
# layout (one detail line + total) instead of an empty work-log
# table with a "Base Pay Subtotal — R 0.00" footer row.
adjs_list = list(adjustments)
advance_adj = None
loan_adj = None
salary_adj = None
if logs.count() == 0 and len(adjs_list) == 1:
if adjs_list[0].type == 'Advance Payment':
advance_adj = adjs_list[0]
elif adjs_list[0].type == 'New Loan':
loan_adj = adjs_list[0]
elif adjs_list[0].type == 'Salary':
salary_adj = adjs_list[0]
context = {
'record': record,
@ -5194,6 +5209,8 @@ def payslip_detail(request, pk):
'advance_adj': advance_adj,
'is_loan': loan_adj is not None,
'loan_adj': loan_adj,
'is_salary': salary_adj is not None,
'salary_adj': salary_adj,
}
return render(request, 'core/payslip.html', context)

View File

@ -89,6 +89,10 @@
--badge-loan-rep-bg: #b48a1a; --badge-loan-rep-fg: #fef4d1;
--badge-advance-bg: #3e5c7b; --badge-advance-fg: #d7e5f2;
--badge-advance-rep-bg: #2f679a; --badge-advance-rep-fg: #d7e5f2;
/* Salary (manager / fixed monthly pay) a distinct teal, not used by
any other type; reads as "regular salary/pay", separate from the
forest-green Bonus and the slate-blue Advance. */
--badge-salary-bg: #1f7a70; --badge-salary-fg: #d6f1ec;
/* === PAYROLL DASHBOARD action-button tokens (dark theme) ===
Soft-fill pastels for the 4 action buttons at the top of /payroll/
@ -162,6 +166,8 @@
--badge-loan-rep-bg: #f7d873; --badge-loan-rep-fg: #5a4418;
--badge-advance-bg: #bccee0; --badge-advance-fg: #243b56;
--badge-advance-rep-bg: #9ec1dd; --badge-advance-rep-fg: #1d3550;
/* Salary (light theme) — pale teal fill, deep-teal text for contrast. */
--badge-salary-bg: #c3e8e2; --badge-salary-fg: #18534b;
/* === PAYROLL DASHBOARD action-button tokens (light theme) === */
--btn-action-lookup-bg: #c7d9e8; --btn-action-lookup-fg: #243b56;
@ -1935,7 +1941,8 @@ body, .card, .modal-content, .form-control, .form-select,
.badge-type-new-loan,
.badge-type-loan-repayment,
.badge-type-advance-payment,
.badge-type-advance-repayment {
.badge-type-advance-repayment,
.badge-type-salary {
display: inline-block;
padding: 0.3rem 0.7rem;
border-radius: 999px;
@ -1952,6 +1959,7 @@ body, .card, .modal-content, .form-control, .form-select,
.badge-type-loan-repayment { background: var(--badge-loan-rep-bg); color: var(--badge-loan-rep-fg); }
.badge-type-advance-payment { background: var(--badge-advance-bg); color: var(--badge-advance-fg); }
.badge-type-advance-repayment { background: var(--badge-advance-rep-bg); color: var(--badge-advance-rep-fg); }
.badge-type-salary { background: var(--badge-salary-bg); color: var(--badge-salary-fg); }
/* --- Status flags that borrow a type's colour for semantic consistency.
The Pending tab's "Loan" worker flag (Has-an-active-loan-or-advance),
@ -2075,6 +2083,7 @@ body, .card, .modal-content, .form-control, .form-select,
.adj-group-header[data-type="Loan Repayment"] { border-left: 4px solid var(--badge-loan-rep-bg); }
.adj-group-header[data-type="Advance Payment"] { border-left: 4px solid var(--badge-advance-bg); }
.adj-group-header[data-type="Advance Repayment"] { border-left: 4px solid var(--badge-advance-rep-bg); }
.adj-group-header[data-type="Salary"] { border-left: 4px solid var(--badge-salary-bg); }
/* --- Chevron rotates to indicate collapsed / expanded state.
Bootstrap sets aria-expanded="false" on the toggle when collapsed;