Files
he-database/sheets/templates/abrechnung.html
2026-02-17 14:59:55 +01:00

371 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% load dict_extras %}
{% block content %}
<div class="spreadsheet-container">
<style>
.spreadsheet-container { padding: 20px; }
.sheet-navigation {
display:flex; justify-content:space-between; align-items:center;
margin-bottom:20px; padding:10px; background:#f8f9fa; border-radius:5px;
}
.sheet-navigation h2 { margin:0; font-size:20px; font-weight:bold; }
.sheet-navigation a { text-decoration:none; color:#007bff; font-weight:bold; }
.unit-cell {
text-align: center;
font-weight: 600;
background: #f9f9f9;
}
.table-container {
background:#fff; border-radius:5px; padding:10px;
box-shadow:0 2px 4px rgba(0,0,0,0.05);
}
.sheet-table { width:100%; border-collapse:collapse; font-size:14px; }
.sheet-table th, .sheet-table td { border:1px solid #dee2e6; padding:4px 6px; }
.sheet-table th { background:#f2f2f2; font-weight:bold; text-align:center; }
.row-label { background:#f9f9f9; font-weight:bold; white-space:nowrap; }
.number-cell { text-align:right; white-space:nowrap; }
.sheet-table tbody tr:nth-child(even) { background:#fcfcfc; }
.sheet-table tbody tr:hover { background:#f1f3f5; }
.cell-input {
width: 100%;
box-sizing: border-box;
text-align: right;
border: 1px solid #ced4da;
border-radius: 3px;
padding: 2px 4px;
font-size: 14px;
background: #fff;
}
.sheet-table {
table-layout: fixed;
width: auto;
}
.cell-input {
width: 80px;
box-sizing: border-box;
}
.sheet-table th,
.sheet-table td {
padding: 4px 6px;
font-size: 13px;
}
.actions-bar { margin-top: 12px; display:flex; gap:10px; align-items:center; }
.btn {
padding: 6px 10px; border:1px solid #ced4da; border-radius:4px;
background:#fff; cursor:pointer; font-weight:600;
}
</style>
<div class="sheet-navigation">
<a href="{% url 'clients_list' %}">&larr; Übersicht</a>
<h2>Abrechnung</h2>
<a href="{% url 'halfyear_balance' %}">Halbjahres-Bilanz</a>
</div>
<div id="autosaveStatus" style="margin:8px 0; font-size: 14px;"></div>
{% if needs_interval %}
<div class="table-container">
<p>Bitte zuerst ein Halbjahr auswählen (auf der Übersicht-Seite), damit der Zeitraum bekannt ist.</p>
</div>
{% else %}
<div class="table-container">
<div style="margin-bottom:10px; font-weight:600;">
Aufstellung Heliumverbrauch für Zeitraum: <b>{{ interval_text }}</b>
</div>
<div class="abrechnung-row" style="display:flex; gap:24px; align-items:flex-start; overflow-x:auto;">
<div class="abrechnung-left" style="flex: 0 0 auto;">
<table class="sheet-table">
<thead>
<tr>
<th style="width:180px;">&larr; Fachgebiet &rarr;</th>
{% for col_key, col_label in columns %}
<th style="width:90px;">{{ col_label }}</th>
{% endfor %}
<th style="width:70px;">Einheit</th> <!-- 👈 ADD THIS -->
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr>
<td class="row-label">{{ r.label }}</td>
{% for c in r.cells %}
<td class="number-cell">
{% if r.row_key == "sonstiges" %}
<span class="sonstiges-text" data-col-key="{{ c.col_key }}">
{{ sonstiges_text|get_item:c.col_key }}
</span>
{% else %}
{% if r.editable %}
<input
class="cell-input editable"
type="text"
value="{% if c.value is not None %}{{ c.value|floatformat }}{% endif %}"
data-row-key="{{ r.row_key }}"
data-col-key="{{ c.col_key }}"
/>
{% else %}
<span class="cell-value"
data-row-key="{{ r.row_key }}"
data-col-key="{{ c.col_key }}">
{% if c.value is not None %}{{ c.value|floatformat:2 }}{% endif %}
</span>
{% endif %}
{% endif %}
</td>
{% endfor %}
<td class="unit-cell">{{ r.unit }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> <!-- end abrechnung-left -->
<div class="abrechnung-right" style="flex: 0 0 auto;">
<table class="sheet-table spreadsheet-table">
<thead>
<tr>
<th>Summe Lichtwiese<br>IJKL</th>
<th style="min-width:180px; text-align:center;">
&larr; Fachgebiet &rarr;
</th>
<th>Summe Lichtwiese<br>NOP (Check)</th>
<th>Summe<br>Stadtmitte</th>
<th>Summe<br>(Check)</th>
</tr>
</thead>
<tbody>
{% for r in right_summary_rows %}
<tr>
<td>
{% if r.is_text %}{{ r.ijkl }}{% else %}{{ r.ijkl|floatformat:2 }}{% endif %}
</td>
<td class="row-label">
{{ r.label }}
{% if r.unit %}
<span style="float:right; opacity:0.7;">{{ r.unit }}</span>
{% endif %}
</td>
<td>
{% if r.is_text %}{{ r.nop }}{% else %}{{ r.nop|floatformat:2 }}{% endif %}
</td>
<td>
{% if r.is_text %}{{ r.stadt }}{% else %}{{ r.stadt|floatformat:2 }}{% endif %}
</td>
<td>
{% if r.is_text %}{{ r.check }}{% else %}{{ r.check|floatformat:2 }}{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> <!-- end abrechnung-row -->
</div>
<script>
(function () {
// ===============================
// Helpers
// ===============================
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(";").shift();
return "";
}
function parseNum(val) {
if (val == null) return 0;
let s = String(val).trim();
if (!s) return 0;
s = s.replace(/\s+/g, "");
const hasComma = s.includes(",");
const hasDot = s.includes(".");
if (hasComma && hasDot) {
// German: 1.234,56 -> 1234.56
s = s.replace(/\./g, "").replace(",", ".");
} else if (hasComma && !hasDot) {
// 35,08 -> 35.08
s = s.replace(",", ".");
} else {
// 35.08 stays 35.08
}
const n = parseFloat(s);
return isNaN(n) ? 0 : n;
}
function fmt2(n) {
return (Math.round(n * 100) / 100).toFixed(2);
}
function getInputValue(rowKey, colKey) {
const inp = document.querySelector(
`.cell-input[data-row-key="${rowKey}"][data-col-key="${colKey}"]`
);
return inp ? inp.value : "";
}
function getCellSpan(rowKey, colKey) {
return document.querySelector(
`.cell-value[data-row-key="${rowKey}"][data-col-key="${colKey}"]`
);
}
function setDisplay(rowKey, colKey, value) {
const el = getCellSpan(rowKey, colKey);
if (el) el.textContent = fmt2(value);
}
function getDisplayNum(rowKey, colKey) {
const el = getCellSpan(rowKey, colKey);
if (!el) return 0;
return parseNum(el.textContent);
}
function getSonstigesCell(colKey) {
return document.querySelector(
`.sonstiges-text[data-col-key="${colKey}"]`
);
}
function setSonstiges(colKey, betragVal) {
const el = getSonstigesCell(colKey);
if (!el) return;
let txt = "--------------";
if (betragVal < 0) txt = "Gutschrift";
else if (betragVal > 0) txt = "Nachzahlung";
el.textContent = txt;
}
// ===============================
// Config
// ===============================
const SAVE_URL = "{% url 'abrechnung_autosave' %}";
const CSRF = getCookie("csrftoken");
// from backend: context["bezugskosten_gashe"] = bezugskosten_gashe
const BEZUGSKOSTEN_GASHE = parseNum("{{ bezugskosten_gashe|floatformat:6 }}");
// ===============================
// Autosave (debounced)
// ===============================
let debounceTimer = null;
let pending = {}; // "row|col" -> {row_key,col_key,value}
function queueSave(rowKey, colKey, value) {
pending[rowKey + "|" + colKey] = { row_key: rowKey, col_key: colKey, value: value };
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(flushPending, 400);
}
async function flushPending() {
const changes = Object.values(pending);
pending = {};
if (!changes.length) return;
try {
const resp = await fetch(SAVE_URL, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": CSRF,
},
body: JSON.stringify({ changes })
});
const text = await resp.text();
if (!resp.ok) {
console.error("Autosave failed:", resp.status, text);
}
} catch (e) {
console.error("Autosave error:", e);
}
}
// ===============================
// Live Recalculation (per column)
// ===============================
function recalcColumn(colKey) {
// Editable inputs
const ghe = parseNum(getInputValue("ghe_bezug", colKey));
const betrag = parseNum(getInputValue("betrag", colKey));
const gutschriften = parseNum(getInputValue("gutschriften", colKey));
// Usually computed display
const personal = getDisplayNum("umlage_personal_5", colKey);
// Computed displays already present from server
const sum14 = getDisplayNum("summe_anteile_1_4", colKey);
const heVerbrauch = getDisplayNum("he_verbrauch", colKey);
// 4-Kosten He-Gas-Bezug = GHe-Bezug * Bezugskosten-GasHe
const kosten4 = ghe * BEZUGSKOSTEN_GASHE;
setDisplay("kosten_he_gas_bezug_4", colKey, kosten4);
// Rechnungsbetrag = Summe Anteile 1-4 Gutschriften + Betrag + 5-Umlage Personal
const rechnungsbetrag = sum14 - gutschriften + betrag + personal;
setDisplay("rechnungsbetrag", colKey, rechnungsbetrag);
// eff. L-He-Preis = Rechnungsbetrag / He-Verbrauch
const eff = heVerbrauch !== 0 ? (rechnungsbetrag / heVerbrauch) : 0;
setDisplay("eff_lhe_preis", colKey, eff);
// Sonstiges = IF(Betrag=0,"--------------",IF(Betrag<0,"Gutschrift","Nachzahlung"))
setSonstiges(colKey, betrag);
}
// ===============================
// Event listener
// ===============================
document.addEventListener("input", function (e) {
const t = e.target;
if (!t.classList.contains("cell-input")) return;
if (!t.classList.contains("editable")) return;
const rowKey = t.dataset.rowKey;
const colKey = t.dataset.colKey;
if (!rowKey || !colKey) return;
// autosave this edited cell
queueSave(rowKey, colKey, t.value);
// live recompute dependent rows for this column
if (rowKey === "ghe_bezug" || rowKey === "betrag" || rowKey === "gutschriften") {
recalcColumn(colKey);
}
});
})();
</script>
{% endif %}
</div>
{% endblock %}