refactored confirmation dialogs to use a unified confirmAction modal
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -1900,6 +1900,23 @@ body.modal-open {
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-modal-message {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.confirm-modal-message p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.confirm-modal-message p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.confirm-modal-choices {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.confirm-modal--danger .confirm-modal-dialog {
|
||||||
|
border-color: color-mix(in srgb, var(--danger-border) 40%, var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
/* Options builder (inside add-question form) */
|
/* Options builder (inside add-question form) */
|
||||||
.options-builder {
|
.options-builder {
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
import { apiPatch } from './api.js';
|
import { apiPatch } from './api.js';
|
||||||
import { showToast } from './app.js';
|
import { showToast } from './app.js';
|
||||||
|
import { confirmAction } from './confirm-modal.js';
|
||||||
|
|
||||||
export function isClientArchived(c) {
|
export function isClientArchived(c) {
|
||||||
return Number(c.archived) === 1;
|
return Number(c.archived) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmAndPatchClientArchive(clientCode, archived) {
|
export async function confirmAndPatchClientArchive(clientCode, archived) {
|
||||||
const msg = archived
|
if (!(await confirmAction({
|
||||||
|
title: archived ? 'Archive client' : 'Restore client',
|
||||||
|
message: archived
|
||||||
? `Archive client "${clientCode}"? They will be hidden from assignments, follow-up, and the mobile app. You can restore them later from the Clients page.`
|
? `Archive client "${clientCode}"? They will be hidden from assignments, follow-up, and the mobile app. You can restore them later from the Clients page.`
|
||||||
: `Restore client "${clientCode}" to active lists?`;
|
: `Restore client "${clientCode}" to active lists?`,
|
||||||
if (!confirm(msg)) return false;
|
confirmLabel: archived ? 'Archive' : 'Restore',
|
||||||
|
variant: archived ? 'danger' : 'default',
|
||||||
|
}))) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiPatch('clients.php', { clientCode, archived });
|
await apiPatch('clients.php', { clientCode, archived });
|
||||||
|
|||||||
148
website/js/confirm-modal.js
Normal file
148
website/js/confirm-modal.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
function esc(s) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.textContent = s ?? '';
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessageHTML(message) {
|
||||||
|
return String(message)
|
||||||
|
.split(/\n\n+/)
|
||||||
|
.map((block) => `<p>${esc(block).replace(/\n/g, '<br>')}</p>`)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChoicesHTML(choices) {
|
||||||
|
const name = 'confirm_modal_choice';
|
||||||
|
return `
|
||||||
|
<fieldset class="password-mode-fieldset confirm-modal-choices">
|
||||||
|
<div class="password-mode-options">
|
||||||
|
${choices.map((choice, i) => `
|
||||||
|
<label class="password-mode-option">
|
||||||
|
<input type="radio" name="${name}" value="${esc(choice.value)}"${i === 0 ? ' checked' : ''}>
|
||||||
|
<span class="password-mode-option-title">${esc(choice.label)}</span>
|
||||||
|
${choice.description
|
||||||
|
? `<span class="password-mode-option-desc">${esc(choice.description)}</span>`
|
||||||
|
: ''}
|
||||||
|
</label>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</fieldset>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {((value: boolean | string | null) => void) | null} */
|
||||||
|
let resolveConfirm = null;
|
||||||
|
|
||||||
|
function ensureConfirmModal() {
|
||||||
|
let modal = document.getElementById('confirmModal');
|
||||||
|
if (modal) return modal;
|
||||||
|
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.id = 'confirmModal';
|
||||||
|
modal.className = 'modal-overlay';
|
||||||
|
modal.hidden = true;
|
||||||
|
modal.setAttribute('role', 'dialog');
|
||||||
|
modal.setAttribute('aria-modal', 'true');
|
||||||
|
modal.setAttribute('aria-labelledby', 'confirmModalTitle');
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-dialog confirm-modal-dialog">
|
||||||
|
<h2 id="confirmModalTitle"></h2>
|
||||||
|
<div class="confirm-modal-message modal-subtitle" id="confirmModalMessage"></div>
|
||||||
|
<div id="confirmModalChoices"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" class="btn btn-sm" data-confirm-cancel>Cancel</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" data-confirm-submit>Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) finishConfirm(null);
|
||||||
|
});
|
||||||
|
modal.querySelector('[data-confirm-cancel]').addEventListener('click', () => finishConfirm(null));
|
||||||
|
modal.querySelector('[data-confirm-submit]').addEventListener('click', () => {
|
||||||
|
const choicesEl = modal.querySelector('#confirmModalChoices');
|
||||||
|
const hasChoices = choicesEl.childElementCount > 0;
|
||||||
|
if (hasChoices) {
|
||||||
|
const selected = modal.querySelector('input[name="confirm_modal_choice"]:checked');
|
||||||
|
finishConfirm(selected ? selected.value : null);
|
||||||
|
} else {
|
||||||
|
finishConfirm(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
const el = document.getElementById('confirmModal');
|
||||||
|
if (!el || el.hidden) return;
|
||||||
|
if (e.key === 'Escape') finishConfirm(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishConfirm(value) {
|
||||||
|
const modal = document.getElementById('confirmModal');
|
||||||
|
if (!modal || modal.hidden) return;
|
||||||
|
modal.hidden = true;
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
const resolve = resolveConfirm;
|
||||||
|
resolveConfirm = null;
|
||||||
|
if (resolve) resolve(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a confirmation dialog.
|
||||||
|
*
|
||||||
|
* Without `choices`: resolves to `true` (confirmed) or `false` (cancelled).
|
||||||
|
* With `choices`: resolves to the selected choice value (string) or `null` (cancelled).
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {string} options.title
|
||||||
|
* @param {string} options.message
|
||||||
|
* @param {string} [options.confirmLabel='Confirm']
|
||||||
|
* @param {string} [options.cancelLabel='Cancel']
|
||||||
|
* @param {'default'|'danger'} [options.variant='default']
|
||||||
|
* @param {{ value: string, label: string, description?: string }[]} [options.choices]
|
||||||
|
* @returns {Promise<boolean|string|null>}
|
||||||
|
*/
|
||||||
|
export function confirmAction(options) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (resolveConfirm) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = ensureConfirmModal();
|
||||||
|
const hasChoices = Array.isArray(options.choices) && options.choices.length > 0;
|
||||||
|
const variant = options.variant === 'danger' ? 'danger' : 'default';
|
||||||
|
|
||||||
|
modal.querySelector('#confirmModalTitle').textContent = options.title || 'Confirm';
|
||||||
|
modal.querySelector('#confirmModalMessage').innerHTML = formatMessageHTML(options.message || '');
|
||||||
|
|
||||||
|
const choicesEl = modal.querySelector('#confirmModalChoices');
|
||||||
|
choicesEl.innerHTML = hasChoices ? renderChoicesHTML(options.choices) : '';
|
||||||
|
|
||||||
|
const cancelBtn = modal.querySelector('[data-confirm-cancel]');
|
||||||
|
const submitBtn = modal.querySelector('[data-confirm-submit]');
|
||||||
|
cancelBtn.textContent = options.cancelLabel || 'Cancel';
|
||||||
|
submitBtn.textContent = options.confirmLabel || (hasChoices ? 'Continue' : 'Confirm');
|
||||||
|
submitBtn.className = `btn btn-sm ${variant === 'danger' ? 'btn-danger' : 'btn-primary'}`;
|
||||||
|
|
||||||
|
modal.classList.toggle('confirm-modal--danger', variant === 'danger');
|
||||||
|
|
||||||
|
resolveConfirm = (value) => {
|
||||||
|
if (hasChoices) {
|
||||||
|
resolve(value);
|
||||||
|
} else {
|
||||||
|
resolve(value === true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.hidden = false;
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
(hasChoices
|
||||||
|
? modal.querySelector('input[name="confirm_modal_choice"]')
|
||||||
|
: submitBtn
|
||||||
|
)?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost } from '../api.js';
|
import { apiGet, apiPost } from '../api.js';
|
||||||
import { pageHeaderHTML, showToast } from '../app.js';
|
import { pageHeaderHTML, showToast } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
|
|
||||||
const CLIENT_PAGE_SIZE = 50;
|
const CLIENT_PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -421,7 +422,11 @@ async function doAssign() {
|
|||||||
if (!coachID) { showToast('Select a target counselor', 'error'); return; }
|
if (!coachID) { showToast('Select a target counselor', 'error'); return; }
|
||||||
|
|
||||||
const coachName = coaches.find(c => c.coachID === coachID)?.username || coachID;
|
const coachName = coaches.find(c => c.coachID === coachID)?.username || coachID;
|
||||||
if (!confirm(`Assign ${selected.length} client(s) to ${coachName}?`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Assign clients',
|
||||||
|
message: `Assign ${selected.length} client(s) to ${coachName}?`,
|
||||||
|
confirmLabel: 'Assign',
|
||||||
|
}))) return;
|
||||||
|
|
||||||
const btn = document.getElementById('assignBtn');
|
const btn = document.getElementById('assignBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||||
import { pageHeaderHTML, showToast } from '../app.js';
|
import { pageHeaderHTML, showToast } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
import {
|
import {
|
||||||
isClientArchived,
|
isClientArchived,
|
||||||
confirmAndPatchClientArchive,
|
confirmAndPatchClientArchive,
|
||||||
@ -818,7 +819,12 @@ async function setClientArchived(clientCode, archived) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteClient(clientCode) {
|
async function deleteClient(clientCode) {
|
||||||
if (!confirm(`Delete client "${clientCode}"? This will also remove all their answers and results. This cannot be undone.`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Delete client',
|
||||||
|
message: `Delete client "${clientCode}"? This will also remove all their answers and results. This cannot be undone.`,
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
await apiDelete('clients.php', { clientCode });
|
await apiDelete('clients.php', { clientCode });
|
||||||
clientsList = clientsList.filter(c => c.clientCode !== clientCode);
|
clientsList = clientsList.filter(c => c.clientCode !== clientCode);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDownloadFetch, redirectToLogin } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDownloadFetch, redirectToLogin } from '../api.js';
|
||||||
import { getRole, pageHeaderHTML, showToast } from '../app.js';
|
import { getRole, pageHeaderHTML, showToast } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
import { navigate } from '../router.js';
|
import { navigate } from '../router.js';
|
||||||
|
|
||||||
const REVOKE_ALL_CONFIRM = 'REVOKE ALL SESSIONS';
|
const REVOKE_ALL_CONFIRM = 'REVOKE ALL SESSIONS';
|
||||||
@ -166,11 +167,12 @@ export function devPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('devRemoveBtn').addEventListener('click', async () => {
|
document.getElementById('devRemoveBtn').addEventListener('click', async () => {
|
||||||
if (!confirm(
|
if (!(await confirmAction({
|
||||||
'Remove all dev test data?\n\n'
|
title: 'Remove dev test data',
|
||||||
+ 'Deletes users matching dev_*, clients DEV-CL-*, and their answers/completions.\n'
|
message: 'Remove all dev test data?\n\nDeletes users matching dev_*, clients DEV-CL-*, and their answers/completions.\nReal accounts are not affected.',
|
||||||
+ 'Real accounts are not affected.'
|
confirmLabel: 'Remove',
|
||||||
)) {
|
variant: 'danger',
|
||||||
|
}))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const btn = document.getElementById('devRemoveBtn');
|
const btn = document.getElementById('devRemoveBtn');
|
||||||
@ -206,11 +208,12 @@ export function devPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('devWipeBtn').addEventListener('click', async () => {
|
document.getElementById('devWipeBtn').addEventListener('click', async () => {
|
||||||
if (!confirm(
|
if (!(await confirmAction({
|
||||||
'Remove ALL clients, completions, counselors, and supervisors?\n\n'
|
title: 'Wipe operational data',
|
||||||
+ 'Admin accounts and questionnaires will NOT be deleted.\n'
|
message: 'Remove ALL clients, completions, counselors, and supervisors?\n\nAdmin accounts and questionnaires will NOT be deleted.\nThis cannot be undone.',
|
||||||
+ 'This cannot be undone.'
|
confirmLabel: 'Remove all',
|
||||||
)) {
|
variant: 'danger',
|
||||||
|
}))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const btn = document.getElementById('devWipeBtn');
|
const btn = document.getElementById('devWipeBtn');
|
||||||
@ -533,10 +536,12 @@ function wireSecuritySettings() {
|
|||||||
|
|
||||||
revokeBtn?.addEventListener('click', async () => {
|
revokeBtn?.addEventListener('click', async () => {
|
||||||
if (revokeInput.value.trim() !== REVOKE_ALL_CONFIRM) return;
|
if (revokeInput.value.trim() !== REVOKE_ALL_CONFIRM) return;
|
||||||
if (!confirm(
|
if (!(await confirmAction({
|
||||||
'Revoke every active session?\n\n'
|
title: 'Revoke all sessions',
|
||||||
+ 'All users (including you) will be signed out on website and mobile.'
|
message: 'Revoke every active session?\n\nAll users (including you) will be signed out on website and mobile.',
|
||||||
)) {
|
confirmLabel: 'Revoke all',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
revokeBtn.disabled = true;
|
revokeBtn.disabled = true;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '../api.js';
|
||||||
import { canEdit, homeNavButton, showToast } from '../app.js';
|
import { canEdit, homeNavButton, showToast } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
import { navigate } from '../router.js';
|
import { navigate } from '../router.js';
|
||||||
import {
|
import {
|
||||||
SOURCE_LANG,
|
SOURCE_LANG,
|
||||||
@ -1668,7 +1669,12 @@ async function deleteQuestion(q) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const msg = 'Remove this question? If client data exists it will be retired (not deleted) so upload history stays intact.';
|
const msg = 'Remove this question? If client data exists it will be retired (not deleted) so upload history stays intact.';
|
||||||
if (!confirm(msg)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Remove question',
|
||||||
|
message: msg,
|
||||||
|
confirmLabel: 'Remove',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
const data = await apiDelete('questions.php', { questionID: q.questionID });
|
const data = await apiDelete('questions.php', { questionID: q.questionID });
|
||||||
questions = questions.filter(x => x.questionID !== q.questionID);
|
questions = questions.filter(x => x.questionID !== q.questionID);
|
||||||
@ -1702,7 +1708,12 @@ async function updateOption(q, answerOptionID, fields) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteOption(q, answerOptionID) {
|
async function deleteOption(q, answerOptionID) {
|
||||||
if (!confirm('Remove this answer option?')) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Remove answer option',
|
||||||
|
message: 'Remove this answer option?',
|
||||||
|
confirmLabel: 'Remove',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
const data = await apiDelete('answer_options.php', { answerOptionID });
|
const data = await apiDelete('answer_options.php', { answerOptionID });
|
||||||
if (!data.retired) {
|
if (!data.retired) {
|
||||||
@ -1976,7 +1987,12 @@ function bindLanguageManager(languages) {
|
|||||||
document.querySelectorAll('.lang-chip-remove').forEach(btn => {
|
document.querySelectorAll('.lang-chip-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const lc = btn.dataset.lc;
|
const lc = btn.dataset.lc;
|
||||||
if (!confirm(`Remove language "${lc}" and ALL its translations?`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Remove language',
|
||||||
|
message: `Remove language "${lc}" and ALL its translations?`,
|
||||||
|
confirmLabel: 'Remove',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
await apiDelete('translations.php', { type: 'language', languageCode: lc });
|
await apiDelete('translations.php', { type: 'language', languageCode: lc });
|
||||||
showToast(`Language "${lc}" removed`, 'success');
|
showToast(`Language "${lc}" removed`, 'success');
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||||
import { canEdit, pageHeaderHTML, showToast, getRole } from '../app.js';
|
import { canEdit, pageHeaderHTML, showToast, getRole } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
import { navigate } from '../router.js';
|
import { navigate } from '../router.js';
|
||||||
|
|
||||||
export async function questionnairesPage() {
|
export async function questionnairesPage() {
|
||||||
@ -111,7 +112,12 @@ function renderCategoryKeysPanel(categoryKeys) {
|
|||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
msg += `\n\n${count} questionnaire(s) will have their category key cleared.`;
|
msg += `\n\n${count} questionnaire(s) will have their category key cleared.`;
|
||||||
}
|
}
|
||||||
if (!confirm(msg)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Delete category key',
|
||||||
|
message: msg,
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const result = await apiPost('questionnaires.php', { action: 'deleteCategoryKey', categoryKey: key });
|
const result = await apiPost('questionnaires.php', { action: 'deleteCategoryKey', categoryKey: key });
|
||||||
@ -297,7 +303,12 @@ function renderGrid(questionnaires, categoryKeys = []) {
|
|||||||
grid.querySelectorAll('.delete-q-btn').forEach(btn => {
|
grid.querySelectorAll('.delete-q-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!confirm('Delete this questionnaire and all its data?')) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Delete questionnaire',
|
||||||
|
message: 'Delete this questionnaire and all its data?',
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
await apiDelete('questionnaires.php', { questionnaireID: btn.dataset.id });
|
await apiDelete('questionnaires.php', { questionnaireID: btn.dataset.id });
|
||||||
showToast('Questionnaire deleted', 'success');
|
showToast('Questionnaire deleted', 'success');
|
||||||
@ -491,7 +502,12 @@ function renderScoringProfilesSection(profiles, questionnaires) {
|
|||||||
});
|
});
|
||||||
list.querySelectorAll('.delete-profile-btn').forEach(btn => {
|
list.querySelectorAll('.delete-profile-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
if (!confirm(`Delete scoring profile "${btn.dataset.name}"?`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Delete scoring profile',
|
||||||
|
message: `Delete scoring profile "${btn.dataset.name}"?`,
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
await apiDelete('scoring_profiles.php', { profileID: btn.dataset.id });
|
await apiDelete('scoring_profiles.php', { profileID: btn.dataset.id });
|
||||||
showToast('Profile deleted', 'success');
|
showToast('Profile deleted', 'success');
|
||||||
@ -761,12 +777,32 @@ function bindImportBundle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const count = bundle.questionnaires.length;
|
const count = bundle.questionnaires.length;
|
||||||
if (!confirm(`Import ${count} questionnaire(s) from this file?`)) return;
|
if (!(await confirmAction({
|
||||||
const replace = confirm(
|
title: 'Import questionnaires',
|
||||||
'If a questionnaire ID from the file already exists:\n\n' +
|
message: `Import ${count} questionnaire(s) from this file?`,
|
||||||
'OK — replace that questionnaire\n' +
|
confirmLabel: 'Import',
|
||||||
'Cancel — import as a new copy (new ID)'
|
}))) return;
|
||||||
);
|
|
||||||
|
const mode = await confirmAction({
|
||||||
|
title: 'Existing questionnaire IDs',
|
||||||
|
message: 'If a questionnaire ID from the file already exists, how should it be handled?',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
value: 'replace',
|
||||||
|
label: 'Replace existing',
|
||||||
|
description: 'Overwrite the questionnaire with that ID.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'copy',
|
||||||
|
label: 'Import as new copy',
|
||||||
|
description: 'Keep the existing questionnaire and import with a new ID.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
confirmLabel: 'Continue',
|
||||||
|
cancelLabel: 'Cancel import',
|
||||||
|
});
|
||||||
|
if (!mode) return;
|
||||||
|
const replace = mode === 'replace';
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const prev = btn.textContent;
|
const prev = btn.textContent;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDelete, apiDownloadFetch } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDelete, apiDownloadFetch } from '../api.js';
|
||||||
import { canEdit, pageHeaderHTML, showToast } from '../app.js';
|
import { canEdit, pageHeaderHTML, showToast } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
import {
|
import {
|
||||||
SOURCE_LANG,
|
SOURCE_LANG,
|
||||||
normalizeEntry,
|
normalizeEntry,
|
||||||
@ -308,7 +309,12 @@ function renderLanguagePanel(languages) {
|
|||||||
panel.querySelectorAll('.lang-chip-remove').forEach(btn => {
|
panel.querySelectorAll('.lang-chip-remove').forEach(btn => {
|
||||||
btn.addEventListener('click', async () => {
|
btn.addEventListener('click', async () => {
|
||||||
const lc = btn.dataset.lc;
|
const lc = btn.dataset.lc;
|
||||||
if (!confirm(`Remove "${lc}" and all its translations?`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Remove language',
|
||||||
|
message: `Remove "${lc}" and all its translations?`,
|
||||||
|
confirmLabel: 'Remove',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
await apiDelete('translations.php', { type: 'language', languageCode: lc });
|
await apiDelete('translations.php', { type: 'language', languageCode: lc });
|
||||||
showToast(`Language "${lc}" removed`, 'success');
|
showToast(`Language "${lc}" removed`, 'success');
|
||||||
@ -671,7 +677,11 @@ function bindTranslationBundleActions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const count = bundle.entries.length;
|
const count = bundle.entries.length;
|
||||||
if (!confirm(`Import translations for ${count} entries from this file?`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Import translations',
|
||||||
|
message: `Import translations for ${count} entries from this file?`,
|
||||||
|
confirmLabel: 'Import',
|
||||||
|
}))) return;
|
||||||
|
|
||||||
importBtn.disabled = true;
|
importBtn.disabled = true;
|
||||||
const prev = importBtn.textContent;
|
const prev = importBtn.textContent;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||||
import { getRole, getUser, pageHeaderHTML, showToast } from '../app.js';
|
import { getRole, getUser, pageHeaderHTML, showToast } from '../app.js';
|
||||||
|
import { confirmAction } from '../confirm-modal.js';
|
||||||
import { openAdminResetPasswordModal } from '../password-modal.js';
|
import { openAdminResetPasswordModal } from '../password-modal.js';
|
||||||
|
|
||||||
// Roles an admin can create; supervisors can only create coaches
|
// Roles an admin can create; supervisors can only create coaches
|
||||||
@ -319,10 +320,12 @@ async function reassignCoachSupervisor(selectEl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function revokeUserSessions(userID, username) {
|
async function revokeUserSessions(userID, username) {
|
||||||
if (!confirm(
|
if (!(await confirmAction({
|
||||||
`Sign out "${username}" on all devices?\n\n`
|
title: 'Sign out everywhere',
|
||||||
+ 'They must log in again on the website and mobile app.'
|
message: `Sign out "${username}" on all devices?\n\nThey must log in again on the website and mobile app.`,
|
||||||
)) {
|
confirmLabel: 'Sign out',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -336,7 +339,12 @@ async function revokeUserSessions(userID, username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(userID, username) {
|
async function deleteUser(userID, username) {
|
||||||
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
if (!(await confirmAction({
|
||||||
|
title: 'Delete user',
|
||||||
|
message: `Delete user "${username}"? This cannot be undone.`,
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
variant: 'danger',
|
||||||
|
}))) return;
|
||||||
try {
|
try {
|
||||||
const data = await apiDelete('users.php', { userID });
|
const data = await apiDelete('users.php', { userID });
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user