save_stages.php 5.3 KB

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