Development Application
Application No: = htmlspecialchars($app['reference']) ?>
Started: = date("d M Y", strtotime($app['created_at'])) ?>
- = $dateText ? htmlspecialchars($prefix . $dateText, ENT_QUOTES, 'UTF-8') : ' ' ?>
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
try {
$pdo = new PDO($dsn, $cfg['db_username'], $cfg['db_password'], $options);
} catch (PDOException $e) {
exit('Database connection failed: ' . $e->getMessage());
}
$app_id_raw = $_GET['id'] ?? '';
$token = $_GET['token'] ?? '';
$app_id = preg_match('/^\d+$/', $app_id_raw) ? $app_id_raw : '0';
// Verify token (optional: match your token logic)
$stmt = $pdo->prepare("SELECT client_email, reference, created_at, submission_date, required_by, clock_paused, clock_paused_at, clock_pause_reason FROM applications WHERE id = ?");
$stmt->execute([$app_id]);
$app = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$app) {
http_response_code(404);
exit("Application not found.");
}
// Fetch stages
$stmt = $pdo->prepare("SELECT * FROM application_stages WHERE application_id = ? ORDER BY position ASC");
$stmt->execute([$app_id]);
$stages = $stmt->fetchAll(PDO::FETCH_ASSOC);
$totalStages = 7;
$currentStage = count(array_filter($stages, function ($s) {
return strtolower(trim($s['status'] ?? '')) === 'complete';
}));
$progress = round(($currentStage / $totalStages) * 100);
$isPaused = (int)($app['clock_paused'] ?? 0) === 1;
$pauseReason = trim((string)($app['clock_pause_reason'] ?? ''));
$decisionDate = null;
// 1) Look for an explicit 'Council Decision Due' stage date
$decisionStage = null;
foreach ($stages as $s) {
if (stripos($s['title'] ?? '', 'decision') !== false && !empty($s['stage_date'])) {
$decisionStage = $s;
break;
}
}
if ($decisionStage) {
$decisionDate = new DateTime($decisionStage['stage_date'], new DateTimeZone('Australia/Hobart'));
} elseif (!empty($app['required_by'])) {
$decisionDate = new DateTime($app['required_by'], new DateTimeZone('Australia/Hobart'));
} elseif (!empty($app['submission_date'])) {
$decisionDate = (new DateTime($app['submission_date'], new DateTimeZone('Australia/Hobart')))->modify('+42 days');
}
// set a friendly “end of business day” time so the countdown isn’t midnight-awkward
if ($decisionDate) { $decisionDate->setTime(17, 0, 0); }
$decisionIso = $decisionDate ? $decisionDate->format('c') : '';
// --- Create correspondence entry ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'add_correspondence') {
$tz = new DateTimeZone('Australia/Hobart');
$typeAllow = ['incoming','outgoing','note'];
$channelAllow = ['email','phone','portal','letter','meeting','other'];
$visibilityAllow= ['client','internal'];
$type = in_array($_POST['type'] ?? 'note', $typeAllow, true) ? $_POST['type'] : 'note';
$channel = in_array($_POST['channel'] ?? 'other', $channelAllow, true) ? $_POST['channel'] : 'other';
$visibility = in_array($_POST['visibility'] ?? 'client', $visibilityAllow, true) ? $_POST['visibility'] : 'client';
$subject = trim($_POST['subject'] ?? '') ?: null;
$author = trim($_POST['author'] ?? '') ?: null;
$pin = isset($_POST['pin']) ? 1 : 0;
$bodyRaw = trim($_POST['body'] ?? '');
if ($bodyRaw === '') { $bodyRaw = '(no content)'; }
// event_at: prefer user input, else "now"
$eventAtRaw = trim($_POST['event_at'] ?? '');
try {
$eventAt = $eventAtRaw ? new DateTime($eventAtRaw, $tz) : new DateTime('now', $tz);
} catch (Exception $e) {
$eventAt = new DateTime('now', $tz);
}
$stmt = $pdo->prepare("
INSERT INTO application_correspondence
(application_id, event_at, type, channel, subject, body, author, visibility, pin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$app_id,
$eventAt->format('Y-m-d H:i:s'),
$type,
$channel,
$subject,
$bodyRaw,
$author,
$visibility,
$pin
]);
// Redirect to avoid resubmission and jump to the timeline section
header("Location: ".$_SERVER['REQUEST_URI']."#correspondence");
exit;
}
// Fetch timeline (newest first; pinned first)
$stmt = $pdo->prepare("
SELECT id, event_at, type, channel, subject, body, author, visibility, pin, created_at
FROM application_correspondence
WHERE application_id = ?
ORDER BY pin DESC, event_at DESC, id DESC
LIMIT 200
");
$stmt->execute([$app_id]);
$correspondence = $stmt->fetchAll(PDO::FETCH_ASSOC);
/* NEW: attachment counts (and optional details) */
$fileCounts = [];
$filesByCorr = []; // if you also want to list the files
if (!empty($correspondence)) {
$ids = array_column($correspondence, 'id');
$ph = implode(',', array_fill(0, count($ids), '?'));
// Count files per correspondence
$qc = $pdo->prepare("
SELECT correspondence_id, COUNT(*) AS n
FROM application_correspondence_files
WHERE correspondence_id IN ($ph)
GROUP BY correspondence_id
");
$qc->execute($ids);
foreach ($qc->fetchAll(PDO::FETCH_ASSOC) as $r) {
$fileCounts[(int)$r['correspondence_id']] = (int)$r['n'];
}
// OPTIONAL: load file details if you want links
$qd = $pdo->prepare("
SELECT correspondence_id, original_name, file_url
FROM application_correspondence_files
WHERE correspondence_id IN ($ph)
ORDER BY id ASC
");
$qd->execute($ids);
foreach ($qd->fetchAll(PDO::FETCH_ASSOC) as $f) {
$cid = (int)$f['correspondence_id'];
if (!isset($filesByCorr[$cid])) $filesByCorr[$cid] = [];
$filesByCorr[$cid][] = $f;
}
}
// ------------------ HELPERS ------------------
function render_body_html(string $text): string {
// escape first
$s = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
// linkify http(s)
$s = preg_replace('~(https?://[^\s<]+)~i', '$1', $s);
// newlines to
return nl2br($s);
}
function stage_icon_for(string $title): string {
$t = strtolower($title);
if (str_contains($t,'submit')) return 'bi-upload';
if (str_contains($t,'ack')) return 'bi-inbox';
if (str_contains($t,'fee') || str_contains($t,'paid')) return 'bi-cash-coin';
if (str_contains($t,'valid') || str_contains($t,'confirm')) return 'bi-check2-circle';
if (str_contains($t,'advert')) return 'bi-megaphone';
if (str_contains($t,'decision')) return 'bi-check-circle-fill';
return 'bi-flag';
}
// Admin bypass: valid HTTP Basic Auth skips the client token check
$_au = $cfg['admin_user'] ?? '';
$_ap = $cfg['admin_pass'] ?? '';
$isAdmin = $_au !== '' && $_ap !== ''
&& isset($_SERVER['PHP_AUTH_USER'])
&& $_SERVER['PHP_AUTH_USER'] === $_au
&& ($_SERVER['PHP_AUTH_PW'] ?? '') === $_ap;
if (!$isAdmin) {
// --- Require signed token from Contracts Admin link ---
$clientid = $_GET['clientid'] ?? '';
$token = $_GET['token'] ?? '';
if (!preg_match('/^[A-Za-z0-9_-]+$/', $clientid)) {
http_response_code(400);
exit('Bad link (clientid).');
}
if ($token === '') {
header('WWW-Authenticate: Basic realm="Modulos Contracts Admin"');
http_response_code(403);
exit('Missing token.');
}
// Build expected token from the .md front matter secret
$expected = progress_expected_token($clientid, $app_id);
if (!$expected || !hash_equals($expected, $token)) {
http_response_code(403);
exit('Invalid or expired link.');
}
}
unset($_au, $_ap);
?>