consultant-clients.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <?php
  2. /**
  3. * dashboard/admin/consultant-clients.php
  4. *
  5. * Admin tool: assign client_records to consultant accounts.
  6. *
  7. * POST actions (CSRF protected):
  8. * action=assign consultant_id + client_id → insert into consultant_clients
  9. * action=remove assignment_id → delete from consultant_clients
  10. * action=set_type user_id + user_type → update users.user_type
  11. */
  12. require_once __DIR__ . '/../../config/database.php';
  13. require_once __DIR__ . '/../../lib/auth.php';
  14. require_once __DIR__ . '/../../lib/csrf.php';
  15. if (session_status() === PHP_SESSION_NONE) {
  16. session_start();
  17. }
  18. requireAdmin();
  19. $pdo = getDBConnection();
  20. $flash = '';
  21. $flashType = 'success';
  22. // ── POST handler ──────────────────────────────────────────────────────────────
  23. if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  24. if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
  25. $flash = 'Invalid request token. Please try again.';
  26. $flashType = 'danger';
  27. } else {
  28. $action = $_POST['action'] ?? '';
  29. if ($action === 'assign') {
  30. $consultantId = (int) ($_POST['consultant_id'] ?? 0);
  31. $clientId = (int) ($_POST['client_id'] ?? 0);
  32. if ($consultantId && $clientId) {
  33. try {
  34. $pdo->prepare("
  35. INSERT IGNORE INTO consultant_clients (consultant_id, client_id, assigned_by)
  36. VALUES (?, ?, ?)
  37. ")->execute([$consultantId, $clientId, getCurrentUserId()]);
  38. $flash = 'Client assigned successfully.';
  39. } catch (PDOException $e) {
  40. $flash = 'Assignment failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
  41. $flashType = 'danger';
  42. }
  43. } else {
  44. $flash = 'Please select both a consultant and a client.';
  45. $flashType = 'warning';
  46. }
  47. } elseif ($action === 'remove') {
  48. $assignmentId = (int) ($_POST['assignment_id'] ?? 0);
  49. if ($assignmentId) {
  50. $pdo->prepare("DELETE FROM consultant_clients WHERE id = ?")->execute([$assignmentId]);
  51. $flash = 'Assignment removed.';
  52. }
  53. } elseif ($action === 'set_type') {
  54. $targetUserId = (int) ($_POST['user_id'] ?? 0);
  55. $userType = $_POST['user_type'] ?? '';
  56. if ($targetUserId && in_array($userType, ['client', 'consultant', 'admin'], true)) {
  57. $pdo->prepare("UPDATE users SET user_type = ? WHERE id = ?")
  58. ->execute([$userType, $targetUserId]);
  59. $flash = 'User type updated.';
  60. } else {
  61. $flash = 'Invalid user type selection.';
  62. $flashType = 'warning';
  63. }
  64. }
  65. }
  66. }
  67. // ── Load data ─────────────────────────────────────────────────────────────────
  68. // All users with their type
  69. $allUsers = $pdo->query("
  70. SELECT id, fullname, email, user_type
  71. FROM users
  72. WHERE active = 1
  73. ORDER BY user_type, fullname
  74. ")->fetchAll();
  75. // Consultants with their assigned clients
  76. $consultants = $pdo->query("
  77. SELECT u.id, u.fullname, u.email
  78. FROM users u
  79. WHERE u.user_type IN ('consultant','admin') AND u.active = 1
  80. ORDER BY u.fullname
  81. ")->fetchAll();
  82. // All client records (for the assign dropdown)
  83. $allClients = $pdo->query("
  84. SELECT id, client, company
  85. FROM client_records
  86. ORDER BY client ASC
  87. ")->fetchAll();
  88. // Assignments: consultant_id → [{assignment_id, client_id, client_name, company, assigned_at}]
  89. $assignmentRows = $pdo->query("
  90. SELECT cc.id AS assignment_id, cc.consultant_id, cc.assigned_at,
  91. cr.id AS client_id, cr.client, cr.company
  92. FROM consultant_clients cc
  93. JOIN client_records cr ON cr.id = cc.client_id
  94. ORDER BY cc.consultant_id, cr.client
  95. ")->fetchAll();
  96. $assignmentsByConsultant = [];
  97. foreach ($assignmentRows as $row) {
  98. $assignmentsByConsultant[$row['consultant_id']][] = $row;
  99. }
  100. // ── Page render ───────────────────────────────────────────────────────────────
  101. $pageTitle = 'Manage Consultants';
  102. $siteName = 'Crop Monitor';
  103. include __DIR__ . '/../../layouts/header.php';
  104. include __DIR__ . '/../../layouts/navbar.php';
  105. ?>
  106. <div id="layoutSidenav">
  107. <div id="layoutSidenav_nav">
  108. <?php include __DIR__ . '/../../layouts/consultant-sidebar.php'; ?>
  109. </div>
  110. <div id="layoutSidenav_content">
  111. <main>
  112. <div class="container-fluid px-4">
  113. <h1 class="mt-4"><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?></h1>
  114. <ol class="breadcrumb mb-4">
  115. <li class="breadcrumb-item"><a href="/dashboard/consultant/index.php">Consultant</a></li>
  116. <li class="breadcrumb-item active">Manage Consultants</li>
  117. </ol>
  118. <?php if ($flash): ?>
  119. <div class="alert alert-<?= $flashType ?> alert-dismissible fade show" role="alert">
  120. <?= htmlspecialchars($flash, ENT_QUOTES, 'UTF-8') ?>
  121. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  122. </div>
  123. <?php endif; ?>
  124. <div class="row g-4">
  125. <!-- ══ LEFT: User roles ═══════════════════════════════════ -->
  126. <div class="col-xl-4">
  127. <div class="card shadow-sm border-0 h-100">
  128. <div class="card-header fw-semibold">
  129. <i class="fas fa-users-cog me-1"></i> User Roles
  130. </div>
  131. <div class="card-body p-0">
  132. <table class="table table-sm table-hover mb-0 align-middle">
  133. <thead class="table-light">
  134. <tr>
  135. <th>Name</th>
  136. <th>Email</th>
  137. <th>Role</th>
  138. <th></th>
  139. </tr>
  140. </thead>
  141. <tbody>
  142. <?php foreach ($allUsers as $u): ?>
  143. <tr>
  144. <td class="fw-semibold small">
  145. <?= htmlspecialchars($u['fullname'], ENT_QUOTES, 'UTF-8') ?>
  146. </td>
  147. <td class="text-muted small">
  148. <?= htmlspecialchars($u['email'], ENT_QUOTES, 'UTF-8') ?>
  149. </td>
  150. <td>
  151. <?php
  152. $badge = match($u['user_type']) {
  153. 'admin' => 'danger',
  154. 'consultant' => 'info',
  155. default => 'secondary',
  156. };
  157. ?>
  158. <span class="badge bg-<?= $badge ?>">
  159. <?= htmlspecialchars(ucfirst($u['user_type']), ENT_QUOTES, 'UTF-8') ?>
  160. </span>
  161. </td>
  162. <td>
  163. <button class="btn btn-xs btn-sm btn-outline-secondary"
  164. data-bs-toggle="modal"
  165. data-bs-target="#modalSetType"
  166. data-user-id="<?= (int)$u['id'] ?>"
  167. data-user-name="<?= htmlspecialchars($u['fullname'], ENT_QUOTES, 'UTF-8') ?>"
  168. data-user-type="<?= htmlspecialchars($u['user_type'], ENT_QUOTES, 'UTF-8') ?>">
  169. Edit
  170. </button>
  171. </td>
  172. </tr>
  173. <?php endforeach; ?>
  174. </tbody>
  175. </table>
  176. </div>
  177. </div>
  178. </div>
  179. <!-- ══ RIGHT: Consultant assignments ══════════════════════ -->
  180. <div class="col-xl-8">
  181. <!-- Assign form -->
  182. <div class="card shadow-sm border-0 mb-4">
  183. <div class="card-header fw-semibold">
  184. <i class="fas fa-user-plus me-1"></i> Assign Client to Consultant
  185. </div>
  186. <div class="card-body">
  187. <form method="POST" class="row g-2 align-items-end">
  188. <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
  189. <input type="hidden" name="action" value="assign">
  190. <div class="col-md-5">
  191. <label class="form-label fw-semibold small">Consultant</label>
  192. <select name="consultant_id" class="form-select form-select-sm" required>
  193. <option value="">Select consultant…</option>
  194. <?php foreach ($consultants as $c): ?>
  195. <option value="<?= (int)$c['id'] ?>">
  196. <?= htmlspecialchars($c['fullname'], ENT_QUOTES, 'UTF-8') ?>
  197. </option>
  198. <?php endforeach; ?>
  199. </select>
  200. </div>
  201. <div class="col-md-5">
  202. <label class="form-label fw-semibold small">Client</label>
  203. <select name="client_id" class="form-select form-select-sm" required>
  204. <option value="">Select client…</option>
  205. <?php foreach ($allClients as $cl): ?>
  206. <option value="<?= (int)$cl['id'] ?>">
  207. <?= htmlspecialchars($cl['client'] . ($cl['company'] ? ' — ' . $cl['company'] : ''), ENT_QUOTES, 'UTF-8') ?>
  208. </option>
  209. <?php endforeach; ?>
  210. </select>
  211. </div>
  212. <div class="col-md-2">
  213. <button class="btn btn-success btn-sm w-100" type="submit">
  214. <i class="fas fa-plus me-1"></i>Assign
  215. </button>
  216. </div>
  217. </form>
  218. </div>
  219. </div>
  220. <!-- Current assignments per consultant -->
  221. <?php if (empty($consultants)): ?>
  222. <div class="alert alert-info">
  223. No consultant accounts yet. Set a user's role to "Consultant" using the panel on the left.
  224. </div>
  225. <?php else: ?>
  226. <?php foreach ($consultants as $con):
  227. $assigned = $assignmentsByConsultant[$con['id']] ?? [];
  228. ?>
  229. <div class="card shadow-sm border-0 mb-3">
  230. <div class="card-header d-flex justify-content-between align-items-center">
  231. <span class="fw-semibold">
  232. <i class="fas fa-user-tie me-1 text-info"></i>
  233. <?= htmlspecialchars($con['fullname'], ENT_QUOTES, 'UTF-8') ?>
  234. <span class="text-muted fw-normal small ms-1">
  235. <?= htmlspecialchars($con['email'], ENT_QUOTES, 'UTF-8') ?>
  236. </span>
  237. </span>
  238. <span class="badge bg-secondary"><?= count($assigned) ?> client<?= count($assigned) !== 1 ? 's' : '' ?></span>
  239. </div>
  240. <?php if (empty($assigned)): ?>
  241. <div class="card-body text-muted small">No clients assigned yet.</div>
  242. <?php else: ?>
  243. <ul class="list-group list-group-flush">
  244. <?php foreach ($assigned as $a): ?>
  245. <li class="list-group-item d-flex justify-content-between align-items-center py-2">
  246. <div>
  247. <span class="fw-semibold">
  248. <?= htmlspecialchars($a['client'], ENT_QUOTES, 'UTF-8') ?>
  249. </span>
  250. <?php if ($a['company']): ?>
  251. <span class="text-muted small ms-1">
  252. — <?= htmlspecialchars($a['company'], ENT_QUOTES, 'UTF-8') ?>
  253. </span>
  254. <?php endif; ?>
  255. <div class="text-muted" style="font-size:.72rem">
  256. Assigned <?= date('j M Y', strtotime($a['assigned_at'])) ?>
  257. </div>
  258. </div>
  259. <form method="POST" class="d-inline"
  260. onsubmit="return confirm('Remove this client assignment?')">
  261. <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
  262. <input type="hidden" name="action" value="remove">
  263. <input type="hidden" name="assignment_id" value="<?= (int)$a['assignment_id'] ?>">
  264. <button class="btn btn-sm btn-outline-danger" type="submit">
  265. <i class="fas fa-times"></i>
  266. </button>
  267. </form>
  268. </li>
  269. <?php endforeach; ?>
  270. </ul>
  271. <?php endif; ?>
  272. </div>
  273. <?php endforeach; ?>
  274. <?php endif; ?>
  275. </div>
  276. </div><!-- /.row -->
  277. </div>
  278. </main>
  279. </div>
  280. </div>
  281. <!-- ── Modal: set user type ───────────────────────────────────────────────── -->
  282. <div class="modal fade" id="modalSetType" tabindex="-1">
  283. <div class="modal-dialog modal-sm">
  284. <div class="modal-content">
  285. <form method="POST">
  286. <input type="hidden" name="csrf_token" value="<?= generateCsrfToken() ?>">
  287. <input type="hidden" name="action" value="set_type">
  288. <input type="hidden" name="user_id" id="modalUserId">
  289. <div class="modal-header">
  290. <h5 class="modal-title">Change Role</h5>
  291. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  292. </div>
  293. <div class="modal-body">
  294. <p class="mb-2 text-muted small" id="modalUserName"></p>
  295. <label class="form-label fw-semibold">User Type</label>
  296. <select name="user_type" id="modalUserType" class="form-select">
  297. <option value="client">Client</option>
  298. <option value="consultant">Consultant</option>
  299. <option value="admin">Admin</option>
  300. </select>
  301. </div>
  302. <div class="modal-footer">
  303. <button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
  304. <button type="submit" class="btn btn-primary btn-sm">Save</button>
  305. </div>
  306. </form>
  307. </div>
  308. </div>
  309. </div>
  310. <script>
  311. document.getElementById('modalSetType').addEventListener('show.bs.modal', function (e) {
  312. const btn = e.relatedTarget;
  313. document.getElementById('modalUserId').value = btn.dataset.userId;
  314. document.getElementById('modalUserName').textContent = btn.dataset.userName;
  315. document.getElementById('modalUserType').value = btn.dataset.userType;
  316. });
  317. </script>
  318. <?php include __DIR__ . '/../../layouts/footer.php'; ?>