| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162 |
- <?php
- /* -------------------------------------------------------------------------- */
- /* CONFIGURATION */
- /* -------------------------------------------------------------------------- */
- date_default_timezone_set("Australia/Hobart");
- //error_reporting(E_ERROR | E_PARSE);
- error_reporting(E_ALL);
- ini_set('display_errors', '0');
- ini_set('log_errors', '1');
- require_once 'connection.php';
- include_once "vendor/autoload.php";
- if (session_status() !== PHP_SESSION_ACTIVE) session_start();
- if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(16));
- $csrf = $_SESSION['csrf'];
- $accessToken = getenv('HUBSPOT_TOKEN') ?: '';
- #$enquiry_date = date("l dS M \'y");
- $drg = isset($_GET['drg']) ? (int)$_GET['drg'] : 0;
- if (!empty($_GET['drg'])) {
- include "table.php";
- }
- // CHECK NEXT AVAILABLE Drawing NUMBER
- $nextQ = mysqli_query($con, "SELECT MAX(drg) AS maxdrg FROM details");
- if (!$nextQ) { printf("Error: %s\n", mysqli_error($con)); exit; }
- $MQrow = mysqli_fetch_assoc($nextQ);
- $maxQ = isset($MQrow['maxdrg']) ? ((int)$MQrow['maxdrg'] + 1) : 1;
- /* -------------------------------------------------------------------------- */
- /* API MODE */
- /* -------------------------------------------------------------------------- */
- if (!defined('SITE_ROOT')) define('SITE_ROOT', dirname(__DIR__));
- // ===== LOA markdown creation helpers and API =====
- if (!defined('CONTRACTS_DIR')) define('CONTRACTS_DIR', SITE_ROOT . '/contracts/contracts');
- // ===== LOA config (must match contracts-admin/loa.php) =====
- if (!defined('LOA_DIR')) define('LOA_DIR', SITE_ROOT . '/contracts/loa');
- if (!defined('LOA_BASE_URL')) define('LOA_BASE_URL', 'https://modulosdesign.com.au/contracts'); // where loa.php lives
- if (!defined('LOA_TOKEN_SECRET')) define('LOA_TOKEN_SECRET', getenv('LOA_TOKEN_SECRET') ?: '');
- if (!function_exists('json_response')) {
- function json_response(array $data, int $code = 200) {
- header('Content-Type: application/json; charset=utf-8');
- http_response_code($code);
- echo json_encode($data);
- }
- }
- if (!function_exists('safe_clientid')) {
- function safe_clientid(string $s): string {
- // collapse whitespace to hyphen, strip invalid chars
- $s = preg_replace('/\s+/', '-', $s);
- $s = preg_replace('/[^A-Za-z0-9\-_]+/', '-', $s);
- $s = preg_replace('/-+/', '-', $s);
- return trim($s, '-_');
- }
- }
- if (!function_exists('contract_path')) {
- function contract_path(string $clientid): string {
- return rtrim(CONTRACTS_DIR, '/\\') . DIRECTORY_SEPARATOR . $clientid . '.md';
- }
- }
- function loa_path(string $job): string {
- $id = preg_replace('/\D+/', '', $job); // digits only to match loa.php
- return rtrim(LOA_DIR, '/\\') . DIRECTORY_SEPARATOR . $id . '.md';
- }
- function loa_public_url(string $job): string {
- $jobClean = preg_replace('/\D+/', '', $job); // digits only
- $token = hash_hmac('sha256', 'loa|' . $jobClean, LOA_TOKEN_SECRET);
- return rtrim(LOA_BASE_URL, '/') . '/loa.php?job=' . rawurlencode($jobClean) . '&token=' . rawurlencode($token);
- }
- /** Minimal front-matter pulls for LOA */
- function extract_loa_fields(string $file): array {
- $out = ['client_name'=>'','client_email'=>'','property_address'=>''];
- $txt = @file_get_contents($file);
- if (!$txt) return $out;
- if (!preg_match('/^---\s*\R(.*?)\R---/s', $txt, $m)) return $out;
- $fm = $m[1]; $ctx = null;
- foreach (preg_split('/\R/', $fm) as $line) {
- if (preg_match('/^\s*client\s*:\s*$/', $line)) { $ctx='client'; continue; }
- if (preg_match('/^\s*property\s*:\s*$/', $line)) { $ctx='property'; continue; }
- if ($ctx==='client') {
- if (preg_match('/^\s*name\s*:\s*(.+)$/', $line, $mm)) $out['client_name'] = trim($mm[1], " \t\"'");
- if (preg_match('/^\s*email\s*:\s*(.+)$/', $line, $mm)) $out['client_email']= trim($mm[1], " \t\"'");
- } elseif ($ctx==='property') {
- if (preg_match('/^\s*address\s*:\s*(.+)$/', $line, $mm)) $out['property_address']= trim($mm[1], " \t\"'");
- }
- }
- return $out;
- }
- /** Tiny contract front-matter extractor used by lookup */
- function extract_front_matter_fields(string $file): array {
- $out = [];
- $txt = @file_get_contents($file);
- if (!$txt) return $out;
- if (!preg_match('/^---\s*\R(.*?)\R---/s', $txt, $m)) return $out;
- $fm = $m[1];
- if (preg_match('/^\s*client\s*:\s*$(.*?)^(?=\S)/ms', $fm."\nX", $block)) {
- $clientBlock = $block[1];
- if (preg_match('/^\s*name\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $clientBlock, $mm)) $out['client_name'] = trim($mm[1]);
- if (preg_match('/^\s*email\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $clientBlock, $mm)) $out['client_email'] = trim($mm[1]);
- if (preg_match('/^\s*address\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi',$clientBlock, $mm)) $out['client_address']= trim($mm[1]);
- }
- if (preg_match('/^\s*project\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $fm, $mm)) $out['project'] = trim($mm[1]);
- if (preg_match('/^\s*job\s*:\s*["\']?([^"\'\r\n]+)["\']?/mi', $fm, $mm)) $out['job'] = trim($mm[1]);
- return $out;
- }
- /** Find best-known client + address for a job (checks existing LOA, then contracts) */
- function lookup_job_for_loa(string $job): array {
- $job = preg_replace('/\D+/', '', (string)($_POST['job'] ?? $drg ?? ''));
- $empty = ['client_name'=>'','client_email'=>'','property_address'=>'','source'=>null];
- // Prefer existing LOA
- $loaFile = loa_path($job);
- if (is_file($loaFile)) {
- return extract_loa_fields($loaFile) + ['source'=>'loa'];
- }
- // Scan contracts for a matching job or filename
- foreach (glob(rtrim(CONTRACTS_DIR,'/\\').'/*.md') as $file) {
- $fm = extract_front_matter_fields($file);
- $fname_id = preg_replace('/\.md$/i','',basename($file));
- if (($fm['job'] ?? '') === $job || $fname_id === $job) {
- $addr = $fm['client_address'] ?? '';
- return [
- 'client_name' => $fm['client_name'] ?? '',
- 'client_email' => $fm['client_email'] ?? '',
- 'property_address' => $addr,
- 'source' => 'contract',
- 'clientid' => $fname_id,
- ];
- }
- }
- return $empty;
- }
- /* ------------------------ FUNCTIONS ------------------------ */
- /** Build a starter Markdown file with YAML front matter. */
- if (!function_exists('build_markdown_template')) {
- function build_markdown_template(string $clientid, ?string $name, ?string $email, ?string $design_style, ?string $phone, ?string $site_address): string {
- $today = date('Y-m-d');
- $name = $name ?? '';
- $email = $email ?? '';
- $design_style = $design_style ?? '';
- $phone = $phone ?? '';
- $site_address = $site_address ?? '';
- // Generate secure random credentials
- $adminUser = bin2hex(random_bytes(4)); // 8 hex chars
- $adminPass = bin2hex(random_bytes(8)); // 16 hex chars
- $adminSecret = bin2hex(random_bytes(16)); // 32 hex chars
- $frontMatter = <<<YAML
- ---
- client:
- id: "{$clientid}"
- name: "{$name}"
- email: "{$email}"
- phone: "{$phone}"
- address: "{$site_address}"
- project: "{$design_style}"
- dates:
- prepared: "{$today}"
- dev:
- name: 'Modulos Design'
- email: 'ben@modulos.com.au'
- phone: '0402 984 082'
- address: '34 Coplestone Street, Scottsdale, Tas 7260'
- version: 1
- quote:
- number: "{$clientid}"
- admin:
- user: "{$adminUser}"
- pass: "{$adminPass}"
- secret: "{$adminSecret}"
- ---
- YAML;
- $body = <<<MD
- # Contract of work
- 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").
- ##### 1. Scope of Services
- MD;
- return $frontMatter . $body;
- }
- }
- function yaml_q($v){ return '"'.str_replace(['\\','"'], ['\\\\','\\"'], (string)$v).'"'; }
- /** Upsert a top-level key like: job: "123" */
- function yaml_upsert_top(string $yaml, string $key, string $val): string {
- if ($val === '' || $val === null) return $yaml;
- $re = '/^\s*'.preg_quote($key,'/').'\s*:\s*.*/mi';
- if (preg_match($re, $yaml)) return preg_replace($re, $key.': '.yaml_q($val), $yaml, 1);
- return rtrim($yaml)."\n".$key.': '.yaml_q($val)."\n";
- }
- /** Upsert keys inside a block (client:, property:, dates:) with two-space indentation */
- function yaml_upsert_block(string $yaml, string $block, array $pairs): string {
- $blockRe = '/(^\s*'.preg_quote($block,'/').'\s*:\s*\R(?:[ \t].*\R)*)(?=^\S|\z)/m';
- if (preg_match($blockRe, $yaml, $m, PREG_OFFSET_CAPTURE)) {
- $start = $m[0][1]; $len = strlen($m[0][0]); $blk = $m[0][0];
- foreach ($pairs as $k=>$v) {
- if ($v === '' || $v === null) continue;
- $lineRe = '/^\s*'.preg_quote($k,'/').'\s*:\s*.*/mi';
- $rep = ' '.$k.': '.yaml_q($v);
- $blk = preg_replace($lineRe, $rep, $blk, 1, $count);
- if ($count === 0) $blk = rtrim($blk)."\n".$rep."\n";
- }
- return substr_replace($yaml, $blk, $start, $len);
- } else {
- $out = rtrim($yaml)."\n".$block.":\n";
- foreach ($pairs as $k=>$v) if ($v !== '' && $v !== null) $out .= ' '.$k.': '.yaml_q($v)."\n";
- return $out;
- }
- }
- /** Given a full .md file, update its front matter and return updated text */
- function update_front_matter_text(string $md, array $kv_top, array $kv_blocks): string {
- if (!preg_match('/^---\R(.*?)\R---(\R|$)/s', $md, $m, PREG_OFFSET_CAPTURE)) {
- // add a front matter header if missing
- $yaml = "";
- foreach ($kv_top as $k=>$v) $yaml = yaml_upsert_top($yaml, $k, $v);
- foreach ($kv_blocks as $b=>$p) $yaml = yaml_upsert_block($yaml, $b, $p);
- return "---\n".rtrim($yaml)."\n---\n".ltrim($md);
- }
- $yaml = $m[1][0];
- $start = $m[0][1];
- $len = strlen($m[0][0]);
- foreach ($kv_top as $k=>$v) $yaml = yaml_upsert_top($yaml, $k, $v);
- foreach ($kv_blocks as $b=>$p) $yaml = yaml_upsert_block($yaml, $b, $p);
- $newHeader = "---\n".rtrim($yaml)."\n---\n";
- return substr_replace($md, $newHeader, $start, $len);
- }
- /* -------------------------------------------------------------------------- */
- /* API MODE */
- /* -------------------------------------------------------------------------- */
- $__action = $_GET['action'] ?? $_POST['action'] ?? null;
- if ($__action === 'loa_create') {
- try {
- $job = safe_clientid($_POST['job'] ?? (string)($drg ?? ''));
- if (!$job) { json_response(['ok'=>false,'error'=>'Missing job #'], 400); exit; }
- // Form inputs (fall back to blanks)
- $name = trim($_POST['client_name'] ?? '');
- $email = trim($_POST['client_email'] ?? '');
- $clientPhone = trim($_POST['client_phone'] ?? ($_POST['client_mobile'] ?? ''));
- $clientAddress = trim($_POST['client_address'] ?? ($_POST['postal_address'] ?? ''));
- $addr = trim($_POST['property_address'] ?? ($_POST['site_address'] ?? ''));
- $propPid = trim($_POST['property_pid'] ?? ($_POST['property_id'] ?? ''));
- $propTitleRaw = trim($_POST['property_title'] ?? ($_POST['title_id'] ?? ''));
- // Split things like "140687/4", "140687 - 4", "140687 4"
- $propVol = ''; $propFolio = '';
- if ($propTitleRaw !== '') {
- if (preg_match('/^\s*([A-Za-z0-9]+)\s*[\/\-\s]\s*([A-Za-z0-9]+)\s*$/', $propTitleRaw, $mm)) {
- $propVol = $mm[1];
- $propFolio = $mm[2];
- } else {
- // If it doesn't split, keep the whole thing in vol so you don't lose info
- $propVol = $propTitleRaw;
- }
- }
- $dateStart = trim($_POST['start_date'] ?? '');
- $dateEnd = trim($_POST['end_date'] ?? '');
- $overwrite = (int)($_POST['overwrite'] ?? 0);
- $dst = loa_path($job);
- if (!is_dir(LOA_DIR)) @mkdir(LOA_DIR, 0775, true);
- // What we want in the YAML
- $top = ['job' => $job];
- $blocks = [
- 'client' => [
- 'name' => $name,
- 'email' => $email,
- 'phone' => $clientPhone,
- 'address' => $clientAddress,
- ],
- 'property' => [
- 'address' => $addr,
- 'pid' => $propPid,
- 'title' => $propTitleRaw,
- ],
- 'dates' => [
- 'start' => $dateStart,
- 'end' => $dateEnd,
- ],
- ];
- // Create new or update existing
- if (!file_exists($dst)) {
- // Load template, then inject values into its front matter
- $tplPath = rtrim(LOA_DIR,'/\\').'/default-authorisation.md';
- $tpl = @file_get_contents($tplPath);
- if ($tpl === false || $tpl === '') $tpl = "# Authorisation\n";
- $out = update_front_matter_text($tpl, $top, $blocks);
- if (@file_put_contents($dst, $out, LOCK_EX) === false) {
- json_response(['ok'=>false,'error'=>'Write failed'], 500); exit;
- }
- json_response(['ok'=>true,'job'=>$job,'path'=>$dst,'public_url'=>loa_public_url($job),'created'=>true]);
- } else {
- if ($overwrite) {
- // Overwrite from template
- $tplPath = rtrim(LOA_DIR,'/\\').'/default-authorisation.md';
- $tpl = @file_get_contents($tplPath);
- if ($tpl === false || $tpl === '') $tpl = "# Authorisation\n";
- $out = update_front_matter_text($tpl, $top, $blocks);
- } else {
- // Update front matter only in the existing file
- $existing = @file_get_contents($dst) ?: '';
- $out = update_front_matter_text($existing, $top, $blocks);
- }
- if (@file_put_contents($dst, $out, LOCK_EX) === false) {
- json_response(['ok'=>false,'error'=>'Write failed'], 500); exit;
- }
- json_response([
- 'ok'=>true,'job'=>$job,'path'=>$dst,'public_url'=>loa_public_url($job),
- 'created'=>false,'updated_frontmatter'=>!$overwrite
- ]);
- }
- } catch (Throwable $e) {
- json_response(['ok'=>false,'error'=>$e->getMessage()], 500);
- }
- exit;
- }
- // ===== end LOA helpers and API =====
- // Create the LOA markdown from default-authorisation.md
- if ($__action === 'loa_create') {
- try {
- $job = safe_clientid($_POST['job'] ?? (string)($drg ?? ''));
- if (!$job) { json_response(['ok'=>false,'error'=>'Missing job #'], 400); exit; }
- // Prefer explicit POST values; otherwise use current form values already on the page
- $name = trim($_POST['client_name'] ?? (trim(($firstname ?? '').' '.($lastname ?? '')) ?: ($joint_name ?? '')));
- $email = trim($_POST['client_email'] ?? ($client_email ?? ''));
- $addr = trim($_POST['property_address'] ?? ($site_address ?? ''));
- $overwrite = (int)($_POST['overwrite'] ?? 0);
- $dst = loa_path($job);
- if (!is_dir(LOA_DIR)) @mkdir(LOA_DIR, 0775, true);
- if (file_exists($dst) && !$overwrite) {
- json_response(['ok'=>false,'error'=>'File already exists','path'=>$dst], 409); exit;
- }
- // Load template (fallback to minimal front-matter if missing)
- $tplPath = rtrim(LOA_DIR,'/\\').'/default-authorisation.md';
- $tpl = @file_get_contents($tplPath);
- if ($tpl === false || $tpl === '') {
- $q = fn($v) => '"'.str_replace(['\\','"'], ['\\\\','\\"'], (string)$v).'"';
- $tpl = "---\njob: ".$q($job)."\nclient:\n name: ".$q($name)."\n email: ".$q($email)."\nproperty:\n address: ".$q($addr)."\n---\n# Authorisation\n";
- } else {
- // Light YAML substitutions
- $tpl = preg_replace_callback('/^---\R(.*?)\R---/s', function($m) use($job,$name,$email,$addr){
- $yaml = $m[1];
- $q = fn($v) => '"'.str_replace(['\\','"'], ['\\\\','\\"'], (string)$v).'"';
- // job
- $yaml = preg_replace('/^\s*job\s*:\s*.*/mi', 'job: '.$q($job), $yaml, 1, $jobCount);
- if ($jobCount === 0) $yaml = "job: ".$q($job)."\n".$yaml;
- // ensure blocks exist
- if (!preg_match('/^\s*client\s*:/mi', $yaml)) $yaml = "client:\n name:\n email:\n".$yaml;
- if (!preg_match('/^\s*property\s*:/mi', $yaml)) $yaml = "property:\n address:\n".$yaml;
- // client.name/email
- $yaml = preg_replace_callback('/(^\s*client\s*:\s*\R(?:.*\R)*?)(?=^\S|\z)/ms', function($b) use($q,$name,$email){
- $blk = $b[0];
- $blk = preg_replace('/^\s*name\s*:\s*.*/mi', ' name: '.$q($name), $blk, 1, $c1);
- if ($c1===0) $blk = rtrim($blk)."\n name: ".$q($name)."\n";
- $blk = preg_replace('/^\s*email\s*:\s*.*/mi', ' email: '.$q($email), $blk, 1, $c2);
- if ($c2===0) $blk = rtrim($blk)."\n email: ".$q($email)."\n";
- return $blk;
- }, $yaml, 1);
- // property.address
- $yaml = preg_replace_callback('/(^\s*property\s*:\s*\R(?:.*\R)*?)(?=^\S|\z)/ms', function($b) use($q,$addr){
- $blk = $b[0];
- $blk = preg_replace('/^\s*address\s*:\s*.*/mi', ' address: '.$q($addr), $blk, 1, $p1);
- if ($p1===0) $blk = rtrim($blk)."\n address: ".$q($addr)."\n";
- return $blk;
- }, $yaml, 1);
- return "---\n".$yaml."\n---";
- }, $tpl, 1) ?? $tpl;
- }
- if (@file_put_contents($dst, $tpl, LOCK_EX) === false) {
- json_response(['ok'=>false,'error'=>'Write failed'], 500); exit;
- }
- json_response(['ok'=>true,'job'=>$job,'path'=>$dst,'public_url'=>loa_public_url($job)]);
- } catch (Throwable $e) {
- json_response(['ok'=>false,'error'=>$e->getMessage()], 500);
- }
- exit;
- }
- // Optional: auto-fill data for a job (used by other screens / future)
- if ($__action === 'loa_lookup') {
- $job = safe_clientid($_GET['job'] ?? $_POST['job'] ?? '');
- $data = $job ? lookup_job_for_loa($job) : ['client_name'=>'','client_email'=>'','property_address'=>'','source'=>null];
- $found = (bool)($data['client_name'] || $data['client_email'] || $data['property_address']);
- json_response(['ok'=>true,'found'=>$found,'data'=>$data]);
- exit;
- }
- /* -------------------------------------------------------------------------- */
- /* HELPER FUNCTIONS */
- /* -------------------------------------------------------------------------- */
- use Google\Client;
- use Google\Service\Drive;
- function createFolder(string $access_token, string $folder_name, string $parent_folder_id = ''): ?string
- {
- try {
- $client = new Google\Client();
- $client->setAccessToken($access_token);
- $driveService = new Drive($client);
- $metadata = new Drive\DriveFile([
- 'name' => $folder_name,
- 'mimeType' => 'application/vnd.google-apps.folder',
- ]);
- if ($parent_folder_id) {
- $metadata->setParents([$parent_folder_id]);
- }
- $file = $driveService->files->create($metadata, ['fields' => 'id,name,webViewLink']);
- return $file->id;
- } catch (Exception $e) {
- error_log('createFolder error: ' . $e->getMessage());
- return null;
- }
- }
- function createDealHubSpot($newDealData)
- {
- global $accessToken;
- $endpoint = 'https://api.hubapi.com/crm/v3/objects/deals';
- $curl = curl_init();
- curl_setopt_array($curl, [
- CURLOPT_URL => $endpoint,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => json_encode($newDealData),
- CURLOPT_HTTPHEADER => [
- 'Authorization: Bearer ' . $accessToken, // Use OAuth 2.0 token OR 'Authorization: Bearer ' . $apiKey for API key
- 'Content-Type: application/json'
- ]
- ]);
- $response = curl_exec($curl);
- curl_close($curl);
- return $response;
- }
- function updateDealHubSpot($dealId, $data)
- {
- global $accessToken;
- $endpoint = 'https://api.hubapi.com/crm/v3/objects/deals/' . $dealId;
- $curl = curl_init();
- curl_setopt_array($curl, [
- CURLOPT_URL => $endpoint,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_CUSTOMREQUEST => 'PATCH',
- CURLOPT_POSTFIELDS => json_encode($data),
- CURLOPT_HTTPHEADER => [
- 'Authorization: Bearer ' . $accessToken, // Use OAuth 2.0 token OR 'Authorization: Bearer ' . $apiKey for API key
- 'Content-Type: application/json'
- ]
- ]);
- $response = curl_exec($curl);
- curl_close($curl);
- return $response;
- }
- function addContactToHubSpot($newContactData) {
- global $accessToken;
- $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts';
- $curl = curl_init();
- curl_setopt_array($curl, [
- CURLOPT_URL => $endpoint,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => json_encode($newContactData),
- CURLOPT_HTTPHEADER => [
- 'Authorization: Bearer ' . $accessToken,
- 'Content-Type: application/json'
- ]
- ]);
- $response = curl_exec($curl);
- curl_close($curl);
- return $response;
- }
- function searchContact($query) {
- global $accessToken;
- $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';
- $queryJson = json_encode($query);
- $ch = curl_init($endpoint);
- curl_setopt($ch, CURLOPT_POST, 1);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $queryJson);
- curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $accessToken, 'Content-Type: application/json'));
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- $response = curl_exec($ch);
- if (curl_errno($ch)) {
- echo 'Error making cURL request: ' . curl_error($ch);
- }
- curl_close($ch);
- return $response;
- }
- function updateContact($contactId, $data) {
- global $accessToken;
- $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/' . $contactId;
- $curl = curl_init();
- curl_setopt_array($curl, [
- CURLOPT_URL => $endpoint,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_CUSTOMREQUEST => 'PUT',
- CURLOPT_POSTFIELDS => json_encode($data),
- CURLOPT_HTTPHEADER => [
- 'Authorization: Bearer ' . $accessToken,
- 'Content-Type: application/json'
- ]
- ]);
- $response = curl_exec($curl);
- curl_close($curl);
- return $response;
- }
- function format_E164($num)
- {
- $phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
- try {
- $phoneProto = $phoneUtil->parse($num, "AU");
- return [
- "phone_number" => $phoneUtil->format($phoneProto, \libphonenumber\PhoneNumberFormat::INTERNATIONAL),
- "phone_type" => $phoneUtil->getNumberType($phoneProto)
- ];
- } catch (\libphonenumber\NumberParseException $e) {
- return null;
- }
- }
- if (!empty($drg) and !empty($_POST['add_client_to_crm']) and empty($contactId)) {
- try {
- $query = [
- "filterGroups" => [
- [
- "filters" => [
- [
- "value" => $_POST['firstname'] . "*",
- "propertyName" => 'firstname',
- "operator" => "EQ"
- ],
- [
- "value" => $_POST['lastname'] . "*",
- "propertyName" => "lastname",
- "operator" => "EQ"
- ],
- [
- "value" => $_POST['client_mobile'] . "*",
- "propertyName" => "phone",
- "operator" => "EQ"
- ]
- ]
- ]
- ],
- ];
- $response = json_decode(searchContact($query), true);
- if ($response['total'] > 0) {
- 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>";
- } else {
- $newContactData = [
- 'properties' => [
- 'firstname' => $_POST['firstname'],
- 'lastname' => $_POST['lastname'],
- 'description' => 'Added via Online Client Brief Form # ' . $drg,
- 'company' => $_POST['joint_name'],
- 'email' => $_POST['client_email'],
- 'address' => $_POST['postal_address'],
- 'city' => $_POST['postal_address_town'],
- 'zip' => $_POST['postal_address_postcode'],
- 'country' => 'AU',
- 'state' => $_POST['postal_address_state'],
- 'hubspot_owner_id' => '585959844',
- ]
- ];
- if($phone) {
- if($phone["phone_type"] == \libphonenumber\PhoneNumberType::MOBILE)
- $newContactData["properties"]["mobilephone"] = $phone["phone_number"];
- else
- $newContactData["properties"]["phone"] = $phone["phone_number"];
- }
- $response = addContactToHubSpot($newContactData);
- file_put_contents("crmadd.log", $response);
- $createdContact = json_decode($response, true);
- if ($createdContact['id'] > 0) {
- $crm_id = intval($createdContact['id']);
- $result = mysqli_query($con, "UPDATE details SET crm_id = " . (int)$crm_id . " WHERE drg = " . (int)$drg);
- if (!$result) {
- printf("Error: %s\n", mysqli_error($con));
- exit();
- } else {
- echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>×</a><h4 style=\"text-align: center;\">Customer: " . htmlspecialchars($_POST['firstname'], ENT_QUOTES, 'UTF-8') . " " . htmlspecialchars($_POST['lastname'], ENT_QUOTES, 'UTF-8') . " added to crm, with id: " . (int)$crm_id . "</h4></div>";
- $isHideDismissableAlert = 1;
- }
- }
- }
- } catch (\Exception $err) {
- echo '<pre>' . $err->getMessage() . '</pre>';
- die();
- }
- } elseif (!empty($drg) and !empty($_POST['edit_client_in_crm']) and !empty($contactId)) {
- try {
- $data = [
- 'properties' => [
- 'firstname' => $_POST['firstname'],
- 'lastname' => $_POST['lastname'],
- 'company' => $_POST['joint_name'],
- 'email' => $_POST['client_email'],
- 'address' => $_POST['postal_address'],
- 'city' => $_POST['postal_address_town'],
- 'zip' => $_POST['postal_address_postcode'],
- 'country' => 'AU',
- 'state' => $_POST['postal_address_state'],
- 'hubspot_owner_id' => '585959844',
- ]
- ];
- if($phone) {
- if($phone["phone_type"] == \libphonenumber\PhoneNumberType::MOBILE)
- $data["properties"]["mobilephone"] = $phone["phone_number"];
- else
- $data["properties"]["phone"] = $phone["phone_number"];
- }
- $query = [
- "filterGroups" => [
- [
- "filters" => [
- [
- "value" => $_POST['firstname'] . "*",
- "propertyName" => 'firstname',
- "operator" => "EQ"
- ],
- [
- "value" => $_POST['lastname'] . "*",
- "propertyName" => "lastname",
- "operator" => "EQ"
- ],
- [
- "value" => $_POST['client_mobile'] . "*",
- "propertyName" => "phone",
- "operator" => "EQ"
- ]
- ]
- ]
- ],
- ];
- $response = json_decode(searchContact($query), true);
- if ($response['total'] > 0) {
- $response = updateContact($contactId, $data);
- $result = mysqli_query($con, "UPDATE details SET crm_id = " . (int)$response['id'] . " WHERE drg = " . (int)$drg);
- file_put_contents("crmupdate.log", $response);
- $response = json_decode($response, true);
- if ($response['id'] > 0) {
- echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>×</a><h4 style=\"text-align: center;\">Customer: " . htmlspecialchars($_POST['firstname'], ENT_QUOTES, 'UTF-8') . " " . htmlspecialchars($_POST['lastname'], ENT_QUOTES, 'UTF-8') . " updated in crm</h4></div>";
- $isHideDismissableAlert = 1;
- }
- } else {
- $response = addContactToHubSpot($newContactData);
- file_put_contents("crmadd.log", $response);
- $createdContact = json_decode($response, true);
- if ($createdContact['id'] > 0) {
- $crm_id = intval($createdContact['id']);
- echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>×</a><h4 style=\"text-align: center;\">Customer: " . htmlspecialchars($_POST['firstname'], ENT_QUOTES, 'UTF-8') . " " . htmlspecialchars($_POST['lastname'], ENT_QUOTES, 'UTF-8') . " added to crm, with id: " . (int)$crm_id . "</h4></div>";
- $isHideDismissableAlert = 1;
- }
- }
- } catch (\Exception $err) {
- echo '<pre>Crm Id: ' . $contactId . ', response: ' . $err->getMessage() . '</pre>';
- die();
- }
- }
- // Create Deal from Enquiry Form
- if (!empty($quid) and !empty($_POST['add_deal_to_hubspot']) and empty($dealId)) {
- try {
- $newDealData = [
- 'associations' => [
- [
- 'types' => [
- [
- 'associationCategory' => 'HUBSPOT_DEFINED',
- 'associationTypeId' => 3,
- ]
- ],
- 'to' => [
- 'id' => $_POST['crm_id']
- ],
- ]
- ],
- 'properties' => [
- 'dealname' => $_POST['quid'] . ' - ' . $_POST['client'],
- 'dealtype' => 'newbusiness', // New or Existing Business - do we do a sql lookup and see if there is some won jobs for this customer ??
- 'hubspot_owner_id' => $_POST['hubspot_owner_id'],
- 'createdate' => $_POST['enquiry_date'],
- //assign deal to contact with hubspot contact id
- //'associatedCompanyIds' => $_POST['company_id'],
- 'pipeline' => '64401721',
- 'dealstage' => '126963334',
- 'width' => str_replace('m', '', $_POST['width']),
- 'length' => str_replace('m', '', $_POST['length']),
- 'height' => str_replace('m', '', $_POST['height']),
- 'quote_type' => $_POST['quote_type'],
- 'type' => $_POST['product'],
- 'roof_type' => $_POST['roof_style'],
- 'sides' => $_POST['side_walls'],
- 'ends' => $_POST['end_walls'],
- 'internal' => $_POST['internal_walls'],
- 'qty_bays' => $_POST['bay_qty'],
- //'bay_width' => $_POST['bay_width'],
- //'uneven_bays' => $_POST['bay_uneven'],
- 'quote' => $_POST['quid'],
- ]
- ];
- $response = createDealHubSpot($newDealData);
- file_put_contents("crmdeal.log", $response);
- $createdDeal = json_decode($response, true);
- if ($createdDeal['id'] > 0) {
- $dealId = intval($createdDeal['id']);
- $result = mysqli_query($con, "UPDATE details SET dealId = " . (int)$dealId . " WHERE drg = " . (int)$drg);
- echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>×</a><h4 style=\"text-align: center;\">Deal: " . htmlspecialchars((string)$quid, ENT_QUOTES, 'UTF-8') . " - " . htmlspecialchars((string)$client, ENT_QUOTES, 'UTF-8') . " added to hubspot</h4></div>";
- $isHideDismissableAlert = 1;
- }
- } catch (\Exception $err) {
- echo '<pre>' . $err->getMessage() . '</pre>';
- die();
- }
- } elseif (!empty($drg) and !empty($_POST['update_deal_in_hubspot']) and !empty($dealId)) {
- try {
- $updateDealData = [
- 'properties' => [
- 'dealname' => $_POST['drg'] . ' - ' . $_POST['client'],
- 'hubspot_owner_id' => $_POST['hubspot_owner_id'],
- 'createdate' => $_POST['enquiry_date'],
- //assign deal to contact with hubspot contact id
- //'associatedCompanyIds' => $_POST['company_id'],
- 'quote_type' => $_POST['quote_type'],
- 'type' => $_POST['product'],
- ],
- 'associations' => [
- 'types' => [
- 'association_category' => 'HUBSPOT_DEFINED',
- 'association_type_id' => 3
- ],
- 'to' => [
- 'id' => $_POST['crm_id']
- ]
- ]
- ];
- $response = updateDealHubSpot($dealId, $updateDealData);
- file_put_contents("crmdeal.log", $response);
- $updatedDeal = json_decode($response, true);
- if ($updatedDeal['id'] > 0) {
- $dealId = intval($updatedDeal['id']);
- echo "<div class='container alert alert-success alert-dismissible d-print-none' role='alert'><a href='#' class='close' data-dismiss='alert' aria-label='close'>×</a><h4 style=\"text-align: center;\">Deal: " . htmlspecialchars((string)$quid, ENT_QUOTES, 'UTF-8') . " - " . htmlspecialchars((string)$client, ENT_QUOTES, 'UTF-8') . " added to hubspot</h4></div>";
- $isHideDismissableAlert = 1;
- }
- } catch (\Exception $err) {
- echo '<pre>' . $err->getMessage() . '</pre>';
- die();
- }
- }
- if (!empty($_GET['drg'])) {
- $contact_button = '';
- if ($crm_id > 5) {
- $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>';
- } else {
- $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>';
- }
- }
- ?>
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title><?php echo $drg; ?> - Client Brief</title>
- <link rel="shortcut icon" href="images/blueprint.ico" type="image/x-icon">
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
- <!-- jQuery first, then Popper.js, then Bootstrap JS -->
- <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
- <link href="css/blueprint.css" rel="stylesheet">
- <link href="css/print.css" rel="stylesheet" media="print">
- <script type="text/javascript" src="https://use.fontawesome.com/1e2844bb90.js"></script>
- <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js" integrity="sha256-T0Vest3yCU7pafRw9r+settMBX6JkKN06dqBnpQ8d30=" crossorigin="anonymous"></script>
- <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>
- </head>
- <body>
- <nav class="navbar bg-brown-dark brown-light border-bottom border-body d-print-none" data-bs-theme="dark">
- <div class="container-fluid">
- <a class="navbar-brand brown-light" href="dashboard.php">
- <img src="images/blueprint-logo-light.png" alt="Logo" width="30" height="24" class="d-inline-block align-text-top">
- Modulos Design
- </a>
- <a href="dashboard.php" class="btn btn-sm btn-outline-light ms-auto"><i class="bi bi-grid-fill"></i> Dashboard</a>
- </div>
- </nav>
- <div class="container">
- <div class="row pt-2">
- <div class="col-sm-4 col-md-4 pt-3">
- <img class="img-fluid logo pt-2" src="images/blueprint-full-logo-medium.png" alt="Modulos Design">
- </div>
- <div class="col-sm-4 col-md-4 m-auto text-center">
- <h3 class="architect text-center">Job: <?php echo $drg; ?></h3>
- </div>
- <div class="col-sm-4 col-md-4 text-end pt-3">
- <h2 class="fw-bold text-end mb-1">Client Brief Form</h2>
- <h4 class="text-end mb-1">
- <span class="fw-bold brown-two"><?php echo $enquiry_date; ?></span>
- </h4>
- </div>
- </div>
- <hr>
- <?php if (!empty($drg)): ?>
- <div class="row d-print-none mb-2">
- <div class="col">
- <div class="d-flex flex-wrap gap-2 align-items-center">
- <small class="text-muted me-1">Job <?= (int)$drg ?>:</small>
- <a href="manilla_folder.php?drg=<?= (int)$drg ?>" class="btn btn-sm bg-brown-three brown-five"><i class="bi bi-folder2-open"></i> Manila Folder</a>
- <a href="draft_page.php?drg=<?= (int)$drg ?>" class="btn btn-sm bg-brown-three brown-five"><i class="bi bi-pencil-fill"></i> Drafts</a>
- <a href="payment_request.php?drg=<?= (int)$drg ?>" class="btn btn-sm bg-brown-three brown-five"><i class="bi bi-credit-card-fill"></i> Payment</a>
- <a href="progress.php?drg=<?= (int)$drg ?>" class="btn btn-sm bg-brown-three brown-five"><i class="bi bi-list-check"></i> Progress</a>
- </div>
- </div>
- </div>
- <hr>
- <?php endif; ?>
- <form id="client-brief" data-remote="true" method="post" accept-charset="UTF-8">
- <input type="hidden" name="csrf" id="csrf" value="<?php echo $csrf; ?>">
- <div class="row">
- <div class="col-12 col-md-6">
- <div class="row ">
- <div class="col ">
- <h4 class="fw-bold">Client Details</h4>
- </div>
- </div>
- <div class="mb-1">
- <div class="row ">
- <div class="col-12 col-md-6">
- <label for="firstname" class="form-label form-label-sm p-0 m-0">Clients Name</label>
- <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">
- </div>
- <div class="col-12 col-md-6">
- <label for="lastname" class="form-label form-label-sm p-0 m-0"></label>
- <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">
- </div>
- </div>
- </div>
- <div class="mb-1">
- <label for="joint_name" class="form-label form-label-sm p-0 m-0">T/As - Joint Names</label>
- <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">
- </div>
- <div class="mb-1">
- <label for="postal_address" class="form-label form-label-sm p-0 m-0">Clients Postal Address</label>
- <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">
- </div>
- <div class="mb-1">
- <div class="row ">
- <div class="col-md-6">
- <label for="phoneNumber" class="form-label form-label-sm p-0 m-0">Clients Mobile</label>
- <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">
- </div>
- <div class="col-12 col-md-3 d-print-none">
- <label for="crm_id" class="form-label form-label-sm p-0 m-0">CID</label>
- <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; ?>" >
- </div>
- <div class="col-12 col-md-3 d-print-none">
- <label for="dealId" class="form-label form-label-sm p-0 m-0">DID</label>
- <input type="text" class="form-control form-control-sm savable-text-field brown-three" id="dealId" name="dealId" value="<?php echo $dealId; ?>" >
- </div>
- </div>
- </div>
- <div class="mb-1">
- <label for="client_email" class="form-label form-label-sm p-0 m-0">Email address</label>
- <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">
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h4 class="fw-bold">Proposed Site Details</h4>
- </div>
- </div>
- <div class="mb-1">
- <label for="site_address" class="form-label form-label-sm p-0 m-0">Site Address</label>
- <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">
- <input type="hidden" class="savable-text-field" id="site_lat" name="site_lat" value="<?php echo htmlspecialchars($site_lat ?? '', ENT_QUOTES, 'UTF-8'); ?>">
- <input type="hidden" class="savable-text-field" id="site_lng" name="site_lng" value="<?php echo htmlspecialchars($site_lng ?? '', ENT_QUOTES, 'UTF-8'); ?>">
- </div>
- <div class="row g-3">
- <div class="col-12 col-md-6">
- <label for="property_id" class="form-label form-label-sm p-0 m-0">Property ID:</label>
- <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">
- </div>
- <div class="col-12 col-md-6">
- <label for="title_id" class="form-label form-label-sm p-0 m-0">Title Reference:</label>
- <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">
- </div>
- </div>
- <div class="mb-1">
- <label for="registered_owner" class="form-label form-label-sm p-0 m-0">Registered Title Owners:</label>
- <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="">
- </div>
- <div class="row g-3">
- <div class="col-12 col-md-5">
- <label for="council" class="form-label form-label-sm p-0 m-0">Council:</label>
- <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">
- </div>
- <div class="col-12 col-md-3">
- <label for="elevation" class="form-label form-label-sm p-0 m-0">Elev (AHD):</label>
- <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)">
- </div>
- <div class="col-12 col-md-4">
- <label for="elevation" class="form-label form-label-sm p-0 m-0">Land Area (m2):</label>
- <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="">
- </div>
- </div>
- <div class="row g-3">
- <div class="col-12 col-md-4">
- <label for="planning_zones" class="form-label form-label-sm p-0 m-0">Planning Zones:</label>
- <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">
- </div>
- <div class="col-12 col-md-8">
- <label for="planning_scheme" class="form-label form-label-sm p-0 m-0">Planning Scheme:</label>
- <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">
- </div>
- </div>
- <div class="row g-3">
- <div class="col-12">
- <label for="planning_codes" class="form-label form-label-sm p-0 m-0">Planning Codes:</label>
- <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">
- </div>
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h4 class="fw-bold">Design Brief</h4>
- </div>
- </div>
- <div class="mb-1">
- <label for="design_style" class="form-label form-label-sm p-0 m-0">Type of Design</label>
- <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'); ?>" >
- </div>
- <div class="row g-3">
- <div class="col-12 col-md-6">
- <label for="build_type" class="form-label form-label-sm p-0 m-0">Build Type:</label>
- <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">
- <option><?php echo $build_type; ?></option>
- <option >New Residential</option>
- <option >Residential Extension</option>
- <option >New Commercial</option>
- <option >Commercial Extension</option>
- <option >Shed or Ancillary Dwelling</option>
- <option >Pool</option>
- <option >Interior Design</option>
- <option >Other</option>
- </select>
- </div>
- <div class="col-12 col-md-6">
- <label for="scope" class="form-label form-label-sm p-0 m-0">Our Scope</label>
- <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'); ?>" >
- </div>
- </div>
- </div>
- <div class="col-12 col-md-6">
- <div class="row d-print-none">
- <div class="col-12 col-md my-1 d-grid">
- <?php echo $contact_button; ?>
- </div>
- <div class="col-12 col-md my-1 d-grid">
- <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>
- </div>
- <div class="col-12 col-md my-1 d-grid">
- <button type="button" class="btn btn-sm bg-brown-three brown-five" id="createFolder"><i class="bi bi-folder-fill"></i> Create Job</button>
- </div>
- <div class="col-12 col-md my-1 d-grid">
- <button type="button" class="btn btn-sm bg-brown-three brown-five" onclick="fetchAndExtractData()"><i class="bi bi-folder-fill"></i> Planbuild</button>
- <!-- <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> -->
- </div>
- </div>
- <div class="row d-print-none">
- <div class="col">
- <hr class="mvt-3">
- </div>
- </div>
- <div class="row ">
- <div class="col-12">
- <h5 class="fw-bold">Brief Checklist</h5>
- <p class="extra-light small">Have the following documents being provided?</p>
- </div>
- </div>
- <div class="row ">
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="copy_title" id="copy_title"
- value="1"
- <?php if ('1' == $copy_title) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="copy_title">Copy of Title</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="original_plans" id="original_plans"
- value="1"
- <?php if ('1' == $original_plans) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="original_plans">Original Plans</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="concepts_styles" id="concepts_styles"
- value="1"
- <?php if ('1' == $concepts_styles) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="concepts_styles">Concepts & Styles</label>
- </div>
- </div>
- </div>
- <div class="row ">
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="loa_signed" id="loa_signed"
- value="1"
- <?php if ('1' == $loa_signed) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="loa_signed">LOA signed</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="da_application" id="da_application"
- value="1"
- <?php if ('1' == $da_application) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="da_application">DA Application</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="ba_application" id="ba_application"
- value="1"
- <?php if ('1' == $ba_application) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="ba_application">BA Application</label>
- </div>
- </div>
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h5 class="fw-bold">Design Outputs</h5>
- <p class="extra-light small"></p>
- </div>
- </div>
- <div class="row ">
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="concepts_3d" id="concepts_3d"
- value="1"
- <?php if ('1' == $concepts_3d) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="concepts_3d">3D Concepts</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="draft_floorPlan" id="draft_floorPlan"
- value="1"
- <?php if ('1' == $draft_floorPlan) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="draft_floorPlan">Draft Floor Plan</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="fire_report" id="fire_report"
- value="1"
- <?php if ('1' == $fire_report) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="fire_report">Fire Report</label>
- </div>
- </div>
- </div>
- <div class="row ">
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="energy_report" id="energy_report"
- value="1"
- <?php if ('1' == $energy_report) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="energy_report">Energy Report</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="tender_set" id="tender_set"
- value="1"
- <?php if ('1' == $tender_set) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="tender_set">Tender Doc Set</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="quantity_survey" id="quantity_survey"
- value="1"
- <?php if ('1' == $quantity_survey) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="quantity_survey">Quantity Survey</label>
- </div>
- </div>
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h5 class="fw-bold">Presentations</h5>
- <p class="extra-light small"></p>
- </div>
- </div>
- <div class="row ">
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="vr_concepts" id="vr_concepts"
- value="1"
- <?php if ('1' == $vr_concepts) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="vr_concepts">VR Concepts</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="render_set" id="render_set"
- value="1"
- <?php if ('1' == $render_set) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="render_set">Full Rended Set</label>
- </div>
- </div>
- <div class="col-6 col-md-4">
- <div class="form-check form-check-inline savable-checkbox">
- <input class="form-check-input savable-checkbox-field"
- type="checkbox"
- name="model_3d" id="model_3d"
- value="1"
- <?php if ('1' == $model_3d) { echo 'checked'; } ?>
- >
- <label class="form-check-label" for="model_3d">3D Model</label>
- </div>
- </div>
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h5 class="fw-bold">Create Documents</h5>
- <p class="extra-light small"></p>
- </div>
- </div>
- <div class="row d-print-none mb-2">
- <div class="col d-inline-flex gap-1">
- <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>
- </div>
- </div>
- <div class="row d-print-none">
- <div class="col d-inline-flex gap-1 flex-wrap">
- <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>
- <?php if (!empty($drg)): ?>
- <a href="<?= htmlspecialchars('../contracts/edit_application.php?id=' . (int)$drg) ?>" class="btn btn-sm btn-outline-secondary" target="_blank"><i class="bi bi-speedometer"></i> Manage Application</a>
- <?php endif; ?>
- <div class="form-check d-inline-flex align-items-center ms-2">
- <input class="form-check-input" type="checkbox" id="updateFrontMatter">
- <label class="form-check-label ms-1" for="updateFrontMatter">
- Update contact details if file exists
- </label>
- </div>
- </div>
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h4 class="fw-bold">Investment Details</h4>
- </div>
- </div>
- <div class="row g-3">
- <div class="col-12 col-md-6">
- <label for="budget_low" class="form-label form-label-sm p-0 m-0">Budget Range (Lower)</label>
- <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">
- </div>
- <div class="col-12 col-md-6">
- <label for="budget_high" class="form-label form-label-sm p-0 m-0">Budget Range (Upper)</label>
- <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">
- </div>
- </div>
- <div class="mb-1">
- <label for="finance_status" class="form-label form-label-sm p-0 m-0">Finance</label>
- <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 ?">
- </div>
- <hr>
- <div class="row ">
- <div class="col ">
- <h4 class="fw-bold">Services Used</h4>
- </div>
- </div>
- <div class="row g-3">
- <div class="col-12">
- <select class="form-control form-control-sm" id="building_surveyors_select" name="building_surveyors">
- <label for="building_surveyors">Building Surveyor:</label>
- <option selected="" disabled="">Select Building Surveyor</option>
- <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>
- <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>
- <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>
- <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>
- </select>
- <input type="hidden" id="building_surveyors_meta" name="building_surveyors_meta" value="<?php echo $building_surveyors_meta ?? ''; ?>">
- </div>
- <div class="col-12 col-md-6">
- </div>
- </div>
- <div class="row mb-1">
- <div class="col-12 col-md-6">
- <div class="row mb-1">
- <div class="col-12 col-md-6">
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <hr>
- <div class="row">
- <div class="col">
- <div class="mb-1">
- <label for="details" class="form-label form-label-sm p-0 m-0">Details</label>
- <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>
- </div>
- </div>
- </div>
- <hr>
- <div class="row d-print-none">
- <div class="col-6">
- <!--Add buttons to initiate auth sequence and sign out-->
- <button id="authorize_button" type="button" class="btn btn-sm bg-brown-three brown-five">Authorize</button>
- <button id="signout_button" type="button" class="btn btn-sm bg-brown-three brown-five">Sign Out</button>
- </div>
- </div>
- <hr>
- <footer class="footer">
- <p class="text-center">© 2023 - Modulos Design</p>
- </footer>
- </form>
- </div>
- <!-- Planbuild/LIST preview modal -->
- <div class="modal fade" id="planbuildPreview" tabindex="-1" role="dialog" aria-labelledby="planbuildPreviewLabel" aria-hidden="true">
- <div class="modal-dialog modal-lg" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="planbuildPreviewLabel">Property info from LIST</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <div id="pb-errors" class="alert alert-danger d-none"></div>
- <div class="row">
- <div class="col-md-6">
- <dl class="row small mb-0">
- <dt class="col-5">PID</dt><dd class="col-7" id="pb_pid">–</dd>
- <dt class="col-5">Title</dt><dd class="col-7" id="pb_title">–</dd>
- <dt class="col-5">Total Area</dt><dd class="col-7" id="pb_area">–</dd>
- <dt class="col-5">Total Area sqm</dt><dd class="col-7" id="area_sqm">–</dd>
- <dt class="col-5">Total Area ha</dt><dd class="col-7" id="area_ha">–</dd>
- <dt class="col-5">Locality</dt><dd class="col-7" id="pb_locality">–</dd>
- </dl>
- </div>
- <div class="col-md-6">
- <dl class="row small mb-0">
- <dt class="col-5">Planning Scheme</dt><dd class="col-7" id="pb_scheme">–</dd>
- <dt class="col-5">Zones</dt><dd class="col-7" id="pb_zones">–</dd>
- <dt class="col-5">Code Overlays</dt><dd class="col-7" id="pb_codes">–</dd>
- </dl>
- </div>
- </div>
- <hr>
- <div class="row">
- <div class="col-md-6">
- <dl class="row small mb-0">
- <dt class="col-5">tenure</dt><dd class="col-7" id="tenure">–</dd>
- <dt class="col-5">lpi</dt><dd class="col-7" id="lpi">–</dd>
- <dt class="col-5">list_guid</dt><dd class="col-7" id="list_guid">–</dd>
- </dl>
- </div>
- <div class="col-md-6">
- </div>
- </div>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
- <button type="button" class="btn btn-primary" id="pb-apply">Apply to form</button>
- </div>
- </div>
- </div>
- </div>
- <!-- Prefilled email text modal -->
- <div class="modal fade" id="emailTemplateModal" tabindex="-1" role="dialog" aria-labelledby="emailTemplateLabel" aria-hidden="true">
- <div class="modal-dialog modal-lg" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="emailTemplateLabel">Email text to council</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <ul class="nav nav-tabs" role="tablist">
- <li class="nav-item">
- <a class="nav-link" data-bs-toggle="tab" href="#tabText" role="tab">Plain text</a>
- </li>
- <li class="nav-item">
- <a class="nav-link active" data-bs-toggle="tab" href="#tabHtml" role="tab">HTML email</a>
- </li>
- </ul>
- <div class="tab-content">
- <div class="tab-pane fade p-3" id="tabText" role="tabpanel">
- <textarea id="emailText" class="form-control small" rows="10" readonly style="font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;"></textarea>
- <p class="text-muted small mt-2 mb-0">
- Uses fields: Site Address, Registered Title Owners, Property ID, Title Reference
- </p>
- </div>
- <div class="tab-pane fade active show p-3" id="tabHtml" role="tabpanel">
- <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>
- <textarea id="emailHtmlSource" class="d-none" readonly></textarea>
- </div>
- </div>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
- <button type="button" class="btn btn-outline-secondary" id="copyEmailText">Copy text</button>
- <button type="button" class="btn btn-outline-secondary" id="copyEmailHtml">Copy HTML</button>
- <a href="#" class="btn btn-primary" id="openMailto">Open email draft</a>
- </div>
- </div>
- </div>
- </div>
- <script type="text/javascript">
- const drg = <?php echo (int)($drg ?? 0); ?>;
- const folder_name = <?php echo json_encode(($drg ?? '') . ' - ' . ($site_address_street ?? '')); ?>;
- $(function () {
- document.querySelectorAll('[data-bs-toggle="popover"]')
- .forEach(el => new bootstrap.Popover(el));
- });
- $(function () {
- $(".datetime").each(function(){
- $(this).datepicker({ format: 'yyyy-mm-dd', autoclose: true, todayBtn: true, todayHighlight: true });
- });
- });
- $(function () {
- $(".datemonth").each(function(){
- $(this).datepicker({ format: 'MM yy', autoclose: true, todayBtn: true, todayHighlight: true });
- });
- });
- // Create LOA from current form fields
- $('#createLOA').on('click', async function () {
- const job = String(drg || '').trim();
- if (!job) { alert('Missing Job #'); return; }
- const clientName = ($('#joint_name').val().trim()) ||
- [$('#firstname').val().trim(), $('#lastname').val().trim()].filter(Boolean).join(' ');
- const clientEmail = $('#client_email').val().trim();
- const propertyAddr = $('#site_address').val().trim();
- // NEW fields
- const clientPhone = $('#phoneNumber').val().trim();
- const clientAddress = $('#postal_address').val().trim();
- const propertyPid = $('#property_id').val().trim();
- const propertyTitle = $('#title_id').val().trim(); // server will split vol/folio if possible
- //const startDate = $('#start_date').val?.().trim?.() || ''; // include if you add inputs
- //const endDate = $('#end_date').val?.().trim?.() || '';
- async function doCreate(overwrite) {
- const fd = new FormData();
- fd.append('action', 'loa_create');
- fd.append('job', job);
- fd.append('client_name', clientName);
- fd.append('client_email', clientEmail);
- fd.append('property_address', propertyAddr);
- // NEW fields
- fd.append('client_phone', clientPhone);
- fd.append('client_address', clientAddress);
- fd.append('property_pid', propertyPid);
- fd.append('property_title', propertyTitle);
- //fd.append('start_date', startDate);
- //fd.append('end_date', endDate);
- fd.append('overwrite', overwrite ? 1 : 0);
- fd.append('csrf', $('#csrf').val());
- const res = await fetch(location.pathname + '?action=loa_create', { method: 'POST', body: fd });
- return res.json();
- }
- try {
- let js = await doCreate(false);
- // If file exists, the server will just update front matter (no prompt).
- // If you still want an overwrite option:
- if (!js.ok && js.error && js.error.toLowerCase().includes('exists')) {
- if (confirm('An LOA already exists. Overwrite the whole file from template?')) {
- js = await doCreate(true);
- } else return;
- }
- if (js.ok) {
- const msg = 'LOA saved for Job #' + job + (js.public_url ? '\n\nOpen signing page now?' : '');
- if (confirm(msg) && js.public_url) window.open(js.public_url, '_blank', 'noopener');
- } else {
- alert(js.error || 'Failed to create/update LOA');
- }
- } catch (e) {
- alert('Error creating LOA: ' + e.message);
- }
- });
- // simple phone length “checker” (kept as-is)
- function check() {
- var mobile = document.getElementById('phoneNumber');
- if(!mobile) return;
- if(mobile.value.length !== 12){ /* no-op */ }
- }
- const isHideDismissableAlert = <?php echo json_encode($isHideDismissableAlert ?? 0); ?>;
- window.autocompletes = {};
- var placeSearch, clickedGeolocationField = null;
- var componentForm = {
- street_number: 'short_name',
- route: 'long_name',
- locality: 'long_name',
- administrative_area_level_1: 'short_name',
- postal_code: 'short_name'
- };
- $(document).ready(function() {
- if (isHideDismissableAlert > 0) {
- $(".alert-dismissible").fadeTo(2000, 500).slideUp(500, function () {
- document.querySelectorAll('.alert-dismissible').forEach(el => {
- bootstrap.Alert.getOrCreateInstance(el).close();
- });
- });
- }
- if (drg > 0) {
- const debouncedStore = $.debounce(500, function(name, value) { storeField(name, value); });
- $('.savable-text-field').on('keyup', function(e) {
- const key = e.keyCode || e.which;
- const systemKeys = [9,13,16,17,18,37,38,39,40,91];
- if (systemKeys.includes(key)) return;
- debouncedStore($(this).prop('name'), $(this).val());
- });
- }
- $('.savable-date-field').on('change', function () {
- storeField($(this).prop('name'), $(this).val());
- });
- $('.savable-dropdown-field').on('change', function () {
- const name = $(this).prop('name');
- const value = $(this).val();
- storeField(name, value);
- $(this).removeClass('form-control-danger');
- if (this.id === 'building_surveyors_select') {
- const meta = this.options[this.selectedIndex]?.dataset?.id || '';
- $('#building_surveyors_meta').val(meta);
- storeField('building_surveyors_meta', meta);
- }
- });
- $('.savable-checkbox-field').on('change', function () {
- storeField($(this).prop('name'), $(this).is(":checked") ? 1 : 0);
- });
- $('.savable-radio-field').on('change', function () {
- storeField($(this).prop('name'), $(this).val());
- });
- // phone mask
- const phoneInput = document.getElementById('phoneNumber');
- if (phoneInput) {
- phoneInput.addEventListener('keyup', function(evt){
- phoneInput.value = phoneFormat(phoneInput.value);
- });
- }
- }); // <--- correctly closes $(document).ready
- String.prototype.toProperCase = function () {
- return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
- };
- var rectangle, map, infoWindow, rectPoly, journey = '', rotate = '';
- function stateDisplay(shortCode) {
- if (!shortCode) return '';
- const s = shortCode.toUpperCase();
- const map = { TAS: 'Tas', VIC: 'Vic', QLD: 'Qld', NSW: 'NSW', WA: 'WA', SA: 'SA', ACT: 'ACT', NT: 'NT' };
- return map[s] || shortCode;
- }
- // Google Places — global so callback can find it
- window.initAutocomplete = function initAutocomplete() {
- const inputs = document.getElementsByClassName('map-autocomplete');
- for (let i = 0; i < inputs.length; i++) {
- const el = inputs[i];
- const ac = new google.maps.places.Autocomplete(
- el,
- { types: ['address'], componentRestrictions: { country: 'au' } }
- );
- ac.setFields(['address_component', 'geometry']);
- window.autocompletes[el.id] = ac; // store for geolocate()
- ac.addListener('place_changed', () => fillInAddress(el, ac));
- }
- };
- function fillInAddress(targetInput, ac) {
- const place = ac.getPlace();
- if (!place || !place.address_components) return;
- const parts = { street_number: '', route: '', locality: '', state: '', postal_code: '' };
- for (const comp of place.address_components) {
- const type = comp.types[0];
- if (type === 'street_number') parts.street_number = comp.short_name || '';
- if (type === 'route') parts.route = comp.long_name || '';
- if (type === 'locality') parts.locality = comp.long_name || '';
- if (type === 'administrative_area_level_1') parts.state = comp.short_name || '';
- if (type === 'postal_code') parts.postal_code = comp.short_name || '';
- }
- if (parts.street_number && parts.route && parts.locality && parts.state && parts.postal_code) {
- const formatted = `${parts.street_number} ${parts.route}, ${parts.locality}, ${stateDisplay(parts.state)}, ${parts.postal_code}`;
- targetInput.value = formatted;
- if (typeof storeField === 'function') {
- try { storeField(targetInput.name || targetInput.id, formatted); } catch (e) {}
- }
- if (targetInput.id === 'site_address' && typeof writeIfReady === 'function') {
- try { writeIfReady(); } catch (e) {}
- }
- // NEW: council lookup for TAS
- if (parts.state && parts.state.toUpperCase() === 'TAS') {
- const url = `classes/council_lookup.php?town=${encodeURIComponent(parts.locality)}&postcode=${encodeURIComponent(parts.postal_code)}&state=TAS`;
- fetch(url)
- .then(r => r.json())
- .then(data => {
- if (data && data.ok && data.council) {
- const councilEl = document.getElementById('council');
- if (councilEl) {
- councilEl.value = data.council;
- }
- if (typeof storeField === 'function') {
- storeField('council', data.council);
- }
- // If you also want to store the shapeFile, uncomment:
- // if (data.shapeFile) storeField('shapeFile', data.shapeFile);
- }
- })
- .catch(() => { /* silent fail is fine */ });
- }
- // store lat/lng if available
- if (place.geometry && place.geometry.location) {
- const lat = place.geometry.location.lat();
- const lng = place.geometry.location.lng();
- $('#site_lat').val(lat);
- $('#site_lng').val(lng);
- storeField('site_lat', lat);
- storeField('site_lng', lng);
- }
- if (typeof storeField === 'function') {
- try { storeField(targetInput.name || targetInput.id, formatted); } catch (e) {}
- }
- }
- }
- function geolocate(el) {
- if (!el || !el.id) return;
- clickedGeolocationField = el.name;
- const ac = window.autocompletes[el.id];
- if (!ac) return;
- if (navigator.geolocation) {
- navigator.geolocation.getCurrentPosition(function(position) {
- const circle = new google.maps.Circle({
- center: { lat: position.coords.latitude, lng: position.coords.longitude },
- radius: position.coords.accuracy
- });
- ac.setBounds(circle.getBounds());
- });
- }
- }
- function getElevation(lat, lng) {
- var elevationApiKey = 'AIzaSyB-QceOYrDe9otynMmQ9iNF3yEZzbpsanM';
- var elevationApiUrl = `https://maps.googleapis.com/maps/api/elevation/json?locations=${lat},${lng}&key=${elevationApiKey}`;
- fetch(elevationApiUrl)
- .then(function (response) { return response.json(); })
- .then(function (data) {
- if (data.results.length > 0) {
- var elevation = data.results[0].elevation;
- var el = document.getElementById('elevation');
- if (el) el.value = elevation;
- }
- })
- .catch(function (error) { console.error('Error fetching elevation: ' + error); });
- }
- // AU-ish phone mask
- function phoneFormat(input){
- input = input.replace(/\D/g,'');
- input = input.substring(0,20);
- var size = input.length;
- if(size === 0){
- input = input;
- } else if(size < 4){
- input = input + ' ';
- } else if(size < 7){
- input = input.substring(0,4)+' '+input.substring(4,6);
- } else if (input.startsWith("04") ) {
- input = input.substring(0,4)+' '+input.substring(4,7)+' '+input.substring(7,20);
- } else if (input.startsWith("+44") ) {
- input = input;
- } else {
- input = input.substring(0,2)+' '+input.substring(2,6)+' '+input.substring(6,20);
- }
- return input;
- }
- function storeField(name, value) {
- $.ajax({
- url: "database.php",
- type: "POST",
- data: {
- action: 'client-brief',
- drg: drg,
- field_name: name,
- field_value: value,
- csrf: $('#csrf').val()
- },
- success: function(data) {
- try {
- data = $.parseJSON(data);
- if (!data.success) alert('Error: ' + data.message);
- } catch(e) {
- console.warn('Non-JSON response', data);
- }
- },
- error: function() { alert("Please check for Errors."); }
- });
- }
- document.getElementById('authorize_button').addEventListener('click', (e) => {
- e.preventDefault();
- handleAuthClick();
- });
- document.getElementById('signout_button').addEventListener('click', (e) => {
- e.preventDefault();
- handleSignoutClick();
- });
- // Google API creds — values injected server-side from .env
- var CLIENT_ID = '<?= htmlspecialchars(getenv('GOOGLE_CLIENT_ID') ?: '', ENT_QUOTES, 'UTF-8') ?>';
- var API_KEY = '<?= htmlspecialchars(getenv('GOOGLE_API_KEY') ?: '', ENT_QUOTES, 'UTF-8') ?>';
- const DISCOVERY_DOC = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';
- var SCOPES = 'https://www.googleapis.com/auth/drive';
- // TODO: Set this to the Google Drive folder ID of the root projects directory
- // (e.g. the "[03] Projects 20XX" folder). Without this, new job folders are
- // created in the signed-in user's Drive root, not the shared projects directory.
- // Find the ID by opening the folder in Drive and copying the ID from the URL:
- // https://drive.google.com/drive/folders/<FOLDER_ID_HERE>
- const PROJECTS_PARENT_FOLDER_ID = ''; // <-- paste folder ID here
- let tokenClient, gapiInited = false, gisInited = false;
- window.gapiLoaded = function () {
- gapi.load('client', async () => {
- try {
- await gapi.client.init({ apiKey: API_KEY, discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'] });
- gapiInited = true;
- maybeEnableButtons();
- } catch (err) {
- console.warn('Discovery failed, falling back to gapi.client.load()', err);
- // small backoff retries
- for (let i = 0; i < 3; i++) {
- try {
- await new Promise(r => setTimeout(r, 400 * (i + 1)));
- await gapi.client.load('drive', 'v3');
- gapiInited = true;
- maybeEnableButtons();
- break;
- } catch (e) {
- if (i === 2) console.error('Drive load failed after retries', e);
- }
- }
- }
- });
- };
- window.gisLoaded = function () {
- tokenClient = google.accounts.oauth2.initTokenClient({
- client_id: CLIENT_ID,
- scope: SCOPES,
- callback: () => {}
- });
- gisInited = true;
- maybeEnableButtons();
- };
- function maybeEnableButtons() {
- if (gapiInited && gisInited) {
- document.getElementById('authorize_button').style.visibility = 'visible';
- document.getElementById('createFolder')?.addEventListener('click', ensureAuthThenCreateFolder);
- }
- }
- function handleAuthClick() {
- tokenClient.callback = (resp) => {
- if (resp.error) { console.error(resp); return; }
- document.getElementById('signout_button').style.visibility = 'visible';
- document.getElementById('authorize_button').innerText = 'Refresh';
- };
- if (!gapi.client.getToken()) tokenClient.requestAccessToken({ prompt: 'consent' });
- else tokenClient.requestAccessToken({ prompt: '' });
- }
- function handleSignoutClick() {
- const tok = gapi.client.getToken();
- if (!tok) return;
- google.accounts.oauth2.revoke(tok.access_token);
- gapi.client.setToken(null);
- document.getElementById('authorize_button').innerText = 'Authorize';
- document.getElementById('signout_button').style.visibility = 'hidden';
- }
- async function ensureAuthThenCreateFolder() {
- if (!gapi.client.getToken()) {
- tokenClient.callback = async (resp) => {
- if (resp && resp.error) { console.error(resp); return; }
- await createFolder();
- };
- tokenClient.requestAccessToken({ prompt: 'consent' });
- } else {
- await createFolder();
- }
- }
- async function createFolder() {
- try {
- const resource = { name: folder_name, mimeType: 'application/vnd.google-apps.folder' };
- if (PROJECTS_PARENT_FOLDER_ID) resource.parents = [PROJECTS_PARENT_FOLDER_ID];
- const res = await gapi.client.drive.files.create({
- resource,
- fields: 'id,name,webViewLink',
- supportsAllDrives: true
- });
- alert('Folder created: ' + res.result.name + '\nID: ' + res.result.id);
- } catch (err) {
- const detail = err?.result?.error || err;
- console.error('Drive create error:', detail);
- alert('Drive error: ' + (detail.message || detail.statusText || JSON.stringify(detail)));
- }
- }
- /* ******************************************************************************************* */
- async function fetchAndExtractData() {
- const lat = $('#site_lat').val();
- const lng = $('#site_lng').val();
- if (!lat || !lng) {
- alert('Please select a Google suggested Site Address first so we can capture coordinates.');
- return;
- }
- // Helper to stringify area object from list_lookup.php
- const formatArea = (area) => {
- if (!area) return '';
- // prefer sqm label, fall back to ha
- return area.sqm_label || area.ha_label || '';
- };
- try {
- const resp = await fetch('classes/list_lookup.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ lat: parseFloat(lat), lng: parseFloat(lng) })
- });
- const data = await resp.json();
- // Show any lookup error in the modal
- if (!resp.ok || !data || data.ok !== true) {
- $('#pb-errors').removeClass('d-none').text((data && data.error) ? data.error : 'Lookup failed');
- bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).show();
- return;
- }
- $('#pb-errors').addClass('d-none').text('');
- // Fill the preview modal
- $('#pb_pid').text(data.pid || '–');
- $('#pb_title').text(data.title_id || '–');
- $('#pb_area').text(formatArea(data.total_area) || '–');
- $('#pb_locality').text(data.locality || '–'); // include if you added locality in PHP later
- //$('#pb_council').text(data.council || '–');
- $('#pb_scheme').text(data.planning_scheme || '–');
- $('#pb_zones').text((Array.isArray(data.planning_zones) ? data.planning_zones.join(', ') : (data.planning_zones || '')) || '–');
- $('#pb_codes').text((Array.isArray(data.planning_codes) ? data.planning_codes.join(', ') : (data.planning_codes || '')) || '–');
- $('#area_sqm').text(data.area_sqm || '–');
- $('#area_ha').text(data.area_ha || '–');
- $('#tenure').text(data.tenure || '–');
- $('#lpi').text(data.lpi || '–');
- $('#list_guid').text(data.list_guid || '–');
- bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).show();
- // Apply button writes to the form and DB
- $('#pb-apply').off('click').on('click', () => {
- const apply = (id, v) => {
- const el = document.getElementById(id);
- if (el && typeof v !== 'undefined' && v !== null) {
- el.value = v;
- storeField(id, v);
- }
- };
- apply('property_id', data.pid || '');
- apply('title_id', data.title_id || '');
- apply('total_area', formatArea(data.total_area) || '');
- apply('planning_scheme', data.planning_scheme || '');
- apply('planning_zones', Array.isArray(data.planning_zones) ? data.planning_zones.join(', ') : (data.planning_zones || ''));
- apply('planning_codes', Array.isArray(data.planning_codes) ? data.planning_codes.join(', ') : (data.planning_codes || ''));
- //apply('council', data.council || '');
- if (document.getElementById('locality')) apply('locality', data.locality || '');
- // Optional extras if you add matching inputs:
- //apply('area_sqm', data.total_area?.sqm_label || '');
- //apply('area_ha', data.total_area?.ha_label || '');
- //apply('tenure', data.tenure || '');
- //apply('lpi', data.lpi || '');
- //apply('list_guid',data.list_guid|| '');
- bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).hide();
- });
- } catch (e) {
- $('#pb-errors').removeClass('d-none').text('Error fetching data: ' + e.message);
- bootstrap.Modal.getOrCreateInstance(document.getElementById('planbuildPreview')).show();
- }
- };
- // Build the exact body text from current form values
- const useDynamicGreeting = false;
- function getGreeting() {
- if (!useDynamicGreeting) return 'Good Morning,';
- const hour = new Date().getHours();
- if (hour < 12) return 'Good Morning,';
- if (hour < 18) return 'Good Afternoon,';
- return 'Good Evening,';
- }
- function escapeHtml(s) {
- return String(s || '').replace(/[&<>"']/g, c => ({
- '&':'&', '<':'<', '>':'>', '"':'"', "'":'''
- }[c]));
- }
- function buildPlanningEmailText() {
- const greet = getGreeting();
- const address = ($('#site_address').val() || '').trim();
- const owners = ($('#registered_owner').val() || '').trim();
- const pid = ($('#property_id').val() || '').trim();
- const title = ($('#title_id').val() || '').trim();
- const fallback = 'N/A';
- const lines = [
- greet,
- '',
- 'I am requesting the planning and plumbing information for the following address:',
- `Property Address: ${address || fallback}`,
- `Property Owners: ${owners || fallback}`,
- `Property ID: ${pid || fallback}`,
- `Title/Vol: ${title || fallback}`,
- '',
- 'Attached is a signed Letter of Authority from the property owners.'
- ];
- return lines.join('\n');
- }
- function buildPlanningEmailHtml() {
- const greet = getGreeting();
- const address = ($('#site_address').val() || '').trim();
- const owners = ($('#registered_owner').val() || '').trim();
- const pid = ($('#property_id').val() || '').trim();
- const title = ($('#title_id').val() || '').trim();
- const fallback = 'N/A';
- return `
- <div style="background:#ffffff;padding:16px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;color:#000000;">
- <p style="margin:0 0 12px 0;">${escapeHtml(greet)}</p>
- <p style="margin:0 0 12px 0;">I am requesting the planning and plumbing information for the following address:</p>
- <table role="presentation" cellpadding="0" cellspacing="0" width="75%" style="border-collapse:collapse;background:#ffffff;color:#000000;">
- <tr>
- <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;width:200px;">Property Address</td>
- <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(address || fallback)}</td>
- </tr>
- <tr>
- <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;">Property Owners</td>
- <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(owners || fallback)}</td>
- </tr>
- <tr>
- <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;">Property ID</td>
- <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(pid || fallback)}</td>
- </tr>
- <tr>
- <td style="padding:8px;border:1px solid #d1d5db;font-weight:bold;">Title/Vol</td>
- <td style="padding:8px;border:1px solid #d1d5db;">${escapeHtml(title || fallback)}</td>
- </tr>
- </table>
- <p style="margin:12px 0 0 0;">Attached is a signed Letter of Authority from the property owners.</p>
- </div>`;
- }
- function copyFromTextarea(selector, btnEl) {
- const val = $(selector).val();
- const doWrite = async () => {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- await navigator.clipboard.writeText(val);
- } else {
- const ta = document.querySelector(selector);
- ta.focus();
- ta.select();
- document.execCommand('copy');
- }
- };
- doWrite().then(() => {
- const original = btnEl.textContent;
- btnEl.textContent = 'Copied';
- btnEl.disabled = true;
- setTimeout(() => { btnEl.textContent = original; btnEl.disabled = false; }, 1200);
- }).catch(() => {
- alert('Copy failed. You can select and copy the text manually.');
- });
- }
- // Single modal hook
- document.getElementById('emailTemplateModal')
- .addEventListener('show.bs.modal', function () {
- const text = buildPlanningEmailText();
- const html = buildPlanningEmailHtml();
- document.getElementById('emailText').value = text;
- document.getElementById('emailHtmlPreview').innerHTML = html;
- document.getElementById('emailHtmlSource').value = html;
- // Build subject using the current site address
- const addr = (document.getElementById('site_address')?.value || '').trim() || 'N/A';
- const subject = encodeURIComponent(`Request for Property Information - ${addr}`);
- const body = encodeURIComponent(text);
- document.getElementById('openMailto').href = `mailto:?subject=${subject}&body=${body}`;
- });
- // Single copy handlers
- $('#copyEmailText').on('click', function () {
- copyFromTextarea('#emailText', this);
- });
- $('#copyEmailHtml').on('click', function () {
- copyFromTextarea('#emailHtmlSource', this);
- });
- </script>
- <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>
- <script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB-QceOYrDe9otynMmQ9iNF3yEZzbpsanM&libraries=places&loading=async&callback=initAutocomplete" ></script>
- <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
- <script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
- </body>
- </html>
|