371 lines
11 KiB
HTML
371 lines
11 KiB
HTML
{% 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' %}">← Ü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;">← Fachgebiet →</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;">
|
||
← Fachgebiet →
|
||
</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 %}
|