Compare commits

..

No commits in common. "72e6c3170ec25dfb710c16a85f9e2f889bcd239c" and "c6a12163bbacc71dd19f0d1a2171a48785a4996d" have entirely different histories.

31 changed files with 229 additions and 3212 deletions

View File

@ -1,139 +0,0 @@
<h2>Full Form Mockup — New Purchase Request</h2>
<p class="subtitle">Showing the improved header section with all dropdowns and the urgency popup. Click "Urgency" to see the popup demo.</p>
<div class="mockup" style="max-width:900px;margin:0 auto;">
<div class="mockup-header">Purchase Request Form — Header Section</div>
<div class="mockup-body" style="background:#f8fafc;padding:20px;">
<!-- Form card -->
<div style="background:#fff;border-radius:10px;border:1px solid #e2e8f0;padding:20px;margin-bottom:16px;">
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:14px;">Project / Department Details</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
<!-- Date -->
<div>
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:4px;">Date <span style="color:#ef4444">*</span></div>
<div style="border:1px solid #d1d5db;border-radius:6px;padding:7px 10px;font-size:12px;color:#374151;background:#fff;">2026-05-24</div>
</div>
<!-- Project Name — dropdown -->
<div>
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:4px;">Project / Site Name <span style="color:#ef4444">*</span></div>
<div style="border:1px solid #d1d5db;border-radius:6px;padding:7px 10px;font-size:12px;color:#374151;background:#fff;display:flex;justify-content:space-between;align-items:center;cursor:pointer;">
<span style="color:#9ca3af;">Select project…</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<!-- Requested By -->
<div>
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:4px;">Requested By <span style="color:#ef4444">*</span></div>
<div style="border:1px solid #d1d5db;border-radius:6px;padding:7px 10px;font-size:12px;color:#374151;background:#fff;">Ahmed Al-Mansoori</div>
</div>
<!-- Required Date / Urgency — popup trigger -->
<div style="position:relative;">
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:4px;">Required Date / Urgency</div>
<div id="urgency-trigger" onclick="toggleUrgencyPopup()" style="border:1px solid #d1d5db;border-radius:6px;padding:7px 10px;font-size:12px;color:#9ca3af;background:#fff;display:flex;justify-content:space-between;align-items:center;cursor:pointer;">
<span id="urgency-display">Select urgency…</span>
<span style="font-size:14px;">🎯</span>
</div>
<!-- Urgency popup -->
<div id="urgency-popup" style="display:none;position:absolute;top:100%;left:0;z-index:50;margin-top:4px;background:#fff;border-radius:12px;border:1.5px solid #e2e8f0;box-shadow:0 8px 24px rgba(0,0,0,0.12);padding:14px;width:280px;">
<div style="font-size:11px;font-weight:600;color:#94a3b8;margin-bottom:10px;text-transform:uppercase;letter-spacing:0.05em;">How soon is this needed?</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;" id="urgency-cards">
<div onclick="pickUrgency('Critical','🚨','#fee2e2','#dc2626')" style="border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;text-align:center;cursor:pointer;transition:all 0.15s;" class="urg-card">
<div style="font-size:20px;margin-bottom:2px;">🚨</div>
<div style="font-size:11px;font-weight:700;color:#dc2626;">Critical</div>
<div style="font-size:10px;color:#94a3b8;">Today</div>
</div>
<div onclick="pickUrgency('Urgent','⚡','#ffedd5','#ea580c')" style="border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;text-align:center;cursor:pointer;" class="urg-card">
<div style="font-size:20px;margin-bottom:2px;"></div>
<div style="font-size:11px;font-weight:700;color:#ea580c;">Urgent</div>
<div style="font-size:10px;color:#94a3b8;">13 days</div>
</div>
<div onclick="pickUrgency('Normal','📋','#fef9c3','#ca8a04')" style="border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;text-align:center;cursor:pointer;" class="urg-card">
<div style="font-size:20px;margin-bottom:2px;">📋</div>
<div style="font-size:11px;font-weight:700;color:#ca8a04;">Normal</div>
<div style="font-size:10px;color:#94a3b8;">This week</div>
</div>
<div onclick="showDatePicker()" style="border:1.5px solid #e2e8f0;border-radius:10px;padding:10px;text-align:center;cursor:pointer;" class="urg-card" id="planned-card">
<div style="font-size:20px;margin-bottom:2px;">🗓️</div>
<div style="font-size:11px;font-weight:700;color:#16a34a;">Planned</div>
<div style="font-size:10px;color:#94a3b8;">Pick date</div>
</div>
</div>
<!-- date picker section, hidden until Planned clicked -->
<div id="date-section" style="display:none;margin-top:10px;border-top:1px solid #f1f5f9;padding-top:10px;">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px;">Specify the required date:</div>
<input type="date" id="planned-date" style="width:100%;border:1.5px solid #3b82f6;border-radius:6px;padding:6px 10px;font-size:12px;" onchange="confirmDate(this.value)">
</div>
</div>
</div>
<!-- Location / Site — dropdown -->
<div>
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:4px;">Location / Site</div>
<div style="border:1px solid #d1d5db;border-radius:6px;padding:7px 10px;font-size:12px;color:#374151;background:#fff;display:flex;justify-content:space-between;align-items:center;cursor:pointer;">
<span style="color:#9ca3af;">Select location…</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<!-- Department -->
<div>
<div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:4px;">Department</div>
<div style="border:1px solid #d1d5db;border-radius:6px;padding:7px 10px;font-size:12px;color:#9ca3af;background:#fff;">e.g. Operations, Production</div>
</div>
</div>
</div>
<!-- Settings sidebar hint -->
<div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:12px;font-size:11px;color:#1d4ed8;">
⚙️ <strong>New Settings section in sidebar</strong> — Admins can manage <em>Locations</em>, <em>Projects</em>, and <em>Urgency Levels</em> from here. Each has a simple list with Add / Edit / Delete.
</div>
</div>
</div>
<script>
function toggleUrgencyPopup() {
var p = document.getElementById('urgency-popup');
p.style.display = p.style.display === 'none' ? 'block' : 'none';
}
function pickUrgency(label, emoji, bg, color) {
document.getElementById('urgency-display').innerHTML = '<span style="background:' + bg + ';color:' + color + ';border-radius:20px;padding:2px 10px;font-weight:600;">' + emoji + ' ' + label + '</span>';
document.getElementById('urgency-popup').style.display = 'none';
document.getElementById('date-section').style.display = 'none';
// reset planned card
document.getElementById('planned-card').style.borderColor = '#e2e8f0';
document.getElementById('planned-card').style.background = '#fff';
}
function showDatePicker() {
var section = document.getElementById('date-section');
section.style.display = section.style.display === 'none' ? 'block' : 'none';
document.getElementById('planned-card').style.borderColor = '#22c55e';
document.getElementById('planned-card').style.background = '#f0fdf4';
}
function confirmDate(val) {
if (!val) return;
var d = new Date(val);
var formatted = d.toLocaleDateString('en-GB', {day:'2-digit',month:'short',year:'numeric'});
document.getElementById('urgency-display').innerHTML = '<span style="background:#f0fdf4;color:#16a34a;border-radius:20px;padding:2px 10px;font-weight:600;">🗓️ ' + formatted + '</span>';
setTimeout(function(){ document.getElementById('urgency-popup').style.display = 'none'; }, 300);
}
// close on outside click
document.addEventListener('click', function(e) {
var popup = document.getElementById('urgency-popup');
var trigger = document.getElementById('urgency-trigger');
if (popup && !popup.contains(e.target) && !trigger.contains(e.target)) {
popup.style.display = 'none';
}
});
</script>

View File

@ -1,87 +0,0 @@
<h2>Required Date / Urgency — which style feels right?</h2>
<p class="subtitle">When the user clicks the field, a popup opens. Pick the style you prefer.</p>
<div class="cards" style="grid-template-columns: repeat(3, 1fr); gap: 20px;">
<!-- Option A -->
<div class="card" data-choice="a" onclick="toggleSelect(this)">
<div class="card-image" style="padding:16px; background:#f8fafc;">
<div style="font-size:11px;color:#64748b;font-weight:600;text-transform:uppercase;margin-bottom:8px;">Urgency</div>
<div style="border:1.5px solid #3b82f6;border-radius:8px;padding:10px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Select urgency level</div>
<div style="display:flex;flex-direction:column;gap:6px;">
<div style="background:#fee2e2;color:#dc2626;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:600;display:flex;align-items:center;gap:6px;">🔴 Critical / Immediate</div>
<div style="background:#ffedd5;color:#ea580c;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:600;display:flex;align-items:center;gap:6px;">🟠 Urgent — Within 3 days</div>
<div style="background:#fef9c3;color:#ca8a04;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:600;display:flex;align-items:center;gap:6px;">🟡 This Week</div>
<div style="background:#dcfce7;color:#16a34a;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:600;display:flex;align-items:center;gap:6px;">🟢 Normal — This Month</div>
<div style="border:1px solid #e2e8f0;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:600;color:#475569;display:flex;align-items:center;gap:6px;">📅 Pick a specific date →</div>
</div>
</div>
</div>
<div class="card-body">
<h3>A — Urgency Levels</h3>
<p>Colored pills with preset labels. Optionally jump to a date picker.</p>
</div>
</div>
<!-- Option B -->
<div class="card" data-choice="b" onclick="toggleSelect(this)">
<div class="card-image" style="padding:16px; background:#f8fafc;">
<div style="font-size:11px;color:#64748b;font-weight:600;text-transform:uppercase;margin-bottom:8px;">Urgency</div>
<div style="border:1.5px solid #3b82f6;border-radius:8px;padding:12px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">How soon is this needed?</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px;">
<div style="border:1.5px solid #dc2626;border-radius:8px;padding:8px;text-align:center;background:#fff5f5;">
<div style="font-size:18px;">🚨</div>
<div style="font-size:11px;font-weight:700;color:#dc2626;">Critical</div>
<div style="font-size:10px;color:#94a3b8;">Today</div>
</div>
<div style="border:1.5px solid #f97316;border-radius:8px;padding:8px;text-align:center;background:#fff7ed;">
<div style="font-size:18px;"></div>
<div style="font-size:11px;font-weight:700;color:#ea580c;">Urgent</div>
<div style="font-size:10px;color:#94a3b8;">13 days</div>
</div>
<div style="border:1.5px solid #eab308;border-radius:8px;padding:8px;text-align:center;background:#fefce8;">
<div style="font-size:18px;">📋</div>
<div style="font-size:11px;font-weight:700;color:#ca8a04;">Normal</div>
<div style="font-size:10px;color:#94a3b8;">This week</div>
</div>
<div style="border:1.5px solid #22c55e;border-radius:8px;padding:8px;text-align:center;background:#f0fdf4;">
<div style="font-size:18px;">🗓️</div>
<div style="font-size:11px;font-weight:700;color:#16a34a;">Planned</div>
<div style="font-size:10px;color:#94a3b8;">Pick date</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>B — Icon Cards</h3>
<p>2×2 grid of icon cards with urgency label + subtitle. Visually distinct.</p>
</div>
</div>
<!-- Option C -->
<div class="card" data-choice="c" onclick="toggleSelect(this)">
<div class="card-image" style="padding:16px; background:#f8fafc;">
<div style="font-size:11px;color:#64748b;font-weight:600;text-transform:uppercase;margin-bottom:8px;">Urgency</div>
<div style="border:1.5px solid #3b82f6;border-radius:8px;padding:12px;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">Quick select or pick a date</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;">
<span style="background:#fee2e2;color:#dc2626;border-radius:20px;padding:5px 12px;font-size:11px;font-weight:600;cursor:pointer;">🔴 Critical</span>
<span style="background:#ffedd5;color:#ea580c;border-radius:20px;padding:5px 12px;font-size:11px;font-weight:600;cursor:pointer;">⚡ Urgent</span>
<span style="background:#fef9c3;color:#ca8a04;border-radius:20px;padding:5px 12px;font-size:11px;font-weight:600;cursor:pointer;">📋 This Week</span>
<span style="background:#dcfce7;color:#16a34a;border-radius:20px;padding:5px 12px;font-size:11px;font-weight:600;cursor:pointer;">✅ Normal</span>
</div>
<div style="border-top:1px solid #f1f5f9;padding-top:8px;">
<div style="font-size:10px;color:#94a3b8;margin-bottom:4px;">Or pick a specific date:</div>
<div style="border:1px solid #e2e8f0;border-radius:6px;padding:5px 10px;font-size:11px;color:#94a3b8;background:#f8fafc;">📅 Select date…</div>
</div>
</div>
</div>
<div class="card-body">
<h3>C — Chips + Date</h3>
<p>Pill-shaped tags for quick picks, plus a date field below for exact dates.</p>
</div>
</div>
</div>

View File

@ -1,3 +0,0 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View File

@ -1 +0,0 @@
{"reason":"idle timeout","timestamp":1779606561971}

View File

@ -1,78 +0,0 @@
<h2>Supplier Modal — 3 Redesign Approaches</h2>
<p class="subtitle">The core problem: the two tabs (Full Order / By Item) are mutually exclusive but look like equals. Choosing one should lock out the other. Here are three ways to fix that.</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Two-Step Wizard <span style="font-size:11px;background:#dbeafe;color:#1d4ed8;padding:2px 8px;border-radius:20px;font-weight:700;margin-left:6px;">Recommended</span></h3>
<p>The modal opens on a "choose your method" screen with two large cards. Clicking one card transitions into that mode — no tabs ever appear. A small "← Change method" back link lets you restart if needed.</p>
<div style="margin-top:10px;background:#f8fafc;border-radius:10px;padding:12px;font-size:12px;">
<div style="display:flex;gap:8px;margin-bottom:6px;">
<div style="background:#eff6ff;border:2px solid #2563eb;border-radius:8px;padding:10px 16px;flex:1;text-align:center;">
<div style="font-size:18px;margin-bottom:4px;">📦</div>
<div style="font-weight:700;color:#0f172a;font-size:12px;">Full Order</div>
<div style="color:#64748b;font-size:10px;margin-top:2px;">One set of suppliers for everything</div>
</div>
<div style="background:#fff;border:1.5px solid #e2e8f0;border-radius:8px;padding:10px 16px;flex:1;text-align:center;">
<div style="font-size:18px;margin-bottom:4px;">🔀</div>
<div style="font-weight:700;color:#0f172a;font-size:12px;">By Item</div>
<div style="color:#64748b;font-size:10px;margin-top:2px;">Different suppliers per item</div>
</div>
</div>
<div style="color:#94a3b8;text-align:center;font-size:10px;">↓ clicking a card opens that flow directly</div>
</div>
<div class="pros-cons" style="margin-top:10px;">
<div class="pros"><h4>Pros</h4><ul><li>Zero ambiguity — only one path at a time</li><li>Clean separation of concerns</li><li>Escape hatch via back link</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>Extra click before seeing suppliers</li></ul></div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Wizard with Step Indicator</h3>
<p>Same two-step wizard as A, but with a visible step breadcrumb at the top: <strong>① Choose Method → ② Select Suppliers</strong>. Adds visual progress context for users who need orientation.</p>
<div style="margin-top:10px;background:#f8fafc;border-radius:10px;padding:12px;font-size:12px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:10px;">
<div style="background:#2563eb;color:#fff;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;">1</div>
<div style="font-size:11px;font-weight:700;color:#2563eb;">Choose Method</div>
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
<div style="background:#e2e8f0;color:#94a3b8;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0;">2</div>
<div style="font-size:11px;color:#94a3b8;">Select Suppliers</div>
</div>
<div style="color:#94a3b8;text-align:center;font-size:10px;">step indicator lives in the modal header</div>
</div>
<div class="pros-cons" style="margin-top:10px;">
<div class="pros"><h4>Pros</h4><ul><li>Users see where they are in the flow</li><li>Familiar wizard UX pattern</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>Slightly more UI chrome</li><li>Overkill for a 2-step flow</li></ul></div>
</div>
</div>
</div>
<div class="option" data-choice="c" onclick="toggleSelect(this)">
<div class="letter">C</div>
<div class="content">
<h3>Prominent Mode Toggle (No Wizard)</h3>
<p>Keep a single-screen modal but replace the tabs with a large segmented toggle at the top. Switching mode clears all selections and shows a confirmation toast. Less structural change, but still communicates mutual exclusivity.</p>
<div style="margin-top:10px;background:#f8fafc;border-radius:10px;padding:12px;font-size:12px;">
<div style="display:flex;border:2px solid #e2e8f0;border-radius:10px;overflow:hidden;margin-bottom:6px;">
<div style="flex:1;padding:8px 12px;background:#2563eb;color:#fff;text-align:center;font-weight:700;font-size:12px;">
📦 Full Order
</div>
<div style="flex:1;padding:8px 12px;background:#fff;color:#94a3b8;text-align:center;font-weight:600;font-size:12px;">
🔀 By Item
</div>
</div>
<div style="color:#f59e0b;font-size:10px;text-align:center;">⚠ Switching clears your current selections</div>
</div>
<div class="pros-cons" style="margin-top:10px;">
<div class="pros"><h4>Pros</h4><ul><li>Fewest changes to existing code</li><li>Both options always visible</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>Still looks like "tabs" — same confusion risk</li><li>Warning toast feels like an afterthought</li></ul></div>
</div>
</div>
</div>
</div>

View File

@ -1,36 +0,0 @@
<h2>Back Button Behavior</h2>
<p class="subtitle">When the user clicks "← Change method" from step 2, what happens to their selections?</p>
<div class="options">
<div class="option" data-choice="clear" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Clear selections on back <span style="font-size:11px;background:#dbeafe;color:#1d4ed8;padding:2px 8px;border-radius:20px;font-weight:700;margin-left:6px;">Recommended</span></h3>
<p>Going back always resets everything. Switching from Full Order → By Item (or vice versa) is a fundamentally different configuration, so a clean slate avoids stale state.</p>
<div style="margin-top:10px;background:#f8fafc;border-radius:10px;padding:12px;font-size:11px;color:#475569;">
<strong>Flow:</strong> Pick method → select suppliers → back → <em>selections cleared</em> → pick method again → fresh start
</div>
<div class="pros-cons" style="margin-top:10px;">
<div class="pros"><h4>Pros</h4><ul><li>Simple, no hidden state</li><li>No risk of mixing Full Order + By Item data</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>User loses work if they go back accidentally</li></ul></div>
</div>
</div>
</div>
<div class="option" data-choice="preserve" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Preserve selections on back</h3>
<p>Selections are remembered per-mode. If the user goes back and re-enters the same mode, their previous picks are still there. Switching to the <em>other</em> mode starts fresh for that mode only.</p>
<div style="margin-top:10px;background:#f8fafc;border-radius:10px;padding:12px;font-size:11px;color:#475569;">
<strong>Flow:</strong> Full Order → select 3 suppliers → back → pick Full Order again → 3 suppliers still checked
</div>
<div class="pros-cons" style="margin-top:10px;">
<div class="pros"><h4>Pros</h4><ul><li>Forgiving — accidental back doesn't lose work</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>More complex state management</li><li>Can confuse users who expect a fresh start</li></ul></div>
</div>
</div>
</div>
</div>

View File

@ -1,125 +0,0 @@
<h2>Wizard Flow Mockup</h2>
<p class="subtitle">Step 1 (method selection) and what Step 2 looks like after choosing. Does this feel right?</p>
<div class="split">
<div class="mockup">
<div class="mockup-header">Step 1 — Choose Method (modal opens here)</div>
<div class="mockup-body" style="padding:0;">
<!-- Modal shell -->
<div style="background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.12);">
<!-- Header -->
<div style="padding:20px 24px 20px;border-bottom:1px solid #f1f5f9;display:flex;align-items:flex-start;justify-content:space-between;">
<div>
<div style="font-size:16px;font-weight:700;color:#0f172a;">Request for Quotation</div>
<div style="font-size:11px;color:#64748b;margin-top:3px;">How do you want to assign suppliers?</div>
</div>
<div style="width:28px;height:28px;background:#f1f5f9;border-radius:7px;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:16px;cursor:pointer;">×</div>
</div>
<!-- Method cards -->
<div style="padding:24px;display:flex;flex-direction:column;gap:12px;">
<div style="border:2px solid #e2e8f0;border-radius:12px;padding:18px 20px;cursor:pointer;display:flex;align-items:center;gap:16px;transition:all .15s;"
onmouseover="this.style.borderColor='#2563eb';this.style.background='#f8fbff'"
onmouseout="this.style.borderColor='#e2e8f0';this.style.background='#fff'">
<div style="width:44px;height:44px;background:#eff6ff;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">📦</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:700;color:#0f172a;">Full Order</div>
<div style="font-size:12px;color:#64748b;margin-top:2px;">One set of suppliers handles the entire purchase request</div>
</div>
<svg width="16" height="16" fill="none" stroke="#cbd5e1" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 18l6-6-6-6"/></svg>
</div>
<div style="border:2px solid #e2e8f0;border-radius:12px;padding:18px 20px;cursor:pointer;display:flex;align-items:center;gap:16px;"
onmouseover="this.style.borderColor='#2563eb';this.style.background='#f8fbff'"
onmouseout="this.style.borderColor='#e2e8f0';this.style.background='#fff'">
<div style="width:44px;height:44px;background:#f0fdf4;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">🔀</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:700;color:#0f172a;">By Item</div>
<div style="font-size:12px;color:#64748b;margin-top:2px;">Assign different suppliers to specific items in this request</div>
</div>
<svg width="16" height="16" fill="none" stroke="#cbd5e1" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 18l6-6-6-6"/></svg>
</div>
</div>
<!-- Footer -->
<div style="padding:14px 24px;border-top:1px solid #f1f5f9;background:#fafafa;display:flex;justify-content:flex-end;">
<button style="padding:8px 18px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">Cancel</button>
</div>
</div>
</div>
</div>
<div class="mockup">
<div class="mockup-header">Step 2 — After choosing "Full Order"</div>
<div class="mockup-body" style="padding:0;">
<div style="background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.12);">
<!-- Header with back link -->
<div style="padding:16px 24px 16px;border-bottom:1px solid #f1f5f9;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:4px;">
<button style="display:flex;align-items:center;gap:5px;font-size:12px;color:#2563eb;background:none;border:none;cursor:pointer;padding:0;font-weight:600;">
<svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/></svg>
Change method
</button>
<div style="font-size:11px;color:#94a3b8;">·</div>
<div style="font-size:11px;background:#eff6ff;color:#2563eb;padding:2px 8px;border-radius:10px;font-weight:700;">📦 Full Order</div>
</div>
<div style="font-size:16px;font-weight:700;color:#0f172a;">Select Suppliers</div>
<div style="font-size:11px;color:#64748b;margin-top:2px;">Choose who receives the quote request</div>
</div>
<!-- Search -->
<div style="padding:12px 24px 8px;">
<div style="position:relative;">
<div style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#94a3b8;font-size:12px;">🔍</div>
<div style="width:100%;padding:8px 12px 8px 30px;border:1px solid #e2e8f0;border-radius:8px;font-size:12px;color:#94a3b8;box-sizing:border-box;">Search suppliers…</div>
</div>
</div>
<!-- Supplier rows -->
<div style="max-height:160px;overflow:hidden;">
<div style="padding:10px 24px;display:flex;align-items:center;gap:12px;border-bottom:1px solid #f8fafc;">
<div style="width:16px;height:16px;border:2px solid #2563eb;border-radius:3px;background:#2563eb;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg width="9" height="9" fill="none" stroke="#fff" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
</div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:600;color:#0f172a;">Al-Rashid Steel Trading</div>
<div style="font-size:10px;color:#94a3b8;">rashid@steel.com · +966 50 123 4567</div>
</div>
<div style="display:flex;border:1px solid #e2e8f0;border-radius:6px;overflow:hidden;font-size:10px;">
<span style="padding:4px 8px;background:#eff6ff;color:#2563eb;font-weight:700;">Email</span>
<span style="padding:4px 8px;color:#94a3b8;border-left:1px solid #e2e8f0;">WA</span>
<span style="padding:4px 8px;color:#94a3b8;border-left:1px solid #e2e8f0;">Both</span>
</div>
</div>
<div style="padding:10px 24px;display:flex;align-items:center;gap:12px;border-bottom:1px solid #f8fafc;">
<div style="width:16px;height:16px;border:2px solid #cbd5e1;border-radius:3px;flex-shrink:0;"></div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:600;color:#0f172a;">Gulf Metals Co.</div>
<div style="font-size:10px;color:#94a3b8;">info@gulfmetals.com</div>
</div>
</div>
<div style="padding:10px 24px;display:flex;align-items:center;gap:12px;border-bottom:1px solid #f8fafc;opacity:.45;pointer-events:none;">
<div style="width:16px;height:16px;border:2px solid #2563eb;border-radius:3px;background:#2563eb;flex-shrink:0;"></div>
<div style="flex:1;">
<div style="font-size:12px;font-weight:600;color:#0f172a;">Emirates Industrial Supply <span style="font-size:9px;background:#dcfce7;color:#15803d;padding:1px 5px;border-radius:8px;margin-left:4px;font-weight:700;">Added</span></div>
<div style="font-size:10px;color:#94a3b8;">+971 4 987 6543</div>
</div>
</div>
</div>
<!-- Footer -->
<div style="padding:12px 24px;border-top:1px solid #f1f5f9;background:#fafafa;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:11px;color:#64748b;">1 supplier selected</div>
<div style="display:flex;gap:8px;">
<button style="padding:7px 16px;border:1px solid #e2e8f0;border-radius:8px;font-size:12px;font-weight:600;color:#64748b;background:#fff;cursor:pointer;">Cancel</button>
<button style="padding:7px 18px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;">Save &amp; Continue →</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1 +0,0 @@
{"reason":"idle timeout","timestamp":1779709143287}

View File

@ -1,168 +0,0 @@
<h2>Add Account Modal — Both Type Variants</h2>
<p class="subtitle">Left: Microsoft 365 (Azure AD) fields. Right: SMTP fields. The type dropdown switches which fields appear. Account name is a slug used in code.</p>
<div class="split">
<!-- Azure variant -->
<div class="mockup">
<div class="mockup-header">Add Mail Account — Microsoft 365</div>
<div class="mockup-body" style="padding:24px;background:#fff;">
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Account Name <span style="color:#9ca3af;font-weight:400;">(used in code)</span></div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#374151;">customer-support</span>
</div>
<div style="font-size:11px;color:#9ca3af;margin-top:3px;">Lowercase, hyphens only. Used as <code style="background:#f3f4f6;padding:1px 5px;border-radius:3px;">Mail::mailer('customer-support')</code></div>
</div>
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Type</div>
<div style="height:38px;background:#fff;border:1px solid #2563eb;border-radius:7px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:13px;color:#374151;">✉️ Microsoft 365 (Azure AD)</span>
<span style="font-size:11px;color:#9ca3af;"></span>
</div>
</div>
<div style="border-top:1px solid #f3f4f6;margin:0 0 16px;padding-top:16px;">
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px;">Azure AD Credentials</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Tenant ID</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span>
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Client ID</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span>
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Client Secret</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:12px;color:#9ca3af;letter-spacing:2px;">••••••••••••</span>
<span style="font-size:11px;color:#6b7280;background:#f9fafb;padding:2px 7px;border-radius:4px;border:1px solid #e5e7eb;">Show</span>
</div>
</div>
</div>
<div style="border-top:1px solid #f3f4f6;padding-top:16px;margin-bottom:16px;">
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px;">Sender</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">From Address</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">support@steelco.com</span>
</div>
</div>
<div>
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">From Name <span style="color:#9ca3af;font-weight:400;">(optional)</span></div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">SteelERP</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f3f4f6;padding-top:16px;">
<span style="font-size:13px;color:#2563eb;text-decoration:underline;cursor:pointer;">🔗 Test Connection</span>
<div style="display:flex;gap:8px;">
<div style="padding:8px 16px;background:#f9fafb;color:#374151;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;">Cancel</div>
<div style="padding:8px 20px;background:#111827;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">Save Account</div>
</div>
</div>
</div>
</div>
<!-- SMTP variant -->
<div class="mockup">
<div class="mockup-header">Add Mail Account — SMTP</div>
<div class="mockup-body" style="padding:24px;background:#fff;">
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Account Name <span style="color:#9ca3af;font-weight:400;">(used in code)</span></div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#374151;">invoices</span>
</div>
<div style="font-size:11px;color:#9ca3af;margin-top:3px;">Lowercase, hyphens only. Used as <code style="background:#f3f4f6;padding:1px 5px;border-radius:3px;">Mail::mailer('invoices')</code></div>
</div>
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Type</div>
<div style="height:38px;background:#fff;border:1px solid #2563eb;border-radius:7px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:13px;color:#374151;">📧 SMTP</span>
<span style="font-size:11px;color:#9ca3af;"></span>
</div>
</div>
<div style="border-top:1px solid #f3f4f6;margin:0 0 16px;padding-top:16px;">
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px;">SMTP Server</div>
<div style="display:flex;gap:10px;margin-bottom:12px;">
<div style="flex:1;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Host</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">smtp.gmail.com</span>
</div>
</div>
<div style="width:80px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Port</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#374151;">587</span>
</div>
</div>
<div style="width:90px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Encryption</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:12px;color:#374151;">TLS</span>
<span style="font-size:10px;color:#9ca3af;"></span>
</div>
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Username</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">invoices@steelco.com</span>
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Password</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:12px;color:#9ca3af;letter-spacing:2px;">••••••••••••</span>
<span style="font-size:11px;color:#6b7280;background:#f9fafb;padding:2px 7px;border-radius:4px;border:1px solid #e5e7eb;">Show</span>
</div>
</div>
</div>
<div style="border-top:1px solid #f3f4f6;padding-top:16px;margin-bottom:16px;">
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:.05em;margin-bottom:12px;">Sender</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">From Address</div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">invoices@steelco.com</span>
</div>
</div>
<div>
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">From Name <span style="color:#9ca3af;font-weight:400;">(optional)</span></div>
<div style="height:36px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:12px;color:#9ca3af;">SteelERP Invoices</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #f3f4f6;padding-top:16px;">
<span style="font-size:13px;color:#2563eb;text-decoration:underline;cursor:pointer;">🔗 Test Connection</span>
<div style="display:flex;gap:8px;">
<div style="padding:8px 16px;background:#f9fafb;color:#374151;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;">Cancel</div>
<div style="padding:8px 20px;background:#111827;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">Save Account</div>
</div>
</div>
</div>
</div>
</div>
<p class="subtitle" style="margin-top:16px;">Both variants use the same modal shell — only the credential section changes based on the Type dropdown. Does this look right? Let me know in the terminal.</p>

View File

@ -1,133 +0,0 @@
<h2>Email Tab — Full Design</h2>
<p class="subtitle">Here's what the Email (Microsoft 365) tab looks like with all its fields. Does this match what you had in mind?</p>
<div class="mockup">
<div class="mockup-header">Settings — Integrations (Email tab active)</div>
<div class="mockup-body" style="background:#f9fafb;padding:24px;">
<div style="max-width:680px;margin:0 auto;">
<!-- Page heading -->
<div style="margin-bottom:20px;">
<div style="font-size:20px;font-weight:700;color:#111827;margin-bottom:4px;">Settings — Integrations</div>
<div style="font-size:13px;color:#6b7280;">Configure third-party service integrations.</div>
</div>
<!-- Pill tabs -->
<div style="display:flex;gap:8px;margin-bottom:20px;">
<div style="padding:7px 18px;background:#fff;color:#374151;border:1px solid #d1d5db;border-radius:999px;font-size:13px;font-weight:500;cursor:pointer;">
💬 WhatsApp
</div>
<div style="padding:7px 18px;background:#111827;color:#fff;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;">
✉️ Email
</div>
</div>
<!-- Email card -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;">
<!-- Card header -->
<div style="padding:16px 24px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:12px;">
<div style="width:32px;height:32px;background:#eff6ff;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:16px;">✉️</div>
<div>
<div style="font-size:14px;font-weight:600;color:#111827;">Microsoft 365 (Azure Mail)</div>
<div style="font-size:12px;color:#6b7280;">Send emails via Microsoft Graph API using Azure AD</div>
</div>
</div>
<!-- Form body -->
<div style="padding:24px;">
<!-- Enable toggle -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div>
<div style="font-size:14px;font-weight:500;color:#374151;margin-bottom:2px;">Enable Email Notifications</div>
<div style="font-size:12px;color:#6b7280;">When disabled, no emails will be sent.</div>
</div>
<div style="width:44px;height:24px;border-radius:12px;background:#d1d5db;position:relative;">
<div style="position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);"></div>
</div>
</div>
<div style="border-top:1px solid #e5e7eb;margin-bottom:20px;"></div>
<!-- Tenant ID -->
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Tenant ID</div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#9ca3af;">xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span>
</div>
</div>
<!-- Client ID -->
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Client ID</div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#9ca3af;">xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span>
</div>
</div>
<!-- Client Secret -->
<div style="margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Client Secret</div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:13px;color:#9ca3af;letter-spacing:2px;">••••••••••••••••</span>
<span style="font-size:11px;color:#6b7280;background:#f9fafb;padding:2px 8px;border-radius:4px;border:1px solid #e5e7eb;cursor:pointer;">Show</span>
</div>
</div>
<!-- From Address -->
<div style="margin-bottom:24px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">From Address</div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#9ca3af;">noreply@yourdomain.com</span>
</div>
<div style="font-size:11px;color:#6b7280;margin-top:4px;">Must be a mailbox in your Microsoft 365 tenant.</div>
</div>
<!-- Actions -->
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:16px;border-top:1px solid #f3f4f6;">
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:13px;color:#2563eb;text-decoration:underline;cursor:pointer;">🔗 Test Connection</span>
</div>
<div style="padding:8px 20px;background:#111827;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">Save Settings</div>
</div>
</div>
</div>
<!-- Send Test Email accordion -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;margin-top:14px;overflow:hidden;">
<div style="padding:14px 24px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;">
<div style="display:flex;align-items:center;gap:10px;">
<span style="font-size:16px;">📧</span>
<span style="font-size:14px;font-weight:600;color:#111827;">Send Test Email</span>
<span style="font-size:12px;color:#6b7280;">— verify the connection works end-to-end</span>
</div>
<span style="font-size:12px;color:#9ca3af;"></span>
</div>
<!-- Expanded state shown -->
<div style="padding:0 24px 20px;border-top:1px solid #e5e7eb;">
<div style="height:12px;"></div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">To</div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#9ca3af;">recipient@example.com</span>
</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Subject</div>
<div style="height:38px;background:#fff;border:1px solid #d1d5db;border-radius:7px;padding:0 12px;display:flex;align-items:center;">
<span style="font-size:13px;color:#6b7280;">Test Email from SteelERP</span>
</div>
</div>
<div style="padding:9px 18px;background:#2563eb;color:#fff;border-radius:8px;font-size:13px;font-weight:600;display:inline-flex;align-items:center;gap:7px;cursor:pointer;">
✉️ Send Email
</div>
</div>
</div>
</div>
</div>
</div>
<p class="subtitle" style="margin-top:16px;">Does this look right? Let me know in the terminal if you'd like any changes — different fields, different layout, etc.</p>

View File

@ -1,117 +0,0 @@
<h2>Integrations Page — Tab Layout</h2>
<p class="subtitle">Here's how the integrations page would look with WhatsApp and Email tabs. Which tab style feels right?</p>
<div class="cards">
<!-- Option A: Pill tabs -->
<div class="card" data-choice="a" onclick="toggleSelect(this)">
<div class="card-image" style="background:#f9fafb;padding:20px;">
<div style="max-width:560px;margin:0 auto;">
<div style="margin-bottom:16px;">
<div style="font-size:18px;font-weight:700;color:#111827;margin-bottom:4px;">Settings — Integrations</div>
<div style="font-size:13px;color:#6b7280;">Configure third-party service integrations.</div>
</div>
<!-- Pill tabs -->
<div style="display:flex;gap:8px;margin-bottom:20px;">
<div style="padding:7px 18px;background:#111827;color:#fff;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;">
<span style="margin-right:6px;">💬</span>WhatsApp
</div>
<div style="padding:7px 18px;background:#fff;color:#374151;border:1px solid #d1d5db;border-radius:999px;font-size:13px;font-weight:500;cursor:pointer;">
<span style="margin-right:6px;">✉️</span>Email
</div>
</div>
<!-- Card -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;">
<div style="padding:16px 20px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:10px;">
<div style="width:10px;height:10px;border-radius:50%;background:#22c55e;"></div>
<span style="font-size:14px;font-weight:600;color:#111827;">WhatsApp (UltraMSG)</span>
<span style="margin-left:auto;font-size:11px;color:#6b7280;background:#f3f4f6;padding:2px 8px;border-radius:999px;">Active</span>
</div>
<div style="padding:16px 20px;">
<div style="height:10px;background:#f3f4f6;border-radius:4px;width:60%;margin-bottom:8px;"></div>
<div style="height:10px;background:#f3f4f6;border-radius:4px;width:40%;"></div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>A — Pill Tabs</h3>
<p>Rounded pill-style tabs. Modern, clean. Active tab is filled dark.</p>
</div>
</div>
<!-- Option B: Underline tabs -->
<div class="card" data-choice="b" onclick="toggleSelect(this)">
<div class="card-image" style="background:#f9fafb;padding:20px;">
<div style="max-width:560px;margin:0 auto;">
<div style="margin-bottom:16px;">
<div style="font-size:18px;font-weight:700;color:#111827;margin-bottom:4px;">Settings — Integrations</div>
<div style="font-size:13px;color:#6b7280;">Configure third-party service integrations.</div>
</div>
<!-- Underline tabs -->
<div style="display:flex;gap:0;border-bottom:2px solid #e5e7eb;margin-bottom:20px;">
<div style="padding:8px 20px;font-size:13px;font-weight:600;color:#111827;border-bottom:2px solid #111827;margin-bottom:-2px;cursor:pointer;">
<span style="margin-right:6px;">💬</span>WhatsApp
</div>
<div style="padding:8px 20px;font-size:13px;font-weight:500;color:#6b7280;cursor:pointer;">
<span style="margin-right:6px;">✉️</span>Email
</div>
</div>
<!-- Card -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;">
<div style="padding:16px 20px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:10px;">
<div style="width:10px;height:10px;border-radius:50%;background:#22c55e;"></div>
<span style="font-size:14px;font-weight:600;color:#111827;">WhatsApp (UltraMSG)</span>
<span style="margin-left:auto;font-size:11px;color:#6b7280;background:#f3f4f6;padding:2px 8px;border-radius:999px;">Active</span>
</div>
<div style="padding:16px 20px;">
<div style="height:10px;background:#f3f4f6;border-radius:4px;width:60%;margin-bottom:8px;"></div>
<div style="height:10px;background:#f3f4f6;border-radius:4px;width:40%;"></div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>B — Underline Tabs</h3>
<p>Classic tab style with bottom border indicator. Matches settings pages in most ERP systems.</p>
</div>
</div>
<!-- Option C: Side nav -->
<div class="card" data-choice="c" onclick="toggleSelect(this)">
<div class="card-image" style="background:#f9fafb;padding:20px;">
<div style="max-width:560px;margin:0 auto;">
<div style="margin-bottom:16px;">
<div style="font-size:18px;font-weight:700;color:#111827;margin-bottom:4px;">Settings — Integrations</div>
<div style="font-size:13px;color:#6b7280;">Configure third-party service integrations.</div>
</div>
<!-- Side nav + content -->
<div style="display:flex;gap:16px;">
<div style="width:140px;flex-shrink:0;display:flex;flex-direction:column;gap:4px;">
<div style="padding:9px 14px;background:#111827;color:#fff;border-radius:7px;font-size:13px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:8px;">
<span>💬</span>WhatsApp
</div>
<div style="padding:9px 14px;color:#374151;border-radius:7px;font-size:13px;font-weight:500;cursor:pointer;display:flex;align-items:center;gap:8px;">
<span>✉️</span>Email
</div>
</div>
<div style="flex:1;background:#fff;border:1px solid #e5e7eb;border-radius:10px;">
<div style="padding:12px 16px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:8px;">
<div style="width:8px;height:8px;border-radius:50%;background:#22c55e;"></div>
<span style="font-size:13px;font-weight:600;color:#111827;">WhatsApp (UltraMSG)</span>
</div>
<div style="padding:12px 16px;">
<div style="height:8px;background:#f3f4f6;border-radius:4px;width:60%;margin-bottom:8px;"></div>
<div style="height:8px;background:#f3f4f6;border-radius:4px;width:40%;"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>C — Sidebar Nav</h3>
<p>Left sidebar with integration list. Scales well if more integrations are added later.</p>
</div>
</div>
</div>

View File

@ -1,106 +0,0 @@
<h2>Email Tab — Multi-Account Design</h2>
<p class="subtitle">The Email tab becomes a list of named accounts. Each account has a type badge, from address, and actions. Here's how it looks with a few accounts configured.</p>
<div class="mockup">
<div class="mockup-header">Settings — Integrations (Email tab active)</div>
<div class="mockup-body" style="background:#f9fafb;padding:24px;">
<div style="max-width:680px;margin:0 auto;">
<!-- Page heading -->
<div style="margin-bottom:20px;">
<div style="font-size:20px;font-weight:700;color:#111827;margin-bottom:4px;">Settings — Integrations</div>
<div style="font-size:13px;color:#6b7280;">Configure third-party service integrations.</div>
</div>
<!-- Pill tabs -->
<div style="display:flex;gap:8px;margin-bottom:20px;">
<div style="padding:7px 18px;background:#fff;color:#374151;border:1px solid #d1d5db;border-radius:999px;font-size:13px;font-weight:500;">💬 WhatsApp</div>
<div style="padding:7px 18px;background:#111827;color:#fff;border-radius:999px;font-size:13px;font-weight:600;">✉️ Email</div>
</div>
<!-- Header row: title + Add button -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div>
<div style="font-size:15px;font-weight:600;color:#111827;">Mail Accounts</div>
<div style="font-size:12px;color:#6b7280;">3 accounts configured</div>
</div>
<div style="padding:8px 16px;background:#111827;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:7px;">
<span style="font-size:16px;line-height:1;">+</span> Add Account
</div>
</div>
<!-- Account list -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;">
<!-- Account 1: Azure -->
<div style="padding:16px 20px;display:flex;align-items:center;gap:14px;border-bottom:1px solid #f3f4f6;">
<div style="width:36px;height:36px;background:#eff6ff;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:17px;">✉️</div>
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:2px;">
<span style="font-size:14px;font-weight:600;color:#111827;">customer-support</span>
<span style="font-size:11px;font-weight:600;color:#2563eb;background:#eff6ff;padding:2px 8px;border-radius:999px;">Microsoft 365</span>
</div>
<div style="font-size:12px;color:#6b7280;">support@steelco.com</div>
</div>
<!-- Toggle: ON -->
<div style="width:44px;height:24px;border-radius:12px;background:#22c55e;position:relative;cursor:pointer;flex-shrink:0;">
<div style="position:absolute;top:2px;left:22px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);"></div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0;">
<button style="font-size:12px;color:#6b7280;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;cursor:pointer;">Edit</button>
<button style="font-size:12px;color:#dc2626;background:#fff5f5;border:1px solid #fecaca;border-radius:6px;padding:5px 10px;cursor:pointer;">Delete</button>
</div>
</div>
<!-- Account 2: SMTP -->
<div style="padding:16px 20px;display:flex;align-items:center;gap:14px;border-bottom:1px solid #f3f4f6;">
<div style="width:36px;height:36px;background:#f0fdf4;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:17px;">📧</div>
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:2px;">
<span style="font-size:14px;font-weight:600;color:#111827;">invoices</span>
<span style="font-size:11px;font-weight:600;color:#16a34a;background:#f0fdf4;padding:2px 8px;border-radius:999px;">SMTP</span>
</div>
<div style="font-size:12px;color:#6b7280;">invoices@steelco.com · smtp.gmail.com:587</div>
</div>
<!-- Toggle: ON -->
<div style="width:44px;height:24px;border-radius:12px;background:#22c55e;position:relative;cursor:pointer;flex-shrink:0;">
<div style="position:absolute;top:2px;left:22px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);"></div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0;">
<button style="font-size:12px;color:#6b7280;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;cursor:pointer;">Edit</button>
<button style="font-size:12px;color:#dc2626;background:#fff5f5;border:1px solid #fecaca;border-radius:6px;padding:5px 10px;cursor:pointer;">Delete</button>
</div>
</div>
<!-- Account 3: SMTP disabled -->
<div style="padding:16px 20px;display:flex;align-items:center;gap:14px;opacity:.6;">
<div style="width:36px;height:36px;background:#f3f4f6;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:17px;">📧</div>
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:2px;">
<span style="font-size:14px;font-weight:600;color:#111827;">notifications</span>
<span style="font-size:11px;font-weight:600;color:#16a34a;background:#f0fdf4;padding:2px 8px;border-radius:999px;">SMTP</span>
</div>
<div style="font-size:12px;color:#6b7280;">no-reply@steelco.com · mail.steelco.com:465</div>
</div>
<!-- Toggle: OFF -->
<div style="width:44px;height:24px;border-radius:12px;background:#d1d5db;position:relative;cursor:pointer;flex-shrink:0;">
<div style="position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);"></div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0;">
<button style="font-size:12px;color:#6b7280;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;cursor:pointer;">Edit</button>
<button style="font-size:12px;color:#dc2626;background:#fff5f5;border:1px solid #fecaca;border-radius:6px;padding:5px 10px;cursor:pointer;">Delete</button>
</div>
</div>
</div>
<!-- Empty state hint -->
<div style="margin-top:10px;font-size:12px;color:#9ca3af;text-align:center;">
Use account names in code: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:11px;">Mail::mailer('customer-support')</code>
</div>
</div>
</div>
</div>
<p class="subtitle" style="margin-top:16px;">This is the Email tab redesigned as a list. Click "Add Account" opens a modal (next screen). Does this list layout look right? Let me know in the terminal.</p>

View File

@ -1,3 +0,0 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View File

@ -1,3 +0,0 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Design approved — writing the implementation plan in terminal...</p>
</div>

View File

@ -1 +0,0 @@
{"reason":"idle timeout","timestamp":1779788510216}

View File

@ -71,7 +71,7 @@ class MailAccountController extends Controller
$mailer = new \Illuminate\Mail\Mailer(
$mailAccount->name,
app('view'),
$mailAccount->buildTransport(),
new \Symfony\Component\Mailer\Mailer($mailAccount->buildTransport()),
app('events')
);
$mailer->raw(
@ -84,12 +84,7 @@ class MailAccountController extends Controller
);
return response()->json(['success' => true]);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('sendTestEmail failed', [
'account' => $mailAccount->name,
'from' => $mailAccount->from_address,
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
\Illuminate\Support\Facades\Log::error('sendTestEmail failed: ' . $e->getMessage(), ['account' => $mailAccount->name]);
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}

View File

@ -3,93 +3,52 @@
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Settings\Company;
use App\Models\Settings\Department;
use App\Models\Settings\Location;
use App\Models\Settings\ProjectSetting;
use App\Services\ProjectImportService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class ProjectSettingController extends Controller
{
public function index()
{
$companies = Company::with([
'projects.locations' => fn ($q) => $q->orderBy('name'),
'departments' => fn ($q) => $q->orderBy('name'),
])->orderBy('name')->get();
$allProjects = $companies->flatMap(fn ($c) => $c->projects);
$projects = ProjectSetting::with(['locations' => function ($q) {
$q->orderBy('name');
}])->orderBy('name')->get();
$stats = [
'total_companies' => $companies->count(),
'total_projects' => $allProjects->count(),
'active_projects' => $allProjects->where('is_active', true)->count(),
'total_locations' => $allProjects->sum(fn ($p) => $p->locations->count()),
'total_departments' => $companies->sum(fn ($c) => $c->departments->count()),
'total_projects' => $projects->count(),
'active_projects' => $projects->where('is_active', true)->count(),
'total_locations' => $projects->sum(fn ($p) => $p->locations->count()),
'active_locations' => $projects->sum(fn ($p) => $p->locations->where('is_active', true)->count()),
];
return view('settings.projects.index', compact('companies', 'stats'));
return view('settings.projects.index', compact('projects', 'stats'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255|unique:settings_projects,name',
'company_id' => 'required|exists:settings_companies,id',
]);
$project = ProjectSetting::create(['name' => $validated['name'], 'company_id' => $validated['company_id'], 'is_active' => true]);
$validated = $request->validate(['name' => 'required|string|max:255|unique:settings_projects,name']);
$project = ProjectSetting::create(['name' => $validated['name'], 'is_active' => true]);
return response()->json(['project' => [
'id' => $project->id,
'name' => $project->name,
'is_active' => $project->is_active,
'company_id' => $project->company_id,
'id' => $project->id,
'name' => $project->name,
'is_active' => $project->is_active,
]]);
}
// ── Company CRUD ──────────────────────────────────────────────────────────
public function storeCompany(Request $request)
{
$validated = $request->validate(['name' => 'required|string|max:255|unique:settings_companies,name']);
$company = Company::create(['name' => $validated['name'], 'is_active' => true]);
return response()->json(['company' => ['id' => $company->id, 'name' => $company->name, 'is_active' => $company->is_active]]);
}
public function updateCompany(Request $request, Company $company)
{
$validated = $request->validate(['name' => 'required|string|max:255|unique:settings_companies,name,' . $company->id]);
$company->update(['name' => $validated['name'], 'is_active' => $request->boolean('is_active', true)]);
return response()->json(['company' => ['id' => $company->id, 'name' => $company->name, 'is_active' => $company->is_active]]);
}
public function destroyCompany(Company $company)
{
$company->delete();
return response()->json(['ok' => true]);
}
public function update(Request $request, ProjectSetting $project)
{
$validated = $request->validate([
'name' => 'required|string|max:255|unique:settings_projects,name,' . $project->id,
'company_id' => 'nullable|exists:settings_companies,id',
'name' => 'required|string|max:255|unique:settings_projects,name,' . $project->id,
]);
$project->update([
'name' => $validated['name'],
'company_id' => $validated['company_id'] ?? $project->company_id,
'is_active' => $request->boolean('is_active', true),
'name' => $validated['name'],
'is_active' => $request->boolean('is_active', true),
]);
return response()->json(['project' => [
'id' => $project->id,
'name' => $project->name,
'is_active' => $project->is_active,
'company_id' => $project->company_id,
'id' => $project->id,
'name' => $project->name,
'is_active' => $project->is_active,
]]);
}
@ -154,120 +113,4 @@ class ProjectSettingController extends Controller
$location->delete();
return response()->json(['ok' => true]);
}
public function storeDepartment(Request $request, Company $company)
{
$validated = $request->validate(['name' => 'required|string|max:255']);
$dept = $company->departments()->create(['name' => $validated['name'], 'is_active' => true]);
return response()->json(['department' => ['id' => $dept->id, 'name' => $dept->name, 'is_active' => $dept->is_active]]);
}
public function updateDepartment(Request $request, Company $company, Department $department)
{
$validated = $request->validate(['name' => 'required|string|max:255']);
$department->update(['name' => $validated['name'], 'is_active' => $request->boolean('is_active', true)]);
return response()->json(['department' => ['id' => $department->id, 'name' => $department->name, 'is_active' => $department->is_active]]);
}
public function destroyDepartment(Company $company, Department $department)
{
$department->delete();
return response()->json(['ok' => true]);
}
// ── Import ────────────────────────────────────────────────────────────────
public function import(Request $request): JsonResponse
{
$request->validate(['file' => 'required|file|mimes:xlsx,xls|max:10240']);
try {
$stats = app(ProjectImportService::class)->import(
$request->file('file')->getPathname()
);
$parts = [];
if ($stats['projects_created']) $parts[] = "{$stats['projects_created']} project(s)";
if ($stats['departments_created']) $parts[] = "{$stats['departments_created']} department(s)";
if ($stats['companies_created']) $parts[] = "{$stats['companies_created']} new company(s)";
$message = $parts
? 'Imported: ' . implode(', ', $parts) . ($stats['skipped'] ? "{$stats['skipped']} row(s) skipped" : '')
: 'Nothing new to import' . ($stats['skipped'] ? " ({$stats['skipped']} rows already exist)" : '');
return response()->json(['success' => true, 'message' => $message, 'stats' => $stats]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 422);
}
}
public function downloadTemplate(): \Symfony\Component\HttpFoundation\BinaryFileResponse
{
$path = storage_path('app/projects_template.xlsx');
$this->buildTemplate($path);
return response()->download($path, 'projects_template.xlsx');
}
private function buildTemplate(string $path): void
{
$spreadsheet = new Spreadsheet();
$headerStyle = [
'font' => ['bold' => true, 'color' => ['rgb' => '1e293b']],
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'DBEAFE']],
'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT],
];
$noteStyle = [
'font' => ['italic' => true, 'color' => ['rgb' => '64748b'], 'size' => 10],
];
// ── Sheet 1: Projects ──────────────────────────────────────────────
$s1 = $spreadsheet->getActiveSheet()->setTitle('Projects');
$s1->setCellValue('A1', 'Company Name')
->setCellValue('B1', 'Project Name');
$s1->getStyle('A1:B1')->applyFromArray($headerStyle);
// Sample rows
$samples = [
['Miknas Industrial', 'New Warehouse'],
['Steel tech', 'Factory Extension'],
['Steel tech', 'New Office Block'],
];
foreach ($samples as $i => $row) {
$s1->setCellValue('A' . ($i + 2), $row[0]);
$s1->setCellValue('B' . ($i + 2), $row[1]);
}
$s1->setCellValue('A6', '* Delete sample rows before importing. Company will be created if it does not exist.');
$s1->getStyle('A6')->applyFromArray($noteStyle);
$s1->mergeCells('A6:B6');
$s1->getColumnDimension('A')->setWidth(32);
$s1->getColumnDimension('B')->setWidth(32);
// ── Sheet 2: Departments ───────────────────────────────────────────
$s2 = new Worksheet($spreadsheet, 'Departments');
$spreadsheet->addSheet($s2);
$s2->setCellValue('A1', 'Company Name')
->setCellValue('B1', 'Department Name');
$s2->getStyle('A1:B1')->applyFromArray($headerStyle);
$deptSamples = [
['Miknas Industrial', 'Finance'],
['Miknas Industrial', 'Operations'],
['Steel tech', 'Production'],
];
foreach ($deptSamples as $i => $row) {
$s2->setCellValue('A' . ($i + 2), $row[0]);
$s2->setCellValue('B' . ($i + 2), $row[1]);
}
$s2->setCellValue('A6', '* Delete sample rows before importing. Company will be created if it does not exist.');
$s2->getStyle('A6')->applyFromArray($noteStyle);
$s2->mergeCells('A6:B6');
$s2->getColumnDimension('A')->setWidth(32);
$s2->getColumnDimension('B')->setWidth(32);
(new Xlsx($spreadsheet))->save($path);
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
protected $table = 'settings_companies';
protected $fillable = ['name', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function projects(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ProjectSetting::class, 'company_id');
}
public function departments(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Department::class, 'company_id');
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class Department extends Model
{
protected $table = 'settings_departments';
protected $fillable = ['name', 'company_id', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Company::class, 'company_id');
}
}

View File

@ -8,7 +8,7 @@ class ProjectSetting extends Model
{
protected $table = 'settings_projects';
protected $fillable = ['name', 'is_active', 'company_id'];
protected $fillable = ['name', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
@ -17,14 +17,8 @@ class ProjectSetting extends Model
return $query->where('is_active', true);
}
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Company::class, 'company_id');
}
public function locations(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(\App\Models\Settings\Location::class, 'project_id');
}
}

View File

@ -1,164 +0,0 @@
<?php
namespace App\Services;
use App\Models\Settings\Company;
use App\Models\Settings\Department;
use App\Models\Settings\ProjectSetting;
use PhpOffice\PhpSpreadsheet\IOFactory;
class ProjectImportService
{
private array $stats = [
'companies_created' => 0,
'projects_created' => 0,
'departments_created' => 0,
'skipped' => 0,
];
/** @var array<string,Company> */
private array $companyCache = [];
public function import(string $filePath): array
{
$spreadsheet = IOFactory::load($filePath);
$projectSheet = $this->findSheet($spreadsheet, ['projects', 'project']);
$deptSheet = $this->findSheet($spreadsheet, ['departments', 'department', 'depts', 'dept']);
if ($projectSheet) {
$this->importProjects($projectSheet);
}
if ($deptSheet) {
$this->importDepartments($deptSheet);
}
// Single-sheet file with no named tabs → treat as projects
if (!$projectSheet && !$deptSheet) {
$this->importProjects($spreadsheet->getActiveSheet());
}
return $this->stats;
}
private function findSheet($spreadsheet, array $names): ?\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
{
foreach ($spreadsheet->getSheetNames() as $i => $sheetName) {
if (in_array(strtolower(trim($sheetName)), $names)) {
return $spreadsheet->getSheet($i);
}
}
return null;
}
private function importProjects(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet): void
{
$rows = $sheet->toArray(null, true, true, false);
$headers = $this->normalizeHeaders((array) array_shift($rows));
$coIdx = $this->findCol($headers, ['company', 'company name', 'company name', 'companyname']);
$projIdx = $this->findCol($headers, ['project', 'project name', 'project name', 'projectname']);
if ($coIdx === null || $projIdx === null) {
return;
}
foreach ($rows as $row) {
$coName = $this->str($row[$coIdx] ?? null);
$projName = $this->str($row[$projIdx] ?? null);
if (!$coName || !$projName) {
$this->stats['skipped']++;
continue;
}
$company = $this->findOrCreateCompany($coName);
$existing = ProjectSetting::whereRaw('LOWER(name) = ?', [strtolower($projName)])
->where('company_id', $company->id)->first();
if ($existing) {
$this->stats['skipped']++;
continue;
}
ProjectSetting::create(['name' => $projName, 'company_id' => $company->id, 'is_active' => true]);
$this->stats['projects_created']++;
}
}
private function importDepartments(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet): void
{
$rows = $sheet->toArray(null, true, true, false);
$headers = $this->normalizeHeaders((array) array_shift($rows));
$coIdx = $this->findCol($headers, ['company', 'company name', 'companyname']);
$deptIdx = $this->findCol($headers, ['department', 'department name', 'departmentname', 'dept', 'dept name']);
if ($coIdx === null || $deptIdx === null) {
return;
}
foreach ($rows as $row) {
$coName = $this->str($row[$coIdx] ?? null);
$deptName = $this->str($row[$deptIdx] ?? null);
if (!$coName || !$deptName) {
$this->stats['skipped']++;
continue;
}
$company = $this->findOrCreateCompany($coName);
$existing = Department::whereRaw('LOWER(name) = ?', [strtolower($deptName)])
->where('company_id', $company->id)->first();
if ($existing) {
$this->stats['skipped']++;
continue;
}
Department::create(['name' => $deptName, 'company_id' => $company->id, 'is_active' => true]);
$this->stats['departments_created']++;
}
}
private function findOrCreateCompany(string $name): Company
{
$key = strtolower($name);
if (isset($this->companyCache[$key])) {
return $this->companyCache[$key];
}
$company = Company::whereRaw('LOWER(name) = ?', [$key])->first();
if (!$company) {
$company = Company::create(['name' => $name, 'is_active' => true]);
$this->stats['companies_created']++;
}
$this->companyCache[$key] = $company;
return $company;
}
private function normalizeHeaders(array $row): array
{
return array_map(
fn ($h) => strtolower(trim(str_replace(['*', '_', '-'], ' ', (string) ($h ?? '')))),
$row
);
}
private function findCol(array $headers, array $aliases): ?int
{
foreach ($headers as $i => $h) {
if (in_array($h, $aliases)) {
return $i;
}
}
return null;
}
private function str(mixed $value): ?string
{
$v = trim((string) ($value ?? ''));
return $v === '' ? null : $v;
}
}

View File

@ -1,30 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('settings_departments', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('settings_projects')->cascadeOnDelete();
$table->string('name', 255);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('settings_departments');
}
};

View File

@ -1,41 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('settings_companies', function (Blueprint $table) {
$table->id();
$table->string('name', 255)->unique();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::table('settings_projects', function (Blueprint $table) {
$table->foreignId('company_id')->nullable()->after('id')
->constrained('settings_companies')->nullOnDelete();
});
// Assign all existing projects to a "General" company
if (\DB::table('settings_projects')->exists()) {
$id = \DB::table('settings_companies')->insertGetId(['name' => 'General', 'is_active' => 1, 'created_at' => now(), 'updated_at' => now()]);
\DB::table('settings_projects')->update(['company_id' => $id]);
}
}
public function down(): void
{
Schema::table('settings_projects', function (Blueprint $table) {
$table->dropForeign(['company_id']);
$table->dropColumn('company_id');
});
Schema::dropIfExists('settings_companies');
}
};

View File

@ -1,47 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Recreate table with company_id instead of project_id (SQLite-safe)
Schema::create('settings_departments_new', function (Blueprint $table) {
$table->id();
$table->foreignId('company_id')->constrained('settings_companies')->onDelete('cascade');
$table->string('name');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
// Migrate: map each department's project_id → that project's company_id
DB::statement('
INSERT INTO settings_departments_new (id, company_id, name, is_active, created_at, updated_at)
SELECT d.id, p.company_id, d.name, d.is_active, d.created_at, d.updated_at
FROM settings_departments d
LEFT JOIN settings_projects p ON p.id = d.project_id
WHERE p.company_id IS NOT NULL
');
Schema::drop('settings_departments');
Schema::rename('settings_departments_new', 'settings_departments');
}
public function down(): void
{
Schema::create('settings_departments_old', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained('settings_projects')->onDelete('cascade');
$table->string('name');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::drop('settings_departments');
Schema::rename('settings_departments_old', 'settings_departments');
}
};

View File

@ -1,772 +0,0 @@
# Integrations Tabs Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add pill-style WhatsApp / Email tabs to the Settings → Integrations page, convert WhatsApp save to AJAX, and add a full Microsoft 365 (Azure Mail) configuration panel.
**Architecture:** Three-file change — routes add 3 new Azure POST endpoints, SettingsController gains 3 new methods and converts `updateWhatsapp()` from redirect to JSON, and `integrations.blade.php` is fully rewritten with Alpine.js tab switching, AJAX saves, and the new Email panel. No new models or migrations needed; all Azure credentials are persisted via the existing `Setting::get/set()` key-value store.
**Tech Stack:** Laravel 12, Alpine.js v3, `PromoSeven\AzureMailer\Graph\TokenManager` (local path package), `Illuminate\Support\Facades\Mail`, `Setting` model key-value store.
---
## File Map
| File | Change |
|------|--------|
| `routes/web.php` | Add 3 POST routes inside the existing `role:Admin` group |
| `app/Http/Controllers/SettingsController.php` | Update `integrations()`, convert `updateWhatsapp()` to JSON, add `updateAzureMail()`, `testAzureMailConnection()`, `sendTestEmail()` |
| `resources/views/settings/integrations.blade.php` | Full rewrite — pill tabs, WhatsApp panel (AJAX), Email panel with all fields + accordions |
---
### Task 1: Add Azure routes
**Files:**
- Modify: `routes/web.php` (around line 125, inside the `role:Admin` group after `send-test-message`)
- [ ] **Step 1: Open `routes/web.php` and locate the settings group**
Find the block that looks like:
```php
Route::post('settings/integrations/send-test-message', [SettingsController::class, 'sendTestMessage'])->name('settings.integrations.send-test-message');
```
- [ ] **Step 2: Add the three Azure routes immediately after that line**
```php
Route::post('settings/integrations/azure-mail', [SettingsController::class, 'updateAzureMail'])->name('settings.integrations.azure-mail');
Route::post('settings/integrations/test-azure-mail', [SettingsController::class, 'testAzureMailConnection'])->name('settings.integrations.test-azure-mail');
Route::post('settings/integrations/send-test-email', [SettingsController::class, 'sendTestEmail'])->name('settings.integrations.send-test-email');
```
- [ ] **Step 3: Verify routes are registered**
Run:
```bash
php artisan route:list --name=settings.integrations
```
Expected: 7 rows — the 4 existing + the 3 new azure ones.
- [ ] **Step 4: Commit**
```bash
git add routes/web.php
git commit -m "feat: add Azure Mail routes to settings integrations"
```
---
### Task 2: Update SettingsController
**Files:**
- Modify: `app/Http/Controllers/SettingsController.php`
The class currently has 4 methods. This task updates `integrations()`, converts `updateWhatsapp()` return type, and adds 3 new methods.
- [ ] **Step 1: Add the new use statements at the top of the file (after the existing ones)**
Add these two lines alongside the existing `use` block:
```php
use Illuminate\Support\Facades\Mail;
use PromoSeven\AzureMailer\Graph\TokenManager;
```
- [ ] **Step 2: Update `integrations()` to fetch and pass Azure settings**
Replace the entire `integrations()` method with:
```php
public function integrations(): View
{
$whatsappSettings = [
'enabled' => Setting::get('ultramsg_enabled', false),
'instance_id' => Setting::get('ultramsg_instance_id', ''),
'token' => Setting::get('ultramsg_token', ''),
'webhook_secret' => Setting::get('ultramsg_webhook_secret', ''),
'webhook_path' => Setting::get('ultramsg_webhook_path', 'ultra-message/webhook'),
];
$azureSettings = [
'enabled' => Setting::get('azure_mail_enabled', false),
'tenant_id' => Setting::get('azure_mail_tenant_id', ''),
'client_id' => Setting::get('azure_mail_client_id', ''),
'client_secret' => Setting::get('azure_mail_client_secret', ''),
'from_address' => Setting::get('azure_mail_from_address', ''),
];
return view('settings.integrations', compact('whatsappSettings', 'azureSettings'));
}
```
- [ ] **Step 3: Convert `updateWhatsapp()` to return JsonResponse instead of RedirectResponse**
Replace the entire `updateWhatsapp()` method with:
```php
public function updateWhatsapp(Request $request): JsonResponse
{
$request->validate([
'instance_id' => ['required', 'string', 'max:100'],
'token' => ['required', 'string', 'max:255'],
'webhook_secret' => ['nullable', 'string', 'max:255'],
'webhook_path' => ['required', 'string', 'max:100'],
]);
Setting::set('ultramsg_enabled', $request->input('enabled') === '1' ? '1' : '0');
Setting::set('ultramsg_instance_id', $request->instance_id);
Setting::set('ultramsg_token', $request->token);
Setting::set('ultramsg_webhook_secret', $request->webhook_secret ?? '');
Setting::set('ultramsg_webhook_path', $request->webhook_path);
return response()->json(['success' => true]);
}
```
Note: `$request->input('enabled') === '1'` because the AJAX payload sends the string `'1'` or `'0'` from the hidden input — not a checkbox boolean.
- [ ] **Step 4: Remove the `RedirectResponse` use statement if it is now unused**
Check if `RedirectResponse` appears anywhere else in the file. If only `updateWhatsapp()` used it, remove:
```php
use Illuminate\Http\RedirectResponse;
```
- [ ] **Step 5: Add `updateAzureMail()` after the closing brace of `updateWhatsapp()`**
```php
public function updateAzureMail(Request $request): JsonResponse
{
$request->validate([
'tenant_id' => ['required', 'string', 'max:100'],
'client_id' => ['required', 'string', 'max:100'],
'client_secret' => ['required', 'string', 'max:500'],
'from_address' => ['required', 'email', 'max:255'],
]);
Setting::set('azure_mail_enabled', $request->input('enabled') === '1' ? '1' : '0');
Setting::set('azure_mail_tenant_id', $request->tenant_id);
Setting::set('azure_mail_client_id', $request->client_id);
Setting::set('azure_mail_client_secret', $request->client_secret);
Setting::set('azure_mail_from_address', $request->from_address);
return response()->json(['success' => true]);
}
```
- [ ] **Step 6: Add `testAzureMailConnection()` after `updateAzureMail()`**
```php
public function testAzureMailConnection(): JsonResponse
{
try {
$config = [
'tenant_id' => Setting::get('azure_mail_tenant_id', ''),
'client_id' => Setting::get('azure_mail_client_id', ''),
'client_secret' => Setting::get('azure_mail_client_secret', ''),
];
$tokenManager = new TokenManager($config);
$tokenManager->getToken();
return response()->json(['success' => true]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
```
- [ ] **Step 7: Add `sendTestEmail()` after `testAzureMailConnection()`**
```php
public function sendTestEmail(Request $request): JsonResponse
{
$request->validate([
'to' => ['required', 'email', 'max:255'],
'subject' => ['required', 'string', 'max:255'],
]);
try {
$to = $request->to;
$subject = $request->subject;
Mail::mailer('azure')->raw(
'This is a test email from SteelERP.',
function ($message) use ($to, $subject) {
$message->to($to)->subject($subject);
}
);
return response()->json(['success' => true]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
```
- [ ] **Step 8: Verify no PHP syntax errors**
Run:
```bash
php artisan route:list --name=settings.integrations
```
Expected: command succeeds with no parse errors (Laravel would fail to boot otherwise).
- [ ] **Step 9: Commit**
```bash
git add app/Http/Controllers/SettingsController.php
git commit -m "feat: update SettingsController for Azure Mail tab — AJAX whatsapp save, 3 new azure methods"
```
---
### Task 3: Rewrite integrations.blade.php
**Files:**
- Modify: `resources/views/settings/integrations.blade.php` (full rewrite)
This task replaces the entire file. Read the current file first to understand what the WhatsApp SVG icon markup and toggle look like, then replace with the complete tabbed layout below.
Key design decisions:
- Outer wrapper uses `x-data="{ tab: 'whatsapp' }"` for Alpine.js tab state
- Pill tabs use `:style` bindings for active/inactive appearance (inline styles per Tailwind JIT rule)
- Email panel uses `x-show="tab==='email'" style="display:none;"` — the `style="display:none"` prevents flash before Alpine initialises
- WhatsApp panel uses `x-show="tab==='whatsapp'"` (no initial display:none because it's the default visible tab)
- Both toggles use a hidden `<input type="hidden">` with value `'1'`/`'0'` that JavaScript reads; the visual track/thumb are manipulated directly
- Password show/hide fields use Alpine `x-data` scoped to the field wrapper
- All saves use the `api()` helper (returns a Promise that rejects on non-2xx)
- Test Connection calls use raw `fetch()` (not `api()`) because the endpoint always returns HTTP 200 with a `success` flag — not a 422 on failure
- [ ] **Step 1: Replace the entire content of `resources/views/settings/integrations.blade.php` with the following**
```blade
@extends('layouts.app')
@section('title', 'Settings — Integrations')
@section('content')
<div class="mb-6">
<h1 class="page-title">Settings — Integrations</h1>
<p class="page-subtitle">Configure third-party service integrations.</p>
</div>
<div style="max-width:680px;" x-data="{ tab: 'whatsapp' }">
{{-- Pill tabs --}}
<div style="display:flex;gap:8px;margin-bottom:20px;">
<button type="button" @click="tab='whatsapp'"
:style="tab==='whatsapp' ? 'background:#1e293b;color:#fff;border:1px solid transparent;' : 'background:#fff;color:#374151;border:1px solid #d1d5db;'"
style="padding:7px 18px;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;">
💬 WhatsApp
</button>
<button type="button" @click="tab='email'"
:style="tab==='email' ? 'background:#1e293b;color:#fff;border:1px solid transparent;' : 'background:#fff;color:#374151;border:1px solid #d1d5db;'"
style="padding:7px 18px;border-radius:999px;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;">
✉️ Email
</button>
</div>
{{-- ===== WhatsApp tab ===== --}}
<div x-show="tab==='whatsapp'">
<div class="card">
<div style="padding:20px 24px 16px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:12px;">
<svg style="width:24px;height:24px;color:#22c55e;flex-shrink:0;" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
<h3 style="font-size:16px;font-weight:600;color:#111827;margin:0;">WhatsApp (UltraMSG)</h3>
</div>
<div style="padding:24px;">
{{-- Enable toggle --}}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div>
<p style="font-size:14px;font-weight:500;color:#374151;margin:0 0 2px;">Enable WhatsApp Notifications</p>
<p style="font-size:12px;color:#6b7280;margin:0;">When disabled, no messages will be sent.</p>
</div>
<div style="position:relative;display:inline-flex;align-items:center;cursor:pointer;">
<input type="hidden" id="wa-enabled-hidden" value="{{ $whatsappSettings['enabled'] ? '1' : '0' }}">
<div id="wa-toggle-track" onclick="toggleWaSwitch()" style="
width:44px;height:24px;border-radius:12px;cursor:pointer;
background:{{ $whatsappSettings['enabled'] ? '#22c55e' : '#d1d5db' }};
position:relative;transition:background .2s;">
<div id="wa-toggle-thumb" style="
position:absolute;top:2px;
left:{{ $whatsappSettings['enabled'] ? '22px' : '2px' }};
width:20px;height:20px;border-radius:50%;background:#fff;
box-shadow:0 1px 3px rgba(0,0,0,.2);transition:left .2s;"></div>
</div>
</div>
</div>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0 0 20px;">
{{-- Instance ID --}}
<div style="margin-bottom:16px;">
<label class="form-label">Instance ID</label>
<input type="text" id="wa-instance-id"
value="{{ $whatsappSettings['instance_id'] }}"
placeholder="e.g. instance177593"
class="form-input">
</div>
{{-- API Token --}}
<div style="margin-bottom:16px;" x-data="{ showWaToken: false }">
<label class="form-label">API Token</label>
<div style="position:relative;">
<input :type="showWaToken ? 'text' : 'password'" id="wa-token"
value="{{ $whatsappSettings['token'] }}"
placeholder="Your UltraMSG token"
class="form-input" style="padding-right:40px;">
<button type="button" @click="showWaToken = !showWaToken"
style="position:absolute;inset-y:0;right:0;padding:0 10px;background:none;border:none;cursor:pointer;color:#9ca3af;"
onmouseover="this.style.color='#4b5563'" onmouseout="this.style.color='#9ca3af'">
<svg x-show="!showWaToken" style="width:16px;height:16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<svg x-show="showWaToken" style="width:16px;height:16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 4.411m0 0L21 21"/>
</svg>
</button>
</div>
</div>
{{-- Webhook Secret --}}
<div style="margin-bottom:16px;" x-data="{ showWaSecret: false }">
<label class="form-label">
Webhook Secret <span style="color:#9ca3af;font-weight:400;">(optional)</span>
</label>
<div style="position:relative;">
<input :type="showWaSecret ? 'text' : 'password'" id="wa-webhook-secret"
value="{{ $whatsappSettings['webhook_secret'] }}"
placeholder="Leave empty to skip HMAC verification"
class="form-input" style="padding-right:40px;">
<button type="button" @click="showWaSecret = !showWaSecret"
style="position:absolute;inset-y:0;right:0;padding:0 10px;background:none;border:none;cursor:pointer;color:#9ca3af;"
onmouseover="this.style.color='#4b5563'" onmouseout="this.style.color='#9ca3af'">
<svg x-show="!showWaSecret" style="width:16px;height:16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<svg x-show="showWaSecret" style="width:16px;height:16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 4.411m0 0L21 21"/>
</svg>
</button>
</div>
</div>
{{-- Webhook Path --}}
<div style="margin-bottom:24px;">
<label class="form-label">Webhook Path</label>
<div style="display:flex;align-items:stretch;">
<span style="display:inline-flex;align-items:center;padding:0 12px;font-size:13px;color:#6b7280;background:#f9fafb;border:1px solid #d1d5db;border-right:none;border-radius:6px 0 0 6px;white-space:nowrap;">{{ url('/') }}/</span>
<input type="text" id="wa-webhook-path"
value="{{ $whatsappSettings['webhook_path'] }}"
class="form-input" style="border-radius:0 6px 6px 0;flex:1;">
</div>
<p style="font-size:12px;color:#6b7280;margin-top:4px;">
Paste this full URL in your UltraMSG dashboard: <strong>{{ url('/') }}/{{ $whatsappSettings['webhook_path'] }}</strong>
</p>
</div>
{{-- Actions --}}
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:16px;border-top:1px solid #f3f4f6;">
<div style="display:flex;align-items:center;gap:12px;">
<button type="button" id="btn-wa-test" onclick="testWaConnection()"
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#2563eb;background:none;border:none;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0;"
onmouseover="this.style.color='#1d4ed8'" onmouseout="this.style.color='#2563eb'">
<svg style="width:15px;height:15px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Test Connection
</button>
<span id="wa-conn-status" style="font-size:13px;display:none;"></span>
</div>
<button type="button" id="btn-wa-save" onclick="saveWhatsapp()" class="btn-primary">Save Settings</button>
</div>
</div>
</div>
{{-- Send Test Message accordion --}}
<div class="card" style="margin-top:16px;">
<button type="button" onclick="toggleWaMsg()"
style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:16px 24px;background:none;border:none;cursor:pointer;text-align:left;">
<div style="display:flex;align-items:center;gap:10px;">
<svg style="width:18px;height:18px;color:#22c55e;flex-shrink:0;" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
<span style="font-size:14px;font-weight:600;color:#111827;">Send Test Message</span>
<span style="font-size:12px;color:#6b7280;">— verify the connection works end-to-end</span>
</div>
<svg id="wa-msg-chevron" style="width:16px;height:16px;color:#9ca3af;transition:transform .2s;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div id="wa-msg-body" style="display:none;padding:0 24px 24px;">
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0 0 20px;">
<div style="margin-bottom:14px;">
<label style="display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Phone Number</label>
<input type="text" id="wa-test-to" placeholder="+97333165444" class="form-input" style="width:100%;">
</div>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Message</label>
<textarea id="wa-test-body" rows="3" class="form-input" style="width:100%;resize:vertical;">Test message from SteelERP — WhatsApp integration is working!</textarea>
</div>
<div style="display:flex;align-items:center;gap:14px;">
<button type="button" id="btn-wa-send" onclick="sendWaTestMessage()"
style="display:inline-flex;align-items:center;gap:7px;padding:9px 18px;font-size:13px;font-weight:600;color:#fff;background:#22c55e;border:none;border-radius:8px;cursor:pointer;"
onmouseover="this.style.background='#16a34a'" onmouseout="this.style.background='#22c55e'">
<svg style="width:15px;height:15px;" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
</svg>
Send Message
</button>
<span id="wa-send-status" style="font-size:13px;display:none;"></span>
</div>
</div>
</div>
</div>{{-- end WhatsApp tab --}}
{{-- ===== Email tab ===== --}}
<div x-show="tab==='email'" style="display:none;">
<div class="card">
<div style="padding:16px 24px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:12px;">
<div style="width:32px;height:32px;background:#eff6ff;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:16px;">✉️</div>
<div>
<div style="font-size:14px;font-weight:600;color:#111827;">Microsoft 365 (Azure Mail)</div>
<div style="font-size:12px;color:#6b7280;">Send emails via Microsoft Graph API using Azure AD</div>
</div>
</div>
<div style="padding:24px;">
{{-- Enable toggle --}}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div>
<p style="font-size:14px;font-weight:500;color:#374151;margin:0 0 2px;">Enable Email Notifications</p>
<p style="font-size:12px;color:#6b7280;margin:0;">When disabled, no emails will be sent.</p>
</div>
<div style="position:relative;display:inline-flex;align-items:center;cursor:pointer;">
<input type="hidden" id="em-enabled-hidden" value="{{ $azureSettings['enabled'] ? '1' : '0' }}">
<div id="em-toggle-track" onclick="toggleEmSwitch()" style="
width:44px;height:24px;border-radius:12px;cursor:pointer;
background:{{ $azureSettings['enabled'] ? '#22c55e' : '#d1d5db' }};
position:relative;transition:background .2s;">
<div id="em-toggle-thumb" style="
position:absolute;top:2px;
left:{{ $azureSettings['enabled'] ? '22px' : '2px' }};
width:20px;height:20px;border-radius:50%;background:#fff;
box-shadow:0 1px 3px rgba(0,0,0,.2);transition:left .2s;"></div>
</div>
</div>
</div>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0 0 20px;">
{{-- Tenant ID --}}
<div style="margin-bottom:16px;">
<label class="form-label">Tenant ID</label>
<input type="text" id="em-tenant-id"
value="{{ $azureSettings['tenant_id'] }}"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
class="form-input">
</div>
{{-- Client ID --}}
<div style="margin-bottom:16px;">
<label class="form-label">Client ID</label>
<input type="text" id="em-client-id"
value="{{ $azureSettings['client_id'] }}"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
class="form-input">
</div>
{{-- Client Secret --}}
<div style="margin-bottom:16px;" x-data="{ showEmSecret: false }">
<label class="form-label">Client Secret</label>
<div style="position:relative;">
<input :type="showEmSecret ? 'text' : 'password'" id="em-client-secret"
value="{{ $azureSettings['client_secret'] }}"
placeholder="Your Azure AD client secret"
class="form-input" style="padding-right:40px;">
<button type="button" @click="showEmSecret = !showEmSecret"
style="position:absolute;inset-y:0;right:0;padding:0 10px;background:none;border:none;cursor:pointer;color:#9ca3af;"
onmouseover="this.style.color='#4b5563'" onmouseout="this.style.color='#9ca3af'">
<svg x-show="!showEmSecret" style="width:16px;height:16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<svg x-show="showEmSecret" style="width:16px;height:16px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 4.411m0 0L21 21"/>
</svg>
</button>
</div>
</div>
{{-- From Address --}}
<div style="margin-bottom:24px;">
<label class="form-label">From Address</label>
<input type="text" id="em-from-address"
value="{{ $azureSettings['from_address'] }}"
placeholder="noreply@yourdomain.com"
class="form-input">
<p style="font-size:12px;color:#6b7280;margin-top:4px;">Must be a mailbox in your Microsoft 365 tenant.</p>
</div>
{{-- Actions --}}
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:16px;border-top:1px solid #f3f4f6;">
<div style="display:flex;align-items:center;gap:12px;">
<button type="button" id="btn-em-test" onclick="testAzureConnection()"
style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#2563eb;background:none;border:none;cursor:pointer;text-decoration:underline;text-underline-offset:2px;padding:0;"
onmouseover="this.style.color='#1d4ed8'" onmouseout="this.style.color='#2563eb'">
<svg style="width:15px;height:15px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Test Connection
</button>
<span id="em-conn-status" style="font-size:13px;display:none;"></span>
</div>
<button type="button" id="btn-em-save" onclick="saveAzureMail()" class="btn-primary">Save Settings</button>
</div>
</div>
</div>
{{-- Send Test Email accordion --}}
<div class="card" style="margin-top:16px;">
<button type="button" onclick="toggleEmMsg()"
style="width:100%;display:flex;align-items:center;justify-content:space-between;padding:16px 24px;background:none;border:none;cursor:pointer;text-align:left;">
<div style="display:flex;align-items:center;gap:10px;">
<span style="font-size:16px;">📧</span>
<span style="font-size:14px;font-weight:600;color:#111827;">Send Test Email</span>
<span style="font-size:12px;color:#6b7280;">— verify the connection works end-to-end</span>
</div>
<svg id="em-msg-chevron" style="width:16px;height:16px;color:#9ca3af;transition:transform .2s;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div id="em-msg-body" style="display:none;padding:0 24px 24px;">
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0 0 20px;">
<div style="margin-bottom:12px;">
<label style="display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">To</label>
<input type="text" id="em-test-to" placeholder="recipient@example.com" class="form-input" style="width:100%;">
</div>
<div style="margin-bottom:16px;">
<label style="display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:5px;">Subject</label>
<input type="text" id="em-test-subject" value="Test Email from SteelERP" class="form-input" style="width:100%;">
</div>
<div style="display:flex;align-items:center;gap:14px;">
<button type="button" id="btn-em-send" onclick="sendTestEmail()"
style="display:inline-flex;align-items:center;gap:7px;padding:9px 18px;font-size:13px;font-weight:600;color:#fff;background:#2563eb;border:none;border-radius:8px;cursor:pointer;"
onmouseover="this.style.background='#1d4ed8'" onmouseout="this.style.background='#2563eb'">
✉️ Send Email
</button>
<span id="em-send-status" style="font-size:13px;display:none;"></span>
</div>
</div>
</div>
</div>{{-- end Email tab --}}
</div>
<script>
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
function api(url, data) {
return fetch(url, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(function(r) {
return r.json().then(function(body) {
if (!r.ok) return Promise.reject(body);
return body;
});
});
}
function toggleWaSwitch() {
var hidden = document.getElementById('wa-enabled-hidden');
var track = document.getElementById('wa-toggle-track');
var thumb = document.getElementById('wa-toggle-thumb');
var on = hidden.value === '1';
hidden.value = on ? '0' : '1';
track.style.background = on ? '#d1d5db' : '#22c55e';
thumb.style.left = on ? '2px' : '22px';
}
function toggleEmSwitch() {
var hidden = document.getElementById('em-enabled-hidden');
var track = document.getElementById('em-toggle-track');
var thumb = document.getElementById('em-toggle-thumb');
var on = hidden.value === '1';
hidden.value = on ? '0' : '1';
track.style.background = on ? '#d1d5db' : '#22c55e';
thumb.style.left = on ? '2px' : '22px';
}
var _waMsgOpen = false;
function toggleWaMsg() {
_waMsgOpen = !_waMsgOpen;
document.getElementById('wa-msg-body').style.display = _waMsgOpen ? 'block' : 'none';
document.getElementById('wa-msg-chevron').style.transform = _waMsgOpen ? 'rotate(180deg)' : '';
}
var _emMsgOpen = false;
function toggleEmMsg() {
_emMsgOpen = !_emMsgOpen;
document.getElementById('em-msg-body').style.display = _emMsgOpen ? 'block' : 'none';
document.getElementById('em-msg-chevron').style.transform = _emMsgOpen ? 'rotate(180deg)' : '';
}
function saveWhatsapp() {
var btn = document.getElementById('btn-wa-save');
btn.disabled = true; btn.style.opacity = '.6';
api('{{ route('settings.integrations.whatsapp') }}', {
enabled: document.getElementById('wa-enabled-hidden').value,
instance_id: document.getElementById('wa-instance-id').value.trim(),
token: document.getElementById('wa-token').value.trim(),
webhook_secret: document.getElementById('wa-webhook-secret').value.trim(),
webhook_path: document.getElementById('wa-webhook-path').value.trim(),
}).then(function() {
showToast('WhatsApp settings saved.', 'success');
}).catch(function(err) {
var msg = err.errors ? Object.values(err.errors)[0][0] : (err.message || 'Error saving settings.');
showToast(msg, 'error');
}).finally(function() { btn.disabled = false; btn.style.opacity = '1'; });
}
function testWaConnection() {
var statusEl = document.getElementById('wa-conn-status');
statusEl.textContent = 'Testing…'; statusEl.style.display = ''; statusEl.style.color = '#6b7280';
fetch('{{ route('settings.integrations.test-whatsapp') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' }
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
statusEl.textContent = 'Connected ✓'; statusEl.style.color = '#16a34a';
} else {
statusEl.textContent = 'Failed: ' + (data.message || 'Unknown error'); statusEl.style.color = '#dc2626';
}
}).catch(function() { statusEl.textContent = 'Request failed.'; statusEl.style.color = '#dc2626'; });
}
function sendWaTestMessage() {
var to = document.getElementById('wa-test-to').value.trim();
var body = document.getElementById('wa-test-body').value.trim();
if (!to) { showToast('Enter a phone number.', 'warn'); return; }
if (!body) { showToast('Enter a message.', 'warn'); return; }
var btn = document.getElementById('btn-wa-send');
var statusEl = document.getElementById('wa-send-status');
btn.disabled = true; btn.style.opacity = '.6';
statusEl.textContent = 'Sending…'; statusEl.style.display = ''; statusEl.style.color = '#6b7280';
api('{{ route('settings.integrations.send-test-message') }}', { to: to, body: body })
.then(function() {
statusEl.textContent = 'Sent ✓'; statusEl.style.color = '#16a34a';
showToast('Test message sent!', 'success');
}).catch(function(err) {
statusEl.textContent = 'Failed.'; statusEl.style.color = '#dc2626';
showToast(err.message || 'Failed to send.', 'error');
}).finally(function() { btn.disabled = false; btn.style.opacity = '1'; });
}
function saveAzureMail() {
var btn = document.getElementById('btn-em-save');
btn.disabled = true; btn.style.opacity = '.6';
api('{{ route('settings.integrations.azure-mail') }}', {
enabled: document.getElementById('em-enabled-hidden').value,
tenant_id: document.getElementById('em-tenant-id').value.trim(),
client_id: document.getElementById('em-client-id').value.trim(),
client_secret: document.getElementById('em-client-secret').value.trim(),
from_address: document.getElementById('em-from-address').value.trim(),
}).then(function() {
showToast('Email settings saved.', 'success');
}).catch(function(err) {
var msg = err.errors ? Object.values(err.errors)[0][0] : (err.message || 'Error saving settings.');
showToast(msg, 'error');
}).finally(function() { btn.disabled = false; btn.style.opacity = '1'; });
}
function testAzureConnection() {
var statusEl = document.getElementById('em-conn-status');
statusEl.textContent = 'Testing…'; statusEl.style.display = ''; statusEl.style.color = '#6b7280';
fetch('{{ route('settings.integrations.test-azure-mail') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success) {
statusEl.textContent = 'Connected ✓'; statusEl.style.color = '#16a34a';
showToast('Azure AD connection successful.', 'success');
} else {
statusEl.textContent = 'Failed: ' + (data.message || 'Unknown error'); statusEl.style.color = '#dc2626';
showToast(data.message || 'Connection failed.', 'error');
}
}).catch(function() { statusEl.textContent = 'Request failed.'; statusEl.style.color = '#dc2626'; showToast('Request failed.', 'error'); });
}
function sendTestEmail() {
var to = document.getElementById('em-test-to').value.trim();
var subject = document.getElementById('em-test-subject').value.trim();
if (!to) { showToast('Enter a recipient email.', 'warn'); return; }
if (!subject) { showToast('Enter a subject.', 'warn'); return; }
var btn = document.getElementById('btn-em-send');
var statusEl = document.getElementById('em-send-status');
btn.disabled = true; btn.style.opacity = '.6';
statusEl.textContent = 'Sending…'; statusEl.style.display = ''; statusEl.style.color = '#6b7280';
api('{{ route('settings.integrations.send-test-email') }}', { to: to, subject: subject })
.then(function() {
statusEl.textContent = 'Sent ✓'; statusEl.style.color = '#16a34a';
showToast('Test email sent!', 'success');
}).catch(function(err) {
statusEl.textContent = 'Failed.'; statusEl.style.color = '#dc2626';
showToast(err.message || 'Failed to send email.', 'error');
}).finally(function() { btn.disabled = false; btn.style.opacity = '1'; });
}
</script>
@endsection
```
- [ ] **Step 2: Verify page renders without errors**
With `php artisan serve` running, visit `http://localhost:8000/settings/integrations` as an Admin user.
Expected:
- Page loads with pill tabs "💬 WhatsApp" (active, dark) and "✉️ Email" (inactive, white/bordered)
- WhatsApp card visible with all existing fields
- Clicking "✉️ Email" tab switches to the Email panel
- Email panel shows Tenant ID, Client ID, Client Secret, From Address fields
- [ ] **Step 3: Test WhatsApp AJAX save**
Fill in the WhatsApp fields and click "Save Settings".
Expected: green toast "WhatsApp settings saved." — no page reload.
- [ ] **Step 4: Test Email tab save (validation)**
Switch to Email tab, click "Save Settings" without filling in Tenant ID.
Expected: red toast showing the validation error message.
- [ ] **Step 5: Commit**
```bash
git add resources/views/settings/integrations.blade.php
git commit -m "feat: rewrite integrations view with WhatsApp/Email pill tabs and AJAX saves"
```
---
## Verification Checklist
After all three tasks are complete:
- [ ] `php artisan route:list --name=settings.integrations` shows 7 routes
- [ ] Integrations page loads at `/settings/integrations` with pill tabs
- [ ] Tab switching works (WhatsApp ↔ Email) without page reload
- [ ] WhatsApp save uses AJAX (no page reload, toast appears)
- [ ] Email save uses AJAX (no page reload, toast appears)
- [ ] Email validation error (empty Tenant ID) shows toast with message
- [ ] Test Connection (WhatsApp) still works
- [ ] "Send Test Message" accordion still opens/closes
- [ ] Email tab fields pre-populate from DB if previously saved

View File

@ -1,149 +0,0 @@
# Integrations Page — Tabbed UI Design Spec
**Date:** 2026-05-26
**Status:** Approved
---
## Overview
Redesign the Settings → Integrations page to use pill-style tabs — **WhatsApp** and **Email** — so both integrations are accessible from a single page. The Email tab exposes the Microsoft 365 Azure AD credentials (stored via the `Setting` model) and provides test connection + send test email actions via AJAX.
---
## Goals
- Add pill tabs (WhatsApp | Email) at the top of the integrations page
- Keep the WhatsApp tab visually identical to the current design
- Add a new Email tab for Microsoft 365 / Azure AD Mail configuration
- Convert both tabs' save operations to AJAX (rule #11 — no page reloads on settings pages)
- Use the existing design system: `.card`, `.form-input`, `.form-label`, `.btn-primary`, `showToast()`
- All inline styles used for one-off values (Tailwind JIT rule)
---
## UI Design
### Tab bar
Pill-style tabs rendered above the card. Alpine.js `x-data="{ tab: 'whatsapp' }"` switches visibility.
- **Active tab:** `background:#1e293b; color:#fff; border-radius:999px; padding:7px 18px;`
- **Inactive tab:** `background:#fff; color:#374151; border:1px solid #d1d5db; border-radius:999px; padding:7px 18px;`
### WhatsApp tab (unchanged fields, AJAX save)
Same fields as today: Enable toggle, Instance ID, API Token (masked), Webhook Secret (optional, masked), Webhook Path. Save button → AJAX POST. Test Connection and Send Test Message accordion remain unchanged.
### Email tab (new)
Card header: envelope icon + "Microsoft 365 (Azure Mail)" title + subtitle.
Fields:
| Field | Input type | Settings key | Validation |
|-------|-----------|-------------|------------|
| Enable Email Notifications | Toggle | `azure_mail_enabled` | boolean |
| Tenant ID | text | `azure_mail_tenant_id` | required, max:100 |
| Client ID | text | `azure_mail_client_id` | required, max:100 |
| Client Secret | password (show/hide) | `azure_mail_client_secret` | required, max:500 |
| From Address | text | `azure_mail_from_address` | required, email, max:255 |
Actions:
- **Test Connection** link → AJAX POST → calls `TokenManager::getToken()` to verify Azure AD responds
- **Save Settings** button → AJAX POST → stores all 5 keys via `Setting::set()`
Send Test Email accordion (same expand/collapse pattern as WhatsApp):
- **To** field (email address)
- **Subject** field (pre-filled: "Test Email from SteelERP")
- **Send Email** button → AJAX POST → `Mail::mailer('azure')->raw(...)`
---
## Architecture
### Files changed
| File | Change |
|------|--------|
| `resources/views/settings/integrations.blade.php` | Full rewrite — pill tabs, both tab panels, AJAX JS |
| `app/Http/Controllers/SettingsController.php` | Add 3 new methods; update `integrations()` and `updateWhatsapp()` |
| `routes/web.php` | Add 3 new POST routes for azure-mail |
### Controller changes
**`integrations(): View`** — pass both `$whatsappSettings` and `$azureSettings` to view.
**`updateWhatsapp(Request $request): JsonResponse`** — same validation, save logic unchanged, but return `response()->json(['success' => true])` instead of redirect.
**`updateAzureMail(Request $request): JsonResponse`**
```
Validates: tenant_id (required, max:100), client_id (required, max:100),
client_secret (required, max:500), from_address (required, email, max:255)
Saves: azure_mail_enabled, azure_mail_tenant_id, azure_mail_client_id,
azure_mail_client_secret, azure_mail_from_address
Returns: JSON {success: true}
```
**`testAzureMailConnection(): JsonResponse`**
```
Reads azure_mail_* settings from DB
Instantiates TokenManager with those settings
Calls getToken() — success means Azure AD is reachable and credentials are valid
Returns: JSON {success: true} or {success: false, message: $e->getMessage()}
```
**`sendTestEmail(Request $request): JsonResponse`**
```
Validates: to (required, email, max:255), subject (required, max:255)
Uses Mail::mailer('azure')->raw('This is a test email from SteelERP.', fn($m) => $m->to($to)->subject($subject))
Returns: JSON {success: true} or {success: false, message: ...}
```
### New routes
```php
Route::post('settings/integrations/azure-mail', [SettingsController::class, 'updateAzureMail'])->name('settings.integrations.azure-mail');
Route::post('settings/integrations/test-azure-mail', [SettingsController::class, 'testAzureMailConnection'])->name('settings.integrations.test-azure-mail');
Route::post('settings/integrations/send-test-email', [SettingsController::class, 'sendTestEmail'])->name('settings.integrations.send-test-email');
```
---
## AJAX Pattern
Both tabs use the global `api()` helper pattern from CLAUDE.md rule #11:
```javascript
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
function api(url, data) {
return fetch(url, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' }
body: JSON.stringify(data)
}).then(function(r) {
return r.json().then(function(body) {
if (!r.ok) return Promise.reject(body);
return body;
});
});
}
```
On success → `showToast('Settings saved.', 'success')`
On error → `showToast(err.message || 'Error', 'error')`
---
## Error Handling
- Laravel validation failure (422) → `Accept: application/json` header ensures JSON error response → `showToast()` with first validation error message
- Azure AD token fetch failure (`AuthenticationException`) → caught in `testAzureMailConnection`, message returned as JSON
- Mail send failure → caught, message returned as JSON
---
## Non-Goals
- No URL changes — integrations page stays at `/settings/integrations`
- No tab state persisted in URL
- No changes to WhatsApp fields or business logic

File diff suppressed because it is too large Load Diff

View File

@ -136,19 +136,11 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Projects settings
Route::get('settings/projects', [ProjectSettingController::class, 'index'])->name('settings.projects.index');
Route::post('settings/projects', [ProjectSettingController::class, 'store'])->name('settings.projects.store');
Route::post('settings/projects/import', [ProjectSettingController::class, 'import'])->name('settings.projects.import');
Route::get('settings/projects/template', [ProjectSettingController::class, 'downloadTemplate'])->name('settings.projects.template');
Route::post('settings/projects/companies', [ProjectSettingController::class, 'storeCompany'])->name('settings.projects.companies.store');
Route::patch('settings/projects/companies/{company}', [ProjectSettingController::class, 'updateCompany'])->name('settings.projects.companies.update');
Route::delete('settings/projects/companies/{company}', [ProjectSettingController::class, 'destroyCompany'])->name('settings.projects.companies.destroy');
Route::patch('settings/projects/{project}', [ProjectSettingController::class, 'update'])->name('settings.projects.update');
Route::delete('settings/projects/{project}', [ProjectSettingController::class, 'destroy'])->name('settings.projects.destroy');
Route::post('settings/projects/{project}/locations', [ProjectSettingController::class, 'storeLocation'])->name('settings.projects.locations.store');
Route::patch('settings/projects/{project}/locations/{location}', [ProjectSettingController::class, 'updateLocation'])->name('settings.projects.locations.update');
Route::delete('settings/projects/{project}/locations/{location}', [ProjectSettingController::class, 'destroyLocation'])->name('settings.projects.locations.destroy');
Route::post('settings/projects/companies/{company}/departments', [ProjectSettingController::class, 'storeDepartment'])->name('settings.projects.companies.departments.store');
Route::patch('settings/projects/companies/{company}/departments/{department}', [ProjectSettingController::class, 'updateDepartment'])->name('settings.projects.companies.departments.update');
Route::delete('settings/projects/companies/{company}/departments/{department}', [ProjectSettingController::class, 'destroyDepartment'])->name('settings.projects.companies.departments.destroy');
});
});