Files
2026-02-10 22:11:06 -05:00

256 lines
11 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<div class="container mt-4">
<h2 class="text-center mb-3">Broadcast Email to Parents</h2>
<?php if ($msg = session()->getFlashdata('message')): ?>
<div class="alert alert-success"><?= esc($msg) ?></div>
<?php endif; ?>
<?php if ($err = session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc($err) ?></div>
<?php endif; ?>
<form method="post" action="<?= site_url('admin/broadcast-email/send') ?>" id="broadcastForm">
<?= csrf_field() ?>
<div class="card mb-3">
<div class="card-header">Email Details</div>
<div class="card-body row g-3">
<div class="col-md-6">
<label class="form-label">From (configured sender)</label>
<select class="form-select" name="from_key" required>
<?php foreach (($fromOptions ?? []) as $opt): ?>
<option value="<?= esc($opt['key']) ?>"><?= esc($opt['label']) ?></option>
<?php endforeach; ?>
</select>
<small class="text-muted">Senders come from MAIL_SENDERS (.env).</small>
</div>
<div class="col-md-6">
<label class="form-label">Send Mode</label>
<select name="mode" class="form-select" id="modeSelect">
<option value="personalized">Personalized (replaces {{name}})</option>
<option value="standard">Standard (no personalization)</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label">Subject</label>
<input type="text" name="subject" class="form-control" placeholder="Subject..." required>
</div>
<div class="col-md-4">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" value="1" id="wrap_layout" name="wrap_layout" checked>
<label class="form-check-label" for="wrap_layout">
Wrap with email layout
</label>
</div>
</div>
<div class="col-12">
<label class="form-label">Message (HTML). Use {{name}} in Personalized mode.</label>
<textarea id="body_html" name="body_html" class="form-control" rows="10" placeholder="Dear {{name}}, ..."></textarea>
</div>
<!-- Optional fields your layout can use -->
<div class="col-md-6">
<label class="form-label">Preheader (optional)</label>
<input type="text" name="preheader" class="form-control" placeholder="Short preview text shown by mail clients">
</div>
<div class="col-md-3">
<label class="form-label">CTA Text (optional)</label>
<input type="text" name="cta_text" class="form-control" placeholder="e.g., View Details">
</div>
<div class="col-md-3">
<label class="form-label">CTA URL (optional)</label>
<input type="url" name="cta_url" class="form-control" placeholder="https://...">
</div>
</div>
<div class="card-footer d-flex gap-2 align-items-center flex-wrap">
<div class="input-group" style="max-width: 420px;">
<span class="input-group-text">Test email</span>
<input type="email" name="test_email" class="form-control" placeholder="you@example.com">
<button class="btn btn-outline-primary" name="send_test_only" value="1" type="submit">Send Test Only</button>
</div>
<span class="ms-auto fw-semibold">Selected: <span id="selectedCount">0</span></span>
<button type="submit" class="btn btn-primary">Send Broadcast</button>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between">
<span>Parents</span>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="selectAll">Select All</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearAll">Clear</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="parentsTable" class="table table-striped table-hover align-middle w-100">
<thead class="table-dark">
<tr>
<th style="width:40px;">#</th>
<th>Name</th>
<th>Email</th>
<th>School ID</th>
</tr>
</thead>
<tbody>
<?php foreach (($parents ?? []) as $p): ?>
<tr>
<td><input type="checkbox" class="row-check" value="<?= (int)$p['id'] ?>"></td>
<td><?= esc(($p['firstname'] ?? '') . ' ' . ($p['lastname'] ?? '')) ?></td>
<td><?= esc($p['email'] ?? '') ?></td>
<td><?= esc($p['school_id'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<input type="hidden" id="selectedIdsCarrier" name="parent_ids[]" value="">
</div>
</div>
</form>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
<!-- Summernote Lite (no Bootstrap dep) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/summernote@0.8.20/dist/summernote-lite.min.css">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.20/dist/summernote-lite.min.js"></script>
<script>
(function() {
const editor = $('#body_html');
const csrfName = '<?= csrf_token() ?>'; // e.g. "csrf_test_name"
let csrfHash = '<?= csrf_hash() ?>';
const CSRF_COOKIE_NAME = <?= json_encode(config('Security')->csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>;
function getCookie(n){
return document.cookie.split(';').map(v=>v.trim()).find(v=>v.startsWith(n+'='))?.split('=')[1] || '';
}
function uploadImageFile(file) {
if (!file || !file.type || !file.type.startsWith('image/')) return;
if (file.size > 5 * 1024 * 1024) { alert('Image too large (max 5MB).'); return; }
const fd = new FormData();
fd.append('image', file);
fd.append(csrfName, csrfHash); // 👈 CSRF in body
fetch('<?= site_url('admin/broadcast-email/upload-image') ?>', {
method: 'POST',
body: fd,
credentials: 'same-origin', // 👈 send cookies
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfHash, // 👈 CSRF in header (CodeIgniter accepts this too)
}
})
.then(async (r) => {
// Grab fresh token if CI rotated it
const newHash = r.headers.get('X-CSRF-HASH');
if (newHash) csrfHash = newHash; else {
// Fallback to cookie (CI rotates token after POST)
try {
const c = decodeURIComponent(getCookie(CSRF_COOKIE_NAME) || '');
if (c) csrfHash = c;
} catch(_) {}
}
// If server returned HTML error (CSRF page), show it
const text = await r.text();
try { return JSON.parse(text); } catch { throw new Error(text || 'Upload failed'); }
})
.then(json => {
if (!json.success) throw new Error(json.error || 'Upload failed');
if (json.csrf_hash) csrfHash = json.csrf_hash; // another place to refresh
editor.summernote('insertImage', json.url, file.name || 'image');
})
.catch(err => {
console.error(err);
alert('Image upload failed: ' + err.message);
});
}
editor.summernote({
height: 260,
toolbar: [
['style', ['bold','italic','underline','clear']],
['para', ['ul','ol','paragraph']],
['insert',['link','table','hr']],
['font', ['superscript','subscript']],
['view', ['undo','redo','codeview']]
],
callbacks: {
onImageUpload: function(files) { for (let f of files) uploadImageFile(f); },
onPaste: function(e) {
const cd = (e.originalEvent || e).clipboardData;
if (!cd || !cd.items) return;
for (let it of cd.items) {
if (it.kind === 'file' && it.type.startsWith('image/')) {
e.preventDefault();
uploadImageFile(it.getAsFile());
}
}
}
}
});
})();
</script>
<script>
(function() {
const selected = new Set();
const table = $('#parentsTable').DataTable({
pageLength: 100,
lengthMenu: [10, 25, 50, 100, 250],
order: [[1, 'asc']]
});
function refreshSelected() {
$('#selectedCount').text(selected.size);
document.getElementById('selectedIdsCarrier').value = Array.from(selected).join(',');
}
$('#parentsTable').on('draw.dt', function() {
document.querySelectorAll('#parentsTable .row-check').forEach(chk => {
const id = parseInt(chk.value, 10);
chk.checked = selected.has(id);
});
});
$('#parentsTable').on('change', '.row-check', function() {
const id = parseInt(this.value, 10);
if (this.checked) selected.add(id); else selected.delete(id);
refreshSelected();
});
document.getElementById('selectAll').addEventListener('click', function() {
table.rows({page:'current'}).nodes().to$().find('.row-check').each(function() {
this.checked = true;
selected.add(parseInt(this.value, 10));
});
refreshSelected();
});
document.getElementById('clearAll').addEventListener('click', function() {
table.rows({page:'current'}).nodes().to$().find('.row-check').each(function() {
this.checked = false;
selected.delete(parseInt(this.value, 10));
});
refreshSelected();
});
})();
</script>
<?= $this->endSection() ?>