/* global jQuery */ /** * Sprint settings page: JSON mutation plumbing + jQuery UI sortable wiring. * * The settings page mounts a single root element with `data-sprint-id` and * `data-csrf`. Everything below scopes itself to that root. */ (function ($) { 'use strict'; const $root = $('[data-sprint-root]'); if ($root.length === 0) { return; } const sprintId = parseInt($root.data('sprint-id'), 10); const csrf = String($root.data('csrf') || ''); // --------------------------------------------------------------------- // AJAX plumbing // --------------------------------------------------------------------- function request(method, url, body) { const opts = { method, headers: { 'Accept': 'application/json', 'X-CSRF-Token': csrf, }, credentials: 'same-origin', }; if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } return fetch(url, opts).then(async function (res) { let payload = null; try { payload = await res.json(); } catch (_) { /* ignore */ } if (!res.ok || !payload || payload.ok !== true) { const msg = (payload && payload.error && payload.error.message) ? payload.error.message : res.statusText || 'Request failed'; const err = new Error(msg); err.status = res.status; err.payload = payload; throw err; } return payload.data; }); } // --------------------------------------------------------------------- // Toast / status line // --------------------------------------------------------------------- const $status = $root.find('[data-status]'); let statusTimer = null; function flash(text, isError) { $status .text(text) .removeClass('text-green-700 text-red-700 bg-green-50 bg-red-50 border-green-200 border-red-200') .addClass(isError ? 'text-red-700 bg-red-50 border-red-200' : 'text-green-700 bg-green-50 border-green-200') .removeClass('opacity-0') .addClass('opacity-100'); clearTimeout(statusTimer); statusTimer = setTimeout(function () { $status.removeClass('opacity-100').addClass('opacity-0'); }, 2500); } // --------------------------------------------------------------------- // Sprint meta — save on change / blur // --------------------------------------------------------------------- function patchMeta(payload) { return request('PATCH', '/sprints/' + sprintId, payload) .then(function () { flash('Saved'); }) .catch(function (e) { flash(e.message, true); }); } const metaDebounce = {}; function debouncedMeta(field, value, ms) { clearTimeout(metaDebounce[field]); metaDebounce[field] = setTimeout(function () { const payload = {}; payload[field] = value; patchMeta(payload); }, ms || 400); } $root.find('[data-meta]').on('change', function () { const $el = $(this); const field = $el.attr('name'); let v = $el.val(); if (field === 'reserve_fraction') { v = Number(v) / 100; // form shows percent } debouncedMeta(field, v, 0); }); // --------------------------------------------------------------------- // Weeks count // --------------------------------------------------------------------- $root.find('[data-weeks-form]').on('submit', function (ev) { ev.preventDefault(); const n = parseInt($(this).find('input[name="n_weeks"]').val(), 10); if (!Number.isInteger(n) || n < 1) { flash('Weeks must be a positive integer', true); return; } request('POST', '/sprints/' + sprintId + '/weeks', { n_weeks: n }) .then(function () { window.location.reload(); }) .catch(function (e) { flash(e.message, true); }); }); // --------------------------------------------------------------------- // Worker picker // --------------------------------------------------------------------- function workerRowTemplate(sw) { const nameSafe = $('