{# Shared form fragments. #} {% macro err(errors, name) %} {%- if errors and name in errors %}{{ errors[name] }}{% endif -%} {% endmacro %} {% macro errors_box(errors) %} {%- if errors %}
Could not save:
{%- endif %} {% endmacro %} {% macro filter_control(col, active, form_id) %} {%- if col.kind == 'enum' -%} {%- else -%} {%- endif -%} {% endmacro %} {% macro sort_links(col, sort, dir, sort_url) %} ▲▼ {%- endmacro %} {% macro filter_help() %} ?what does !text mean? Filters (one box per column, combined with AND)
textcontains
!textdoes not contain
=textequals
>5 <5larger / smaller (numbers, dates as 2026-09-01, otherwise text order)
-empty
*not empty
Lists: cmd-click (ctrl-click) to select several. Sorting: ▲ A to Z, ▼ Z to A next to each heading; empty values sort last. Filters, sorting and search stay in the URL, so a view can be bookmarked.
{%- endmacro %} {% macro filter_form(form_id, action, active, q=None, sort='', dir='asc') %}
{% if q is not none %}{% endif %} {% if sort %}{% endif %} {{ filter_help() }} {% if active or sort %}clear filters and sorting{% endif %}
{% endmacro %} {% macro drafts_section(company, draft_contact, drafts, signal, observation, page) %}

Message drafts{% if draft_contact %} for {{ draft_contact.first_name }}{% endif %} ({{ language_names[drafts[0].language] }})

Three deliberately different angles, no AI. Square brackets are yours to fill. Edit in place, copy, or open it in the log form. The language is the contact's draft language, else the company's country; the picker here changes it for this page only.

{% for d in drafts %}
{{ d.label }}
log as sent
{% endfor %}
{% endmacro %} {# An interaction is always with a person. With `contact` (the contact page) the person is fixed; elsewhere you pick one. A note has no direction. #} {% macro interaction_form(action, company, values, errors=None, submit='Log interaction', contact=None) %}
{{ errors_box(errors) }}
{% for ch in channels %} {% endfor %} {% for d in directions %} {% endfor %} {% if contact %} {% else %} {% endif %}
{{ custom_fields(custom_fields_for('interaction'), values, errors) }} {# The body is the record. Custom fields sit beside it and never touch it. #}
{% endmacro %} {% macro custom_fields(defs, values, errors) %} {%- if defs %}
{% for f in defs %} {{ f.label }} {%- if f.type == 'select' %} {%- elif f.type == 'date' %} {%- elif f.type == 'number' %} {%- else %} {%- endif %} {{ err(errors, 'custom_' ~ f.key) }} {%- if f.help %} {{ f.help }}{% endif %} {% endfor %}
{%- endif %} {% endmacro %} {% macro company_fields(values, errors) %}
{{ custom_fields(custom_fields_for('company'), values, errors) }}
{% if type_names() %}{% endif %}
{% endmacro %} {% macro contact_fields(values, errors) %}
{% set lang = (values.language or '') | trim | lower %}
{{ custom_fields(custom_fields_for('contact'), values, errors) }} {% endmacro %} {% macro timeline(company, interactions) %} {% endmacro %} {% macro type_select(current='', autosubmit=false, name='type') -%} {%- set options = type_names() -%} {%- if options -%} {%- endif -%} {%- endmacro %} {% macro type_cell(company, t, index, contact='') -%} {% if type_names() and not t.done %}
{{ type_select(t.type, autosubmit=true) }}
{% else %}{{ type_chip(t.type) }}{% endif %} {%- endmacro %} {% macro type_chip(name) -%} {%- if name -%} {%- set colour = type_colour(name) -%} {{ name }} {%- endif -%} {%- endmacro %} {% macro todo_actions(company, todo, back='', delete=false) -%} {%- if todo.is_field -%}
{% if back %}{% endif %}
{%- if delete %}
{% if back %}{% endif %}
{%- endif %} {%- else -%}
{% if back %}{% endif %}
{%- if delete %}
{% if back %}{% endif %}
{%- endif %} {%- endif -%} {%- endmacro %} {% macro task_section(company, today) -%} {% set first = company.next_todo() %}

Tasks

For the company and its people. The next step is the open task due first; change a due date to change what is next. This company on the Tasks page.

{% if type_names() %}{% endif %} {% for t in company.open_todos() + (company.todos() | selectattr('done') | list) %} {% set is_next = first and not first.done and not t.done and t.key == first.key %} {% if type_names() %}{{ type_cell(company, t, t.index, t.contact.slug if t.contact else '') }}{% endif %} {% else %} {% endfor %}
tasktypeforduestatus
{{ t.text or '(no text)' }}{% if is_next %} next step{% endif %}{% if not type_names() %} {{ type_chip(t.type) }}{% endif %}{% if t.contact %}{{ t.contact.name }}{% else %}company{% endif %} {{ fmt_date(t.due) if t.due else 'no date' }} {% if t.done %}done{% if t.done_on %} {{ fmt_date(t.done_on) }}{% endif %}{% else %}open{% endif %} {{ todo_actions(company, t, delete=true) }} {% if is_next and t.due %}Add to Google Calendar{% endif %}
No tasks yet.
{% if type_names() %}{% endif %}
{%- endmacro %} {% macro task_list(company, tasks, today, contact='', adding=true) -%}
{% for t in tasks %} {% if type_names() %}{{ type_cell(company, t, loop.index0, contact) }}{% endif %} {% else %} {% endfor %}
{{ t.text }}{% if not type_names() %} {{ type_chip(t.type) }}{% endif %}{{ fmt_date(t.due) if t.due else '' }} {% if t.done %}done{% if t.done_on %} {{ fmt_date(t.done_on) }}{% endif %}{% endif %}
Nothing on the list.
{% if adding %}
{% if type_names() %}{% endif %}
{% endif %} {%- endmacro %}