diff --git a/plugin/itcca-allievi/assets/css/form.css b/plugin/itcca-allievi/assets/css/form.css index 85fe9c1..25af131 100644 --- a/plugin/itcca-allievi/assets/css/form.css +++ b/plugin/itcca-allievi/assets/css/form.css @@ -2,6 +2,10 @@ :root { --itcca-primary: var(--wp--preset--color--primary, currentColor); + --itcca-font-family: var( + --wp--preset--font-family--body, + var(--global--font-family-body, inherit) + ); --itcca-error: #b3261e; --itcca-success: #1e6b3a; --itcca-border: rgba(0, 0, 0, 0.18); @@ -10,6 +14,13 @@ --itcca-gap: 1rem; } +.itcca-form, +.itcca-form-success, +.itcca-form-error, +.itcca-form-summary { + font-family: var(--itcca-font-family), sans-serif; +} + .itcca-form { display: flex; flex-direction: column; @@ -26,19 +37,68 @@ opacity: 0 !important; } +.itcca-form-notice-wrap { + max-width: 760px; + margin: 1.25rem auto; + padding: 0 1rem; +} + +.itcca-form-success, +.itcca-form-error, +.itcca-form-summary { + max-width: 760px; + margin: 0 auto 1.25rem; + padding: 1rem 1.25rem; + border-radius: var(--itcca-radius); +} + +.itcca-alert-success, .itcca-form-success { border-left: 4px solid var(--itcca-success); background: rgba(30, 107, 58, 0.08); - padding: 0.875rem 1rem; - border-radius: var(--itcca-radius); - margin-bottom: 1.25rem; } +.itcca-alert-error, .itcca-form-error { border-left: 4px solid var(--itcca-error); background: rgba(179, 38, 30, 0.08); - padding: 0.875rem 1rem; - border-radius: var(--itcca-radius); +} + +.itcca-alert-title { + font-weight: 700; + font-size: 1.05rem; + margin: 0 0 0.35rem; +} + +.itcca-alert-success .itcca-alert-title::before { + content: "✓ "; + color: var(--itcca-success); +} + +.itcca-alert-error .itcca-alert-title::before { + content: "✕ "; + color: var(--itcca-error); +} + +.itcca-form-success p, +.itcca-form-error p, +.itcca-form-summary p { + margin: 0; +} + +.itcca-form-success p + p, +.itcca-form-error p + p, +.itcca-form-summary p + p { + margin-top: 0.35rem; +} + +.itcca-error-list { + margin: 0.5rem 0 0; + padding-left: 1.25rem; +} + +.itcca-error-list li { + margin: 0.15rem 0; } .itcca-section { @@ -68,7 +128,7 @@ .itcca-field .itcca-label, .itcca-field label { - font-size: 0.92rem; + font-size: calc(0.92rem + 4pt); font-weight: 500; } @@ -76,6 +136,12 @@ color: var(--itcca-error); } +.itcca-form input:invalid, +.itcca-form select:invalid { + box-shadow: none; + border-color: var(--itcca-border); +} + .itcca-field input[type="text"], .itcca-field input[type="email"], .itcca-field input[type="tel"], @@ -101,7 +167,7 @@ .itcca-field-error { color: var(--itcca-error); - font-size: 0.85rem; + font-size: calc(0.85rem + 4pt); margin: 0; } @@ -126,7 +192,7 @@ .itcca-checkbox { align-items: flex-start; - font-size: 0.92rem; + font-size: calc(0.92rem + 4pt); } .itcca-checkbox input { diff --git a/plugin/itcca-allievi/assets/js/form.js b/plugin/itcca-allievi/assets/js/form.js index bf6a896..010665b 100644 --- a/plugin/itcca-allievi/assets/js/form.js +++ b/plugin/itcca-allievi/assets/js/form.js @@ -33,51 +33,145 @@ return (v || '').replace(/[\s\-]/g, ''); } - function validateInput(input) { - const field = input.closest('.itcca-field'); - if (!field) return true; + function shouldShowErrors(field, form) { + return ( + field.classList.contains('itcca-touched') || + form.classList.contains('itcca-submit-attempted') + ); + } + + function markTouched(field) { + field.classList.add('itcca-touched'); + } + + function getInputValidation(input) { const type = input.dataset.type || ''; const value = (input.value || '').trim(); + if (input.required && value === '') { - showError(field, 'Campo obbligatorio.'); - return false; + return { valid: false, message: 'Campo obbligatorio.' }; } if (value === '') { - clearError(field); - return true; + return { valid: true, message: '' }; } if (type === 'cf') { if (!ITCCA_CF_RE.test(value)) { - showError(field, 'Codice fiscale non valido.'); - return false; + return { valid: false, message: 'Codice fiscale non valido.' }; } } else if (type === 'email') { const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; if (!emailRe.test(value)) { - showError(field, 'Indirizzo email non valido.'); - return false; + return { valid: false, message: 'Indirizzo email non valido.' }; } } else if (type === 'cap') { if (!ITCCA_CAP_RE.test(value)) { - showError(field, 'CAP deve essere di 5 cifre.'); - return false; + return { valid: false, message: 'CAP deve essere di 5 cifre.' }; } } else if (type === 'tel') { if (!ITCCA_PHONE_RE.test(normalizePhone(value))) { - showError(field, 'Numero non valido.'); - return false; + return { valid: false, message: 'Numero non valido.' }; } } else if (type === 'date') { const d = new Date(value); if (isNaN(d.getTime()) || d > new Date()) { - showError(field, 'Data non valida.'); - return false; + return { valid: false, message: 'Data non valida.' }; } } + return { valid: true, message: '' }; + } + + function validateInput(input, form) { + const field = input.closest('.itcca-field'); + if (!field) { + return true; + } + + const result = getInputValidation(input); + if (!shouldShowErrors(field, form)) { + return result.valid; + } + + if (!result.valid) { + showError(field, result.message); + return false; + } + clearError(field); return true; } + function validateRadioGroup(field, form) { + const radios = field.querySelectorAll('input[type="radio"]'); + if (radios.length === 0) { + return true; + } + + const required = Array.from(radios).some((radio) => radio.required); + const checked = Array.from(radios).some((radio) => radio.checked); + const valid = !required || checked; + + if (!shouldShowErrors(field, form)) { + return valid; + } + + if (!valid) { + showError(field, 'Campo obbligatorio.'); + return false; + } + + clearError(field); + return true; + } + + function validatePrivacy(field, form) { + const checkbox = field.querySelector('input[name="privacy"]'); + if (!checkbox) { + return true; + } + + const valid = checkbox.checked; + if (!shouldShowErrors(field, form)) { + return valid; + } + + if (!valid) { + showError(field, 'Devi accettare l\'informativa.'); + return false; + } + + clearError(field); + return true; + } + + function showClientSubmitSummary(form) { + let summary = form.querySelector('.itcca-form-summary-client'); + if (!summary) { + summary = document.createElement('div'); + summary.className = 'itcca-form-error itcca-form-summary itcca-form-summary-client itcca-alert-error'; + summary.setAttribute('role', 'alert'); + summary.setAttribute('aria-live', 'assertive'); + form.insertBefore(summary, form.firstChild); + } + + summary.innerHTML = + '
Controlla i campi
' + + 'Correggi i campi evidenziati prima di inviare.
'; + } + + function clearClientSubmitSummary(form) { + const summary = form.querySelector('.itcca-form-summary-client'); + if (summary) { + summary.remove(); + } + } + + function scrollToAlert() { + const alert = document.querySelector('.itcca-form-success, .itcca-form-error, .itcca-form-summary'); + if (alert) { + alert.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + function init() { const forms = document.querySelectorAll('form.itcca-form'); forms.forEach((form) => { @@ -86,33 +180,89 @@ cfInput.addEventListener('input', uppercaseCF); } - const inputs = form.querySelectorAll('input[data-type], input[required]'); - inputs.forEach((input) => { - input.addEventListener('blur', () => validateInput(input)); + form.querySelectorAll('.itcca-field-has-error').forEach((field) => { + markTouched(field); + }); + + const textInputs = form.querySelectorAll('input[data-type], input[required]:not([type="radio"]):not([type="checkbox"])'); + textInputs.forEach((input) => { + input.addEventListener('blur', () => { + const field = input.closest('.itcca-field'); + if (field) { + markTouched(field); + } + validateInput(input, form); + }); + }); + + form.querySelectorAll('.itcca-field').forEach((field) => { + const radios = field.querySelectorAll('input[type="radio"]'); + if (radios.length > 0) { + const radioGroup = field.querySelector('.itcca-radio-group'); + const validateGroup = () => validateRadioGroup(field, form); + + radios.forEach((radio) => { + radio.addEventListener('change', () => { + markTouched(field); + validateGroup(); + }); + }); + + if (radioGroup) { + radioGroup.addEventListener('blur', () => { + markTouched(field); + validateGroup(); + }); + } + } + + const privacy = field.querySelector('input[name="privacy"]'); + if (privacy) { + privacy.addEventListener('change', () => { + markTouched(field); + validatePrivacy(field, form); + }); + privacy.addEventListener('blur', () => { + markTouched(field); + validatePrivacy(field, form); + }); + } }); form.addEventListener('submit', (e) => { + form.classList.add('itcca-submit-attempted'); let ok = true; - inputs.forEach((input) => { - if (!validateInput(input)) { + + textInputs.forEach((input) => { + if (!validateInput(input, form)) { ok = false; } }); - const privacy = form.querySelector('input[name="privacy"]'); - if (privacy && !privacy.checked) { - const field = privacy.closest('.itcca-field'); - if (field) showError(field, 'Devi accettare l\'informativa.'); - ok = false; - } + + form.querySelectorAll('.itcca-field').forEach((field) => { + if (field.querySelector('input[type="radio"]') && !validateRadioGroup(field, form)) { + ok = false; + } + if (field.querySelector('input[name="privacy"]') && !validatePrivacy(field, form)) { + ok = false; + } + }); + if (!ok) { e.preventDefault(); + showClientSubmitSummary(form); const firstErr = form.querySelector('.itcca-field-has-error'); if (firstErr) { firstErr.scrollIntoView({ behavior: 'smooth', block: 'center' }); } + return; } + + clearClientSubmitSummary(form); }); }); + + scrollToAlert(); } if (document.readyState === 'loading') { diff --git a/plugin/itcca-allievi/includes/class-allievi-list-table.php b/plugin/itcca-allievi/includes/class-allievi-list-table.php index 44dede9..8806dca 100644 --- a/plugin/itcca-allievi/includes/class-allievi-list-table.php +++ b/plugin/itcca-allievi/includes/class-allievi-list-table.php @@ -124,6 +124,7 @@ 'sync' => __('Sincronizza con Google Sheet', 'itcca-allievi'), 'deactivate' => __('Imposta A = D (disattivo)', 'itcca-allievi'), 'activate' => __('Imposta A = A (attivo)', 'itcca-allievi'), + 'delete' => __('Elimina definitivamente', 'itcca-allievi'), ]; } @@ -289,13 +290,30 @@ admin_url('admin-post.php?action=itcca_sync_user&user_id=' . (int) $item->ID), 'itcca_sync_user_' . (int) $item->ID ); - return sprintf( + $delete_url = wp_nonce_url( + admin_url('admin-post.php?action=itcca_delete_allievo&user_id=' . (int) $item->ID), + 'itcca_delete_allievo_' . (int) $item->ID + ); + $confirm = esc_js(__('Eliminare definitivamente questo allievo? L\'operazione non è reversibile.', 'itcca-allievi')); + + $links = sprintf( '%s | %s', esc_url($edit), esc_html__('Modifica', 'itcca-allievi'), esc_url($sync_url), esc_html__('Pusha su foglio', 'itcca-allievi') ); + + if (current_user_can('delete_users')) { + $links .= sprintf( + ' | %s', + esc_url($delete_url), + $confirm, + esc_html__('Elimina', 'itcca-allievi') + ); + } + + return $links; } protected function extra_tablenav($which): void diff --git a/plugin/itcca-allievi/includes/class-allievi-list.php b/plugin/itcca-allievi/includes/class-allievi-list.php index 6ff2f1c..bbe07e8 100644 --- a/plugin/itcca-allievi/includes/class-allievi-list.php +++ b/plugin/itcca-allievi/includes/class-allievi-list.php @@ -18,6 +18,7 @@ { add_action('admin_menu', [self::class, 'add_menu']); add_action('admin_post_itcca_sync_user', [self::class, 'handle_sync_single']); + add_action('admin_post_itcca_delete_allievo', [self::class, 'handle_delete_single']); add_action('admin_post_itcca_export_csv', [ExportCsv::class, 'handle_export']); } @@ -150,10 +151,12 @@ 'synced' => __('Sincronizzazione eseguita.', 'itcca-allievi'), 'sync_failed' => __('Sincronizzazione fallita. Controlla i log nelle impostazioni.', 'itcca-allievi'), 'updated' => __('Operazione eseguita.', 'itcca-allievi'), + 'deleted' => __('Allievo eliminato.', 'itcca-allievi'), + 'delete_failed' => __('Impossibile eliminare l\'allievo selezionato.', 'itcca-allievi'), ]; $msg = $messages[$notice] ?? ''; if ($msg !== '') { - $class = $notice === 'sync_failed' ? 'notice-error' : 'notice-success'; + $class = in_array($notice, ['sync_failed', 'delete_failed'], true) ? 'notice-error' : 'notice-success'; printf('%s
' . esc_html__('Iscrizione completata', 'itcca-allievi') . '
'; + echo '' . esc_html__( + 'La tua richiesta è stata registrata correttamente. A breve riceverai una email con le istruzioni per impostare la password e accedere al tuo profilo.', + 'itcca-allievi' + ) . '
'; + echo '