add django templates
This commit is contained in:
+2
-2
@@ -1,5 +1,4 @@
|
||||
sample.txt
|
||||
*.html
|
||||
*.log
|
||||
*.pyc
|
||||
*.bak
|
||||
@@ -16,5 +15,6 @@ web/rota
|
||||
|
||||
cons/
|
||||
|
||||
|
||||
output/*.html
|
||||
output/tests/*.html
|
||||
*.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>
|
||||
Reference in New Issue
Block a user