()
| 2373 | // ───────────────────── |
| 2374 | |
| 2375 | async function saveClientPortalsFromUI() { |
| 2376 | const body = document.getElementById('clientPortalsBody'); |
| 2377 | const status = document.getElementById('clientPortalsStatus'); |
| 2378 | if (!body) return; |
| 2379 | |
| 2380 | const cards = body.querySelectorAll('.card[data-portal-slug]'); |
| 2381 | const portals = {}; |
| 2382 | const portalsCache = {}; |
| 2383 | const invalid = []; |
| 2384 | const portalUserInvalid = []; |
| 2385 | let firstInvalidField = null; |
| 2386 | |
| 2387 | // Clear previous visual errors |
| 2388 | cards.forEach(card => { |
| 2389 | card.style.boxShadow = ''; |
| 2390 | card.style.borderColor = ''; |
| 2391 | card.classList.remove('portal-card-has-error'); |
| 2392 | |
| 2393 | const hint = card.querySelector('.portal-card-error-hint'); |
| 2394 | if (hint) hint.remove(); |
| 2395 | }); |
| 2396 | |
| 2397 | const markCardMissingRequired = (card, message) => { |
| 2398 | // Mark visually |
| 2399 | card.classList.add('portal-card-has-error'); |
| 2400 | card.style.borderColor = '#dc3545'; |
| 2401 | card.style.boxShadow = '0 0 0 2px rgba(220,53,69,0.6)'; |
| 2402 | |
| 2403 | // Expand the card so the error is visible even if it was collapsed |
| 2404 | const header = card.querySelector('.portal-card-header'); |
| 2405 | const bodyEl = card.querySelector('.portal-card-body') || card; |
| 2406 | const caret = card.querySelector('.portal-card-caret'); |
| 2407 | |
| 2408 | if (header && bodyEl) { |
| 2409 | header.setAttribute('aria-expanded', 'true'); |
| 2410 | bodyEl.style.display = 'block'; |
| 2411 | if (caret) caret.textContent = '▾'; |
| 2412 | } |
| 2413 | |
| 2414 | // Small inline hint at top of the card body |
| 2415 | let hint = bodyEl.querySelector('.portal-card-error-hint'); |
| 2416 | if (!hint) { |
| 2417 | hint = document.createElement('div'); |
| 2418 | hint.className = 'portal-card-error-hint text-danger small'; |
| 2419 | hint.style.marginBottom = '6px'; |
| 2420 | hint.textContent = message || 'Slug and folder are required. This portal will not be saved until both are filled.'; |
| 2421 | bodyEl.insertBefore(hint, bodyEl.firstChild); |
| 2422 | } else { |
| 2423 | hint.textContent = message || hint.textContent; |
| 2424 | } |
| 2425 | }; |
| 2426 | |
| 2427 | cards.forEach(card => { |
| 2428 | const origSlug = card.getAttribute('data-portal-slug') || ''; |
| 2429 | const isNewPortal = card.getAttribute('data-portal-new') === '1'; |
| 2430 | let slug = origSlug.trim(); |
| 2431 | const existingPortal = __portalsCache[origSlug] || {}; |
| 2432 | const existingPortalUser = (existingPortal.portalUser && typeof existingPortal.portalUser === 'object') |
nothing calls this directly
no test coverage detected