save_stages.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. <?php
  2. error_reporting(E_ALL);
  3. ini_set("display_errors", 0);
  4. ini_set("log_errors", 1);
  5. date_default_timezone_set("Australia/Hobart");
  6. $cfg = require __DIR__ . '/config.php';
  7. $_au = $cfg['admin_user'] ?? '';
  8. $_ap = $cfg['admin_pass'] ?? '';
  9. if ($_au === '' || $_ap === '' ||
  10. !isset($_SERVER['PHP_AUTH_USER']) ||
  11. $_SERVER['PHP_AUTH_USER'] !== $_au ||
  12. ($_SERVER['PHP_AUTH_PW'] ?? '') !== $_ap) {
  13. header('WWW-Authenticate: Basic realm="Modulos Contracts Admin"');
  14. header('HTTP/1.0 401 Unauthorized');
  15. echo 'Authentication required.';
  16. exit;
  17. }
  18. unset($_au, $_ap);
  19. $dsn = 'mysql:host=' . $cfg['db_host'] . ';dbname=' . $cfg['db_name'] . ';charset=utf8mb4';
  20. $options = [
  21. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  22. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  23. ];
  24. try {
  25. $pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
  26. } catch (PDOException $e) {
  27. exit('Database connection failed: ' . $e->getMessage());
  28. }
  29. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  30. http_response_code(405);
  31. exit('Method Not Allowed');
  32. }
  33. $app_id = (int)($_POST['application_id'] ?? 0);
  34. $submission_date = trim($_POST['submission_date'] ?? '') ?: null;
  35. $required_by = trim($_POST['required_by'] ?? '') ?: null;
  36. $stages = $_POST['stages'] ?? [];
  37. $paused = isset($_POST['clock_paused']) ? 1 : 0;
  38. $reason = trim($_POST['clock_pause_reason'] ?? '');
  39. $uploadDir = __DIR__ . "/uploads/app_$app_id";
  40. if (!is_dir($uploadDir)) @mkdir($uploadDir, 0775, true);
  41. $pdo->beginTransaction();
  42. try {
  43. // --- Applications: single atomic update (dates + clock state)
  44. $cur = $pdo->prepare("SELECT clock_paused FROM applications WHERE id = ?");
  45. $cur->execute([$app_id]);
  46. $before = $cur->fetch(PDO::FETCH_ASSOC) ?: ['clock_paused' => 0];
  47. $wasPaused = (int)($before['clock_paused'] ?? 0);
  48. $sql = "
  49. UPDATE applications
  50. SET submission_date = :submission,
  51. required_by = :required_by,
  52. clock_paused = :paused,
  53. clock_pause_reason = :reason,
  54. clock_paused_at = CASE
  55. WHEN :paused = 1 AND :wasPaused = 0 THEN NOW()
  56. WHEN :paused = 0 THEN NULL
  57. ELSE clock_paused_at
  58. END
  59. WHERE id = :id
  60. ";
  61. $st = $pdo->prepare($sql);
  62. $st->execute([
  63. ':submission' => $submission_date,
  64. ':required_by'=> $required_by,
  65. ':paused' => $paused,
  66. ':wasPaused' => $wasPaused,
  67. ':reason' => ($reason !== '' ? $reason : null),
  68. ':id' => $app_id,
  69. ]);
  70. // --- Stage helpers
  71. $getRow = $pdo->prepare("SELECT id, pdf_path FROM application_stages WHERE id = ? AND application_id = ?");
  72. $ins = $pdo->prepare("
  73. INSERT INTO application_stages
  74. (application_id, position, title, description, status, stage_date, pdf_path, created_at, updated_at)
  75. VALUES
  76. (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
  77. ON DUPLICATE KEY UPDATE
  78. title = VALUES(title),
  79. description = VALUES(description),
  80. status = VALUES(status),
  81. stage_date = VALUES(stage_date),
  82. pdf_path = COALESCE(VALUES(pdf_path), pdf_path),
  83. updated_at = NOW()
  84. ");
  85. $upd = $pdo->prepare("
  86. UPDATE application_stages
  87. SET title = ?, description = ?, status = ?, stage_date = ?, pdf_path = ?, updated_at = NOW()
  88. WHERE id = ? AND application_id = ?
  89. ");
  90. // --- Upsert all stages
  91. foreach ($stages as $i => $stage) {
  92. $id = (int)($stage['id'] ?? 0);
  93. $position = isset($stage['position']) ? (int)$stage['position'] : $i;
  94. $title = trim($stage['title'] ?? '');
  95. $status = $stage['status'] ?? 'pending';
  96. $date = $stage['date'] ?: null;
  97. $notes = trim($stage['notes'] ?? '');
  98. // Auto-set date when marking complete with empty date
  99. if ($status === 'complete' && empty($date)) {
  100. $date = date('Y-m-d');
  101. }
  102. // File handling
  103. $newPdfPath = null;
  104. $removePdf = !empty($stage['remove_pdf']);
  105. $existingPdf = null;
  106. if ($id > 0) {
  107. $getRow->execute([$id, $app_id]);
  108. if ($row = $getRow->fetch()) $existingPdf = $row['pdf_path'];
  109. }
  110. // New upload?
  111. if (isset($_FILES['stages']['error'][$i]['pdf']) && $_FILES['stages']['error'][$i]['pdf'] === UPLOAD_ERR_OK) {
  112. $tmpPath = $_FILES['stages']['tmp_name'][$i]['pdf'];
  113. $originalName = basename($_FILES['stages']['name'][$i]['pdf']);
  114. // Validate MIME type and extension — PDFs only
  115. $finfo = new finfo(FILEINFO_MIME_TYPE);
  116. $mime = $finfo->file($tmpPath) ?: '';
  117. $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
  118. if ($mime !== 'application/pdf' || $ext !== 'pdf') {
  119. // Skip invalid file silently
  120. } else {
  121. $safeName = date('Ymd_His') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
  122. $targetAbs = $uploadDir . '/' . $safeName;
  123. if (move_uploaded_file($tmpPath, $targetAbs)) {
  124. $newPdfPath = "uploads/app_$app_id/$safeName";
  125. if ($existingPdf && is_file(__DIR__ . '/' . $existingPdf)) @unlink(__DIR__ . '/' . $existingPdf);
  126. }
  127. }
  128. } elseif ($removePdf && $existingPdf) {
  129. if (is_file(__DIR__ . '/' . $existingPdf)) @unlink(__DIR__ . '/' . $existingPdf);
  130. $existingPdf = null;
  131. $newPdfPath = null; // explicit null keeps cleared
  132. }
  133. if ($id > 0) {
  134. $pdfToStore = $newPdfPath !== null ? $newPdfPath : $existingPdf; // keep unless replaced/removed
  135. $upd->execute([$title, $notes, $status, $date, $pdfToStore, $id, $app_id]);
  136. } else {
  137. $pdfForInsert = $newPdfPath; // may be NULL
  138. $ins->execute([$app_id, $position, $title, $notes, $status, $date, $pdfForInsert]);
  139. }
  140. }
  141. $pdo->commit();
  142. header("Location: edit_application.php?id=$app_id");
  143. exit;
  144. } catch (Throwable $e) {
  145. $pdo->rollBack();
  146. http_response_code(500);
  147. error_log('Save failed: ' . $e->getMessage());
  148. http_response_code(500);
  149. exit('Save failed');
  150. }