|
|
@@ -15,7 +15,11 @@
|
|
|
* drag-reorder of worker rows and task rows
|
|
|
* - the existing JSON-envelope PATCH/POST endpoints for live edits
|
|
|
*
|
|
|
- * Capacity math MUST stay in lock-step with App\Services\CapacityCalculator.
|
|
|
+ * Capacity math lives only in App\Services\CapacityCalculator. The client
|
|
|
+ * does a plain ressourcen sum for the typing-responsive feel; Reserves /
|
|
|
+ * Available come from the PATCH response (`per_worker`), which is the
|
|
|
+ * single source of truth.
|
|
|
+ *
|
|
|
* Behaviours (all preserved verbatim from the previous jQuery version):
|
|
|
* - Day cells (per worker, per week) snap to 0.5 on blur, batch-saved via
|
|
|
* PATCH /sprints/{id}/week-cells with 400 ms debounce.
|
|
|
@@ -37,11 +41,10 @@
|
|
|
const root = document.querySelector('[data-sprint-root]');
|
|
|
if (!root) { return; }
|
|
|
|
|
|
- const sprintId = parseInt(root.getAttribute('data-sprint-id'), 10);
|
|
|
- const csrf = String(root.getAttribute('data-csrf') || '');
|
|
|
- const reserveFraction = Number(root.getAttribute('data-reserve-fraction') || 0);
|
|
|
- const isBeamer = Number(root.getAttribute('data-beamer')) === 1;
|
|
|
- const keySuffix = isBeamer ? ':beamer' : '';
|
|
|
+ const sprintId = parseInt(root.getAttribute('data-sprint-id'), 10);
|
|
|
+ const csrf = String(root.getAttribute('data-csrf') || '');
|
|
|
+ const isBeamer = Number(root.getAttribute('data-beamer')) === 1;
|
|
|
+ const keySuffix = isBeamer ? ':beamer' : '';
|
|
|
|
|
|
const taskSection = root.querySelector('[data-task-section]');
|
|
|
const taskStatusEnabled =
|
|
|
@@ -71,11 +74,10 @@
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
- // Capacity math — must match App\Services\CapacityCalculator
|
|
|
+ // Number snapping / formatting helpers
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
|
- function roundHalf(x) { return Math.round(x * 2) / 2; }
|
|
|
- const snap05 = roundHalf;
|
|
|
+ const snap05 = (x) => Math.round(x * 2) / 2;
|
|
|
const snap005 = (x) => Math.round(x * 20) / 20;
|
|
|
|
|
|
function fmtDays(x) {
|
|
|
@@ -85,33 +87,6 @@
|
|
|
}
|
|
|
function fmtRtb(x) { return Number(x).toFixed(2); }
|
|
|
|
|
|
- function capacity(ressourcen, committedPrio1) {
|
|
|
- committedPrio1 = committedPrio1 || 0;
|
|
|
- const afterReserves = roundHalf(ressourcen * (1 - reserveFraction));
|
|
|
- return {
|
|
|
- ressourcen,
|
|
|
- afterReserves,
|
|
|
- committedPrio1,
|
|
|
- available: afterReserves - committedPrio1,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- // Sum of prio-1 task assignment cells per sprint worker, read from DOM.
|
|
|
- function committedPrio1FromDom() {
|
|
|
- const per = {};
|
|
|
- qsa('tr[data-task-row]').forEach(function (row) {
|
|
|
- if (parseInt(row.getAttribute('data-prio'), 10) !== 1) { return; }
|
|
|
- qsa('[data-assign]', row).forEach(function (inp) {
|
|
|
- const key = String(inp.getAttribute('data-sw-id'));
|
|
|
- const v = Number(inp.value);
|
|
|
- if (!Number.isNaN(v) && v > 0) {
|
|
|
- per[key] = (per[key] || 0) + v;
|
|
|
- }
|
|
|
- });
|
|
|
- });
|
|
|
- return per;
|
|
|
- }
|
|
|
-
|
|
|
// ---------------------------------------------------------------------
|
|
|
// HTTP helper — spec §7 envelopes
|
|
|
// ---------------------------------------------------------------------
|
|
|
@@ -166,10 +141,15 @@
|
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
- // Recompute worker row sum + capacity summary locally
|
|
|
+ // Worker row Σ-days + capacity Ressourcen — plain DOM sum.
|
|
|
+ //
|
|
|
+ // After-reserves / Available are NOT computed here; the server PATCH
|
|
|
+ // response (`per_worker` envelope) is authoritative — see
|
|
|
+ // `applyServerCapacity` below. That keeps the capacity formula in
|
|
|
+ // CapacityCalculator.php as the single source of truth.
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
|
|
- function recomputeRow(swId, commitMap) {
|
|
|
+ function recomputeRow(swId) {
|
|
|
const row = qs('[data-sw-row][data-sw-id="' + swId + '"]');
|
|
|
if (!row) { return; }
|
|
|
let sum = 0;
|
|
|
@@ -177,29 +157,10 @@
|
|
|
const v = Number(inp.value);
|
|
|
if (!Number.isNaN(v)) { sum += v; }
|
|
|
});
|
|
|
-
|
|
|
- const committed = (commitMap || committedPrio1FromDom())[String(swId)] || 0;
|
|
|
- const cap = capacity(sum, committed);
|
|
|
-
|
|
|
- const sumEl = qs('[data-sum-days]', row);
|
|
|
- if (sumEl) { sumEl.textContent = fmtDays(cap.ressourcen); }
|
|
|
+ const sumEl = qs('[data-sum-days]', row);
|
|
|
+ if (sumEl) { sumEl.textContent = fmtDays(sum); }
|
|
|
qsa('[data-cap-ressourcen][data-sw-id="' + swId + '"]').forEach(function (r) {
|
|
|
- r.textContent = fmtDays(cap.ressourcen);
|
|
|
- });
|
|
|
- qsa('[data-cap-after-reserves][data-sw-id="' + swId + '"]').forEach(function (a) {
|
|
|
- a.textContent = fmtDays(cap.afterReserves);
|
|
|
- });
|
|
|
- qsa('[data-cap-available][data-sw-id="' + swId + '"]').forEach(function (av) {
|
|
|
- av.textContent = fmtDays(cap.available);
|
|
|
- av.classList.toggle('text-red-700', cap.available < 0);
|
|
|
- av.classList.toggle('text-slate-900', cap.available >= 0);
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- function recomputeAllCapacity() {
|
|
|
- const commit = committedPrio1FromDom();
|
|
|
- qsa('[data-sw-row]').forEach(function (row) {
|
|
|
- recomputeRow(parseInt(row.getAttribute('data-sw-id'), 10), commit);
|
|
|
+ r.textContent = fmtDays(sum);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -445,7 +406,6 @@
|
|
|
flash('Saved');
|
|
|
applyFilters();
|
|
|
applyServerCapacity(data && data.per_worker);
|
|
|
- recomputeAllCapacity();
|
|
|
})
|
|
|
.catch((e) => flash(e.message, true));
|
|
|
});
|
|
|
@@ -464,7 +424,6 @@
|
|
|
.then(function (data) {
|
|
|
tr.remove();
|
|
|
applyServerCapacity(data && data.per_worker);
|
|
|
- recomputeAllCapacity();
|
|
|
flash('Task deleted');
|
|
|
if (qsa('tr[data-task-row]', taskTbody).length === 0) {
|
|
|
window.location.reload();
|
|
|
@@ -522,8 +481,6 @@
|
|
|
});
|
|
|
const totEl = qs('[data-task-tot]', tr);
|
|
|
if (totEl) { totEl.textContent = fmtDays(tot); }
|
|
|
-
|
|
|
- recomputeAllCapacity();
|
|
|
});
|
|
|
on(root, 'blur', '[data-assign]', function () { this.dispatchEvent(new Event('change', { bubbles: true })); });
|
|
|
|
|
|
@@ -1113,11 +1070,11 @@
|
|
|
closeTaskMenu();
|
|
|
if (Number.isFinite(destId) && Number.isFinite(taskId)) {
|
|
|
request('POST', '/tasks/' + taskId + '/move', { sprint_id: destId })
|
|
|
- .then(function () {
|
|
|
+ .then(function (data) {
|
|
|
flash('Moved to sprint');
|
|
|
const tr = qs('tr[data-task-id="' + taskId + '"]', taskTbody);
|
|
|
if (tr) { tr.remove(); }
|
|
|
- recomputeAllCapacity();
|
|
|
+ applyServerCapacity(data && data.per_worker);
|
|
|
if (qsa('tr[data-task-row]', taskTbody).length === 0) {
|
|
|
window.location.reload();
|
|
|
}
|