client-brief.php 109 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145
  1. <?php
  2. /* -------------------------------------------------------------------------- */
  3. /* CONFIGURATION */
  4. /* -------------------------------------------------------------------------- */
  5. date_default_timezone_set("Australia/Hobart");
  6. //error_reporting(E_ERROR | E_PARSE);
  7. error_reporting(E_ALL);
  8. ini_set('display_errors', '0');
  9. ini_set('log_errors', '1');
  10. require_once 'connection.php';
  11. include_once "vendor/autoload.php";
  12. if (session_status() !== PHP_SESSION_ACTIVE) session_start();
  13. if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16));
  14. $csrf = $_SESSION['csrf'];
  15. $accessToken = getenv('HUBSPOT_TOKEN') ?: '';
  16. #$enquiry_date = date("l dS M \'y");
  17. $drg = isset($_GET['drg']) ? $_GET['drg'] : '';
  18. if (!empty($_GET['drg'])) {
  19. include "table.php";
  20. }
  21. // CHECK NEXT AVAILABLE Drawing NUMBER
  22. $nextQ = mysqli_query($con, "SELECT MAX(drg) AS maxdrg FROM details");
  23. if (!$nextQ) { printf("Error: %s\n", mysqli_error($con)); exit; }
  24. $MQrow = mysqli_fetch_assoc($nextQ);
  25. $maxQ = isset($MQrow['maxdrg']) ? ((int)$MQrow['maxdrg'] + 1) : 1;
  26. /* -------------------------------------------------------------------------- */
  27. /* API MODE */
  28. /* -------------------------------------------------------------------------- */
  29. if (!defined('SITE_ROOT')) define('SITE_ROOT', dirname(__DIR__));
  30. // ===== LOA markdown creation helpers and API =====
  31. if (!defined('CONTRACTS_DIR')) define('CONTRACTS_DIR', SITE_ROOT . '/contracts/contracts');
  32. // ===== LOA config (must match contracts-admin/loa.php) =====
  33. if (!defined('LOA_DIR')) define('LOA_DIR', SITE_ROOT . '/contracts/loa');
  34. if (!defined('LOA_BASE_URL')) define('LOA_BASE_URL', 'https://modulosdesign.com.au/contracts'); // where loa.php lives
  35. if (!defined('LOA_TOKEN_SECRET')) define('LOA_TOKEN_SECRET', getenv('LOA_TOKEN_SECRET') ?: '');
  36. if (!function_exists('json_response')) {
  37. function json_response(array $data, int $code = 200) {
  38. header('Content-Type: application/json; charset=utf-8');
  39. http_response_code($code);
  40. echo json_encode($data);
  41. }
  42. }
  43. if (!function_exists('safe_clientid')) {
  44. function safe_clientid(string $s): string {
  45. // collapse whitespace to hyphen, strip invalid chars
  46. $s = preg_replace('/\s+/', '-', $s);
  47. $s = preg_replace('/[^A-Za-z0-9\-_]+/', '-', $s);
  48. $s = preg_replace('/-+/', '-', $s);
  49. return trim($s, '-_');
  50. }
  51. }
  52. if (!function_exists('contract_path')) {
  53. function contract_path(string $clientid): string {
  54. return rtrim(CONTRACTS_DIR, '/\\') . DIRECTORY_SEPARATOR . $clientid . '.md';
  55. }
  56. }
  57. function loa_path(string $job): string {
  58. $id = preg_replace('/\D+/', '', $job); // digits only to match loa.php
  59. return rtrim(LOA_DIR, '/\\') . DIRECTORY_SEPARATOR . $id . '.md';
  60. }
  61. function loa_public_url(string $job): string {
  62. $jobClean = preg_replace('/\D+/', '', $job); // digits only
  63. $token = hash_hmac('sha256', 'loa|' . $jobClean, LOA_TOKEN_SECRET);
  64. return rtrim(LOA_BASE_URL, '/') . '/loa.php?job=' . rawurlencode($jobClean) . '&token=' . rawurlencode($token);
  65. }
  66. /** Minimal front-matter pulls for LOA */
  67. function extract_loa_fields(string $file): array {
  68. $out = ['client_name'=>'','client_email'=>'','property_address'=>''];
  69. $txt = @file_get_contents($file);
  70. if (!$txt) return $out;
  71. if (!preg_match('/^---\s*\R(.*?)\R---/s', $txt, $m)) return $out;
  72. $fm = $m[1]; $ctx = null;
  73. foreach (preg_split('/\R/', $fm) as $line) {
  74. if (preg_match('/^\s*client\s*:\s*$/', $line)) { $ctx='client'; continue; }
  75. if (preg_match('/^\s*property\s*:\s*$/', $line)) { $ctx='property'; continue; }
  76. if ($ctx==='client') {
  77. if (preg_match('/^\s*name\s*:\s*(.+)$/', $line, $mm)) $out['client_name'] = trim($mm[1], " \t\"'");
  78. if (preg_match('/^\s*email\s*:\s*(.+)$/', $line, $mm)) $out['client_email']= trim($mm[1], " \t\"'");
  79. } elseif ($ctx==='property') {
  80. if (preg_match('/^\s*address\s*:\s*(.+)$/', $line, $mm)) $out['property_address']= trim($mm[1], " \t\"'");
  81. }
  82. }
  83. return $out;
  84. }
  85. /** Tiny contract front-matter extractor used by lookup */
  86. function extract_front_matter_fields(string $file): array {
  87. $out = [];
  88. $txt = @file_get_contents($file);
  89. if (!$txt) return $out;
  90. if (!preg_match('/^---\s*\R(.*?)\R---/s', $txt, $m)) return $out;
  91. $fm = $m[1];
  92. if (preg_match('/^\s*client\s*:\s*$(.*?)^(?=\S)/ms', $fm."\nX", $block)) {
  93. $clientBlock = $block[1];
  94. if (preg_match('/^\s*name\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $clientBlock, $mm)) $out['client_name'] = trim($mm[1]);
  95. if (preg_match('/^\s*email\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $clientBlock, $mm)) $out['client_email'] = trim($mm[1]);
  96. if (preg_match('/^\s*address\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi',$clientBlock, $mm)) $out['client_address']= trim($mm[1]);
  97. }
  98. if (preg_match('/^\s*project\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $fm, $mm)) $out['project'] = trim($mm[1]);
  99. if (preg_match('/^\s*job\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $fm, $mm)) $out['job'] = trim($mm[1]);
  100. return $out;
  101. }
  102. /** Find best-known client + address for a job (checks existing LOA, then contracts) */
  103. function lookup_job_for_loa(string $job): array {
  104. $job = preg_replace('/\D+/', '', (string)($_POST['job'] ?? $drg ?? ''));
  105. $empty = ['client_name'=>'','client_email'=>'','property_address'=>'','source'=>null];
  106. // Prefer existing LOA
  107. $loaFile = loa_path($job);
  108. if (is_file($loaFile)) {
  109. return extract_loa_fields($loaFile) + ['source'=>'loa'];
  110. }
  111. // Scan contracts for a matching job or filename
  112. foreach (glob(rtrim(CONTRACTS_DIR,'/\\').'/*.md') as $file) {
  113. $fm = extract_front_matter_fields($file);
  114. $fname_id = preg_replace('/\.md$/i','',basename($file));
  115. if (($fm['job'] ?? '') === $job || $fname_id === $job) {
  116. $addr = $fm['client_address'] ?? '';
  117. return [
  118. 'client_name' => $fm['client_name'] ?? '',
  119. 'client_email' => $fm['client_email'] ?? '',
  120. 'property_address' => $addr,
  121. 'source' => 'contract',
  122. 'clientid' => $fname_id,
  123. ];
  124. }
  125. }
  126. return $empty;
  127. }
  128. /* ------------------------ FUNCTIONS ------------------------ */
  129. /** Build a starter Markdown file with YAML front matter. */
  130. if (!function_exists('build_markdown_template')) {
  131. function build_markdown_template(string $clientid, ?string $name, ?string $email, ?string $design_style, ?string $phone, ?string $site_address): string {
  132. $today = date('Y-m-d');
  133. $name = $name ?? '';
  134. $email = $email ?? '';
  135. $design_style = $design_style ?? '';
  136. $phone = $phone ?? '';
  137. $site_address = $site_address ?? '';
  138. // Generate secure random credentials
  139. $adminUser = bin2hex(random_bytes(4)); // 8 hex chars
  140. $adminPass = bin2hex(random_bytes(8)); // 16 hex chars
  141. $adminSecret = bin2hex(random_bytes(16)); // 32 hex chars
  142. $frontMatter = <<<YAML
  143. ---
  144. client:
  145. id: "{$clientid}"
  146. name: "{$name}"
  147. email: "{$email}"
  148. phone: "{$phone}"
  149. address: "{$site_address}"
  150. project: "{$design_style}"
  151. dates:
  152. prepared: "{$today}"
  153. dev:
  154. name: 'Modulos Design'
  155. email: 'ben@modulos.com.au'
  156. phone: '0402 984 082'
  157. address: '34 Coplestone Street, Scottsdale, Tas 7260'
  158. version: 1
  159. quote:
  160. number: "{$clientid}"
  161. admin:
  162. user: "{$adminUser}"
  163. pass: "{$adminPass}"
  164. secret: "{$adminSecret}"
  165. ---
  166. YAML;
  167. $body = <<<MD
  168. # Contract of work
  169. This Contract is made and entered into as of the date above by and between **[dev.name]** and **[client.name]** (hereinafter referred to as "Client").
  170. ##### 1. Scope of Services
  171. MD;
  172. return $frontMatter . $body;
  173. }
  174. }
  175. function yaml_q($v){ return '"'.str_replace(['\\','"'], ['\\\\','\\"'], (string)$v).'"'; }
  176. /** Upsert a top-level key like: job: "123" */
  177. function yaml_upsert_top(string $yaml, string $key, string $val): string {
  178. if ($val === '' || $val === null) return $yaml;
  179. $re = '/^\s*'.preg_quote($key,'/').'\s*:\s*.*/mi';
  180. if (preg_match($re, $yaml)) return preg_replace($re, $key.': '.yaml_q($val), $yaml, 1);
  181. return rtrim($yaml)."\n".$key.': '.yaml_q($val)."\n";
  182. }
  183. /** Upsert keys inside a block (client:, property:, dates:) with two-space indentation */
  184. function yaml_upsert_block(string $yaml, string $block, array $pairs): string {
  185. $blockRe = '/(^\s*'.preg_quote($block,'/').'\s*:\s*\R(?:[ \t].*\R)*)(?=^\S|\z)/m';
  186. if (preg_match($blockRe, $yaml, $m, PREG_OFFSET_CAPTURE)) {
  187. $start = $m[0][1]; $len = strlen($m[0][0]); $blk = $m[0][0];
  188. foreach ($pairs as $k=>$v) {
  189. if ($v === '' || $v === null) continue;
  190. $lineRe = '/^\s*'.preg_quote($k,'/').'\s*:\s*.*/mi';
  191. $rep = ' '.$k.': '.yaml_q($v);
  192. $blk = preg_replace($lineRe, $rep, $blk, 1, $count);
  193. if ($count === 0) $blk = rtrim($blk)."\n".$rep."\n";
  194. }
  195. return substr_replace($yaml, $blk, $start, $len);
  196. } else {
  197. $out = rtrim($yaml)."\n".$block.":\n";
  198. foreach ($pairs as $k=>$v) if ($v !== '' && $v !== null) $out .= ' '.$k.': '.yaml_q($v)."\n";
  199. return $out;
  200. }
  201. }
  202. /** Given a full .md file, update its front matter and return updated text */
  203. function update_front_matter_text(string $md, array $kv_top, array $kv_blocks): string {
  204. if (!preg_match('/^---\R(.*?)\R---(\R|$)/s', $md, $m, PREG_OFFSET_CAPTURE)) {
  205. // add a front matter header if missing
  206. $yaml = "";
  207. foreach ($kv_top as $k=>$v) $yaml = yaml_upsert_top($yaml, $k, $v);
  208. foreach ($kv_blocks as $b=>$p) $yaml = yaml_upsert_block($yaml, $b, $p);
  209. return "---\n".rtrim($yaml)."\n---\n".ltrim($md);
  210. }
  211. $yaml = $m[1][0];
  212. $start = $m[0][1];
  213. $len = strlen($m[0][0]);
  214. foreach ($kv_top as $k=>$v) $yaml = yaml_upsert_top($yaml, $k, $v);
  215. foreach ($kv_blocks as $b=>$p) $yaml = yaml_upsert_block($yaml, $b, $p);
  216. $newHeader = "---\n".rtrim($yaml)."\n---\n";
  217. return substr_replace($md, $newHeader, $start, $len);
  218. }
  219. /* -------------------------------------------------------------------------- */
  220. /* API MODE */
  221. /* -------------------------------------------------------------------------- */
  222. $__action = $_GET['action'] ?? $_POST['action'] ?? null;
  223. if ($__action === 'loa_create') {
  224. try {
  225. $job = safe_clientid($_POST['job'] ?? (string)($drg ?? ''));
  226. if (!$job) { json_response(['ok'=>false,'error'=>'Missing job #'], 400); exit; }
  227. // Form inputs (fall back to blanks)
  228. $name = trim($_POST['client_name'] ?? '');
  229. $email = trim($_POST['client_email'] ?? '');
  230. $clientPhone = trim($_POST['client_phone'] ?? ($_POST['client_mobile'] ?? ''));
  231. $clientAddress = trim($_POST['client_address'] ?? ($_POST['postal_address'] ?? ''));
  232. $addr = trim($_POST['property_address'] ?? ($_POST['site_address'] ?? ''));
  233. $propPid = trim($_POST['property_pid'] ?? ($_POST['property_id'] ?? ''));
  234. $propTitleRaw = trim($_POST['property_title'] ?? ($_POST['title_id'] ?? ''));
  235. // Split things like "140687/4", "140687 - 4", "140687 4"
  236. $propVol = ''; $propFolio = '';
  237. if ($propTitleRaw !== '') {
  238. if (preg_match('/^\s*([A-Za-z0-9]+)\s*[\/\-\s]\s*([A-Za-z0-9]+)\s*$/', $propTitleRaw, $mm)) {
  239. $propVol = $mm[1];
  240. $propFolio = $mm[2];
  241. } else {
  242. // If it doesn't split, keep the whole thing in vol so you don't lose info
  243. $propVol = $propTitleRaw;
  244. }
  245. }
  246. $dateStart = trim($_POST['start_date'] ?? '');
  247. $dateEnd = trim($_POST['end_date'] ?? '');
  248. $overwrite = (int)($_POST['overwrite'] ?? 0);
  249. $dst = loa_path($job);
  250. if (!is_dir(LOA_DIR)) @mkdir(LOA_DIR, 0775, true);
  251. // What we want in the YAML
  252. $top = ['job' => $job];
  253. $blocks = [
  254. 'client' => [
  255. 'name' => $name,
  256. 'email' => $email,
  257. 'phone' => $clientPhone,
  258. 'address' => $clientAddress,
  259. ],
  260. 'property' => [
  261. 'address' => $addr,
  262. 'pid' => $propPid,
  263. 'title' => $propTitleRaw,
  264. ],
  265. 'dates' => [
  266. 'start' => $dateStart,
  267. 'end' => $dateEnd,
  268. ],
  269. ];
  270. // Create new or update existing
  271. if (!file_exists($dst)) {
  272. // Load template, then inject values into its front matter
  273. $tplPath = rtrim(LOA_DIR,'/\\').'/default-authorisation.md';
  274. $tpl = @file_get_contents($tplPath);
  275. if ($tpl === false || $tpl === '') $tpl = "# Authorisation\n";
  276. $out = update_front_matter_text($tpl, $top, $blocks);
  277. if (@file_put_contents($dst, $out, LOCK_EX) === false) {
  278. json_response(['ok'=>false,'error'=>'Write failed'], 500); exit;
  279. }
  280. json_response(['ok'=>true,'job'=>$job,'path'=>$dst,'public_url'=>loa_public_url($job),'created'=>true]);
  281. } else {
  282. if ($overwrite) {
  283. // Overwrite from template
  284. $tplPath = rtrim(LOA_DIR,'/\\').'/default-authorisation.md';
  285. $tpl = @file_get_contents($tplPath);
  286. if ($tpl === false || $tpl === '') $tpl = "# Authorisation\n";
  287. $out = update_front_matter_text($tpl, $top, $blocks);
  288. } else {
  289. // Update front matter only in the existing file
  290. $existing = @file_get_contents($dst) ?: '';
  291. $out = update_front_matter_text($existing, $top, $blocks);
  292. }
  293. if (@file_put_contents($dst, $out, LOCK_EX) === false) {
  294. json_response(['ok'=>false,'error'=>'Write failed'], 500); exit;
  295. }
  296. json_response([
  297. 'ok'=>true,'job'=>$job,'path'=>$dst,'public_url'=>loa_public_url($job),
  298. 'created'=>false,'updated_frontmatter'=>!$overwrite
  299. ]);
  300. }
  301. } catch (Throwable $e) {
  302. json_response(['ok'=>false,'error'=>$e->getMessage()], 500);
  303. }
  304. exit;
  305. }
  306. // ===== end LOA helpers and API =====
  307. // Create the LOA markdown from default-authorisation.md
  308. if ($__action === 'loa_create') {
  309. try {
  310. $job = safe_clientid($_POST['job'] ?? (string)($drg ?? ''));
  311. if (!$job) { json_response(['ok'=>false,'error'=>'Missing job #'], 400); exit; }
  312. // Prefer explicit POST values; otherwise use current form values already on the page
  313. $name = trim($_POST['client_name'] ?? (trim(($firstname ?? '').' '.($lastname ?? '')) ?: ($joint_name ?? '')));
  314. $email = trim($_POST['client_email'] ?? ($client_email ?? ''));
  315. $addr = trim($_POST['property_address'] ?? ($site_address ?? ''));
  316. $overwrite = (int)($_POST['overwrite'] ?? 0);
  317. $dst = loa_path($job);
  318. if (!is_dir(LOA_DIR)) @mkdir(LOA_DIR, 0775, true);
  319. if (file_exists($dst) && !$overwrite) {
  320. json_response(['ok'=>false,'error'=>'File already exists','path'=>$dst], 409); exit;
  321. }
  322. // Load template (fallback to minimal front-matter if missing)
  323. $tplPath = rtrim(LOA_DIR,'/\\').'/default-authorisation.md';
  324. $tpl = @file_get_contents($tplPath);
  325. if ($tpl === false || $tpl === '') {
  326. $q = fn($v) => '"'.str_replace(['\\','"'], ['\\\\','\\"'], (string)$v).'"';
  327. $tpl = "---\njob: ".$q($job)."\nclient:\n name: ".$q($name)."\n email: ".$q($email)."\nproperty:\n address: ".$q($addr)."\n---\n# Authorisation\n";
  328. } else {
  329. // Light YAML substitutions
  330. $tpl = preg_replace_callback('/^---\R(.*?)\R---/s', function($m) use($job,$name,$email,$addr){
  331. $yaml = $m[1];
  332. $q = fn($v) => '"'.str_replace(['\\','"'], ['\\\\','\\"'], (string)$v).'"';
  333. // job
  334. $yaml = preg_replace('/^\s*job\s*:\s*.*/mi', 'job: '.$q($job), $yaml, 1, $jobCount);
  335. if ($jobCount === 0) $yaml = "job: ".$q($job)."\n".$yaml;
  336. // ensure blocks exist
  337. if (!preg_match('/^\s*client\s*:/mi', $yaml)) $yaml = "client:\n name:\n email:\n".$yaml;
  338. if (!preg_match('/^\s*property\s*:/mi', $yaml)) $yaml = "property:\n address:\n".$yaml;
  339. // client.name/email
  340. $yaml = preg_replace_callback('/(^\s*client\s*:\s*\R(?:.*\R)*?)(?=^\S|\z)/ms', function($b) use($q,$name,$email){
  341. $blk = $b[0];
  342. $blk = preg_replace('/^\s*name\s*:\s*.*/mi', ' name: '.$q($name), $blk, 1, $c1);
  343. if ($c1===0) $blk = rtrim($blk)."\n name: ".$q($name)."\n";
  344. $blk = preg_replace('/^\s*email\s*:\s*.*/mi', ' email: '.$q($email), $blk, 1, $c2);
  345. if ($c2===0) $blk = rtrim($blk)."\n email: ".$q($email)."\n";
  346. return $blk;
  347. }, $yaml, 1);
  348. // property.address
  349. $yaml = preg_replace_callback('/(^\s*property\s*:\s*\R(?:.*\R)*?)(?=^\S|\z)/ms', function($b) use($q,$addr){
  350. $blk = $b[0];
  351. $blk = preg_replace('/^\s*address\s*:\s*.*/mi', ' address: '.$q($addr), $blk, 1, $p1);
  352. if ($p1===0) $blk = rtrim($blk)."\n address: ".$q($addr)."\n";
  353. return $blk;
  354. }, $yaml, 1);
  355. return "---\n".$yaml."\n---";
  356. }, $tpl, 1) ?? $tpl;
  357. }
  358. if (@file_put_contents($dst, $tpl, LOCK_EX) === false) {
  359. json_response(['ok'=>false,'error'=>'Write failed'], 500); exit;
  360. }
  361. json_response(['ok'=>true,'job'=>$job,'path'=>$dst,'public_url'=>loa_public_url($job)]);
  362. } catch (Throwable $e) {
  363. json_response(['ok'=>false,'error'=>$e->getMessage()], 500);
  364. }
  365. exit;
  366. }
  367. // Optional: auto-fill data for a job (used by other screens / future)
  368. if ($__action === 'loa_lookup') {
  369. $job = safe_clientid($_GET['job'] ?? $_POST['job'] ?? '');
  370. $data = $job ? lookup_job_for_loa($job) : ['client_name'=>'','client_email'=>'','property_address'=>'','source'=>null];
  371. $found = (bool)($data['client_name'] || $data['client_email'] || $data['property_address']);
  372. json_response(['ok'=>true,'found'=>$found,'data'=>$data]);
  373. exit;
  374. }
  375. /* -------------------------------------------------------------------------- */
  376. /* HELPER FUNCTIONS */
  377. /* -------------------------------------------------------------------------- */
  378. use Google\Client;
  379. use Google\Service\Drive;
  380. function createFolder(string $access_token, string $folder_name, string $parent_folder_id = ''): ?string
  381. {
  382. try {
  383. $client = new Google\Client();
  384. $client->setAccessToken($access_token);
  385. $driveService = new Drive($client);
  386. $metadata = new Drive\DriveFile([
  387. 'name' => $folder_name,
  388. 'mimeType' => 'application/vnd.google-apps.folder',
  389. ]);
  390. if ($parent_folder_id) {
  391. $metadata->setParents([$parent_folder_id]);
  392. }
  393. $file = $driveService->files->create($metadata, ['fields' => 'id,name,webViewLink']);
  394. return $file->id;
  395. } catch (Exception $e) {
  396. error_log('createFolder error: ' . $e->getMessage());
  397. return null;
  398. }
  399. }
  400. function createDealHubSpot($newDealData)
  401. {
  402. global $accessToken;
  403. $endpoint = 'https://api.hubapi.com/crm/v3/objects/deals';
  404. $curl = curl_init();
  405. curl_setopt_array($curl, [
  406. CURLOPT_URL => $endpoint,
  407. CURLOPT_RETURNTRANSFER => true,
  408. CURLOPT_POST => true,
  409. CURLOPT_POSTFIELDS => json_encode($newDealData),
  410. CURLOPT_HTTPHEADER => [
  411. 'Authorization: Bearer ' . $accessToken, // Use OAuth 2.0 token OR 'Authorization: Bearer ' . $apiKey for API key
  412. 'Content-Type: application/json'
  413. ]
  414. ]);
  415. $response = curl_exec($curl);
  416. curl_close($curl);
  417. return $response;
  418. }
  419. function updateDealHubSpot($dealId, $data)
  420. {
  421. global $accessToken;
  422. $endpoint = 'https://api.hubapi.com/crm/v3/objects/deals/' . $dealId;
  423. $curl = curl_init();
  424. curl_setopt_array($curl, [
  425. CURLOPT_URL => $endpoint,
  426. CURLOPT_RETURNTRANSFER => true,
  427. CURLOPT_CUSTOMREQUEST => 'PATCH',
  428. CURLOPT_POSTFIELDS => json_encode($data),
  429. CURLOPT_HTTPHEADER => [
  430. 'Authorization: Bearer ' . $accessToken, // Use OAuth 2.0 token OR 'Authorization: Bearer ' . $apiKey for API key
  431. 'Content-Type: application/json'
  432. ]
  433. ]);
  434. $response = curl_exec($curl);
  435. curl_close($curl);
  436. return $response;
  437. }
  438. function addContactToHubSpot($newContactData) {
  439. global $accessToken;
  440. $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts';
  441. $curl = curl_init();
  442. curl_setopt_array($curl, [
  443. CURLOPT_URL => $endpoint,
  444. CURLOPT_RETURNTRANSFER => true,
  445. CURLOPT_POST => true,
  446. CURLOPT_POSTFIELDS => json_encode($newContactData),
  447. CURLOPT_HTTPHEADER => [
  448. 'Authorization: Bearer ' . $accessToken,
  449. 'Content-Type: application/json'
  450. ]
  451. ]);
  452. $response = curl_exec($curl);
  453. curl_close($curl);
  454. return $response;
  455. }
  456. function searchContact($query) {
  457. global $accessToken;
  458. $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';
  459. $queryJson = json_encode($query);
  460. $ch = curl_init($endpoint);
  461. curl_setopt($ch, CURLOPT_POST, 1);
  462. curl_setopt($ch, CURLOPT_POSTFIELDS, $queryJson);
  463. curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $accessToken, 'Content-Type: application/json'));
  464. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  465. $response = curl_exec($ch);
  466. if (curl_errno($ch)) {
  467. echo 'Error making cURL request: ' . curl_error($ch);
  468. }
  469. curl_close($ch);
  470. return $response;
  471. }
  472. function updateContact($contactId, $data) {
  473. global $accessToken;
  474. $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/' . $contactId;
  475. $curl = curl_init();
  476. curl_setopt_array($curl, [
  477. CURLOPT_URL => $endpoint,
  478. CURLOPT_RETURNTRANSFER => true,
  479. CURLOPT_CUSTOMREQUEST => 'PUT',
  480. CURLOPT_POSTFIELDS => json_encode($data),
  481. CURLOPT_HTTPHEADER => [
  482. 'Authorization: Bearer ' . $accessToken,
  483. 'Content-Type: application/json'
  484. ]
  485. ]);
  486. $response = curl_exec($curl);
  487. curl_close($curl);
  488. return $response;
  489. }
  490. function format_E164($num)
  491. {
  492. $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
  493. try {
  494. $phoneProto = $phoneUtil->parse($num, "AU");
  495. return [
  496. "phone_number" => $phoneUtil->format($phoneProto, \libphonenumber\PhoneNumberFormat::INTERNATIONAL),
  497. "phone_type" => $phoneUtil->getNumberType($phoneProto)
  498. ];
  499. } catch (\libphonenumber\NumberParseException $e) {
  500. return null;
  501. }
  502. }
  503. if (!empty($drg) and !empty($_POST['add_client_to_crm']) and empty($contactId)) {
  504. try {
  505. $query = [
  506. "filterGroups" => [
  507. [
  508. "filters" => [
  509. [
  510. "value" => $_POST['firstname'] . "*",
  511. "propertyName" => 'firstname',
  512. "operator" => "EQ"
  513. ],
  514. [
  515. "value" => $_POST['lastname'] . "*",
  516. "propertyName" => "lastname",
  517. "operator" => "EQ"
  518. ],
  519. [
  520. "value" => $_POST['client_mobile'] . "*",
  521. "propertyName" => "phone",
  522. "operator" => "EQ"
  523. ]
  524. ]
  525. ]
  526. ],
  527. ];
  528. $response = json_decode(searchContact($query), true);
  529. if ($response['total'] > 0) {
  530. echo "<h4 style=\"text-align: center;color: red;padding: 10px;\">Customer: {$_POST['firstname']} {$_POST['lastname']} already exists in crm, with id: {$response['results'][0]['id']}</h4>";
  531. } else {
  532. $newContactData = [
  533. 'properties' => [
  534. 'firstname' => $_POST['firstname'],
  535. 'lastname' => $_POST['lastname'],
  536. 'description' => 'Added via Online Client Brief Form # ' . $drg,
  537. 'company' => $_POST['joint_name'],
  538. 'email' => $_POST['client_email'],
  539. 'address' => $_POST['postal_address'],
  540. 'city' => $_POST['postal_address_town'],
  541. 'zip' => $_POST['postal_address_postcode'],
  542. 'country' => 'AU',
  543. 'state' => $_POST['postal_address_state'],
  544. 'hubspot_owner_id' => '585959844',
  545. ]
  546. ];
  547. if($phone) {
  548. if($phone["phone_type"] == \libphonenumber\PhoneNumberType::MOBILE)
  549. $newContactData["properties"]["mobilephone"] = $phone["phone_number"];
  550. else
  551. $newContactData["properties"]["phone"] = $phone["phone_number"];
  552. }
  553. $response = addContactToHubSpot($newContactData);
  554. file_put_contents("crmadd.log", $response);
  555. $createdContact = json_decode($response, true);
  556. if ($createdContact['id'] > 0) {
  557. $crm_id = intval($createdContact['id']);
  558. $result = mysqli_query($con, "UPDATE details SET crm_id = '{$crm_id}' WHERE drg = '{$drg}'");
  559. if (!$result) {
  560. printf("Error: %s\n", mysqli_error($con));
  561. exit();
  562. } else {
  563. echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>&times;</a><h4 style=\"text-align: center;\">Customer: {$_POST['firstname']} {$_POST['lastname']} added to crm, with id: {$crm_id}</h4></div>";
  564. $isHideDismissableAlert = 1;
  565. }
  566. }
  567. }
  568. } catch (\Exception $err) {
  569. echo '<pre>' . $err->getMessage() . '</pre>';
  570. die();
  571. }
  572. } elseif (!empty($drg) and !empty($_POST['edit_client_in_crm']) and !empty($contactId)) {
  573. try {
  574. $data = [
  575. 'properties' => [
  576. 'firstname' => $_POST['firstname'],
  577. 'lastname' => $_POST['lastname'],
  578. 'company' => $_POST['joint_name'],
  579. 'email' => $_POST['client_email'],
  580. 'address' => $_POST['postal_address'],
  581. 'city' => $_POST['postal_address_town'],
  582. 'zip' => $_POST['postal_address_postcode'],
  583. 'country' => 'AU',
  584. 'state' => $_POST['postal_address_state'],
  585. 'hubspot_owner_id' => '585959844',
  586. ]
  587. ];
  588. if($phone) {
  589. if($phone["phone_type"] == \libphonenumber\PhoneNumberType::MOBILE)
  590. $data["properties"]["mobilephone"] = $phone["phone_number"];
  591. else
  592. $data["properties"]["phone"] = $phone["phone_number"];
  593. }
  594. $query = [
  595. "filterGroups" => [
  596. [
  597. "filters" => [
  598. [
  599. "value" => $_POST['firstname'] . "*",
  600. "propertyName" => 'firstname',
  601. "operator" => "EQ"
  602. ],
  603. [
  604. "value" => $_POST['lastname'] . "*",
  605. "propertyName" => "lastname",
  606. "operator" => "EQ"
  607. ],
  608. [
  609. "value" => $_POST['client_mobile'] . "*",
  610. "propertyName" => "phone",
  611. "operator" => "EQ"
  612. ]
  613. ]
  614. ]
  615. ],
  616. ];
  617. $response = json_decode(searchContact($query), true);
  618. if ($response['total'] > 0) {
  619. $response = updateContact($contactId, $data);
  620. $result = mysqli_query($con, "UPDATE details SET crm_id = '{$response['id']}' WHERE drg = '{$drg}'");
  621. file_put_contents("crmupdate.log", $response);
  622. $response = json_decode($response, true);
  623. if ($response['id'] > 0) {
  624. echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>&times;</a><h4 style=\"text-align: center;\">Customer: {$_POST['firstname']} {$_POST['lastname']} updated in crm</h4></div>";
  625. $isHideDismissableAlert = 1;
  626. }
  627. } else {
  628. $response = addContactToHubSpot($newContactData);
  629. file_put_contents("crmadd.log", $response);
  630. $createdContact = json_decode($response, true);
  631. if ($createdContact['id'] > 0) {
  632. $crm_id = intval($createdContact['id']);
  633. echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>&times;</a><h4 style=\"text-align: center;\">Customer: {$_POST['firstname']} {$_POST['lastname']} added to crm, with id: {$crm_id}</h4></div>";
  634. $isHideDismissableAlert = 1;
  635. }
  636. }
  637. } catch (\Exception $err) {
  638. echo '<pre>Crm Id: ' . $contactId . ', response: ' . $err->getMessage() . '</pre>';
  639. die();
  640. }
  641. }
  642. // Create Deal from Enquiry Form
  643. if (!empty($quid) and !empty($_POST['add_deal_to_hubspot']) and empty($dealId)) {
  644. try {
  645. $newDealData = [
  646. 'associations' => [
  647. [
  648. 'types' => [
  649. [
  650. 'associationCategory' => 'HUBSPOT_DEFINED',
  651. 'associationTypeId' => 3,
  652. ]
  653. ],
  654. 'to' => [
  655. 'id' => $_POST['crm_id']
  656. ],
  657. ]
  658. ],
  659. 'properties' => [
  660. 'dealname' => $_POST['quid'] . ' - ' . $_POST['client'],
  661. 'dealtype' => 'newbusiness', // New or Existing Business - do we do a sql lookup and see if there is some won jobs for this customer ??
  662. 'hubspot_owner_id' => $_POST['hubspot_owner_id'],
  663. 'createdate' => $_POST['enquiry_date'],
  664. //assign deal to contact with hubspot contact id
  665. //'associatedCompanyIds' => $_POST['company_id'],
  666. 'pipeline' => '64401721',
  667. 'dealstage' => '126963334',
  668. 'width' => str_replace('m', '', $_POST['width']),
  669. 'length' => str_replace('m', '', $_POST['length']),
  670. 'height' => str_replace('m', '', $_POST['height']),
  671. 'quote_type' => $_POST['quote_type'],
  672. 'type' => $_POST['product'],
  673. 'roof_type' => $_POST['roof_style'],
  674. 'sides' => $_POST['side_walls'],
  675. 'ends' => $_POST['end_walls'],
  676. 'internal' => $_POST['internal_walls'],
  677. 'qty_bays' => $_POST['bay_qty'],
  678. //'bay_width' => $_POST['bay_width'],
  679. //'uneven_bays' => $_POST['bay_uneven'],
  680. 'quote' => $_POST['quid'],
  681. ]
  682. ];
  683. $response = createDealHubSpot($newDealData);
  684. file_put_contents("crmdeal.log", $response);
  685. $createdDeal = json_decode($response, true);
  686. if ($createdDeal['id'] > 0) {
  687. $dealId = intval($createdDeal['id']);
  688. $result = mysqli_query($con, "UPDATE details SET dealId = '{$dealId}' WHERE drg = '{$drg}'");
  689. echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>&times;</a><h4 style=\"text-align: center;\">Deal: {$quid} - {$client} added to hubspot</h4></div>";
  690. $isHideDismissableAlert = 1;
  691. }
  692. } catch (\Exception $err) {
  693. echo '<pre>' . $err->getMessage() . '</pre>';
  694. die();
  695. }
  696. } elseif (!empty($drg) and !empty($_POST['update_deal_in_hubspot']) and !empty($dealId)) {
  697. try {
  698. $updateDealData = [
  699. 'properties' => [
  700. 'dealname' => $_POST['drg'] . ' - ' . $_POST['client'],
  701. 'hubspot_owner_id' => $_POST['hubspot_owner_id'],
  702. 'createdate' => $_POST['enquiry_date'],
  703. //assign deal to contact with hubspot contact id
  704. //'associatedCompanyIds' => $_POST['company_id'],
  705. 'quote_type' => $_POST['quote_type'],
  706. 'type' => $_POST['product'],
  707. ],
  708. 'associations' => [
  709. 'types' => [
  710. 'association_category' => 'HUBSPOT_DEFINED',
  711. 'association_type_id' => 3
  712. ],
  713. 'to' => [
  714. 'id' => $_POST['crm_id']
  715. ]
  716. ]
  717. ];
  718. $response = updateDealHubSpot($dealId, $updateDealData);
  719. file_put_contents("crmdeal.log", $response);
  720. $updatedDeal = json_decode($response, true);
  721. if ($updatedDeal['id'] > 0) {
  722. $dealId = intval($updatedDeal['id']);
  723. echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>&times;</a><h4 style=\"text-align: center;\">Deal: {$quid} - {$client} added to hubspot</h4></div>";
  724. $isHideDismissableAlert = 1;
  725. }
  726. } catch (\Exception $err) {
  727. echo '<pre>' . $err->getMessage() . '</pre>';
  728. die();
  729. }
  730. }
  731. if (!empty($_GET['drg'])) {
  732. $contact_button = '';
  733. if ($crm_id > 5) {
  734. $contact_button = '<button type="button" class="btn btn-sm bg-brown-five brown-three" data-bs-toggle="modal" data-bs-target="#crmUpdateModal"><i class="fab fa-hubspot"></i> Update Hubspot</button>';
  735. } else {
  736. $contact_button = '<button type="submit" class="btn btn-sm bg-brown-five brown-three" name="add_client_to_crm" value="1" form="client-brief" ><i class="bi bi-briefcase-fill"></i> Save Customer</button>';
  737. }
  738. }
  739. ?>
  740. <!doctype html>
  741. <html lang="en">
  742. <head>
  743. <meta charset="utf-8">
  744. <meta name="viewport" content="width=device-width, initial-scale=1">
  745. <title><?php echo $drg; ?> - Client Brief</title>
  746. <link rel="shortcut icon" href="images/blueprint.ico" type="image/x-icon">
  747. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
  748. <!-- jQuery first, then Popper.js, then Bootstrap JS -->
  749. <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
  750. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
  751. <link href="css/blueprint.css" rel="stylesheet">
  752. <link href="css/print.css" rel="stylesheet" media="print">
  753. <script type="text/javascript" src="https://use.fontawesome.com/1e2844bb90.js"></script>
  754. <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js" integrity="sha256-T0Vest3yCU7pafRw9r+settMBX6JkKN06dqBnpQ8d30=" crossorigin="anonymous"></script>
  755. <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-throttle-debounce/1.1/jquery.ba-throttle-debounce.min.js" integrity="sha512-JZSo0h5TONFYmyLMqp8k4oPhuo6yNk9mHM+FY50aBjpypfofqtEWsAgRDQm94ImLCzSaHeqNvYuD9382CEn2zw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  756. </head>
  757. <body>
  758. <nav class="navbar bg-brown-dark brown-light border-bottom border-body d-print-none" data-bs-theme="dark">
  759. <div class="container-fluid">
  760. <a class="navbar-brand brown-light" href="#">
  761. <img src="images/blueprint-logo-light.png" alt="Logo" width="30" height="24" class="d-inline-block align-text-top">
  762. Modulos Design
  763. </a>
  764. </div>
  765. </nav>
  766. <div class="container">
  767. <div class="row pt-2">
  768. <div class="col-sm-4 col-md-4 pt-3">
  769. <img class="img-fluid logo pt-2" src="images/blueprint-full-logo-medium.png" alt="Modulos Design">
  770. </div>
  771. <div class="col-sm-4 col-md-4 m-auto text-center">
  772. <h3 class="architect text-center">Job: <?php echo $drg; ?></h3>
  773. </div>
  774. <div class="col-sm-4 col-md-4 text-end pt-3">
  775. <h2 class="fw-bold text-end mb-1">Client Brief Form</h2>
  776. <h4 class="text-end mb-1">
  777. <span class="fw-bold brown-two"><?php echo $enquiry_date; ?></span>
  778. </h4>
  779. </div>
  780. </div>
  781. <hr>
  782. <form id="client-brief" data-remote="true" method="post" accept-charset="UTF-8">
  783. <input type="hidden" name="csrf" id="csrf" value="<?php echo $csrf; ?>">
  784. <div class="row">
  785. <div class="col-12 col-md-6">
  786. <div class="row ">
  787. <div class="col ">
  788. <h4 class="fw-bold">Client Details</h4>
  789. </div>
  790. </div>
  791. <div class="mb-1">
  792. <div class="row ">
  793. <div class="col-12 col-md-6">
  794. <label for="firstname" class="form-label form-label-sm p-0 m-0">Clients Name</label>
  795. <input type="text" class="form-control form-control-sm savable-text-field architect brown-four" name="firstname" id="firstname" tabindex="1" value="<?php echo htmlspecialchars($firstname ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="First Name" autocomplete="given-name">
  796. </div>
  797. <div class="col-12 col-md-6">
  798. <label for="lastname" class="form-label form-label-sm p-0 m-0"></label>
  799. <input type="text" class="form-control form-control-sm savable-text-field architect brown-four" name="lastname" id="lastname" tabindex="2" value="<?php echo htmlspecialchars($lastname ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Last Name" autocomplete="family-name">
  800. </div>
  801. </div>
  802. </div>
  803. <div class="mb-1">
  804. <label for="joint_name" class="form-label form-label-sm p-0 m-0">T/As - Joint Names</label>
  805. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="joint_name" id="joint_name" value="<?php echo htmlspecialchars($joint_name ?? '', ENT_QUOTES, 'UTF-8'); ?>" tabindex="3">
  806. </div>
  807. <div class="mb-1">
  808. <label for="postal_address" class="form-label form-label-sm p-0 m-0">Clients Postal Address</label>
  809. <input type="text" class="form-control form-control-sm fw-bold savable-text-field architect brown-three map-autocomplete" id="postal_address" name="postal_address" value="<?php echo htmlspecialchars($postal_address ?? '', ENT_QUOTES, 'UTF-8'); ?>" onFocus="geolocate(this)" tabindex="4" autocomplete="on">
  810. </div>
  811. <div class="mb-1">
  812. <div class="row ">
  813. <div class="col-md-6">
  814. <label for="phoneNumber" class="form-label form-label-sm p-0 m-0">Clients Mobile</label>
  815. <input type="phone" class="form-control form-control-sm savable-text-field architect brown-three" minlength="12" id="phoneNumber" name="client_mobile" value="<?php echo htmlspecialchars($client_mobile ?? '', ENT_QUOTES, 'UTF-8'); ?>" required onkeyup="check(); return false;" tabindex="5" autocomplete="tel">
  816. </div>
  817. <div class="col-12 col-md-3 d-print-none">
  818. <label for="crm_id" class="form-label form-label-sm p-0 m-0">CID</label>
  819. <input type="text" class="form-control form-control-sm savable-text-field brown-three" id="crm_id" name="crm_id" value="<?php echo $crm_id; ?>" >
  820. </div>
  821. <div class="col-12 col-md-3 d-print-none">
  822. <label for="dealId" class="form-label form-label-sm p-0 m-0">DID</label>
  823. <input type="text" class="form-control form-control-sm savable-text-field brown-three" id="dealId" name="dealId" value="<?php echo $dealId; ?>" >
  824. </div>
  825. </div>
  826. </div>
  827. <div class="mb-1">
  828. <label for="client_email" class="form-label form-label-sm p-0 m-0">Email address</label>
  829. <input type="email" class="form-control form-control-sm savable-text-field architect brown-three" name="client_email" id="client_email" value="<?php echo htmlspecialchars($client_email ?? '', ENT_QUOTES, 'UTF-8'); ?>" tabindex="6" autocomplete="email">
  830. </div>
  831. <hr>
  832. <div class="row ">
  833. <div class="col ">
  834. <h4 class="fw-bold">Proposed Site Details</h4>
  835. </div>
  836. </div>
  837. <div class="mb-1">
  838. <label for="site_address" class="form-label form-label-sm p-0 m-0">Site Address</label>
  839. <input type="text" class="form-control form-control-sm fw-bold savable-text-field architect brown-four map-autocomplete" id="site_address" name="site_address" value="<?php echo htmlspecialchars($site_address ?? '', ENT_QUOTES, 'UTF-8'); ?>" onFocus="geolocate(this)" tabindex="7">
  840. <input type="hidden" class="savable-text-field" id="site_lat" name="site_lat" value="<?php echo htmlspecialchars($site_lat ?? '', ENT_QUOTES, 'UTF-8'); ?>">
  841. <input type="hidden" class="savable-text-field" id="site_lng" name="site_lng" value="<?php echo htmlspecialchars($site_lng ?? '', ENT_QUOTES, 'UTF-8'); ?>">
  842. </div>
  843. <div class="row g-3">
  844. <div class="col-12 col-md-6">
  845. <label for="property_id" class="form-label form-label-sm p-0 m-0">Property ID:</label>
  846. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="property_id" id="property_id" value="<?php echo htmlspecialchars($property_id ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Pid">
  847. </div>
  848. <div class="col-12 col-md-6">
  849. <label for="title_id" class="form-label form-label-sm p-0 m-0">Title Reference:</label>
  850. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="title_id" id="title_id" value="<?php echo htmlspecialchars($title_id ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Title/Vol">
  851. </div>
  852. </div>
  853. <div class="mb-1">
  854. <label for="registered_owner" class="form-label form-label-sm p-0 m-0">Registered Title Owners:</label>
  855. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="registered_owner" id="registered_owner" value="<?php echo htmlspecialchars($registered_owner ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="">
  856. </div>
  857. <div class="row g-3">
  858. <div class="col-12 col-md-5">
  859. <label for="council" class="form-label form-label-sm p-0 m-0">Council:</label>
  860. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="council" id="council" value="<?php echo htmlspecialchars($council ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Local Council">
  861. </div>
  862. <div class="col-12 col-md-3">
  863. <label for="elevation" class="form-label form-label-sm p-0 m-0">Elev (AHD):</label>
  864. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="elevation" id="elevation" value="<?php echo htmlspecialchars($elevation ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Elevation (m)">
  865. </div>
  866. <div class="col-12 col-md-4">
  867. <label for="elevation" class="form-label form-label-sm p-0 m-0">Land Area (m2):</label>
  868. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="total_area" id="total_area" value="<?php echo htmlspecialchars($total_area ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="">
  869. </div>
  870. </div>
  871. <div class="row g-3">
  872. <div class="col-12 col-md-4">
  873. <label for="planning_zones" class="form-label form-label-sm p-0 m-0">Planning Zones:</label>
  874. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="planning_zones" id="planning_zones" value="<?php echo htmlspecialchars($planning_zones ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Planning Zones">
  875. </div>
  876. <div class="col-12 col-md-8">
  877. <label for="planning_scheme" class="form-label form-label-sm p-0 m-0">Planning Scheme:</label>
  878. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="planning_scheme" id="planning_scheme" value="<?php echo htmlspecialchars($planning_scheme ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Planning Scheme">
  879. </div>
  880. </div>
  881. <div class="row g-3">
  882. <div class="col-12">
  883. <label for="planning_codes" class="form-label form-label-sm p-0 m-0">Planning Codes:</label>
  884. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="planning_codes" id="planning_codes" value="<?php echo htmlspecialchars($planning_codes ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Planning Codes">
  885. </div>
  886. </div>
  887. <hr>
  888. <div class="row ">
  889. <div class="col ">
  890. <h4 class="fw-bold">Design Brief</h4>
  891. </div>
  892. </div>
  893. <div class="mb-1">
  894. <label for="design_style" class="form-label form-label-sm p-0 m-0">Type of Design</label>
  895. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="design_style" id="design_style" value="<?php echo htmlspecialchars($design_style ?? '', ENT_QUOTES, 'UTF-8'); ?>" >
  896. </div>
  897. <div class="row g-3">
  898. <div class="col-12 col-md-6">
  899. <label for="build_type" class="form-label form-label-sm p-0 m-0">Build Type:</label>
  900. <select type="text" name="build_type" id="build_type" class="form-select form-select-sm savable-dropdown-field architect brown-three" aria-required="true" aria-invalid="false">
  901. <option><?php echo $build_type; ?></option>
  902. <option >New Residential</option>
  903. <option >Residential Extension</option>
  904. <option >New Commercial</option>
  905. <option >Commercial Extension</option>
  906. <option >Shed or Ancillary Dwelling</option>
  907. <option >Pool</option>
  908. <option >Interior Design</option>
  909. <option >Other</option>
  910. </select>
  911. </div>
  912. <div class="col-12 col-md-6">
  913. <label for="scope" class="form-label form-label-sm p-0 m-0">Our Scope</label>
  914. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" id="scope" name="scope" value="<?php echo htmlspecialchars($scope ?? '', ENT_QUOTES, 'UTF-8'); ?>" >
  915. </div>
  916. </div>
  917. </div>
  918. <div class="col-12 col-md-6">
  919. <div class="row d-print-none">
  920. <div class="col-12 col-md my-1 d-grid">
  921. <?php echo $contact_button; ?>
  922. </div>
  923. <div class="col-12 col-md my-1 d-grid">
  924. <button type="button" class="btn btn-sm bg-brown-dark brown-three" data-bs-toggle="modal" data-bs-target="#emailTemplateModal"><i class="bi bi-clipboard"></i> Planning request text</button>
  925. </div>
  926. <div class="col-12 col-md my-1 d-grid">
  927. <button type="button" class="btn btn-sm bg-brown-three brown-five" id="createFolder"><i class="bi bi-folder-fill"></i> Create Job</button>
  928. </div>
  929. <div class="col-12 col-md my-1 d-grid">
  930. <button type="button" class="btn btn-sm bg-brown-three brown-five" onclick="fetchAndExtractData()"><i class="bi bi-folder-fill"></i> Planbuild</button>
  931. <!-- <button type="button" class="btn btn-sm bg-brown-light brown-five bd-brown-five"><i class="bi bi-award-fill"></i> Request Title</button> -->
  932. </div>
  933. </div>
  934. <div class="row d-print-none">
  935. <div class="col">
  936. <hr class="mvt-3">
  937. </div>
  938. </div>
  939. <div class="row ">
  940. <div class="col-12">
  941. <h5 class="fw-bold">Brief Checklist</h5>
  942. <p class="extra-light small">Have the following documents being provided?</p>
  943. </div>
  944. </div>
  945. <div class="row ">
  946. <div class="col-6 col-md-4">
  947. <div class="form-check form-check-inline savable-checkbox">
  948. <input class="form-check-input savable-checkbox-field"
  949. type="checkbox"
  950. name="copy_title" id="copy_title"
  951. value="1"
  952. <?php if ('1' == $copy_title) { echo 'checked'; } ?>
  953. >
  954. <label class="form-check-label" for="copy_title">Copy of Title</label>
  955. </div>
  956. </div>
  957. <div class="col-6 col-md-4">
  958. <div class="form-check form-check-inline savable-checkbox">
  959. <input class="form-check-input savable-checkbox-field"
  960. type="checkbox"
  961. name="original_plans" id="original_plans"
  962. value="1"
  963. <?php if ('1' == $original_plans) { echo 'checked'; } ?>
  964. >
  965. <label class="form-check-label" for="original_plans">Original Plans</label>
  966. </div>
  967. </div>
  968. <div class="col-6 col-md-4">
  969. <div class="form-check form-check-inline savable-checkbox">
  970. <input class="form-check-input savable-checkbox-field"
  971. type="checkbox"
  972. name="concepts_styles" id="concepts_styles"
  973. value="1"
  974. <?php if ('1' == $concepts_styles) { echo 'checked'; } ?>
  975. >
  976. <label class="form-check-label" for="concepts_styles">Concepts & Styles</label>
  977. </div>
  978. </div>
  979. </div>
  980. <div class="row ">
  981. <div class="col-6 col-md-4">
  982. <div class="form-check form-check-inline savable-checkbox">
  983. <input class="form-check-input savable-checkbox-field"
  984. type="checkbox"
  985. name="loa_signed" id="loa_signed"
  986. value="1"
  987. <?php if ('1' == $loa_signed) { echo 'checked'; } ?>
  988. >
  989. <label class="form-check-label" for="loa_signed">LOA signed</label>
  990. </div>
  991. </div>
  992. <div class="col-6 col-md-4">
  993. <div class="form-check form-check-inline savable-checkbox">
  994. <input class="form-check-input savable-checkbox-field"
  995. type="checkbox"
  996. name="da_application" id="da_application"
  997. value="1"
  998. <?php if ('1' == $da_application) { echo 'checked'; } ?>
  999. >
  1000. <label class="form-check-label" for="da_application">DA Application</label>
  1001. </div>
  1002. </div>
  1003. <div class="col-6 col-md-4">
  1004. <div class="form-check form-check-inline savable-checkbox">
  1005. <input class="form-check-input savable-checkbox-field"
  1006. type="checkbox"
  1007. name="ba_application" id="ba_application"
  1008. value="1"
  1009. <?php if ('1' == $ba_application) { echo 'checked'; } ?>
  1010. >
  1011. <label class="form-check-label" for="ba_application">BA Application</label>
  1012. </div>
  1013. </div>
  1014. </div>
  1015. <hr>
  1016. <div class="row ">
  1017. <div class="col ">
  1018. <h5 class="fw-bold">Design Outputs</h5>
  1019. <p class="extra-light small"></p>
  1020. </div>
  1021. </div>
  1022. <div class="row ">
  1023. <div class="col-6 col-md-4">
  1024. <div class="form-check form-check-inline savable-checkbox">
  1025. <input class="form-check-input savable-checkbox-field"
  1026. type="checkbox"
  1027. name="concepts_3d" id="concepts_3d"
  1028. value="1"
  1029. <?php if ('1' == $concepts_3d) { echo 'checked'; } ?>
  1030. >
  1031. <label class="form-check-label" for="concepts_3d">3D Concepts</label>
  1032. </div>
  1033. </div>
  1034. <div class="col-6 col-md-4">
  1035. <div class="form-check form-check-inline savable-checkbox">
  1036. <input class="form-check-input savable-checkbox-field"
  1037. type="checkbox"
  1038. name="draft_floorPlan" id="draft_floorPlan"
  1039. value="1"
  1040. <?php if ('1' == $draft_floorPlan) { echo 'checked'; } ?>
  1041. >
  1042. <label class="form-check-label" for="draft_floorPlan">Draft Floor Plan</label>
  1043. </div>
  1044. </div>
  1045. <div class="col-6 col-md-4">
  1046. <div class="form-check form-check-inline savable-checkbox">
  1047. <input class="form-check-input savable-checkbox-field"
  1048. type="checkbox"
  1049. name="fire_report" id="fire_report"
  1050. value="1"
  1051. <?php if ('1' == $fire_report) { echo 'checked'; } ?>
  1052. >
  1053. <label class="form-check-label" for="fire_report">Fire Report</label>
  1054. </div>
  1055. </div>
  1056. </div>
  1057. <div class="row ">
  1058. <div class="col-6 col-md-4">
  1059. <div class="form-check form-check-inline savable-checkbox">
  1060. <input class="form-check-input savable-checkbox-field"
  1061. type="checkbox"
  1062. name="energy_report" id="energy_report"
  1063. value="1"
  1064. <?php if ('1' == $energy_report) { echo 'checked'; } ?>
  1065. >
  1066. <label class="form-check-label" for="energy_report">Energy Report</label>
  1067. </div>
  1068. </div>
  1069. <div class="col-6 col-md-4">
  1070. <div class="form-check form-check-inline savable-checkbox">
  1071. <input class="form-check-input savable-checkbox-field"
  1072. type="checkbox"
  1073. name="tender_set" id="tender_set"
  1074. value="1"
  1075. <?php if ('1' == $tender_set) { echo 'checked'; } ?>
  1076. >
  1077. <label class="form-check-label" for="tender_set">Tender Doc Set</label>
  1078. </div>
  1079. </div>
  1080. <div class="col-6 col-md-4">
  1081. <div class="form-check form-check-inline savable-checkbox">
  1082. <input class="form-check-input savable-checkbox-field"
  1083. type="checkbox"
  1084. name="quantity_survey" id="quantity_survey"
  1085. value="1"
  1086. <?php if ('1' == $quantity_survey) { echo 'checked'; } ?>
  1087. >
  1088. <label class="form-check-label" for="quantity_survey">Quantity Survey</label>
  1089. </div>
  1090. </div>
  1091. </div>
  1092. <hr>
  1093. <div class="row ">
  1094. <div class="col ">
  1095. <h5 class="fw-bold">Presentations</h5>
  1096. <p class="extra-light small"></p>
  1097. </div>
  1098. </div>
  1099. <div class="row ">
  1100. <div class="col-6 col-md-4">
  1101. <div class="form-check form-check-inline savable-checkbox">
  1102. <input class="form-check-input savable-checkbox-field"
  1103. type="checkbox"
  1104. name="vr_concepts" id="vr_concepts"
  1105. value="1"
  1106. <?php if ('1' == $vr_concepts) { echo 'checked'; } ?>
  1107. >
  1108. <label class="form-check-label" for="vr_concepts">VR Concepts</label>
  1109. </div>
  1110. </div>
  1111. <div class="col-6 col-md-4">
  1112. <div class="form-check form-check-inline savable-checkbox">
  1113. <input class="form-check-input savable-checkbox-field"
  1114. type="checkbox"
  1115. name="render_set" id="render_set"
  1116. value="1"
  1117. <?php if ('1' == $render_set) { echo 'checked'; } ?>
  1118. >
  1119. <label class="form-check-label" for="render_set">Full Rended Set</label>
  1120. </div>
  1121. </div>
  1122. <div class="col-6 col-md-4">
  1123. <div class="form-check form-check-inline savable-checkbox">
  1124. <input class="form-check-input savable-checkbox-field"
  1125. type="checkbox"
  1126. name="model_3d" id="model_3d"
  1127. value="1"
  1128. <?php if ('1' == $model_3d) { echo 'checked'; } ?>
  1129. >
  1130. <label class="form-check-label" for="model_3d">3D Model</label>
  1131. </div>
  1132. </div>
  1133. </div>
  1134. <hr>
  1135. <div class="row ">
  1136. <div class="col ">
  1137. <h5 class="fw-bold">Create Documents</h5>
  1138. <p class="extra-light small"></p>
  1139. </div>
  1140. </div>
  1141. <div class="row d-print-none mb-2">
  1142. <div class="col d-inline-flex gap-1">
  1143. <button type="button" class="btn btn-sm bg-brown-three brown-five" id="createLOA"><i class="bi bi-file-earmark-text-fill"></i> Create LOA</button>
  1144. </div>
  1145. </div>
  1146. <div class="row d-print-none">
  1147. <div class="col d-inline-flex gap-1">
  1148. <button type="button" class="btn btn-sm bg-brown-five brown-three" id="createContract"><i class="bi bi-file-earmark-text-fill"></i> Create Contract</button>
  1149. <div class="form-check d-inline-flex align-items-center ms-2">
  1150. <input class="form-check-input" type="checkbox" id="updateFrontMatter">
  1151. <label class="form-check-label ms-1" for="updateFrontMatter">
  1152. Update contact details if file exists
  1153. </label>
  1154. </div>
  1155. </div>
  1156. </div>
  1157. <hr>
  1158. <div class="row ">
  1159. <div class="col ">
  1160. <h4 class="fw-bold">Investment Details</h4>
  1161. </div>
  1162. </div>
  1163. <div class="row g-3">
  1164. <div class="col-12 col-md-6">
  1165. <label for="budget_low" class="form-label form-label-sm p-0 m-0">Budget Range (Lower)</label>
  1166. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="budget_low" id="budget_low" value="<?php echo htmlspecialchars($budget_low ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="$300,000">
  1167. </div>
  1168. <div class="col-12 col-md-6">
  1169. <label for="budget_high" class="form-label form-label-sm p-0 m-0">Budget Range (Upper)</label>
  1170. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="budget_high" id="budget_high" value="<?php echo htmlspecialchars($budget_high ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="$2,000,000">
  1171. </div>
  1172. </div>
  1173. <div class="mb-1">
  1174. <label for="finance_status" class="form-label form-label-sm p-0 m-0">Finance</label>
  1175. <input type="text" class="form-control form-control-sm savable-text-field architect brown-three" name="finance_status" id="finance_status" value="<?php echo htmlspecialchars($finance_status ?? '', ENT_QUOTES, 'UTF-8'); ?>" placeholder="Have you had a loan (pre)approved ?">
  1176. </div>
  1177. <hr>
  1178. <div class="row ">
  1179. <div class="col ">
  1180. <h4 class="fw-bold">Services Used</h4>
  1181. </div>
  1182. </div>
  1183. <div class="row g-3">
  1184. <div class="col-12">
  1185. <select class="form-control form-control-sm" id="building_surveyors_select" name="building_surveyors">
  1186. <label for="building_surveyors">Building Surveyor:</label>
  1187. <option selected="" disabled="">Select Building Surveyor</option>
  1188. <option class='' data-id='Building Surveying Services Pty Ltd|Wayne Wilson|0487 911 123|building@buildingsurveyingservices.com.au|7 Marlborough Street, Longford, Tas, 7301' id='' value='building@buildingsurveyingservices.com.au' >Building Surveying Services - Wayne Wilson</option>
  1189. <option class='' data-id='Plumb Building Surveying|Matthew Dewse|0476 830 336|hello@plumbbuildingsurveying.au|PO Box 120, Kings Meadows, Tas, 7249' id='' value='hello@plumbbuildingsurveying.au' >Plumb Building Surveying - Matthew Dewse</option>
  1190. <option class='' data-id='Local Group|Barry Magnus|0455 681 912|bmagnus@localgroup.com.au|81 Elizabeth Street, Launceston , Tas, 7250' id='' value='bmagnus@localgroup.com.au' >Local Group - Barry Magnus</option>
  1191. <option class='' data-id='Worthington Building Surveying|Peter Worthington|0475 854 904|peter@worthbuilding.com.au|, Frankford, Tas, 7275' id='' value='peter@worthbuilding.com.au' >Worthington Building Surveying - Peter Worthington</option>
  1192. </select>
  1193. <input type="hidden" id="building_surveyors_meta" name="building_surveyors_meta" value="<?php echo $building_surveyors_meta ?? ''; ?>">
  1194. </div>
  1195. <div class="col-12 col-md-6">
  1196. </div>
  1197. </div>
  1198. <div class="row mb-1">
  1199. <div class="col-12 col-md-6">
  1200. <div class="row mb-1">
  1201. <div class="col-12 col-md-6">
  1202. </div>
  1203. </div>
  1204. </div>
  1205. </div>
  1206. </div>
  1207. </div>
  1208. <hr>
  1209. <div class="row">
  1210. <div class="col">
  1211. <div class="mb-1">
  1212. <label for="details" class="form-label form-label-sm p-0 m-0">Details</label>
  1213. <textarea class="form-control form-control-sm savable-text-field small architect" name="details" id="details" rows="6"><?php echo htmlspecialchars($details ?? '', ENT_QUOTES, 'UTF-8'); ?></textarea>
  1214. </div>
  1215. </div>
  1216. </div>
  1217. <hr>
  1218. <div class="row d-print-none">
  1219. <div class="col-6">
  1220. <!--Add buttons to initiate auth sequence and sign out-->
  1221. <button id="authorize_button" type="button" class="btn btn-sm bg-brown-three brown-five">Authorize</button>
  1222. <button id="signout_button" type="button" class="btn btn-sm bg-brown-three brown-five">Sign Out</button>
  1223. </div>
  1224. </div>
  1225. <hr>
  1226. <footer class="footer">
  1227. <p class="text-center">&copy; 2023 - Modulos Design</p>
  1228. </footer>
  1229. </form>
  1230. </div>
  1231. <!-- Planbuild/LIST preview modal -->
  1232. <div class="modal fade" id="planbuildPreview" tabindex="-1" role="dialog" aria-labelledby="planbuildPreviewLabel" aria-hidden="true">
  1233. <div class="modal-dialog modal-lg" role="document">
  1234. <div class="modal-content">
  1235. <div class="modal-header">
  1236. <h5 class="modal-title" id="planbuildPreviewLabel">Property info from LIST</h5>
  1237. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  1238. </div>
  1239. <div class="modal-body">
  1240. <div id="pb-errors" class="alert alert-danger d-none"></div>
  1241. <div class="row">
  1242. <div class="col-md-6">
  1243. <dl class="row small mb-0">
  1244. <dt class="col-5">PID</dt><dd class="col-7" id="pb_pid">–</dd>
  1245. <dt class="col-5">Title</dt><dd class="col-7" id="pb_title">–</dd>
  1246. <dt class="col-5">Total Area</dt><dd class="col-7" id="pb_area">–</dd>
  1247. <dt class="col-5">Total Area sqm</dt><dd class="col-7" id="area_sqm">–</dd>
  1248. <dt class="col-5">Total Area ha</dt><dd class="col-7" id="area_ha">–</dd>
  1249. <dt class="col-5">Locality</dt><dd class="col-7" id="pb_locality">–</dd>
  1250. </dl>
  1251. </div>
  1252. <div class="col-md-6">
  1253. <dl class="row small mb-0">
  1254. <dt class="col-5">Planning Scheme</dt><dd class="col-7" id="pb_scheme">–</dd>
  1255. <dt class="col-5">Zones</dt><dd class="col-7" id="pb_zones">–</dd>
  1256. <dt class="col-5">Code Overlays</dt><dd class="col-7" id="pb_codes">–</dd>
  1257. </dl>
  1258. </div>
  1259. </div>
  1260. <hr>
  1261. <div class="row">
  1262. <div class="col-md-6">
  1263. <dl class="row small mb-0">
  1264. <dt class="col-5">tenure</dt><dd class="col-7" id="tenure">–</dd>
  1265. <dt class="col-5">lpi</dt><dd class="col-7" id="lpi">–</dd>
  1266. <dt class="col-5">list_guid</dt><dd class="col-7" id="list_guid">–</dd>
  1267. </dl>
  1268. </div>
  1269. <div class="col-md-6">
  1270. </div>
  1271. </div>
  1272. </div>
  1273. <div class="modal-footer">
  1274. <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
  1275. <button type="button" class="btn btn-primary" id="pb-apply">Apply to form</button>
  1276. </div>
  1277. </div>
  1278. </div>
  1279. </div>
  1280. <!-- Prefilled email text modal -->
  1281. <div class="modal fade" id="emailTemplateModal" tabindex="-1" role="dialog" aria-labelledby="emailTemplateLabel" aria-hidden="true">
  1282. <div class="modal-dialog modal-lg" role="document">
  1283. <div class="modal-content">
  1284. <div class="modal-header">
  1285. <h5 class="modal-title" id="emailTemplateLabel">Email text to council</h5>
  1286. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  1287. </div>
  1288. <div class="modal-body">
  1289. <ul class="nav nav-tabs" role="tablist">
  1290. <li class="nav-item">
  1291. <a class="nav-link" data-bs-toggle="tab" href="#tabText" role="tab">Plain text</a>
  1292. </li>
  1293. <li class="nav-item">
  1294. <a class="nav-link active" data-bs-toggle="tab" href="#tabHtml" role="tab">HTML email</a>
  1295. </li>
  1296. </ul>
  1297. <div class="tab-content">
  1298. <div class="tab-pane fade p-3" id="tabText" role="tabpanel">
  1299. <textarea id="emailText" class="form-control small" rows="10" readonly style="font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;"></textarea>
  1300. <p class="text-muted small mt-2 mb-0">
  1301. Uses fields: Site Address, Registered Title Owners, Property ID, Title Reference
  1302. </p>
  1303. </div>
  1304. <div class="tab-pane fade active show p-3" id="tabHtml" role="tabpanel">
  1305. <div id="emailHtmlPreview" class="border rounded p-3" style="background:#ffffff; color:#000000; font-family:Arial,Helvetica,sans-serif; font-size:14px; line-height:1.5;"></div>
  1306. <!--<textarea id="emailHtmlSource" class="form-control small mt-2" rows="8" readonly></textarea>
  1307. <p class="text-muted small mt-2 mb-0">
  1308. Preview above. Copy the HTML if you paste into an HTML-capable composer such as Gmail or Outlook.
  1309. </p>-->
  1310. </div>
  1311. </div>
  1312. </div>
  1313. <div class="modal-footer">
  1314. <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
  1315. <button type="button" class="btn btn-outline-secondary" id="copyEmailText">Copy text</button>
  1316. <button type="button" class="btn btn-outline-secondary" id="copyEmailHtml">Copy HTML</button>
  1317. <a href="#" class="btn btn-primary" id="openMailto">Open email draft</a>
  1318. </div>
  1319. </div>
  1320. </div>
  1321. </div>
  1322. <script type="text/javascript">
  1323. const drg = <?php echo (int)($drg ?? 0); ?>;
  1324. const folder_name = <?php echo json_encode(($drg ?? '') . ' - ' . ($site_address_street ?? '')); ?>;
  1325. $(function () {
  1326. document.querySelectorAll('[data-bs-toggle="popover"]')
  1327. .forEach(el => new bootstrap.Popover(el));
  1328. });
  1329. $(function () {
  1330. $(".datetime").each(function(){
  1331. $(this).datepicker({ format: 'yyyy-mm-dd', autoclose: true, todayBtn: true, todayHighlight: true });
  1332. });
  1333. });
  1334. $(function () {
  1335. $(".datemonth").each(function(){
  1336. $(this).datepicker({ format: 'MM yy', autoclose: true, todayBtn: true, todayHighlight: true });
  1337. });
  1338. });
  1339. // Create LOA from current form fields
  1340. $('#createLOA').on('click', async function () {
  1341. const job = String(drg || '').trim();
  1342. if (!job) { alert('Missing Job #'); return; }
  1343. const clientName = ($('#joint_name').val().trim()) ||
  1344. [$('#firstname').val().trim(), $('#lastname').val().trim()].filter(Boolean).join(' ');
  1345. const clientEmail = $('#client_email').val().trim();
  1346. const propertyAddr = $('#site_address').val().trim();
  1347. // NEW fields
  1348. const clientPhone = $('#phoneNumber').val().trim();
  1349. const clientAddress = $('#postal_address').val().trim();
  1350. const propertyPid = $('#property_id').val().trim();
  1351. const propertyTitle = $('#title_id').val().trim(); // server will split vol/folio if possible
  1352. //const startDate = $('#start_date').val?.().trim?.() || ''; // include if you add inputs
  1353. //const endDate = $('#end_date').val?.().trim?.() || '';
  1354. async function doCreate(overwrite) {
  1355. const fd = new FormData();
  1356. fd.append('action', 'loa_create');
  1357. fd.append('job', job);
  1358. fd.append('client_name', clientName);
  1359. fd.append('client_email', clientEmail);
  1360. fd.append('property_address', propertyAddr);
  1361. // NEW fields
  1362. fd.append('client_phone', clientPhone);
  1363. fd.append('client_address', clientAddress);
  1364. fd.append('property_pid', propertyPid);
  1365. fd.append('property_title', propertyTitle);
  1366. //fd.append('start_date', startDate);
  1367. //fd.append('end_date', endDate);
  1368. fd.append('overwrite', overwrite ? 1 : 0);
  1369. fd.append('csrf', $('#csrf').val());
  1370. const res = await fetch(location.pathname + '?action=loa_create', { method: 'POST', body: fd });
  1371. return res.json();
  1372. }
  1373. try {
  1374. let js = await doCreate(false);
  1375. // If file exists, the server will just update front matter (no prompt).
  1376. // If you still want an overwrite option:
  1377. if (!js.ok && js.error && js.error.toLowerCase().includes('exists')) {
  1378. if (confirm('An LOA already exists. Overwrite the whole file from template?')) {
  1379. js = await doCreate(true);
  1380. } else return;
  1381. }
  1382. if (js.ok) {
  1383. const msg = 'LOA saved for Job #' + job + (js.public_url ? '\n\nOpen signing page now?' : '');
  1384. if (confirm(msg) && js.public_url) window.open(js.public_url, '_blank', 'noopener');
  1385. } else {
  1386. alert(js.error || 'Failed to create/update LOA');
  1387. }
  1388. } catch (e) {
  1389. alert('Error creating LOA: ' + e.message);
  1390. }
  1391. });
  1392. // simple phone length “checker” (kept as-is)
  1393. function check() {
  1394. var mobile = document.getElementById('phoneNumber');
  1395. if(!mobile) return;
  1396. if(mobile.value.length !== 12){ /* no-op */ }
  1397. }
  1398. const isHideDismissableAlert = <?php echo json_encode($isHideDismissableAlert ?? 0); ?>;
  1399. window.autocompletes = {};
  1400. var placeSearch, clickedGeolocationField = null;
  1401. var componentForm = {
  1402. street_number: 'short_name',
  1403. route: 'long_name',
  1404. locality: 'long_name',
  1405. administrative_area_level_1: 'short_name',
  1406. postal_code: 'short_name'
  1407. };
  1408. $(document).ready(function() {
  1409. if (isHideDismissableAlert > 0) {
  1410. $(".alert-dismissible").fadeTo(2000, 500).slideUp(500, function () {
  1411. document.querySelectorAll('.alert-dismissible').forEach(el => {
  1412. bootstrap.Alert.getOrCreateInstance(el).close();
  1413. });
  1414. });
  1415. }
  1416. if (drg > 0) {
  1417. const debouncedStore = $.debounce(500, function(name, value) { storeField(name, value); });
  1418. $('.savable-text-field').on('keyup', function(e) {
  1419. const key = e.keyCode || e.which;
  1420. const systemKeys = [9,13,16,17,18,37,38,39,40,91];
  1421. if (systemKeys.includes(key)) return;
  1422. debouncedStore($(this).prop('name'), $(this).val());
  1423. });
  1424. }
  1425. $('.savable-date-field').on('change', function () {
  1426. storeField($(this).prop('name'), $(this).val());
  1427. });
  1428. $('.savable-dropdown-field').on('change', function () {
  1429. const name = $(this).prop('name');
  1430. const value = $(this).val();
  1431. storeField(name, value);
  1432. $(this).removeClass('form-control-danger');
  1433. if (this.id === 'building_surveyors_select') {
  1434. const meta = this.options[this.selectedIndex]?.dataset?.id || '';
  1435. $('#building_surveyors_meta').val(meta);
  1436. storeField('building_surveyors_meta', meta);
  1437. }
  1438. });
  1439. $('.savable-checkbox-field').on('change', function () {
  1440. storeField($(this).prop('name'), $(this).is(":checked") ? 1 : 0);
  1441. });
  1442. $('.savable-radio-field').on('change', function () {
  1443. storeField($(this).prop('name'), $(this).val());
  1444. });
  1445. // phone mask
  1446. const phoneInput = document.getElementById('phoneNumber');
  1447. if (phoneInput) {
  1448. phoneInput.addEventListener('keyup', function(evt){
  1449. phoneInput.value = phoneFormat(phoneInput.value);
  1450. });
  1451. }
  1452. }); // <--- correctly closes $(document).ready
  1453. String.prototype.toProperCase = function () {
  1454. return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
  1455. };
  1456. var rectangle, map, infoWindow, rectPoly, journey = '', rotate = '';
  1457. function stateDisplay(shortCode) {
  1458. if (!shortCode) return '';
  1459. const s = shortCode.toUpperCase();
  1460. const map = { TAS: 'Tas', VIC: 'Vic', QLD: 'Qld', NSW: 'NSW', WA: 'WA', SA: 'SA', ACT: 'ACT', NT: 'NT' };
  1461. return map[s] || shortCode;
  1462. }
  1463. // Google Places — global so callback can find it
  1464. window.initAutocomplete = function initAutocomplete() {
  1465. const inputs = document.getElementsByClassName('map-autocomplete');
  1466. for (let i = 0; i < inputs.length; i++) {
  1467. const el = inputs[i];
  1468. const ac = new google.maps.places.Autocomplete(
  1469. el,
  1470. { types: ['address'], componentRestrictions: { country: 'au' } }
  1471. );
  1472. ac.setFields(['address_component', 'geometry']);
  1473. window.autocompletes[el.id] = ac; // store for geolocate()
  1474. ac.addListener('place_changed', () => fillInAddress(el, ac));
  1475. }
  1476. };
  1477. function fillInAddress(targetInput, ac) {
  1478. const place = ac.getPlace();
  1479. if (!place || !place.address_components) return;
  1480. const parts = { street_number: '', route: '', locality: '', state: '', postal_code: '' };
  1481. for (const comp of place.address_components) {
  1482. const type = comp.types[0];
  1483. if (type === 'street_number') parts.street_number = comp.short_name || '';
  1484. if (type === 'route') parts.route = comp.long_name || '';
  1485. if (type === 'locality') parts.locality = comp.long_name || '';
  1486. if (type === 'administrative_area_level_1') parts.state = comp.short_name || '';
  1487. if (type === 'postal_code') parts.postal_code = comp.short_name || '';
  1488. }
  1489. if (parts.street_number && parts.route && parts.locality && parts.state && parts.postal_code) {
  1490. const formatted = `${parts.street_number} ${parts.route}, ${parts.locality}, ${stateDisplay(parts.state)}, ${parts.postal_code}`;
  1491. targetInput.value = formatted;
  1492. if (typeof storeField === 'function') {
  1493. try { storeField(targetInput.name || targetInput.id, formatted); } catch (e) {}
  1494. }
  1495. if (targetInput.id === 'site_address' && typeof writeIfReady === 'function') {
  1496. try { writeIfReady(); } catch (e) {}
  1497. }
  1498. // NEW: council lookup for TAS
  1499. if (parts.state && parts.state.toUpperCase() === 'TAS') {
  1500. const url = `classes/council_lookup.php?town=${encodeURIComponent(parts.locality)}&postcode=${encodeURIComponent(parts.postal_code)}&state=TAS`;
  1501. fetch(url)
  1502. .then(r => r.json())
  1503. .then(data => {
  1504. if (data && data.ok && data.council) {
  1505. const councilEl = document.getElementById('council');
  1506. if (councilEl) {
  1507. councilEl.value = data.council;
  1508. }
  1509. if (typeof storeField === 'function') {
  1510. storeField('council', data.council);
  1511. }
  1512. // If you also want to store the shapeFile, uncomment:
  1513. // if (data.shapeFile) storeField('shapeFile', data.shapeFile);
  1514. }
  1515. })
  1516. .catch(() => { /* silent fail is fine */ });
  1517. }
  1518. // store lat/lng if available
  1519. if (place.geometry && place.geometry.location) {
  1520. const lat = place.geometry.location.lat();
  1521. const lng = place.geometry.location.lng();
  1522. $('#site_lat').val(lat);
  1523. $('#site_lng').val(lng);
  1524. storeField('site_lat', lat);
  1525. storeField('site_lng', lng);
  1526. }
  1527. if (typeof storeField === 'function') {
  1528. try { storeField(targetInput.name || targetInput.id, formatted); } catch (e) {}
  1529. }
  1530. }
  1531. }
  1532. function geolocate(el) {
  1533. if (!el || !el.id) return;
  1534. clickedGeolocationField = el.name;
  1535. const ac = window.autocompletes[el.id];
  1536. if (!ac) return;
  1537. if (navigator.geolocation) {
  1538. navigator.geolocation.getCurrentPosition(function(position) {
  1539. const circle = new google.maps.Circle({
  1540. center: { lat: position.coords.latitude, lng: position.coords.longitude },
  1541. radius: position.coords.accuracy
  1542. });
  1543. ac.setBounds(circle.getBounds());
  1544. });
  1545. }
  1546. }
  1547. function getElevation(lat, lng) {
  1548. var elevationApiKey = 'AIzaSyB-QceOYrDe9otynMmQ9iNF3yEZzbpsanM';
  1549. var elevationApiUrl = `https://maps.googleapis.com/maps/api/elevation/json?locations=${lat},${lng}&key=${elevationApiKey}`;
  1550. fetch(elevationApiUrl)
  1551. .then(function (response) { return response.json(); })
  1552. .then(function (data) {
  1553. if (data.results.length > 0) {
  1554. var elevation = data.results[0].elevation;
  1555. var el = document.getElementById('elevation');
  1556. if (el) el.value = elevation;
  1557. }
  1558. })
  1559. .catch(function (error) { console.error('Error fetching elevation: ' + error); });
  1560. }
  1561. // AU-ish phone mask
  1562. function phoneFormat(input){
  1563. input = input.replace(/\D/g,'');
  1564. input = input.substring(0,20);
  1565. var size = input.length;
  1566. if(size === 0){
  1567. input = input;
  1568. } else if(size < 4){
  1569. input = input + ' ';
  1570. } else if(size < 7){
  1571. input = input.substring(0,4)+' '+input.substring(4,6);
  1572. } else if (input.startsWith("04") ) {
  1573. input = input.substring(0,4)+' '+input.substring(4,7)+' '+input.substring(7,20);
  1574. } else if (input.startsWith("+44") ) {
  1575. input = input;
  1576. } else {
  1577. input = input.substring(0,2)+' '+input.substring(2,6)+' '+input.substring(6,20);
  1578. }
  1579. return input;
  1580. }
  1581. function storeField(name, value) {
  1582. $.ajax({
  1583. url: "database.php",
  1584. type: "POST",
  1585. data: {
  1586. action: 'client-brief',
  1587. drg: drg,
  1588. field_name: name,
  1589. field_value: value,
  1590. csrf: $('#csrf').val()
  1591. },
  1592. success: function(data) {
  1593. try {
  1594. data = $.parseJSON(data);
  1595. if (!data.success) alert('Error: ' + data.message);
  1596. } catch(e) {
  1597. console.warn('Non-JSON response', data);
  1598. }
  1599. },
  1600. error: function() { alert("Please check for Errors."); }
  1601. });
  1602. }
  1603. document.getElementById('authorize_button').addEventListener('click', (e) => {
  1604. e.preventDefault();
  1605. handleAuthClick();
  1606. });
  1607. document.getElementById('signout_button').addEventListener('click', (e) => {
  1608. e.preventDefault();
  1609. handleSignoutClick();
  1610. });
  1611. // Google API creds — values injected server-side from .env
  1612. var CLIENT_ID = '<?= htmlspecialchars(getenv('GOOGLE_CLIENT_ID') ?: '', ENT_QUOTES, 'UTF-8') ?>';
  1613. var API_KEY = '<?= htmlspecialchars(getenv('GOOGLE_API_KEY') ?: '', ENT_QUOTES, 'UTF-8') ?>';
  1614. const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
  1615. var SCOPES = 'https://www.googleapis.com/auth/drive';
  1616. // TODO: Set this to the Google Drive folder ID of the root projects directory
  1617. // (e.g. the "[03] Projects 20XX" folder). Without this, new job folders are
  1618. // created in the signed-in user's Drive root, not the shared projects directory.
  1619. // Find the ID by opening the folder in Drive and copying the ID from the URL:
  1620. // https://drive.google.com/drive/folders/<FOLDER_ID_HERE>
  1621. const PROJECTS_PARENT_FOLDER_ID = ''; // <-- paste folder ID here
  1622. let tokenClient, gapiInited = false, gisInited = false;
  1623. window.gapiLoaded = function () {
  1624. gapi.load('client', async () => {
  1625. try {
  1626. await gapi.client.init({ apiKey: API_KEY, discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'] });
  1627. gapiInited = true;
  1628. maybeEnableButtons();
  1629. } catch (err) {
  1630. console.warn('Discovery failed, falling back to gapi.client.load()', err);
  1631. // small backoff retries
  1632. for (let i = 0; i < 3; i++) {
  1633. try {
  1634. await new Promise(r => setTimeout(r, 400 * (i + 1)));
  1635. await gapi.client.load('drive', 'v3');
  1636. gapiInited = true;
  1637. maybeEnableButtons();
  1638. break;
  1639. } catch (e) {
  1640. if (i === 2) console.error('Drive load failed after retries', e);
  1641. }
  1642. }
  1643. }
  1644. });
  1645. };
  1646. window.gisLoaded = function () {
  1647. tokenClient = google.accounts.oauth2.initTokenClient({
  1648. client_id: CLIENT_ID,
  1649. scope: SCOPES,
  1650. callback: () => {}
  1651. });
  1652. gisInited = true;
  1653. maybeEnableButtons();
  1654. };
  1655. function maybeEnableButtons() {
  1656. if (gapiInited && gisInited) {
  1657. document.getElementById('authorize_button').style.visibility = 'visible';
  1658. document.getElementById('createFolder')?.addEventListener('click', ensureAuthThenCreateFolder);
  1659. }
  1660. }
  1661. function handleAuthClick() {
  1662. tokenClient.callback = (resp) => {
  1663. if (resp.error) { console.error(resp); return; }
  1664. document.getElementById('signout_button').style.visibility = 'visible';
  1665. document.getElementById('authorize_button').innerText = 'Refresh';
  1666. };
  1667. if (!gapi.client.getToken()) tokenClient.requestAccessToken({ prompt: 'consent' });
  1668. else tokenClient.requestAccessToken({ prompt: '' });
  1669. }
  1670. function handleSignoutClick() {
  1671. const tok = gapi.client.getToken();
  1672. if (!tok) return;
  1673. google.accounts.oauth2.revoke(tok.access_token);
  1674. gapi.client.setToken(null);
  1675. document.getElementById('authorize_button').innerText = 'Authorize';
  1676. document.getElementById('signout_button').style.visibility = 'hidden';
  1677. }
  1678. async function ensureAuthThenCreateFolder() {
  1679. if (!gapi.client.getToken()) {
  1680. tokenClient.callback = async (resp) => {
  1681. if (resp && resp.error) { console.error(resp); return; }
  1682. await createFolder();
  1683. };
  1684. tokenClient.requestAccessToken({ prompt: 'consent' });
  1685. } else {
  1686. await createFolder();
  1687. }
  1688. }
  1689. async function createFolder() {
  1690. try {
  1691. const resource = { name: folder_name, mimeType: 'application/vnd.google-apps.folder' };
  1692. if (PROJECTS_PARENT_FOLDER_ID) resource.parents = [PROJECTS_PARENT_FOLDER_ID];
  1693. const res = await gapi.client.drive.files.create({
  1694. resource,
  1695. fields: 'id,name,webViewLink',
  1696. supportsAllDrives: true
  1697. });
  1698. alert('Folder created: ' + res.result.name + '\nID: ' + res.result.id);
  1699. } catch (err) {
  1700. const detail = err?.result?.error || err;
  1701. console.error('Drive create error:', detail);
  1702. alert('Drive error: ' + (detail.message || detail.statusText || JSON.stringify(detail)));
  1703. }
  1704. }
  1705. /* ******************************************************************************************* */
  1706. async function fetchAndExtractData() {
  1707. const lat = $('#site_lat').val();
  1708. const lng = $('#site_lng').val();
  1709. if (!lat || !lng) {
  1710. alert('Please select a Google suggested Site Address first so we can capture coordinates.');
  1711. return;
  1712. }
  1713. // Helper to stringify area object from list_lookup.php
  1714. const formatArea = (area) => {
  1715. if (!area) return '';
  1716. // prefer sqm label, fall back to ha
  1717. return area.sqm_label || area.ha_label || '';
  1718. };
  1719. try {
  1720. const resp = await fetch('classes/list_lookup.php', {
  1721. method: 'POST',
  1722. headers: { 'Content-Type': 'application/json' },
  1723. body: JSON.stringify({ lat: parseFloat(lat), lng: parseFloat(lng) })
  1724. });
  1725. const data = await resp.json();
  1726. // Show any lookup error in the modal
  1727. if (!resp.ok || !data || data.ok !== true) {
  1728. $('#pb-errors').removeClass('d-none').text((data && data.error) ? data.error : 'Lookup failed');
  1729. bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).show();
  1730. return;
  1731. }
  1732. $('#pb-errors').addClass('d-none').text('');
  1733. // Fill the preview modal
  1734. $('#pb_pid').text(data.pid || '–');
  1735. $('#pb_title').text(data.title_id || '–');
  1736. $('#pb_area').text(formatArea(data.total_area) || '–');
  1737. $('#pb_locality').text(data.locality || '–'); // include if you added locality in PHP later
  1738. //$('#pb_council').text(data.council || '–');
  1739. $('#pb_scheme').text(data.planning_scheme || '–');
  1740. $('#pb_zones').text((Array.isArray(data.planning_zones) ? data.planning_zones.join(', ') : (data.planning_zones || '')) || '–');
  1741. $('#pb_codes').text((Array.isArray(data.planning_codes) ? data.planning_codes.join(', ') : (data.planning_codes || '')) || '–');
  1742. $('#area_sqm').text(data.area_sqm || '–');
  1743. $('#area_ha').text(data.area_ha || '–');
  1744. $('#tenure').text(data.tenure || '–');
  1745. $('#lpi').text(data.lpi || '–');
  1746. $('#list_guid').text(data.list_guid || '–');
  1747. bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).show();
  1748. // Apply button writes to the form and DB
  1749. $('#pb-apply').off('click').on('click', () => {
  1750. const apply = (id, v) => {
  1751. const el = document.getElementById(id);
  1752. if (el && typeof v !== 'undefined' && v !== null) {
  1753. el.value = v;
  1754. storeField(id, v);
  1755. }
  1756. };
  1757. apply('property_id', data.pid || '');
  1758. apply('title_id', data.title_id || '');
  1759. apply('total_area', formatArea(data.total_area) || '');
  1760. apply('planning_scheme', data.planning_scheme || '');
  1761. apply('planning_zones', Array.isArray(data.planning_zones) ? data.planning_zones.join(', ') : (data.planning_zones || ''));
  1762. apply('planning_codes', Array.isArray(data.planning_codes) ? data.planning_codes.join(', ') : (data.planning_codes || ''));
  1763. //apply('council', data.council || '');
  1764. if (document.getElementById('locality')) apply('locality', data.locality || '');
  1765. // Optional extras if you add matching inputs:
  1766. //apply('area_sqm', data.total_area?.sqm_label || '');
  1767. //apply('area_ha', data.total_area?.ha_label || '');
  1768. //apply('tenure', data.tenure || '');
  1769. //apply('lpi', data.lpi || '');
  1770. //apply('list_guid',data.list_guid|| '');
  1771. bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).hide();
  1772. });
  1773. } catch (e) {
  1774. $('#pb-errors').removeClass('d-none').text('Error fetching data: ' + e.message);
  1775. bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).show();
  1776. }
  1777. };
  1778. // Build the exact body text from current form values
  1779. const useDynamicGreeting = false;
  1780. function getGreeting() {
  1781. if (!useDynamicGreeting) return 'Good Morning,';
  1782. const hour = new Date().getHours();
  1783. if (hour < 12) return 'Good Morning,';
  1784. if (hour < 18) return 'Good Afternoon,';
  1785. return 'Good Evening,';
  1786. }
  1787. function escapeHtml(s) {
  1788. return String(s || '').replace(/[&<>"']/g, c => ({
  1789. '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;'
  1790. }[c]));
  1791. }
  1792. function buildPlanningEmailText() {
  1793. const greet = getGreeting();
  1794. const address = ($('#site_address').val() || '').trim();
  1795. const owners = ($('#registered_owner').val() || '').trim();
  1796. const pid = ($('#property_id').val() || '').trim();
  1797. const title = ($('#title_id').val() || '').trim();
  1798. const fallback = 'N/A';
  1799. const lines = [
  1800. greet,
  1801. '',
  1802. 'I am requesting the planning and plumbing information for the following address:',
  1803. `Property Address: ${address || fallback}`,
  1804. `Property Owners: ${owners || fallback}`,
  1805. `Property ID: ${pid || fallback}`,
  1806. `Title/Vol: ${title || fallback}`,
  1807. '',
  1808. 'Attached is a signed Letter of Authority from the property owners.'
  1809. ];
  1810. return lines.join('\n');
  1811. }
  1812. function buildPlanningEmailHtml() {
  1813. const greet = getGreeting();
  1814. const address = ($('#site_address').val() || '').trim();
  1815. const owners = ($('#registered_owner').val() || '').trim();
  1816. const pid = ($('#property_id').val() || '').trim();
  1817. const title = ($('#title_id').val() || '').trim();
  1818. const fallback = 'N/A';
  1819. return `
  1820. <div style="background:#ffffff;padding:16px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;color:#000000;">
  1821. <p style="margin:0 0 12px 0;">${escapeHtml(greet)}</p>
  1822. <p style="margin:0 0 12px 0;">I am requesting the planning and plumbing information for the following address:</p>
  1823. <table role="presentation" cellpadding="0" cellspacing="0" width="75%" style="border-collapse:collapse;background:#ffffff;color:#000000;">
  1824. <tr>
  1825. <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;width:200px;">Property Address</td>
  1826. <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(address || fallback)}</td>
  1827. </tr>
  1828. <tr>
  1829. <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;">Property Owners</td>
  1830. <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(owners || fallback)}</td>
  1831. </tr>
  1832. <tr>
  1833. <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;">Property ID</td>
  1834. <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(pid || fallback)}</td>
  1835. </tr>
  1836. <tr>
  1837. <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;">Title/Vol</td>
  1838. <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(title || fallback)}</td>
  1839. </tr>
  1840. </table>
  1841. <p style="margin:12px 0 0 0;">Attached is a signed Letter of Authority from the property owners.</p>
  1842. </div>`;
  1843. }
  1844. function copyFromTextarea(selector, btnEl) {
  1845. const val = $(selector).val();
  1846. const doWrite = async () => {
  1847. if (navigator.clipboard && navigator.clipboard.writeText) {
  1848. await navigator.clipboard.writeText(val);
  1849. } else {
  1850. const ta = document.querySelector(selector);
  1851. ta.focus();
  1852. ta.select();
  1853. document.execCommand('copy');
  1854. }
  1855. };
  1856. doWrite().then(() => {
  1857. const original = btnEl.textContent;
  1858. btnEl.textContent = 'Copied';
  1859. btnEl.disabled = true;
  1860. setTimeout(() => { btnEl.textContent = original; btnEl.disabled = false; }, 1200);
  1861. }).catch(() => {
  1862. alert('Copy failed. You can select and copy the text manually.');
  1863. });
  1864. }
  1865. // Single modal hook
  1866. document.getElementById('emailTemplateModal')
  1867. .addEventListener('show.bs.modal', function () {
  1868. const text = buildPlanningEmailText();
  1869. const html = buildPlanningEmailHtml();
  1870. document.getElementById('emailText').value = text;
  1871. document.getElementById('emailHtmlPreview').innerHTML = html;
  1872. document.getElementById('emailHtmlSource').value = html;
  1873. // Build subject using the current site address
  1874. const addr = (document.getElementById('site_address')?.value || '').trim() || 'N/A';
  1875. const subject = encodeURIComponent(`Request for Property Information - ${addr}`);
  1876. const body = encodeURIComponent(text);
  1877. document.getElementById('openMailto').href = `mailto:?subject=${subject}&body=${body}`;
  1878. });
  1879. // Single copy handlers
  1880. $('#copyEmailText').on('click', function () {
  1881. copyFromTextarea('#emailText', this);
  1882. });
  1883. $('#copyEmailHtml').on('click', function () {
  1884. copyFromTextarea('#emailHtmlSource', this);
  1885. });
  1886. </script>
  1887. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
  1888. <script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB-QceOYrDe9otynMmQ9iNF3yEZzbpsanM&libraries=places&loading=async&callback=initAutocomplete" ></script>
  1889. <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
  1890. <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
  1891. </body>
  1892. </html>