256 lines
11 KiB
PHP
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() ?>
|