add django templates

This commit is contained in:
Ross
2025-12-09 20:58:00 +00:00
parent 48c219281e
commit ecd09f3701
19 changed files with 715 additions and 2 deletions
+2 -2
View File
@@ -1,5 +1,4 @@
sample.txt sample.txt
*.html
*.log *.log
*.pyc *.pyc
*.bak *.bak
@@ -16,5 +15,6 @@ web/rota
cons/ cons/
output/*.html
output/tests/*.html
*.sqlite3 *.sqlite3
@@ -0,0 +1,27 @@
{% load static %}
<html>
<head>
<title>Rotas</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Rotas</h1>
<p>
<a class="button is-link" href="{% url 'rota:worker_add' %}">Add worker</a>
<a class="button is-primary" href="{% url 'rota:rota_add' %}">Create rota</a>
</p>
<div class="box">
<ul>
{% for rota in rotas %}
<li><a href="{% url 'rota:rota_detail' rota.id %}">{{ rota.name }}</a> ({{ rota.start_date }} → {{ rota.end_date }})</li>
{% empty %}
<li>No rotas yet</li>
{% endfor %}
</ul>
</div>
</div>
</section>
</body>
</html>
@@ -0,0 +1,44 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script>
function initFlatpickr(context=document) {
try {
context.querySelectorAll('.datepicker').forEach(function(el) {
// prevent double-init
if (el._flatpickr) return;
flatpickr(el, {dateFormat: 'Y-m-d'});
});
} catch (e) {
console.error('flatpickr init error', e);
}
}
document.addEventListener('DOMContentLoaded', function () { initFlatpickr(); });
// HTMX: re-run init after swaps so dynamically loaded forms get datepickers
// Use `document` so this script can be included before `<body>` exists.
document.addEventListener('htmx:afterSwap', function(evt) {
// evt.detail ? htmx fires on the document; evt.target is the swapped element
initFlatpickr(evt.target || document);
});
// Ensure HTMX includes Django CSRF token on non-GET requests by reading cookie
// This helps POSTs from modal forms succeed under Django's CSRF protection.
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
document.addEventListener('htmx:configRequest', function(evt){
try {
const csrftoken = getCookie('csrftoken');
if(csrftoken){
evt.detail.headers['X-CSRFToken'] = csrftoken;
}
} catch(e) {
console.error('htmx csrf attach error', e);
}
});
</script>
@@ -0,0 +1,52 @@
{% load crispy_forms_tags %}
<script>
function _close_rota_option_modal(){
try{ var m=document.getElementById('modal'); if(m) m.innerHTML=''; }catch(e){}
}
</script>
<div class="modal is-active" id="rota-option-modal">
<div class="modal-background" onclick="_close_rota_option_modal()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ initial.key|default:'Add option' }}</p>
<button class="delete" aria-label="close" onclick="_close_rota_option_modal()"></button>
</header>
<section class="modal-card-body">
<form method="post" hx-post="{{ form_action }}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
<div class="field">
<label class="label">Key</label>
<div class="control">
<input class="input" name="key" type="text" value="{{ initial.key }}" {% if initial.key %}readonly{% endif %} />
</div>
</div>
<div class="field">
<label class="label">Type</label>
<div class="control">
<div class="select">
<select name="type">
<option value="string" {% if initial.type == 'string' %}selected{% endif %}>String</option>
<option value="int" {% if initial.type == 'int' %}selected{% endif %}>Integer</option>
<option value="float" {% if initial.type == 'float' %}selected{% endif %}>Float</option>
<option value="bool" {% if initial.type == 'bool' %}selected{% endif %}>Boolean</option>
<option value="json" {% if initial.type == 'json' %}selected{% endif %}>JSON (list/object)</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">Value</label>
<div class="control">
<textarea name="value" class="textarea" rows="4">{{ initial.value }}</textarea>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
<button type="button" class="button" onclick="_close_rota_option_modal()">Cancel</button>
</div>
</div>
</form>
</section>
</div>
</div>
@@ -0,0 +1,30 @@
<div id="rota-options-display">
<div style="margin-bottom:0.5em;">
<button class="button is-small" hx-get="{% url 'rota:rota_option_add' rota.id %}" hx-target="#modal" hx-swap="innerHTML">Add option</button>
</div>
<table class="table is-fullwidth is-striped">
<thead><tr><th>Key</th><th>Value</th><th>Actions</th></tr></thead>
<tbody>
{% if rota.options %}
{% for k,v in rota.options.items %}
<tr>
<td><code>{{ k }}</code></td>
<td><pre style="white-space:pre-wrap;">{{ v|json_script:k }}</pre></td>
<td>
<button class="button is-small is-info" hx-get="{% url 'rota:rota_option_edit' rota.id %}?key={{ k }}" hx-target="#modal" hx-swap="innerHTML">Edit</button>
<form method="post" action="{% url 'rota:rota_option_delete' rota.id %}" style="display:inline" hx-post="{% url 'rota:rota_option_delete' rota.id %}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
<input type="hidden" name="key" value="{{ k }}" />
<button class="button is-small is-danger" type="submit">Delete</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="3"><em>No options set</em></td></tr>
{% endfor %}
{% else %}
<tr><td colspan="3"><em>No options set</em></td></tr>
{% endif %}
</tbody>
</table>
</div>
@@ -0,0 +1,57 @@
{% load crispy_forms_tags %}
<script>
function _close_rota_options_modal(){
try{ var m=document.getElementById('modal'); if(m) m.innerHTML=''; }catch(e){}
}
</script>
<div class="modal is-active" id="rota-options-modal">
<div class="modal-background" onclick="_close_rota_options_modal()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Edit Rota Options</p>
<button class="delete" aria-label="close" onclick="_close_rota_options_modal()"></button>
</header>
<section class="modal-card-body">
{% if form %}
<form method="post" hx-post="{{ form_action }}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
{% comment %} Render only option fields (those prefixed with `opt__`) so the modal
focuses on rota-builder options and constraints. {% endcomment %}
{% for field in form.visible_fields %}
{% if field.name|slice:":5" == "opt__" %}
<div class="field">
<label class="label">{{ field.label }}</label>
<div class="control">{{ field }}</div>
{% if field.help_text %}<p class="help">{{ field.help_text }}</p>{% endif %}
{% for err in field.errors %}<p class="help is-danger">{{ err }}</p>{% endfor %}
</div>
{% endif %}
{% endfor %}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
<button type="button" class="button" onclick="_close_rota_options_modal()">Cancel</button>
</div>
</div>
</form>
{% else %}
<form method="post" hx-post="{{ form_action }}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
<div class="field">
<label class="label">Options (JSON)</label>
<div class="control">
<textarea name="options_json" class="textarea" rows="10">{{ initial.options_json }}</textarea>
</div>
<p class="help">Edit rota options as JSON. See available constraint keys on the rota detail page.</p>
</div>
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
<button type="button" class="button" onclick="_close_rota_options_modal()">Cancel</button>
</div>
</div>
</form>
{% endif %}
</section>
</div>
</div>
@@ -0,0 +1,21 @@
<div class="modal is-active" id="shift-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Confirm delete</p>
<button class="delete" aria-label="close" onclick="document.getElementById('modal').innerHTML='' "></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to delete shift <strong>{{ shift.name }}</strong>?</p>
<form method="post" hx-post="{% url 'rota:shift_delete' rota.id idx %}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
<div class="field mt-4">
<div class="control">
<button class="button is-danger" type="submit">Delete</button>
<button type="button" class="button" onclick="document.getElementById('modal').innerHTML=''">Cancel</button>
</div>
</div>
</form>
</section>
</div>
</div>
@@ -0,0 +1,10 @@
{% load crispy_forms_tags %}
<form method="post" hx-post="{% url 'rota:shift_add' rota.id %}" hx-target="#shift-list" hx-swap="outerHTML">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Add shift</button>
</div>
</div>
</form>
@@ -0,0 +1,28 @@
{% load crispy_forms_tags %}
<script>
function _close_shift_modal_targets(){
try{ var m=document.getElementById('modal'); if(m) m.innerHTML=''; }catch(e){}
try{ var s=document.getElementById('shift-form'); if(s) s.innerHTML=''; }catch(e){}
}
</script>
<div class="modal is-active" id="shift-modal">
<div class="modal-background" onclick="_close_shift_modal_targets()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Shift</p>
<button class="delete" aria-label="close" onclick="_close_shift_modal_targets()"></button>
</header>
<section class="modal-card-body">
<form method="post" hx-post="{{ form_action }}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
<button type="button" class="button" onclick="_close_shift_modal_targets()">Cancel</button>
</div>
</div>
</form>
</section>
</div>
</div>
@@ -0,0 +1,21 @@
<div id="shift-list">
<h3 class="subtitle">Shifts</h3>
<div class="box">
<ul>
{% for s in rota.shifts %}
<li>
<strong>{{ s.name }}</strong> — sites: {{ s.sites|join:", " }} — days: {{ s.days|join:", " }} — workers: {{ s.workers_required }}
{% if s.balance_offset is defined and s.balance_offset %}
— balance offset: {{ s.balance_offset }}
{% endif %}
<span class="buttons is-right">
<button class="button is-small is-info" hx-get="{% url 'rota:shift_edit' rota.id forloop.counter0 %}" hx-target="#modal" hx-swap="innerHTML">Edit</button>
<button class="button is-small is-danger" hx-get="{% url 'rota:shift_delete' rota.id forloop.counter0 %}" hx-target="#modal" hx-swap="innerHTML">Delete</button>
</span>
</li>
{% empty %}
<li>No shifts defined yet.</li>
{% endfor %}
</ul>
</div>
</div>
@@ -0,0 +1,21 @@
<div class="modal is-active" id="worker-delete-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Confirm delete</p>
<button class="delete" aria-label="close" onclick="document.getElementById('modal').innerHTML='' "></button>
</header>
<section class="modal-card-body">
<p>Are you sure you want to delete worker <strong>{{ worker.name }}</strong>?</p>
<form method="post" hx-post="{% url 'rota:worker_delete' rota.id worker.id %}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
<div class="field mt-4">
<div class="control">
<button class="button is-danger" type="submit">Delete</button>
<button type="button" class="button" onclick="document.getElementById('modal').innerHTML=''">Cancel</button>
</div>
</div>
</form>
</section>
</div>
</div>
@@ -0,0 +1,26 @@
{% load crispy_forms_tags %}
<div class="modal is-active" id="worker-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Worker</p>
<button class="delete" aria-label="close" onclick="document.getElementById('modal').innerHTML='' "></button>
</header>
<section class="modal-card-body">
<form method="post" hx-post="{{ form_action }}" hx-target="#modal" hx-swap="innerHTML">
{% csrf_token %}
{# Preserve rota_id if provided so POST handlers can attach the worker to the rota #}
{% if rota_id %}
<input type="hidden" name="rota_id" value="{{ rota_id }}" />
{% endif %}
{{ form|crispy }}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
<button type="button" class="button" onclick="document.getElementById('modal').innerHTML=''">Cancel</button>
</div>
</div>
</form>
</section>
</div>
</div>
@@ -0,0 +1,16 @@
<div id="worker-list">
<h2 class="subtitle">Workers</h2>
<div class="content">
<ul>
{% for w in rota.workers.all %}
<li>
<a href="{% url 'rota:worker_detail' w.id %}">{{ w.name }}</a>
<button class="button is-small is-light ml-2" hx-get="{% url 'rota:worker_edit' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">Edit</button>
<button class="button is-small is-danger ml-2" hx-get="{% url 'rota:worker_delete' rota.id w.id %}" hx-target="#modal" hx-swap="innerHTML">Delete</button>
</li>
{% empty %}
<li>No workers assigned</li>
{% endfor %}
</ul>
</div>
</div>
@@ -0,0 +1,144 @@
<html>
<head>
<title>Rota: {{ rota.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
{% include 'rota/partials/flatpickr_includes.html' %}
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">{{ rota.name }}</h1>
<p>
<a class="button is-small" href="{% url 'rota:rota_edit' rota.id %}">Edit rota</a>
</p>
<p class="subtitle">{{ rota.description }}</p>
<p>Period: {{ rota.start_date }} → {{ rota.end_date }}</p>
<div class="mb-4">
<p>
<button class="button is-link" hx-get="{% url 'rota:worker_add' %}?rota_id={{ rota.id }}" hx-target="#modal" hx-swap="innerHTML">Add worker</button>
</p>
{% include 'rota/partials/worker_list.html' %}
</div>
<div class="mb-4">
<h2 class="subtitle">Historic runs</h2>
<div class="box">
<div style="margin-bottom:0.5em;">
<form method="post" action="{% url 'rota:rota_runs_clear' rota.id %}" style="display:inline" onsubmit="return confirm('Delete all historic runs for this rota? This cannot be undone.')">
{% csrf_token %}
<button class="button is-danger is-light is-small" type="submit">Clear all runs</button>
</form>
</div>
<ul>
{% for run in rota.runs.all %}
<li>
<a href="{% url 'rota:rota_run_detail' run.id %}">Run {{ run.id }}</a>
{{ run.get_status_display }}
{% if run.finished_at %} — finished {{ run.finished_at }}{% endif %}
<span class="ml-2">
<a class="button is-small is-light" href="{% url 'rota:rota_run_export_html' run.id %}" target="_blank">Export</a>
</span>
</li>
{% empty %}
<li>No previous runs</li>
{% endfor %}
</ul>
</div>
</div>
<div class="mb-4">
<h2 class="subtitle">Builder export</h2>
<p>
<a class="button is-link" href="{% url 'rota:rota_export_builder' rota.id %}" target="_blank">View generated HTML (no solver)</a>
<a class="button" href="{% url 'rota:rota_export_builder' rota.id %}?solve=1">Generate + Solve (may be slow)</a>
</p>
</div>
<div class="mb-4">
<h2 class="subtitle">Rota Settings</h2>
<div class="box">
<p class="help">View and edit the rota's `options` used by the builder. Click Edit to open a modal editor.</p>
<div style="margin-bottom:0.5em;">
<button class="button is-link" hx-get="{% url 'rota:rota_options' rota.id %}" hx-target="#modal" hx-swap="innerHTML">Edit options</button>
</div>
<div>
{% include 'rota/partials/rota_options_display.html' %}
</div>
<hr />
<h3 class="subtitle is-6">Available constraint keys</h3>
<div style="max-height:400px; overflow:auto;">
<table class="table is-fullwidth is-striped is-narrow">
<thead>
<tr>
<th>Key</th>
<th>Default</th>
<th>Current</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for key, info in available_constraints.items %}
<tr>
<td><code>{{ key }}</code></td>
<td><pre style="white-space:pre-wrap;">{{ info.default|default:"" }}</pre></td>
<td><pre style="white-space:pre-wrap;">{{ info.current|default:"" }}</pre></td>
<td>
{{ info.description|default:"" }}
{% comment %} Quick action: add default value if none set {% endcomment %}
{% if info.current is None or info.current == "" %}
<div style="margin-top:0.4em;">
<button class="button is-small is-light" hx-get="{% url 'rota:rota_option_add' rota.id %}?key={{ key }}" hx-target="#modal" hx-swap="innerHTML">Add default</button>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="4">No constraint metadata available</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<label class="checkbox">
<input type="checkbox" name="generate_builder" value="1"> Generate HTML export
</label>
</div>
<div class="control">
<label class="checkbox">
<input type="checkbox" name="builder_solve" value="1"> Solve when exporting (may be slow)
</label>
</div>
<div class="control">
<button class="button is-primary" type="submit">Run scheduler (background)</button>
</div>
</div>
</form>
<hr>
<div>
<h2 class="subtitle">Edit shifts</h2>
<p>
<button class="button is-link" hx-get="{% url 'rota:shift_add' rota.id %}" hx-target="#shift-form" hx-swap="innerHTML">Add shift</button>
</p>
<div id="shift-form">
{# Load form via HTMX when user clicks 'Add shift' - initial empty state #}
</div>
<div id="modal" hx-on="closeModal: this.innerHTML='' "></div>
{% include 'rota/partials/shift_list.html' %}
</div>
</div>
</section>
</body>
</html>
@@ -0,0 +1,81 @@
{% load crispy_forms_tags %}
<html>
<head>
<title>Create Rota</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
</head>
{% include 'rota/partials/flatpickr_includes.html' %}
<body>
<section class="section">
<div class="container">
<h1 class="title">{% if editing %}Edit Rota{% else %}Create Rota{% endif %}</h1>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<script>
(function(){
function parseISO(s){
if(!s) return null;
// YYYY-MM-DD
const parts = s.split('-');
if(parts.length!==3) return null;
return new Date(parts[0], parts[1]-1, parts[2]);
}
function formatISO(d){
if(!d) return '';
const yyyy = d.getFullYear();
const mm = String(d.getMonth()+1).padStart(2,'0');
const dd = String(d.getDate()).padStart(2,'0');
return `${yyyy}-${mm}-${dd}`;
}
function computeEnd(){
const startEl = document.querySelector('[name="start_date"]');
const weeksEl = document.querySelector('[name="weeks"]');
const endEl = document.querySelector('[name="end_date"]');
if(!startEl||!weeksEl||!endEl) return;
const startVal = startEl.value;
const weeksVal = parseInt(weeksEl.value || '0', 10);
const startDate = parseISO(startVal);
if(startDate && weeksVal>0){
const endDate = new Date(startDate.getTime());
endDate.setDate(endDate.getDate() + weeksVal*7);
const iso = formatISO(endDate);
// set flatpickr-aware if present
if(endEl._flatpickr){
endEl._flatpickr.setDate(iso, true);
} else {
endEl.value = iso;
}
}
}
document.addEventListener('DOMContentLoaded', function(){
const startEl = document.querySelector('[name="start_date"]');
const weeksEl = document.querySelector('[name="weeks"]');
if(startEl) startEl.addEventListener('change', computeEnd);
if(weeksEl) weeksEl.addEventListener('input', computeEnd);
// initial compute
setTimeout(computeEnd, 50);
});
// listen on document so this works when inserted dynamically
document.addEventListener('htmx:afterSwap', function(evt){
// rebind when form is replaced or loaded via HTMX
const startEl = document.querySelector('[name="start_date"]');
const weeksEl = document.querySelector('[name="weeks"]');
if(startEl) startEl.addEventListener('change', computeEnd);
if(weeksEl) weeksEl.addEventListener('input', computeEnd);
});
})();
</script>
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">{% if editing %}Save{% else %}Create{% endif %}</button>
</div>
</div>
</form>
</div>
</section>
</body>
</html>
@@ -0,0 +1,47 @@
{% load tz %}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Rota run {{ run.id }}</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
<style>pre { white-space: pre-wrap; }</style>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Rota run for {{ run.rota.name }}</h1>
<p class="subtitle">Status: {{ run.get_status_display }} ({{ run.status }})</p>
<p>Created: {{ run.created_at|localtime }} started: {{ run.started_at|localtime }} finished: {{ run.finished_at|localtime }}</p>
<div class="box">
<h2 class="subtitle">Log</h2>
<pre>{{ run.log }}</pre>
</div>
<div class="box">
<h2 class="subtitle">Result</h2>
<pre>{{ run.result }}</pre>
<p class="mt-4">
{% if run.export_html %}
<a class="button is-link" href="{% url 'rota:rota_run_export_html' run.id %}" target="_blank">View Stored Export</a>
<a class="button" href="{% url 'rota:rota_run_export_download' run.id %}">Download Stored Export</a>
{% else %}
<span class="tag is-light">No stored export</span>
{% endif %}
<form method="post" action="{% url 'rota:rota_run_export_regenerate' run.id %}" style="display:inline; margin-left:0.5em;">
{% csrf_token %}
<input type="hidden" name="solve" value="0">
<button class="button is-small is-light" type="submit">Regenerate export (no solve)</button>
</form>
<form method="post" action="{% url 'rota:rota_run_export_regenerate' run.id %}" style="display:inline; margin-left:0.5em;">
{% csrf_token %}
<input type="hidden" name="solve" value="1">
<button class="button is-small is-primary" type="submit">Regenerate + Solve (may be slow)</button>
</form>
</p>
</div>
</div>
</section>
</body>
</html>
@@ -0,0 +1,23 @@
<html>
<head>
<meta charset="utf-8">
<title>Rota Run Export - {{ run.rota.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Run export for {{ run.rota.name }}</h1>
<p class="subtitle">Status: {{ run.status }}{% if run.finished_at %} — finished {{ run.finished_at }}{% endif %}</p>
<h2 class="subtitle">Raw result</h2>
<pre style="white-space:pre-wrap;">{{ run.result|safe }}</pre>
<p class="mt-4">
<a class="button is-link" href="{% url 'rota:rota_run_export_download' run.id %}">Download HTML</a>
<a class="button" href="{% url 'rota:rota_run_detail' run.id %}">Back</a>
</p>
</div>
</section>
</body>
</html>
@@ -0,0 +1,40 @@
{% load crispy_forms_tags %}
<html>
<head>
<title>{{ worker.name }}</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
{% include 'rota/partials/flatpickr_includes.html' %}
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">{{ worker.name }}</h1>
<p class="subtitle">{{ worker.email }} - {{ worker.site }}</p>
<div class="box">
<h2 class="subtitle">Request leave</h2>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Add leave</button>
</div>
</div>
</form>
</div>
<h2 class="subtitle">Existing leave</h2>
<div class="content">
<ul>
{% for l in worker.leaves.all %}
<li>{{ l.start_date }} → {{ l.end_date }} {% if l.reason %}({{ l.reason }}){% endif %}</li>
{% empty %}
<li>No leave recorded</li>
{% endfor %}
</ul>
</div>
</div>
</section>
</body>
</html>
@@ -0,0 +1,25 @@
{% load crispy_forms_tags %}
<html>
<head>
<title>Add Worker</title>
<link href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% include 'rota/partials/flatpickr_includes.html' %}
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Add Worker</h1>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Save</button>
</div>
</div>
</form>
</div>
</section>
</body>
</html>