false, 'error' => 'Use POST with application/json']);
exit;
}
// Reject non-JSON content types early — prevents json_decode silently
// returning null on form-encoded or multipart bodies.
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
if (strpos($ct, 'application/json') === false) {
http_response_code(415);
echo json_encode(['ok' => false, 'error' => 'Content-Type must be application/json']);
exit;
}
$raw = file_get_contents('php://input') ?: '';
$in = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// --------- Schema (minimum viable) ----------
$d = [
// site
'address' => trim((string)($in['address'] ?? '')),
'lat' => floatval($in['lat'] ?? 0),
'lng' => floatval($in['lng'] ?? 0),
'pid' => trim((string)($in['pid'] ?? '')),
'title_id' => trim((string)($in['title_id'] ?? '')),
'total_area' => $in['total_area'] ?? null, // e.g. {sqm_label:"1,652 m²", ha_label:"0.1652 ha"} or string
'area_sqm' => $in['area_sqm'] ?? '',
'area_ha' => $in['area_ha'] ?? '',
'tenure' => trim((string)($in['tenure'] ?? '')),
'lpi' => trim((string)($in['lpi'] ?? '')),
'list_guid' => trim((string)($in['list_guid'] ?? '')),
'locality' => trim((string)($in['locality'] ?? '')),
'council' => trim((string)($in['council'] ?? '')),
'planning_scheme' => trim((string)($in['planning_scheme'] ?? 'Tasmanian Planning Scheme')),
'planning_zones' => array_values(array_filter((array)($in['planning_zones'] ?? []))),
'planning_codes' => array_values(array_filter((array)($in['planning_codes'] ?? []))),
// proposal
'use_class' => trim((string)($in['use_class'] ?? 'Educational and Occasional Care')),
'proposal_summary' => trim((string)($in['proposal_summary'] ?? '')), // 1–3 paras freeform
'operations' => (array)($in['operations'] ?? []), // ['hours','staff','children']
'parking' => (array)($in['parking'] ?? []), // ['cars','bikes','accessible','motorcycle']
'signage' => (array)($in['signage'] ?? []), // [{type,desc}, ...]
'consultants' => (array)($in['consultants'] ?? []), // ['TIA'=>'…','Acoustic'=>'…','Bushfire'=>'…']
// overlays
'overlays' => (array)($in['overlays'] ?? []), // ['bushfire'=>bool, 'airport_noise'=>bool, ...]
// assessments matrix (zone + codes)
'standards' => (array)($in['standards'] ?? []), // [{clause, standard, acceptable, relies_on_pc:[], notes}]
// assets (optional)
'map_png' => (string)($in['map_png'] ?? ''), // data:image/png;base64,...
'appendices' => array_values(array_filter((array)($in['appendices'] ?? []))),
// meta
'prepared_for' => trim((string)($in['prepared_for'] ?? '')),
'prepared_by' => trim((string)($in['prepared_by'] ?? 'Modulos Design')),
'author' => trim((string)($in['author'] ?? '')),
'job_number' => trim((string)($in['job_number'] ?? '')),
'version' => trim((string)($in['version'] ?? 'Draft')),
'prepared_date' => trim((string)($in['prepared_date'] ?? date('j F Y'))),
];
// Minimal required fields
foreach (['address','planning_scheme'] as $k) {
if ($d[$k] === '' || $d[$k] === null) {
throw new RuntimeException("Missing required field: {$k}");
}
}
// Helpers -----------------------------------------------------------
$join = fn(array $arr, string $sep=', ') => implode($sep, array_values(array_filter($arr, fn($x)=>$x!=='' && $x!==null)));
$fmt_area = function($d) {
if (is_array($d['total_area'] ?? null)) {
return $d['total_area']['ha_label'] ?? $d['total_area']['sqm_label'] ?? '';
}
return $d['area_ha'] ?: $d['area_sqm'];
};
$md_table_row = fn(array $cols) => '| ' . implode(' | ', array_map(fn($c)=>trim((string)$c) ?: '–', $cols)) . ' |';
$render_standards = function(array $rows) use ($md_table_row) {
if (!$rows) return "_No specific standards provided in this draft._\n";
$out = [];
$out[] = $md_table_row(['Clause','Standard','AS','PC','Notes']);
$out[] = $md_table_row(['---','---','---','---','---']);
foreach ($rows as $r) {
$out[] = $md_table_row([
$r['clause'] ?? '',
$r['standard'] ?? '',
$r['acceptable'] ?? '',
$r['relies_on_pc'] ? implode(', ', (array)$r['relies_on_pc']) : '',
$r['notes'] ?? ''
]);
}
return implode("\n", $out)."\n";
};
// Markdown builder --------------------------------------------------
$md = [];
$md[] = '# Supporting Planning Report';
$md[] = '';
$md[] = "Prepared for: **" . ($d['prepared_for'] ?: '—') . "**";
$md[] = "Prepared by: **" . ($d['prepared_by'] ?: '—') . "**" . ($d['author'] ? " ({$d['author']})" : "");
$md[] = "Job No.: **" . ($d['job_number'] ?: '—') . "**";
$md[] = "Version: **" . ($d['version'] ?: '—') . "**";
$md[] = "Date: **" . ($d['prepared_date'] ?: '—') . "**";
$md[] = '';
$md[] = '> ' . $d['acknowledgement'];
$md[] = '';
// Permit overview
$md[] = '## Permit overview';
$md[] = '### Permit application details';
$md[] = $md_table_row(['Applicant','Owner','Address','Title']);
$md[] = $md_table_row(['---','---','---','---']);
$md[] = $md_table_row([
$d['prepared_for'] ?: '—',
'—',
$d['address'],
($d['title_id'] ?: '—')
]);
$md[] = '';
$md[] = '### Relevant Planning Provisions';
$md[] = "- Applicable planning scheme: **{$d['planning_scheme']}**";
if ($d['planning_zones']) $md[] = '- Zone(s): **' . $join($d['planning_zones']) . '**';
if ($d['planning_codes']) $md[] = '- Codes: **' . $join($d['planning_codes']) . '**';
$md[] = '';
// Proposal
$md[] = '## 1. Introduction';
$md[] = '**Purpose of the report.** This report seeks planning approval for the proposed use and development described below and assesses the application against the relevant provisions of the Tasmanian Planning Scheme.';
$md[] = '';
$md[] = '## 2. Proposal';
if ($d['proposal_summary']) $md[] = $d['proposal_summary'];
$ops = [];
if (!empty($d['operations']['hours'])) $ops[] = "**Hours:** {$d['operations']['hours']}";
if (!empty($d['operations']['staff'])) $ops[] = "**Staff:** {$d['operations']['staff']}";
if (!empty($d['operations']['children'])) $ops[] = "**Children capacity:** {$d['operations']['children']}";
if ($ops) $md[] = implode(' \n', $ops);
if ($d['parking']) {
$pbits = [];
if (isset($d['parking']['cars'])) $pbits[] = "{$d['parking']['cars']} car spaces";
if (isset($d['parking']['accessible'])) $pbits[] = "{$d['parking']['accessible']} accessible";
if (isset($d['parking']['bikes'])) $pbits[] = "{$d['parking']['bikes']} bicycle spaces";
if (isset($d['parking']['motorcycle'])) $pbits[] = "{$d['parking']['motorcycle']} motorcycle spaces";
if ($pbits) $md[] = '**Parking:** ' . implode(', ', $pbits) . '.';
}
if ($d['signage']) {
$md[] = '**Signage:**';
foreach ($d['signage'] as $s) {
$md[] = "- " . trim(is_array($s) ? (($s['type'] ?? 'Sign') . ': ' . ($s['desc'] ?? '')) : (string)$s);
}
}
if (!empty($d['map_png'])) {
$md[] = '';
$md[] = '';
$md[] = '_Figure: Site context (auto-captured from map)._';
}
$md[] = '';
// Site
$md[] = '## 3. Site description';
$md[] = $md_table_row(['Property ID','Title ID','Locality','Area','Tenure','LIST GUID']);
$md[] = $md_table_row(['---','---','---','---','---','---']);
$md[] = $md_table_row([
$d['pid'] ?: '—',
$d['title_id'] ?: '—',
$d['locality'] ?: '—',
$fmt_area($d) ?: '—',
$d['tenure'] ?: '—',
$d['list_guid'] ?: '—'
]);
$md[] = '';
// Zoning
$md[] = '## 4. Zoning assessment';
$md[] = "**Zone:** " . ($d['planning_zones'] ? $join($d['planning_zones']) : '—');
$md[] = "**Use class:** {$d['use_class']} (assessment against applicable use and development standards).";
$md[] = '';
// Standards matrix
$md[] = '### 4.x Applicable standards (zone + codes overview)';
$md[] = $render_standards($d['standards']);
// Codes — notes (driven by the standards rows)
$md[] = '## 5. Code assessment';
if (in_array('Signs', $d['planning_codes'])) $md[] = '- **Signs Code** – see relevant rows above.';
if (in_array('Parking and Sustainable Transport', $d['planning_codes'])) $md[] = '- **Parking and Sustainable Transport Code** – see relevant rows above.';
if (in_array('Road and Railway Assets', $d['planning_codes'])) $md[] = '- **Road and Railway Assets Code** – see relevant rows above.';
if (!empty($d['overlays']['bushfire'])) $md[] = '- **Bushfire-Prone Areas Code** – see relevant rows above.';
if (!empty($d['overlays']['airport_noise'])) $md[] = '- **Safeguarding of Airports Code** – see relevant rows above.';
$md[] = '';
// Consultants
if ($d['consultants']) {
$md[] = '### Supporting technical reports';
foreach ($d['consultants'] as $k => $v) {
$md[] = "- **{$k}:** {$v}";
}
$md[] = '';
}
// Conclusion (quick tally)
$t_as = 0; $t_pc = 0;
foreach ($d['standards'] as $r) {
if (!empty($r['acceptable'])) $t_as++;
if (!empty($r['relies_on_pc'])) $t_pc++;
}
$md[] = '## 6. Conclusion';
$md[] = "This application has been assessed against the Tasmanian Planning Scheme provisions relevant to the site and proposal. Based on the information provided and the supporting technical assessments, the proposal **complies with acceptable solutions for ~{$t_as} standards** and **relies on performance criteria for ~{$t_pc} standards**. Approval is therefore requested subject to any reasonable conditions.";
$md[] = '';
// Appendices (labels only)
if ($d['appendices']) {
$md[] = '## Appendices';
foreach ($d['appendices'] as $i => $label) {
$md[] = ($i+1) . ". " . trim((string)$label);
}
$md[] = '';
}
$markdown = implode("\n", $md);
// Minimal Markdown → HTML
$html = md_to_html_min($markdown);
// Optional: save a .md copy (ensure directory exists & is writable)
$SAVE_DIR = __DIR__ . '/../../reports';
if (is_dir($SAVE_DIR) && is_writable($SAVE_DIR)) {
$fname = sanitize_filename("Planning Report - {$d['address']} - " . date('Ymd_His')) . ".md";
@file_put_contents($SAVE_DIR . '/' . $fname, $markdown);
}
echo json_encode([
'ok' => true,
'markdown' => $markdown,
'html' => $html,
'meta' => [
'address' => $d['address'],
'scheme' => $d['planning_scheme'],
'zones' => $d['planning_zones'],
'codes' => $d['planning_codes'],
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Throwable $e) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
exit;
}
/** -------- Helpers -------- */
function sanitize_filename(string $s): string {
$s = preg_replace('~[^\w\-. ]+~u', '', $s);
$s = preg_replace('~\s+~', ' ', $s);
return trim($s) ?: 'report';
}
function esc_html(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function md_to_html_min(string $md): string {
// Extremely small converter: headers, bold/italics, lists, images, tables, paragraphs.
$h = esc_html($md);
// Images 
$h = preg_replace('~!\[([^\]]*)\]\(([^)]+)\)~', '', $h);
// Bold / italic
$h = preg_replace('~\*\*([^*]+)\*\*~', '$1', $h);
$h = preg_replace('~\*([^*]+)\*~', '$1', $h);
// Headers
foreach ([6,5,4,3,2,1] as $n) {
$re = '~^' . str_repeat('#', $n) . '\s*(.+)$~m';
$h = preg_replace($re, "
| '.esc_html($c ?: '–').' | ', $cells)) . '
'.$p.'
'; } } $h = implode("\n", $parts); return '