Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

247 řádky
9.4KB

  1. /* kanban-modal.js — card create/edit modal */
  2. (function () {
  3. 'use strict';
  4. var modal = document.getElementById('cardModal');
  5. var bsModal = new bootstrap.Modal(modal);
  6. var titleEl = document.getElementById('cardModalLabel');
  7. var cardIdEl = document.getElementById('card-id');
  8. var colIdEl = document.getElementById('card-column-id');
  9. var laneIdEl = document.getElementById('card-lane-id');
  10. var jobNumEl = document.getElementById('card-job-number');
  11. var jobNameEl = document.getElementById('card-job-name');
  12. var errEl = document.getElementById('card-modal-error');
  13. var btnSave = document.getElementById('btn-save-card');
  14. var btnDelete = document.getElementById('btn-delete-card');
  15. var custNameEl = document.getElementById('card-customer-name');
  16. var delivDateEl = document.getElementById('card-delivery-date');
  17. var qtyEl = document.getElementById('card-quantity');
  18. var notesEl = document.getElementById('card-notes');
  19. var fullNoteEl = document.getElementById('card-full-note');
  20. var historyWrap = document.getElementById('card-history-wrap');
  21. var historyList = document.getElementById('card-history-list');
  22. var boardId = KANBAN.boardId;
  23. /* ── Helpers ─────────────────────────────────────────────── */
  24. function post(url, data, cb) {
  25. var params = new URLSearchParams();
  26. Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
  27. fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString() })
  28. .then(function (r) { return r.json(); })
  29. .then(cb)
  30. .catch(function (e) { showError('Network error: ' + e); });
  31. }
  32. function showError(msg) {
  33. errEl.textContent = msg;
  34. errEl.classList.remove('d-none');
  35. }
  36. function clearError() {
  37. errEl.textContent = '';
  38. errEl.classList.add('d-none');
  39. }
  40. function escapeHtml(s) {
  41. return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
  42. return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
  43. });
  44. }
  45. function formatMovedAt(s) {
  46. if (!s) return '';
  47. // Movement timestamps are stored in UTC without a timezone suffix.
  48. var d = new Date(String(s).replace(' ', 'T') + 'Z');
  49. if (isNaN(d.getTime())) return s;
  50. return d.toLocaleString();
  51. }
  52. function loadHistory(id) {
  53. historyList.innerHTML = '';
  54. fetch('/cards/' + id + '/history')
  55. .then(function (r) { return r.json(); })
  56. .then(function (res) {
  57. if (!res.ok || !res.movements || !res.movements.length) {
  58. historyWrap.classList.add('d-none');
  59. return;
  60. }
  61. historyWrap.classList.remove('d-none');
  62. res.movements.forEach(function (m) {
  63. var li = document.createElement('li');
  64. li.className = 'list-group-item px-0 py-1';
  65. li.innerHTML =
  66. '<strong>' + escapeHtml(m.from_column_name || 'Unknown column') +
  67. ' / ' + escapeHtml(m.from_swim_lane_name || 'Unknown row') +
  68. ' &rarr; ' + escapeHtml(m.to_column_name || 'Unknown column') +
  69. ' / ' + escapeHtml(m.to_swim_lane_name || 'Unknown row') + '</strong>' +
  70. ' &mdash; ' + escapeHtml(m.moved_by || 'Unknown user') +
  71. ' <span class="text-muted">(' + escapeHtml(formatMovedAt(m.moved_at)) + ')</span>';
  72. historyList.appendChild(li);
  73. });
  74. })
  75. .catch(function () { historyWrap.classList.add('d-none'); });
  76. }
  77. /* ── Open for create ──────────────────────────────────────── */
  78. function openCreate(bId, colId, laneId) {
  79. titleEl.textContent = 'Add Card';
  80. cardIdEl.value = '';
  81. colIdEl.value = colId || '';
  82. laneIdEl.value = laneId || '';
  83. jobNumEl.value = '';
  84. jobNameEl.value = '';
  85. custNameEl.value = '';
  86. delivDateEl.value = '';
  87. qtyEl.value = '';
  88. notesEl.value = '';
  89. fullNoteEl.value = '';
  90. btnDelete.classList.add('d-none');
  91. historyWrap.classList.add('d-none');
  92. historyList.innerHTML = '';
  93. clearError();
  94. bsModal.show();
  95. jobNumEl.focus();
  96. }
  97. /* ── Open for edit ───────────────────────────────────────── */
  98. function openEdit(id, colId, laneId, jobNum, jobName, custName, delivDate, qty, notes, fullNote) {
  99. titleEl.textContent = 'Edit Card';
  100. cardIdEl.value = id;
  101. colIdEl.value = colId;
  102. laneIdEl.value = laneId;
  103. jobNumEl.value = jobNum || '';
  104. jobNameEl.value = jobName || '';
  105. custNameEl.value = custName || '';
  106. delivDateEl.value = delivDate || '';
  107. qtyEl.value = qty || '';
  108. notesEl.value = notes || '';
  109. fullNoteEl.value = fullNote || '';
  110. btnDelete.classList.remove('d-none');
  111. loadHistory(id);
  112. clearError();
  113. bsModal.show();
  114. jobNumEl.focus();
  115. }
  116. /* ── Save ─────────────────────────────────────────────────── */
  117. btnSave.addEventListener('click', function () {
  118. clearError();
  119. var id = cardIdEl.value;
  120. var colId = colIdEl.value;
  121. var laneId = laneIdEl.value;
  122. var jNum = jobNumEl.value.trim();
  123. var jName = jobNameEl.value.trim();
  124. var cust = custNameEl.value.trim();
  125. var dDate = delivDateEl.value;
  126. var qty = qtyEl.value.trim();
  127. var notes = notesEl.value.trim();
  128. var fullNote = fullNoteEl.value;
  129. if (!jNum && !jName) {
  130. showError('Enter at least a job number or job name.');
  131. return;
  132. }
  133. if (id) {
  134. // Update existing
  135. post('/cards/' + id, { job_number: jNum, job_name: jName, customer_name: cust, delivery_date: dDate, quantity: qty, notes: notes, full_note: fullNote }, function (res) {
  136. if (res.ok) {
  137. bsModal.hide();
  138. window.KanbanBoard.onCardUpdated(id, res);
  139. } else {
  140. showError(res.error || 'Save failed.');
  141. }
  142. });
  143. } else {
  144. // Create new — if no col/lane selected show column/lane picker
  145. if (!colId || !laneId) {
  146. showError('Please choose a column and swim lane first.');
  147. return;
  148. }
  149. post('/cards', {
  150. board_id: boardId,
  151. column_id: colId,
  152. swim_lane_id: laneId,
  153. job_number: jNum,
  154. job_name: jName,
  155. customer_name: cust,
  156. delivery_date: dDate,
  157. quantity: qty,
  158. notes: notes,
  159. full_note: fullNote
  160. }, function (res) {
  161. if (res.ok) {
  162. bsModal.hide();
  163. window.KanbanBoard.onCardCreated(res);
  164. } else {
  165. showError(res.error || 'Save failed.');
  166. }
  167. });
  168. }
  169. });
  170. /* ── Delete ──────────────────────────────────────────────── */
  171. btnDelete.addEventListener('click', function () {
  172. if (!confirm('Delete this card?')) return;
  173. var id = cardIdEl.value;
  174. post('/cards/' + id + '/delete', {}, function (res) {
  175. if (res.ok) {
  176. bsModal.hide();
  177. window.KanbanBoard.onCardDeleted(id);
  178. } else {
  179. showError(res.error || 'Delete failed.');
  180. }
  181. });
  182. });
  183. /* ── Column/Lane picker when Add Card clicked with no cell ── */
  184. // Populated lazily from board data
  185. modal.addEventListener('shown.bs.modal', function () {
  186. if (!cardIdEl.value && (!colIdEl.value || !laneIdEl.value)) {
  187. injectPicker();
  188. }
  189. });
  190. function injectPicker() {
  191. if (document.getElementById('card-picker')) return;
  192. var picker = document.createElement('div');
  193. picker.id = 'card-picker';
  194. picker.className = 'row g-2 mb-3';
  195. var colSel = '<select class="form-select form-select-sm" id="pick-col"><option value="">-- Column --</option>';
  196. var laneSel = '<select class="form-select form-select-sm" id="pick-lane"><option value="">-- Swim Lane --</option>';
  197. document.querySelectorAll('.kanban-col-header').forEach(function (el) {
  198. colSel += '<option value="' + el.dataset.colId + '">' + el.querySelector('.col-label').textContent + '</option>';
  199. });
  200. document.querySelectorAll('.kanban-lane-header').forEach(function (el) {
  201. laneSel += '<option value="' + el.dataset.laneId + '">' + el.querySelector('.lane-label').textContent + '</option>';
  202. });
  203. colSel += '</select>';
  204. laneSel += '</select>';
  205. picker.innerHTML =
  206. '<div class="col"><label class="form-label small">Column</label>' + colSel + '</div>' +
  207. '<div class="col"><label class="form-label small">Swim Lane</label>' + laneSel + '</div>';
  208. var first = document.getElementById('card-job-number').closest('.mb-3');
  209. modal.querySelector('.modal-body').insertBefore(picker, first);
  210. document.getElementById('pick-col').addEventListener('change', function () {
  211. colIdEl.value = this.value;
  212. });
  213. document.getElementById('pick-lane').addEventListener('change', function () {
  214. laneIdEl.value = this.value;
  215. });
  216. }
  217. /* ── Public API ──────────────────────────────────────────── */
  218. window.KanbanModal = { openCreate: openCreate, openEdit: openEdit };
  219. })();

Powered by TurnKey Linux.