Answer-table pattern: add reusable macros, integrate with Rolodex; bulk actions retained. Field prompts/help: generic focus-based help in forms (case, rolodex); add JS support. Rebuild Docker.
This commit is contained in:
Binary file not shown.
@@ -133,11 +133,12 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-2 text-muted small" id="fieldHelp" aria-live="polite">Focus a field to see help.</div>
|
||||
<form method="post" action="/case/{{ case.id }}/update">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<select class="form-select" id="status" name="status" data-help="Active or Closed. Closed cases appear in reports as closed and may restrict edits.">
|
||||
<option value="active" {% if case.status == 'active' %}selected{% endif %}>Active</option>
|
||||
<option value="closed" {% if case.status == 'closed' %}selected{% endif %}>Closed</option>
|
||||
</select>
|
||||
@@ -145,24 +146,24 @@ Case {{ case.file_no if case else '' }} · Delphi Database
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="case_type" class="form-label">Case Type</label>
|
||||
<input type="text" class="form-control" id="case_type" name="case_type"
|
||||
<input type="text" class="form-control" id="case_type" name="case_type" data-help="F1 to select area of law; type to filter."
|
||||
value="{{ case.case_type or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3">{{ case.description or '' }}</textarea>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" data-help="Brief summary of the matter; appears on statements.">{{ case.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="open_date" class="form-label">Open Date</label>
|
||||
<input type="date" class="form-control" id="open_date" name="open_date"
|
||||
<input type="date" class="form-control" id="open_date" name="open_date" data-help="Date the case was opened."
|
||||
value="{{ case.open_date.strftime('%Y-%m-%d') if case.open_date else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="close_date" class="form-label">Close Date</label>
|
||||
<input type="date" class="form-control" id="close_date" name="close_date"
|
||||
<input type="date" class="form-control" id="close_date" name="close_date" data-help="Set when the case is completed/closed."
|
||||
value="{{ case.close_date.strftime('%Y-%m-%d') if case.close_date else '' }}">
|
||||
</div>
|
||||
|
||||
|
||||
96
app/templates/partials/answer_table_macros.html
Normal file
96
app/templates/partials/answer_table_macros.html
Normal file
@@ -0,0 +1,96 @@
|
||||
{#
|
||||
Reusable macros for answer-table pattern: summary, table with selection,
|
||||
bulk actions bar, and pagination controls.
|
||||
|
||||
Usage in a page:
|
||||
{% from "partials/answer_table_macros.html" import results_summary, pagination, answer_table, bulk_actions_bar %}
|
||||
|
||||
<div class="col-12 text-muted small">{{ results_summary(start_index, end_index, total) }}</div>
|
||||
|
||||
{% set headers = [
|
||||
{ 'title': 'Name', 'width': '220px' },
|
||||
{ 'title': 'Company' },
|
||||
{ 'title': 'Address' },
|
||||
{ 'title': 'City' },
|
||||
{ 'title': 'State', 'width': '80px' },
|
||||
{ 'title': 'ZIP', 'width': '110px' },
|
||||
{ 'title': 'Phones', 'width': '200px' },
|
||||
{ 'title': 'Actions', 'width': '140px', 'align': 'end' },
|
||||
] %}
|
||||
|
||||
{% call(answer_table(headers, form_action='/reports/phone-book', select_name='client_ids', enable_bulk=enable_bulk)) %}
|
||||
{# render <tr>...</tr> rows here #}
|
||||
{% endcall %}
|
||||
|
||||
{% if enable_bulk %}
|
||||
{% call(bulk_actions_bar()) %}
|
||||
{# render bulk action buttons/links here #}
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{{ pagination('/rolodex', page, total_pages, page_size, {'q': q, 'phone': phone}) }}
|
||||
#}
|
||||
|
||||
{% macro results_summary(start_index, end_index, total) -%}
|
||||
{% if total and total > 0 %}
|
||||
Showing {{ start_index }}–{{ end_index }} of {{ total }}
|
||||
{% else %}
|
||||
No results
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro answer_table(headers, form_action=None, select_name='selected_ids', enable_bulk=False) -%}
|
||||
<form method="post" action="{{ form_action or '' }}" class="js-answer-table">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
{% if enable_bulk %}
|
||||
<th style="width: 40px;"><input class="form-check-input js-select-all" type="checkbox"></th>
|
||||
{% endif %}
|
||||
{% for h in headers %}
|
||||
<th{% if h.width %} style="width: {{ h.width }};"{% endif %}{% if h.align == 'end' %} class="text-end"{% endif %}>{{ h.title }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ caller() }}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro bulk_actions_bar() -%}
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro pagination(base_url, page, total_pages, page_size, params=None) -%}
|
||||
{% if total_pages and total_pages > 1 %}
|
||||
<nav aria-label="Pagination">
|
||||
<ul class="pagination mb-0">
|
||||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||
<a class="page-link" href="{{ base_url }}?page={{ 1 if page <= 1 else page - 1 }}&page_size={{ page_size }}{% if params %}{% for k, v in params.items() %}{% if v %}&{{ k }}={{ v | urlencode }}{% endif %}{% endfor %}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% set start_page = 1 if page - 2 < 1 else page - 2 %}
|
||||
{% set end_page = total_pages if page + 2 > total_pages else page + 2 %}
|
||||
{% for p in range(start_page, end_page + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ base_url }}?page={{ p }}&page_size={{ page_size }}{% if params %}{% for k, v in params.items() %}{% if v %}&{{ k }}={{ v | urlencode }}{% endif %}{% endfor %}{% endif %}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
<a class="page-link" href="{{ base_url }}?page={{ total_pages if page >= total_pages else page + 1 }}&page_size={{ page_size }}{% if params %}{% for k, v in params.items() %}{% if v %}&{{ k }}={{ v | urlencode }}{% endif %}{% endfor %}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
{% block title %}Rolodex · Delphi Database{% endblock %}
|
||||
|
||||
{% from "partials/answer_table_macros.html" import results_summary, pagination, answer_table, bulk_actions_bar %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3 align-items-center mb-3">
|
||||
<div class="col-auto">
|
||||
@@ -33,121 +35,72 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-12 text-muted small">
|
||||
{% if total and total > 0 %}
|
||||
Showing {{ start_index }}–{{ end_index }} of {{ total }}
|
||||
{% else %}
|
||||
No results
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12 text-muted small">{{ results_summary(start_index, end_index, total) }}</div>
|
||||
<div class="col-12">
|
||||
<div class="table-responsive">
|
||||
<form method="post" action="/reports/phone-book" id="bulkForm">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
{% set headers = [
|
||||
{ 'title': 'Name', 'width': '220px' },
|
||||
{ 'title': 'Company' },
|
||||
{ 'title': 'Address' },
|
||||
{ 'title': 'City' },
|
||||
{ 'title': 'State', 'width': '80px' },
|
||||
{ 'title': 'ZIP', 'width': '110px' },
|
||||
{ 'title': 'Phones', 'width': '200px' },
|
||||
{ 'title': 'Actions', 'width': '140px', 'align': 'end' },
|
||||
] %}
|
||||
{% call(answer_table(headers, form_action='/reports/phone-book', select_name='client_ids', enable_bulk=enable_bulk)) %}
|
||||
{% if clients and clients|length > 0 %}
|
||||
{% for c in clients %}
|
||||
<tr>
|
||||
{% if enable_bulk %}
|
||||
<th style="width: 40px;"><input class="form-check-input" type="checkbox" id="selectAll"></th>
|
||||
<td><input class="form-check-input" type="checkbox" name="client_ids" value="{{ c.id }}"></td>
|
||||
{% endif %}
|
||||
<th style="width: 220px;">Name</th>
|
||||
<th>Company</th>
|
||||
<th>Address</th>
|
||||
<th>City</th>
|
||||
<th style="width: 80px;">State</th>
|
||||
<th style="width: 110px;">ZIP</th>
|
||||
<th style="width: 200px;">Phones</th>
|
||||
<th class="text-end" style="width: 140px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if clients and clients|length > 0 %}
|
||||
{% for c in clients %}
|
||||
<tr>
|
||||
{% if enable_bulk %}
|
||||
<td>
|
||||
<input class="form-check-input" type="checkbox" name="client_ids" value="{{ c.id }}">
|
||||
</td>
|
||||
<td><span class="fw-semibold">{{ c.last_name or '' }}, {{ c.first_name or '' }}</span></td>
|
||||
<td>{{ c.company or '' }}</td>
|
||||
<td>{{ c.address or '' }}</td>
|
||||
<td>{{ c.city or '' }}</td>
|
||||
<td>{{ c.state or '' }}</td>
|
||||
<td>{{ c.zip_code or '' }}</td>
|
||||
<td>
|
||||
{% if c.phones and c.phones|length > 0 %}
|
||||
{% for p in c.phones[:3] %}
|
||||
<span class="badge bg-light text-dark me-1">{{ p.phone_number }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
<td>
|
||||
<span class="fw-semibold">{{ c.last_name or '' }}, {{ c.first_name or '' }}</span>
|
||||
</td>
|
||||
<td>{{ c.company or '' }}</td>
|
||||
<td>{{ c.address or '' }}</td>
|
||||
<td>{{ c.city or '' }}</td>
|
||||
<td>{{ c.state or '' }}</td>
|
||||
<td>{{ c.zip_code or '' }}</td>
|
||||
<td>
|
||||
{% if c.phones and c.phones|length > 0 %}
|
||||
{% for p in c.phones[:3] %}
|
||||
<span class="badge bg-light text-dark me-1">{{ p.phone_number }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-sm btn-outline-primary" href="/rolodex/{{ c.id }}">
|
||||
<i class="bi bi-person-lines-fill me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">No clients found.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-sm btn-outline-primary" href="/rolodex/{{ c.id }}">
|
||||
<i class="bi bi-person-lines-fill me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center text-muted py-4">No clients found.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% if enable_bulk %}
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-journal-text me-1"></i>Phone Book (Selected)
|
||||
</button>
|
||||
<a class="btn btn-outline-secondary" href="/reports/phone-book?format=csv{% if q %}&q={{ q | urlencode }}{% endif %}">
|
||||
<i class="bi bi-filetype-csv me-1"></i>Phone Book CSV (Current Filter)
|
||||
</a>
|
||||
</div>
|
||||
{% call(bulk_actions_bar()) %}
|
||||
<button type="submit" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-journal-text me-1"></i>Phone Book (Selected)
|
||||
</button>
|
||||
<a class="btn btn-outline-secondary" href="/reports/phone-book?format=csv{% if q %}&q={{ q | urlencode }}{% endif %}">
|
||||
<i class="bi bi-filetype-csv me-1"></i>Phone Book CSV (Current Filter)
|
||||
</a>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
{% if total_pages and total_pages > 1 %}
|
||||
<nav aria-label="Rolodex pagination">
|
||||
<ul class="pagination mb-0">
|
||||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||
<a class="page-link" href="/rolodex?page={{ page - 1 if page > 1 else 1 }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}{% if phone %}&phone={{ phone | urlencode }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% for p in page_numbers %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="/rolodex?page={{ p }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}{% if phone %}&phone={{ phone | urlencode }}{% endif %}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
<a class="page-link" href="/rolodex?page={{ page + 1 if page < total_pages else total_pages }}&page_size={{ page_size }}{% if q %}&q={{ q | urlencode }}{% endif %}{% if phone %}&phone={{ phone | urlencode }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{{ pagination('/rolodex', page, total_pages, page_size, {'q': q, 'phone': phone}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', function() {
|
||||
document.querySelectorAll('input[name="client_ids"]').forEach(cb => cb.checked = selectAll.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -16,18 +16,19 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ '/rolodex/create' if not client else '/rolodex/' ~ client.id ~ '/update' }}">
|
||||
<div class="mb-2 text-muted small" id="fieldHelp" aria-live="polite">Focus a field to see help.</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="last_name" class="form-label">Last Name</label>
|
||||
<input type="text" class="form-control" id="last_name" name="last_name" value="{{ client.last_name if client else '' }}">
|
||||
<input type="text" class="form-control" id="last_name" name="last_name" data-help="Client last name (surname)." value="{{ client.last_name if client else '' }}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="first_name" class="form-label">First Name</label>
|
||||
<input type="text" class="form-control" id="first_name" name="first_name" value="{{ client.first_name if client else '' }}">
|
||||
<input type="text" class="form-control" id="first_name" name="first_name" data-help="Client given name." value="{{ client.first_name if client else '' }}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="company" class="form-label">Company</label>
|
||||
<input type="text" class="form-control" id="company" name="company" value="{{ client.company if client else '' }}">
|
||||
<input type="text" class="form-control" id="company" name="company" data-help="Organization or employer (optional)." value="{{ client.company if client else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
@@ -36,20 +37,20 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="city" class="form-label">City</label>
|
||||
<input type="text" class="form-control" id="city" name="city" value="{{ client.city if client else '' }}">
|
||||
<input type="text" class="form-control" id="city" name="city" data-help="City or locality." value="{{ client.city if client else '' }}">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label for="state" class="form-label">State</label>
|
||||
<input type="text" class="form-control" id="state" name="state" value="{{ client.state if client else '' }}">
|
||||
<input type="text" class="form-control" id="state" name="state" data-help="2-letter state code (e.g., NY)." value="{{ client.state if client else '' }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label for="zip_code" class="form-label">ZIP</label>
|
||||
<input type="text" class="form-control" id="zip_code" name="zip_code" value="{{ client.zip_code if client else '' }}">
|
||||
<input type="text" class="form-control" id="zip_code" name="zip_code" data-help="5-digit ZIP or ZIP+4." value="{{ client.zip_code if client else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="rolodex_id" class="form-label">Legacy Rolodex Id</label>
|
||||
<input type="text" class="form-control" id="rolodex_id" name="rolodex_id" value="{{ client.rolodex_id if client else '' }}">
|
||||
<input type="text" class="form-control" id="rolodex_id" name="rolodex_id" data-help="Legacy ID used for migration and lookup; may be alphanumeric." value="{{ client.rolodex_id if client else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
Reference in New Issue
Block a user