soil-report.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. <?php
  2. /**
  3. * dashboard/crop-analysis/soil-test-data/soil-report.php
  4. *
  5. * Soil Analysis Report — editable sections with auto-save and Ollama AI interpretation.
  6. */
  7. require_once __DIR__ . '/../../../config/database.php';
  8. require_once __DIR__ . '/../../../lib/auth.php';
  9. require_once __DIR__ . '/../../../lib/csrf.php';
  10. require_once __DIR__ . '/../../../lib/soil_calculations.php';
  11. requireLogin();
  12. $client_id = (int) ($_GET['cid'] ?? 0);
  13. $record_id = (int) ($_GET['rid'] ?? 0);
  14. $rand_id = trim( $_GET['rand'] ?? '');
  15. if (!$record_id || $rand_id === '') {
  16. http_response_code(400);
  17. die('Invalid request parameters');
  18. }
  19. $acHa = 2.4710559990832394739; // kg/Ac → kg/ha conversion
  20. try {
  21. $pdo = getDBConnection();
  22. $userId = getCurrentUserId();
  23. // Load soil record — verify ownership via rand token
  24. $stmt = $pdo->prepare('SELECT * FROM soil_records WHERE id = ? AND rand = ?');
  25. $stmt->execute([$record_id, $rand_id]);
  26. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  27. if (!$row) {
  28. http_response_code(404);
  29. die('Soil record not found');
  30. }
  31. // Load specification ranges for this soil type
  32. $spec = [];
  33. if (!empty($row['soil_type'])) {
  34. $stmtSpec = $pdo->prepare('SELECT * FROM soil_specifications WHERE soil_type = ? LIMIT 1');
  35. $stmtSpec->execute([$row['soil_type']]);
  36. $spec = $stmtSpec->fetch(PDO::FETCH_ASSOC) ?: [];
  37. }
  38. // Load saved report comments (JSON blob in reports table)
  39. $savedComments = [];
  40. $stmtRpt = $pdo->prepare(
  41. 'SELECT comment FROM reports WHERE record_id = ? AND modx_user_id = ? ORDER BY id DESC LIMIT 1'
  42. );
  43. $stmtRpt->execute([$record_id, $userId]);
  44. $savedRow = $stmtRpt->fetchColumn();
  45. if ($savedRow) {
  46. $decoded = json_decode($savedRow, true);
  47. if (is_array($decoded)) {
  48. $savedComments = $decoded;
  49. }
  50. }
  51. } catch (PDOException $e) {
  52. error_log('DB error in soil-report.php: ' . $e->getMessage());
  53. die('Database error occurred');
  54. }
  55. // ── Escaped display vars ────────────────────────────────────────────────────
  56. $client = htmlspecialchars($row['client_name'] ?? '', ENT_QUOTES, 'UTF-8');
  57. $address = htmlspecialchars($row['site_address'] ?? '', ENT_QUOTES, 'UTF-8');
  58. $state = htmlspecialchars($row['state_postcode'] ?? '', ENT_QUOTES, 'UTF-8');
  59. $email = htmlspecialchars($row['email'] ?? '', ENT_QUOTES, 'UTF-8');
  60. $labNo = htmlspecialchars($row['lab_no'] ?? '', ENT_QUOTES, 'UTF-8');
  61. $sampleDate = htmlspecialchars($row['date_sampled'] ?? '', ENT_QUOTES, 'UTF-8');
  62. $sample = htmlspecialchars($row['site_id'] ?? '', ENT_QUOTES, 'UTF-8');
  63. $cropName = htmlspecialchars($row['sample_id'] ?? '', ENT_QUOTES, 'UTF-8');
  64. $soilType = htmlspecialchars($row['soil_type'] ?? '', ENT_QUOTES, 'UTF-8');
  65. $batchNo = htmlspecialchars($row['batch_no'] ?? '', ENT_QUOTES, 'UTF-8');
  66. $today = date('jS F Y');
  67. $pageTitle = 'Soil Report' . ($client !== '' ? ' — ' . $client : '');
  68. $siteName = 'Crop Monitor';
  69. // ── Five-year plan element definitions ─────────────────────────────────────
  70. // [label, soil_records column, min column (record), max column (record), unit]
  71. // When min/max are empty the spec column of the same name provides the target.
  72. $planElements = [
  73. ['Calcium', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'kg/ha'],
  74. ['Magnesium', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'kg/ha'],
  75. ['Potassium', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'kg/ha'],
  76. ['Sodium', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'kg/ha'],
  77. ['Phosphate', 'p_colwell', '', '', 'kg/ha'],
  78. ['Sulfur', 's_morgan', '', '', 'kg/ha'],
  79. ['Boron', 'b_cacl2', '', '', 'kg/ha'],
  80. ['Manganese', 'mn_dtpa', '', '', 'kg/ha'],
  81. ['Zinc', 'zn_dtpa', '', '', 'kg/ha'],
  82. ['Copper', 'cu_dtpa', '', '', 'kg/ha'],
  83. ];
  84. /**
  85. * Calculate total kg/ha deficit for one element, given soil row + spec row.
  86. * Returns 0 if already at or above target.
  87. */
  88. function calcDeficit(array $row, array $spec, string $col, string $minCol, string $maxCol): float
  89. {
  90. global $acHa;
  91. $value = (float)($row[$col] ?? 0);
  92. if ($maxCol !== '') {
  93. $maxVal = (float)($row[$maxCol] ?? 0);
  94. } else {
  95. $maxVal = (float)($spec[$col] ?? 0);
  96. }
  97. $deficit = ($maxVal - $value) * $acHa;
  98. return $deficit > 0 ? round($deficit, 2) : 0.0;
  99. }
  100. include __DIR__ . '/../../../layouts/header.php';
  101. ?>
  102. <link rel="stylesheet" href="/client-assets/home/css/graphPrint.css" media="print">
  103. <style>
  104. * { margin: 0; padding: 0; box-sizing: border-box; }
  105. body { background: #fff; }
  106. @media print {
  107. @page {
  108. size: A4 portrait;
  109. }
  110. @page :left {
  111. margin-left: 0.5cm;
  112. }
  113. @page :right {
  114. margin-right: 0.5cm;
  115. }
  116. @page :top {
  117. margin-top: 0cm;
  118. }
  119. @page :bottom {
  120. margin-bottom: 0cm;
  121. }
  122. body {
  123. min-width: 992px !important;
  124. }
  125. .container {
  126. min-width: 992px !important;
  127. }
  128. .report-textarea {
  129. border:none;
  130. }
  131. }
  132. .element-required-module .card-group .col {
  133. padding: 0 10px;
  134. }
  135. </style>
  136. <div class="container-fluid px-4" id="content">
  137. <!-- ── Page heading ──────────────────────────────────────────────────── -->
  138. <div class="d-flex align-items-center justify-content-between mt-4 mb-3">
  139. <h1 class="h3 mb-0">Soil Analysis Report</h1>
  140. <div class="d-flex gap-2 d-print-none">
  141. <a href="/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>&cid=<?= $client_id ?>"
  142. class="btn btn-outline-secondary btn-sm">
  143. &larr; Analysis
  144. </a>
  145. <a href="/dashboard/crop-analysis/soil-test-data/soil-report-pdf.php?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>"
  146. class="btn btn-outline-success btn-sm" target="_blank">
  147. <i class="fas fa-file-pdf me-1"></i>PDF — Report
  148. </a>
  149. <a href="/pdf-files/headlessChrome_pdf.php?type=soil&rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>&cid=<?= $client_id ?>"
  150. class="btn btn-success btn-sm">
  151. <i class="fas fa-file-pdf me-1"></i>PDF — Analysis &amp; Report
  152. </a>
  153. <button type="button" class="btn btn-primary btn-sm" id="btn-generate-all">
  154. <i class="fas fa-robot me-1"></i>Interpret All with AI
  155. </button>
  156. </div>
  157. </div>
  158. <!-- ── Client / Sample info card ─────────────────────────────────────── -->
  159. <div class="card mb-4">
  160. <div class="card-body py-2">
  161. <div class="row g-2">
  162. <div class="col-md-2">
  163. <img class="img-fluid" src="/client-assets/images/crop-monitor.png" alt="Crop Monitor"
  164. style="max-height:60px;">
  165. </div>
  166. <div class="col-md-10">
  167. <div class="row row-cols-2 row-cols-md-3 g-1 small">
  168. <div><strong>Client:</strong> <?= $client ?></div>
  169. <div><strong>Sample ID:</strong> <?= $sample ?></div>
  170. <div><strong>Date Sampled:</strong> <?= $sampleDate ?></div>
  171. <div><strong>Address:</strong> <?= $address ?>, <?= $state ?></div>
  172. <div><strong>Crop:</strong> <?= $cropName ?></div>
  173. <div><strong>Lab No:</strong> <?= $labNo ?></div>
  174. <div><strong>Soil Type:</strong> <?= $soilType ?></div>
  175. <div><strong>Batch:</strong> <?= $batchNo ?></div>
  176. <div><strong>Report Date:</strong> <?= htmlspecialchars($today, ENT_QUOTES, 'UTF-8') ?></div>
  177. </div>
  178. </div>
  179. </div>
  180. </div>
  181. </div>
  182. <div id="save-status" class="text-muted small mb-2" style="min-height:1.2rem;"></div>
  183. <form class="report-form" method="post">
  184. <input type="hidden" name="csrf_token"
  185. value="<?= htmlspecialchars(generateCsrfToken(), ENT_QUOTES, 'UTF-8') ?>">
  186. <input type="hidden" name="rid" value="<?= $record_id ?>">
  187. <input type="hidden" name="rand" value="<?= htmlspecialchars($rand_id, ENT_QUOTES, 'UTF-8') ?>">
  188. <!-- ── 1. Element requirements ────────────────────────────────────── -->
  189. <div class="card mb-4">
  190. <div class="card-header fw-bold">
  191. Total kilograms per hectare of each element needed to balance soil
  192. </div>
  193. <div class="card-body">
  194. <div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-6 g-2">
  195. <?php
  196. echo soilAnalysisReportCalcs('Ca', 'BS_ca_ppm', 'ca_ppm_min', 'ca_ppm_max', 'Calcium', 'kg', 'col', $record_id, $rand_id);
  197. echo soilAnalysisReportCalcs('Mg', 'BS_mg_ppm', 'mg_ppm_min', 'mg_ppm_max', 'Magnesium', 'kg', 'col', $record_id, $rand_id);
  198. echo soilAnalysisReportCalcs('K', 'BS_k_ppm', 'k_ppm_min', 'k_ppm_max', 'Potassium', 'kg', 'col', $record_id, $rand_id);
  199. echo soilAnalysisReportCalcs('Na', 'BS_na_ppm', 'na_ppm_min', 'na_ppm_max', 'Sodium', 'kg', 'col', $record_id, $rand_id);
  200. echo soilAnalysisReportCalcs('P', 'p_colwell', '', '', 'Phosphate', 'kg', 'col', $record_id, $rand_id);
  201. echo soilAnalysisReportCalcs('S', 's_morgan', '', '', 'Sulfur', 'kg', 'col', $record_id, $rand_id);
  202. echo soilAnalysisReportCalcs('Mn', 'mn_dtpa', '', '', 'Manganese', 'kg', 'col', $record_id, $rand_id);
  203. echo soilAnalysisReportCalcs('Fe', 'fe_dtpa', '', '', 'Iron', 'kg', 'col', $record_id, $rand_id);
  204. echo soilAnalysisReportCalcs('Zn', 'zn_dtpa', '', '', 'Zinc', 'kg', 'col', $record_id, $rand_id);
  205. echo soilAnalysisReportCalcs('Cu', 'cu_dtpa', '', '', 'Copper', 'kg', 'col', $record_id, $rand_id);
  206. echo soilAnalysisReportCalcs('AmN', 'NH3_N', '', '', 'Amm. Nitrogen', 'kg', 'col', $record_id, $rand_id);
  207. echo soilAnalysisReportCalcs('B', 'b_cacl2', '', '', 'Boron', 'kg', 'col', $record_id, $rand_id);
  208. echo soilAnalysisReportCalcs('NN', 'NO3_N', '', '', 'Nit. Nitrogen', 'kg', 'col', $record_id, $rand_id);
  209. ?>
  210. </div>
  211. </div>
  212. </div>
  213. <!-- ── 2. Five-year balancing plan ───────────────────────────────── -->
  214. <div class="card mb-4">
  215. <div class="card-header fw-bold">
  216. Ideal Soil Balancing Program — 5-Year Plan (kg/ha per year)
  217. </div>
  218. <div class="card-body p-0">
  219. <div class="table-responsive">
  220. <table class="table table-sm table-bordered table-hover mb-0">
  221. <thead class="table-light">
  222. <tr>
  223. <th>Element</th>
  224. <th class="text-center">Total Deficit</th>
  225. <?php for ($y = 1; $y <= 5; $y++): ?>
  226. <th class="text-center">Year <?= $y ?></th>
  227. <?php endfor; ?>
  228. </tr>
  229. </thead>
  230. <tbody>
  231. <?php foreach ($planElements as [$label, $col, $minCol, $maxCol, $unit]):
  232. $total = calcDeficit($row, $spec, $col, $minCol, $maxCol);
  233. $perYear = $total > 0 ? round($total / 5, 2) : 0;
  234. ?>
  235. <tr>
  236. <td><?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?></td>
  237. <td class="text-center <?= $total > 0 ? 'text-danger fw-semibold' : 'text-success' ?>">
  238. <?= $total > 0 ? $total . ' ' . $unit : '&#10003; Adequate' ?>
  239. </td>
  240. <?php for ($y = 1; $y <= 5; $y++): ?>
  241. <td class="text-center">
  242. <?= $perYear > 0 ? $perYear . ' ' . $unit : '—' ?>
  243. </td>
  244. <?php endfor; ?>
  245. </tr>
  246. <?php endforeach; ?>
  247. </tbody>
  248. </table>
  249. </div>
  250. </div>
  251. </div>
  252. <!-- ── 3. Overview / Executive Summary ───────────────────────────── -->
  253. <div class="card mb-4">
  254. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  255. <span>Overview</span>
  256. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  257. data-section="overview" data-target="#overview">
  258. <i class="fas fa-robot me-1"></i>Generate with AI
  259. </button>
  260. </div>
  261. <div class="card-body">
  262. <textarea id="overview" name="overview" class="form-control report-textarea" rows="6"
  263. placeholder="Enter an overview of the soil analysis results..."
  264. ><?= htmlspecialchars($savedComments['overview'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
  265. </div>
  266. </div>
  267. <!-- ── 4. AI Interpretation ───────────────────────────────────────── -->
  268. <div class="card mb-4">
  269. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  270. <span>AI Soil Interpretation</span>
  271. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  272. data-section="ai_interpretation" data-target="#ai_interpretation">
  273. <i class="fas fa-robot me-1"></i>Interpret with AI
  274. </button>
  275. </div>
  276. <div class="card-body">
  277. <p class="text-muted small mb-2">
  278. AI-generated agronomic interpretation. Review and edit before including in the final report.
  279. </p>
  280. <textarea id="ai_interpretation" name="ai_interpretation"
  281. class="form-control report-textarea" rows="10"
  282. placeholder="Click 'Interpret with AI' to generate an agronomic interpretation, or type manually..."
  283. ><?= htmlspecialchars($savedComments['ai_interpretation'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
  284. </div>
  285. </div>
  286. <!-- ── 5. Foliar program ──────────────────────────────────────────── -->
  287. <div class="card mb-4">
  288. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  289. <span>
  290. <input type="text" name="header1" class="form-control form-control-sm d-inline-block w-auto fw-bold"
  291. value="<?= htmlspecialchars($savedComments['header1'] ?? 'Foliar Program', ENT_QUOTES, 'UTF-8') ?>">
  292. </span>
  293. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  294. data-section="foliar" data-target="#foliar_Details">
  295. <i class="fas fa-robot me-1"></i>Generate with AI
  296. </button>
  297. </div>
  298. <div class="card-body">
  299. <textarea id="foliar_Details" name="foliar_Details"
  300. class="form-control report-textarea" rows="6"
  301. placeholder="Enter the foliar spray program details..."
  302. ><?= htmlspecialchars($savedComments['foliar_details'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
  303. </div>
  304. </div>
  305. <!-- ── 6. Microbial program ───────────────────────────────────────── -->
  306. <div class="card mb-4">
  307. <div class="card-header d-flex justify-content-between align-items-center fw-bold">
  308. <span>Microbial Program</span>
  309. <button type="button" class="btn btn-outline-primary btn-sm ai-generate-btn"
  310. data-section="microbial" data-target="#microbe_Program">
  311. <i class="fas fa-robot me-1"></i>Generate with AI
  312. </button>
  313. </div>
  314. <div class="card-body">
  315. <textarea id="microbe_Program" name="microbe_Program"
  316. class="form-control report-textarea" rows="6"
  317. placeholder="Enter the microbial / biological program details..."
  318. ><?= htmlspecialchars($savedComments['microbe_program'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
  319. </div>
  320. </div>
  321. <!-- ── 7. Disclaimer ──────────────────────────────────────────────── -->
  322. <div class="card mb-4 border-0 bg-light">
  323. <div class="card-body">
  324. <p class="text-muted mb-0" style="font-size:0.75rem;">
  325. Any recommendations provided by Crop Monitor are advice only. We are not paid consultants
  326. and accept no responsibility for any of our suggestions. No control can be exercised over
  327. storage, handling, mixing, application, or use, or over weather, plant or soil conditions
  328. before, during or after application — all of which may affect the performance of our
  329. program. No responsibility for, or liability for, any failure in performance, losses,
  330. damage or injuries consequential or otherwise arising from such storage, mixing,
  331. application or use will be accepted under any circumstances whatsoever. The buyer assumes
  332. all responsibility for the use of any of our products.
  333. </p>
  334. </div>
  335. </div>
  336. </form>
  337. </div><!-- /.container-fluid -->
  338. <script>
  339. (function () {
  340. 'use strict';
  341. var saveTimer = null;
  342. var statusEl = document.getElementById('save-status');
  343. var SAVE_URL = '/dashboard/crop-analysis/updatecomment.php'
  344. + '?rid=<?= $record_id ?>&rand=<?= urlencode($rand_id) ?>';
  345. var AI_URL = '/controllers/ollamaGenerate.php';
  346. var CSRF_TOKEN = <?= json_encode(generateCsrfToken()) ?>;
  347. function setStatus(msg, cls) {
  348. statusEl.textContent = msg;
  349. statusEl.className = 'small mb-2 text-' + (cls || 'secondary');
  350. }
  351. // ── Auto-save ────────────────────────────────────────────────────────── //
  352. document.querySelectorAll('.report-form .report-textarea, .report-form input[name="header1"]')
  353. .forEach(function (el) {
  354. el.addEventListener('input', function () {
  355. clearTimeout(saveTimer);
  356. saveTimer = setTimeout(saveReport, 1200);
  357. });
  358. });
  359. function saveReport() {
  360. var form = document.querySelector('.report-form');
  361. var data = new URLSearchParams(new FormData(form));
  362. setStatus('Saving…', 'secondary');
  363. fetch(SAVE_URL, { method: 'POST', body: data })
  364. .then(function (r) { return r.json(); })
  365. .then(function (d) {
  366. if (d.success) {
  367. var t = new Date();
  368. setStatus('Saved — ' + t.toLocaleTimeString(), 'success');
  369. } else {
  370. setStatus('Save failed: ' + (d.message || 'unknown error'), 'danger');
  371. }
  372. })
  373. .catch(function () {
  374. setStatus('Network error — not saved', 'danger');
  375. });
  376. }
  377. document.querySelector('.report-form').addEventListener('submit', function (e) {
  378. e.preventDefault();
  379. saveReport();
  380. });
  381. // ── AI generation ────────────────────────────────────────────────────── //
  382. function generateSection(btn, section, targetSelector) {
  383. var textarea = document.querySelector(targetSelector);
  384. if (!textarea) return;
  385. var origHTML = btn.innerHTML;
  386. btn.disabled = true;
  387. btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Generating…';
  388. setStatus('Requesting AI interpretation…', 'secondary');
  389. fetch(AI_URL, {
  390. method: 'POST',
  391. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  392. body: new URLSearchParams({
  393. csrf_token: CSRF_TOKEN,
  394. rid: <?= $record_id ?>,
  395. rand: <?= json_encode($rand_id) ?>,
  396. section: section,
  397. }),
  398. })
  399. .then(function (r) { return r.json(); })
  400. .then(function (d) {
  401. if (d.success && d.text) {
  402. textarea.value = d.text;
  403. textarea.dispatchEvent(new Event('input')); // trigger auto-save
  404. setStatus('AI text generated — review before publishing', 'success');
  405. } else {
  406. setStatus('AI error: ' + (d.error || 'no response returned'), 'danger');
  407. }
  408. })
  409. .catch(function () {
  410. setStatus('Could not reach AI service. Is Ollama running on port 11434?', 'danger');
  411. })
  412. .finally(function () {
  413. btn.disabled = false;
  414. btn.innerHTML = origHTML;
  415. });
  416. }
  417. document.querySelectorAll('.ai-generate-btn').forEach(function (btn) {
  418. btn.addEventListener('click', function () {
  419. generateSection(btn, btn.dataset.section, btn.dataset.target);
  420. });
  421. });
  422. // "Interpret All" — stagger requests so Ollama isn't flooded
  423. document.getElementById('btn-generate-all').addEventListener('click', function () {
  424. var sections = [
  425. { section: 'overview', target: '#overview' },
  426. { section: 'ai_interpretation', target: '#ai_interpretation' },
  427. { section: 'foliar', target: '#foliar_Details' },
  428. { section: 'microbial', target: '#microbe_Program' },
  429. ];
  430. sections.forEach(function (s, i) {
  431. setTimeout(function () {
  432. var sectionBtn = document.querySelector('.ai-generate-btn[data-section="' + s.section + '"]');
  433. generateSection(sectionBtn || document.getElementById('btn-generate-all'), s.section, s.target);
  434. }, i * 4000);
  435. });
  436. });
  437. })();
  438. </script>
  439. <?php include __DIR__ . '/../../../layouts/footer.php'; ?>