| Server IP : 72.60.21.38 / Your IP : 216.73.217.140 Web Server : LiteSpeed System : Linux uk-fast-web1372.main-hosting.eu 4.18.0-553.121.1.lve.el8.x86_64 #1 SMP Thu Apr 30 16:40:41 UTC 2026 x86_64 User : u390967363 ( 390967363) PHP Version : 8.2.30 Disable Function : system, exec, shell_exec, passthru, mysql_list_dbs, ini_alter, dl, symlink, link, chgrp, leak, popen, apache_child_terminate, virtual, mb_send_mail MySQL : OFF | cURL : ON | WGET : ON | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /home/u390967363/domains/aibenproperties.com/public_html/app/ |
Upload File : |
<?php
if (session_status() === PHP_SESSION_NONE) { session_start(); }
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/functions.php';
require_once __DIR__ . '/includes/mailer.php';
require_once __DIR__ . '/includes/doc_templates.php';
require_once __DIR__ . '/includes/doc_generator.php';
$role = $_SESSION['user_role'] ?? 'guest';
$roleNorm = strtolower(trim((string)$role));
$roleNorm = str_replace([' ', '-'], '_', $roleNorm);
$uid = (int)($_SESSION['user_id'] ?? 0);
$allowed = in_array($roleNorm, ['admin','head_admin','admin_head','admin_officer','super_admin','estate_manager','sales_manager','chairman_ceo','executive'], true);
if (!$allowed) { header('Location: dashboard.php'); exit; }
$companyId = function_exists('getCurrentCompanyId') ? getCurrentCompanyId() : ($_SESSION['company_id'] ?? null);
if (function_exists('ensureAllocationBillingTable')) { ensureAllocationBillingTable($pdo); }
if (function_exists('ensureAllocationLetterSectionsTable')) { ensureAllocationLetterSectionsTable($pdo); }
if (function_exists('ensureAllocationLetterDataTable')) { ensureAllocationLetterDataTable($pdo); }
function colx($t,$c){ return function_exists('tableHasColumn') ? tableHasColumn($t,$c) : true; }
function pickColumnToken($pdo, $table, $column, array $candidates) {
$tokens = array_values(array_filter(array_unique(array_map(static function ($value) {
return is_string($value) ? trim($value) : '';
}, $candidates))));
if (empty($tokens)) { return ''; }
try {
$stmt = $pdo->prepare("SELECT DATA_TYPE, COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1");
$stmt->execute([$table, $column]);
$meta = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
if (strtolower((string)($meta['DATA_TYPE'] ?? '')) === 'enum') {
preg_match_all("/'((?:[^'\\\\]|\\\\.)*)'/", (string)($meta['COLUMN_TYPE'] ?? ''), $matches);
$allowed = array_map(static function ($value) {
return str_replace("\\'", "'", (string)$value);
}, $matches[1] ?? []);
foreach ($tokens as $token) {
if (in_array($token, $allowed, true)) {
return $token;
}
}
return (string)($allowed[0] ?? $tokens[0]);
}
} catch (Throwable $e) {}
return $tokens[0];
}
function allocationLetterMoney(float $n): string {
return '₦' . number_format($n, 2);
}
function allocationLetterInlineImage(string $url): string {
$u = trim($url);
if ($u === '' || preg_match('/^https?:\/\//i', $u) || preg_match('/^data:image\//i', $u)) { return $u; }
$root = __DIR__;
$abs = $root . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, ltrim($u, "/\\"));
if (!file_exists($abs)) { return $u; }
$bin = @file_get_contents($abs);
if ($bin === false) { return $u; }
$mime = 'image/png';
if (function_exists('mime_content_type')) { $mt = @mime_content_type($abs); if ($mt) $mime = $mt; }
return 'data:' . $mime . ';base64,' . base64_encode($bin);
}
function loadAllocationLetterContext(PDO $pdo, int $allocationId, $companyId, bool $isSuperAdmin): array {
$ctx = [
'allocation_id' => $allocationId,
'client' => [],
'property' => [],
'billing' => [],
'computed' => [],
'sections' => ['details' => [], 'terms' => [], 'guidelines' => [], 'acknowledgement' => []],
'assets' => ['page2' => '', 'cover' => ''],
];
if ($allocationId <= 0) { return $ctx; }
$alloc = [];
try {
$sql = "SELECT * FROM allocations WHERE id = ?";
$params = [$allocationId];
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) { $sql .= " AND (company_id = ? OR company_id IS NULL)"; $params[] = (int)$companyId; }
$sql .= " LIMIT 1";
$st = $pdo->prepare($sql);
$st->execute($params);
$alloc = $st->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) { $alloc = []; }
if (!$alloc) { return $ctx; }
$clientId = (int)($alloc['user_id'] ?? 0);
$propertyId = (int)($alloc['property_id'] ?? 0);
$dealId = (int)($alloc['deal_id'] ?? 0);
$estateId = (int)($alloc['estate_id'] ?? 0);
$user = [];
if ($clientId > 0) {
try {
$st = $pdo->prepare("SELECT * FROM users WHERE id = ? LIMIT 1");
$st->execute([$clientId]);
$user = $st->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) { $user = []; }
}
$formData = [];
if ($clientId > 0) {
try {
$st = $pdo->prepare("SELECT form_data FROM client_forms WHERE client_id = ? ORDER BY updated_at DESC, created_at DESC LIMIT 1");
$st->execute([$clientId]);
$raw = (string)($st->fetchColumn() ?: '');
if ($raw !== '') {
$tmp = json_decode($raw, true);
if (is_array($tmp)) { $formData = $tmp; }
}
} catch (Throwable $e) { $formData = []; }
}
$property = [];
if ($propertyId > 0) {
try {
$st = $pdo->prepare("SELECT * FROM properties WHERE id = ? LIMIT 1");
$st->execute([$propertyId]);
$property = $st->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) { $property = []; }
if ($estateId <= 0 && !empty($property['estate_id'])) { $estateId = (int)$property['estate_id']; }
}
$estate = [];
if ($estateId > 0) {
try {
$st = $pdo->prepare("SELECT * FROM estates WHERE id = ? LIMIT 1");
$st->execute([$estateId]);
$estate = $st->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) { $estate = []; }
}
$deal = [];
if ($dealId > 0) {
try {
$st = $pdo->prepare("SELECT * FROM deals_submit WHERE id = ? LIMIT 1");
$st->execute([$dealId]);
$deal = $st->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) { $deal = []; }
}
$dealMeta = [];
if (!empty($deal['meta_json'])) {
try {
$tmp = json_decode((string)$deal['meta_json'], true);
if (is_array($tmp)) { $dealMeta = $tmp; }
} catch (Throwable $e) { $dealMeta = []; }
}
$fullName = trim((string)($user['name'] ?? ''));
if ($fullName === '') { $fullName = trim((string)($user['full_name'] ?? '')); }
if ($fullName === '') {
$fullName = trim((string)(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')));
}
if ($fullName === '') { $fullName = 'Client #' . $clientId; }
$phone = '';
if (!empty($user['phone'])) { $phone = (string)$user['phone']; }
elseif (!empty($user['phone_number'])) { $phone = (string)$user['phone_number']; }
elseif (!empty($formData['phone'])) { $phone = (string)$formData['phone']; }
$email = '';
if (!empty($user['email'])) { $email = (string)$user['email']; }
elseif (!empty($user['email_address'])) { $email = (string)$user['email_address']; }
elseif (!empty($formData['email'])) { $email = (string)$formData['email']; }
$address = (string)($formData['address'] ?? ($formData['residential_address'] ?? ($user['address'] ?? '')));
$passportUrl = (string)($formData['passport_photo_path'] ?? '');
if ($passportUrl === '') {
foreach (['passport_url','photo_url','avatar','profile_photo'] as $k) {
if (!empty($user[$k]) && is_scalar($user[$k])) { $passportUrl = (string)$user[$k]; break; }
}
}
if ($passportUrl === '' && function_exists('getClientAvatarUrl')) {
try { $passportUrl = (string)getClientAvatarUrl($pdo, $clientId); } catch (Throwable $e) { $passportUrl = ''; }
}
$estateName = trim((string)($estate['name'] ?? ''));
if ($estateName === '') { $estateName = trim((string)($deal['estate_name'] ?? ($dealMeta['estate_name'] ?? ''))); }
if ($estateName === '') { $estateName = trim((string)($deal['project_name'] ?? ($dealMeta['project_name'] ?? ''))); }
$propertyName = trim((string)($property['title'] ?? ($property['name'] ?? '')));
if ($propertyName === '') { $propertyName = trim((string)($deal['project_desc'] ?? ($dealMeta['project_desc'] ?? ''))); }
if ($propertyName === '') { $propertyName = trim((string)($deal['project_name'] ?? ($dealMeta['project_name'] ?? ''))); }
if ($propertyName === '') { $propertyName = 'Allocation #' . $allocationId; }
$plotNumber = '';
if (!empty($alloc['plot_number'])) { $plotNumber = (string)$alloc['plot_number']; }
elseif (!empty($alloc['building_number'])) { $plotNumber = (string)$alloc['building_number']; }
elseif (!empty($alloc['unit_number'])) { $plotNumber = (string)$alloc['unit_number']; }
elseif (!empty($property['code'])) { $plotNumber = (string)$property['code']; }
elseif (!empty($property['plot_code'])) { $plotNumber = (string)$property['plot_code']; }
$sqm = '';
if (!empty($alloc['plot_size'])) { $sqm = (string)$alloc['plot_size']; }
elseif (!empty($alloc['space_size'])) { $sqm = (string)$alloc['space_size']; }
elseif (!empty($property['plot_size'])) { $sqm = (string)$property['plot_size']; }
elseif (!empty($property['area_sqm'])) { $sqm = (string)$property['area_sqm']; }
elseif (!empty($deal['sqm'])) { $sqm = (string)$deal['sqm']; }
elseif (!empty($dealMeta['sqm'])) { $sqm = (string)$dealMeta['sqm']; }
$buildingUse = trim((string)($alloc['building_use'] ?? ''));
if ($buildingUse === '') { $buildingUse = trim((string)($property['purpose'] ?? ($property['type'] ?? ''))); }
if ($buildingUse === '') { $buildingUse = 'Residential'; }
$houseType = trim((string)($alloc['house_type'] ?? ''));
if ($houseType === '') {
foreach (['house_type','building_type','title_type','property_type'] as $k) {
if (!empty($property[$k]) && is_scalar($property[$k])) { $houseType = trim((string)$property[$k]); break; }
}
}
$preferredHouseType = '';
foreach (['preferred_property','preferred_house_type','house_type','house_type_preference','preferred_type'] as $k) {
if (!empty($formData[$k]) && is_scalar($formData[$k])) { $preferredHouseType = trim((string)$formData[$k]); break; }
}
if ($preferredHouseType !== '') {
$htLow = strtolower(trim($houseType));
if ($houseType === '' || $htLow === 'residential') { $houseType = $preferredHouseType; }
}
$allocationDate = (string)($alloc['allocation_date'] ?? ($alloc['created_at'] ?? date('Y-m-d')));
$ref = '';
if (!empty($alloc['allocation_ref'])) { $ref = (string)$alloc['allocation_ref']; }
if ($ref === '') { $ref = 'AL-' . str_pad((string)$allocationId, 6, '0', STR_PAD_LEFT); }
$fileNo = $ref;
$sections = function_exists('allocationLetterSectionsGet') ? allocationLetterSectionsGet($pdo, $allocationId) : ['details'=>[],'terms'=>[],'guidelines'=>[],'acknowledgement'=>[]];
$detailsOverride = is_array($sections['details'] ?? null) ? $sections['details'] : [];
$preparedBy = '';
$notes = '';
try {
if (function_exists('allocationLetterDataGet')) {
$ld = allocationLetterDataGet($pdo, $allocationId);
if (!empty($ld)) {
if (trim((string)($ld['phone'] ?? '')) !== '') { $phone = trim((string)$ld['phone']); }
if (trim((string)($ld['email'] ?? '')) !== '') { $email = trim((string)$ld['email']); }
if (trim((string)($ld['address'] ?? '')) !== '') { $address = (string)$ld['address']; }
if (trim((string)($ld['passport'] ?? '')) !== '') { $passportUrl = trim((string)$ld['passport']); }
if (trim((string)($ld['plot_no'] ?? '')) !== '') { $plotNumber = trim((string)$ld['plot_no']); }
if (trim((string)($ld['sqm'] ?? '')) !== '') { $sqm = trim((string)$ld['sqm']); }
if (trim((string)($ld['house_type'] ?? '')) !== '') { $houseType = trim((string)$ld['house_type']); }
if (trim((string)($ld['prepared_by'] ?? '')) !== '') { $preparedBy = trim((string)$ld['prepared_by']); }
if (trim((string)($ld['notes'] ?? '')) !== '') { $notes = trim((string)$ld['notes']); }
}
}
} catch (Throwable $e) {}
$ov = trim((string)($detailsOverride['full_name'] ?? '')); if ($ov !== '') { $fullName = $ov; }
$ov = trim((string)($detailsOverride['phone'] ?? '')); if ($ov !== '') { $phone = $ov; }
$ov = trim((string)($detailsOverride['email'] ?? '')); if ($ov !== '') { $email = $ov; }
$ovRaw = (string)($detailsOverride['address'] ?? ''); if (trim($ovRaw) !== '') { $address = $ovRaw; }
$ov = trim((string)($detailsOverride['passport_url'] ?? '')); if ($ov !== '') { $passportUrl = $ov; }
$ov = trim((string)($detailsOverride['estate_name'] ?? '')); if ($ov !== '') { $estateName = $ov; }
$ov = trim((string)($detailsOverride['property_name'] ?? '')); if ($ov !== '') { $propertyName = $ov; }
$ov = trim((string)($detailsOverride['plot_number'] ?? '')); if ($ov !== '') { $plotNumber = $ov; }
$ov = trim((string)($detailsOverride['sqm'] ?? '')); if ($ov !== '') { $sqm = $ov; }
$ov = trim((string)($detailsOverride['property_type'] ?? '')); if ($ov !== '') { $buildingUse = $ov; }
$ov = trim((string)($detailsOverride['house_type'] ?? '')); if ($ov !== '') { $houseType = $ov; }
$ov = trim((string)($detailsOverride['allocation_date'] ?? '')); if ($ov !== '') { $allocationDate = $ov; }
$ov = trim((string)($detailsOverride['file_number'] ?? '')); if ($ov !== '') { $fileNo = $ov; }
$ov = trim((string)($detailsOverride['reference_number'] ?? '')); if ($ov !== '') { $ref = $ov; }
$ov = trim((string)($detailsOverride['prepared_by'] ?? '')); if ($ov !== '') { $preparedBy = $ov; }
$ov = trim((string)($detailsOverride['notes'] ?? '')); if ($ov !== '') { $notes = $ov; }
$baseLandCost = 0.0;
if (!empty($alloc['total_price'])) { $baseLandCost = (float)parseMoneyValue($alloc['total_price']); }
if ($baseLandCost <= 0 && !empty($alloc['price'])) { $baseLandCost = (float)parseMoneyValue($alloc['price']); }
if ($baseLandCost <= 0 && !empty($deal['amount_offered'])) { $baseLandCost = (float)parseMoneyValue($deal['amount_offered']); }
if ($baseLandCost <= 0 && !empty($property['price'])) { $baseLandCost = (float)parseMoneyValue($property['price']); }
$billing = [];
try {
$st = $pdo->prepare("SELECT * FROM allocation_billing WHERE allocation_id = ? LIMIT 1");
$st->execute([$allocationId]);
$billing = $st->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) { $billing = []; }
$dealPlanRaw = trim((string)($deal['payment_plan'] ?? ($dealMeta['payment_plan'] ?? ($dealMeta['plan_type'] ?? ''))));
$dealPlanMonths = 3;
if (!empty($deal['custom_months'])) {
$dealPlanMonths = max(1, (int)$deal['custom_months']);
} elseif ($dealPlanRaw !== '') {
if ($dealPlanRaw === 'full' || $dealPlanRaw === 'outright') {
$dealPlanMonths = 1;
} elseif ($dealPlanRaw === '3_months') {
$dealPlanMonths = 3;
} elseif ($dealPlanRaw === '6_months') {
$dealPlanMonths = 6;
} elseif ($dealPlanRaw === '8_months') {
$dealPlanMonths = 8;
} elseif (preg_match('/custom_(\d+)_months/i', $dealPlanRaw, $mPlan)) {
$dealPlanMonths = max(1, (int)$mPlan[1]);
} elseif ($dealPlanRaw === 'installment') {
$dealPlanMonths = 3;
}
}
if ($billing) {
$billUpdatedAt = trim((string)($billing['updated_at'] ?? ''));
$billMonths = (int)($billing['payment_plan_months'] ?? 0);
$billLooksDefault = ($billMonths <= 0) || ($billMonths === 3);
if ($dealPlanMonths > 0 && $billLooksDefault && $billUpdatedAt === '' && $billMonths !== $dealPlanMonths) {
$billing['payment_plan_months'] = $dealPlanMonths;
try {
$pdo->prepare("UPDATE allocation_billing SET payment_plan_months = ?, updated_at = NOW() WHERE allocation_id = ?")->execute([(int)$dealPlanMonths, (int)$allocationId]);
} catch (Throwable $e) {}
}
}
if (!$billing) {
$billing = [
'allocation_id' => $allocationId,
'company_id' => $companyId,
'land_cost' => $baseLandCost,
'infra_mode' => 'percent',
'infra_percent' => function_exists('getSetting') ? (float)parseMoneyValue(getSetting('infra_lease_percent', '20')) : 20.0,
'infra_amount' => 0,
'excavation_fee' => 0,
'construction_supervision' => 0,
'approval_fee' => 0,
'application_form_fee' => function_exists('getSetting') ? (float)parseMoneyValue(getSetting('application_fee', '0')) : 0.0,
'fence_gate_cost' => 0,
'carcass_cost' => 0,
'exterior_finishing_cost' => 0,
'dpc_cost' => 0,
'include_fence' => 0,
'include_carcass' => 0,
'include_exterior_finishing' => 0,
'include_dpc' => 0,
'vat_percent' => function_exists('getSetting') ? (float)parseMoneyValue(getSetting('vat_percent', '7.5')) : 7.5,
'payment_plan_months' => $dealPlanMonths,
];
}
$computed = function_exists('computeAllocationBilling') ? computeAllocationBilling($billing) : ['grand_total' => (float)($billing['land_cost'] ?? 0)];
$coverAsset = function_exists('getSetting') ? ((getSetting('allocation_letter_cover_img_path', '') ?: '') ?: ((getSetting('allocation_letter_cover_path', '') ?: '') ?: (getSetting('allocation_letter_cover_image', '') ?: ''))) : '';
$page2Asset = function_exists('getSetting') ? ((getSetting('allocation_letter_page2_img_path', '') ?: '') ?: ((getSetting('allocation_letter_page2_path', '') ?: '') ?: (getSetting('allocation_letter_page2_image', '') ?: ''))) : '';
$ctx['client'] = [
'id' => $clientId,
'full_name' => $fullName,
'phone' => $phone,
'email' => $email,
'address' => $address,
'passport_url' => $passportUrl,
];
$ctx['property'] = [
'estate_name' => $estateName,
'property_name' => $propertyName,
'plot_number' => $plotNumber,
'sqm' => $sqm,
'property_type' => $buildingUse,
'house_type' => $houseType,
];
$ctx['meta'] = [
'allocation_date' => $allocationDate,
'file_number' => $fileNo,
'reference_number' => $ref,
'prepared_by' => $preparedBy,
'notes' => $notes,
];
$ctx['billing'] = $billing;
$ctx['computed'] = $computed;
$ctx['sections'] = $sections;
$ctx['assets'] = ['cover' => $coverAsset, 'page2' => $page2Asset];
return $ctx;
}
$isSuperAdmin = ($roleNorm === 'super_admin');
$isHeadAdmin = function_exists('isHeadAdminRole') ? isHeadAdminRole($roleNorm) : in_array($roleNorm, ['head_admin','admin_head'], true);
$isAdminOfficer = function_exists('isAdminOfficerRole') ? isAdminOfficerRole($roleNorm) : ($roleNorm === 'admin_officer');
$canManageDraft = in_array($roleNorm, ['admin','head_admin','admin_head','admin_officer','super_admin','estate_manager','sales_manager'], true);
$canSendToExecutive = $isSuperAdmin || $isHeadAdmin;
$canReleaseToClient = $isSuperAdmin || $isHeadAdmin;
$canManageApproval = $canManageDraft;
$canReviewExecutiveQueue = in_array($roleNorm, ['executive','chairman_ceo','super_admin'], true);
$viewMode = strtolower(trim((string)($_REQUEST['view'] ?? '')));
$allowedViewModes = ['admin', 'executive'];
if (!in_array($viewMode, $allowedViewModes, true)) {
$viewMode = $canManageApproval ? 'admin' : 'executive';
}
if ($viewMode === 'admin' && !$canManageDraft) {
$viewMode = 'executive';
}
if ($viewMode === 'executive' && !$canReviewExecutiveQueue) {
$viewMode = 'admin';
}
$allocationLettersBaseHref = 'allocation-letters.php?view=' . urlencode($viewMode);
$action = $_GET['action'] ?? '';
$allocId = isset($_GET['allocation_id']) ? (int)$_GET['allocation_id'] : (isset($_GET['alloc_id']) ? (int)$_GET['alloc_id'] : 0);
$statusPreset = strtolower(trim((string)($_GET['status'] ?? 'all')));
if (!in_array($statusPreset, ['all','pending','approved','completed','rejected'], true)) {
$statusPreset = 'all';
}
$focusedAllocationId = $allocId > 0 ? $allocId : 0;
if ($action === 'send_for_approval' && $_SERVER['REQUEST_METHOD'] === 'GET' && $allocId > 0 && $canSendToExecutive) {
require_once __DIR__ . '/includes/header.php';
?>
<div class="container-fluid px-4 py-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<h1 class="h4 mb-1">Send Allocation Letter for Executive Approval</h1>
<div class="text-muted small">This will move the allocation letter to the executive approval queue.</div>
</div>
<a href="allocations.php" class="btn btn-outline-secondary btn-sm">Back</a>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="alert alert-warning mb-3">Confirm you want to send allocation #<?= (int)$allocId ?> to the executive queue.</div>
<form method="post" action="allocation-letters.php">
<input type="hidden" name="form_action" value="send_for_approval">
<input type="hidden" name="view" value="admin">
<input type="hidden" name="allocation_id" value="<?= (int)$allocId ?>">
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-paper-plane me-2"></i>Send for Approval</button>
<a href="allocation-letters.php?view=admin" class="btn btn-light border ms-2">Open Workflow</a>
</form>
</div>
</div>
</div>
<?php
require_once __DIR__ . '/includes/footer.php';
exit;
}
if ($viewMode === 'executive' && $_SERVER['REQUEST_METHOD'] === 'GET' && $allocId > 0 && in_array($action, ['letter_details','terms','guidelines','final_preview'], true)) {
header('Location: allocation-letters.php?view=executive&action=details&allocation_id=' . (int)$allocId);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $allocId > 0 && $canManageDraft && in_array($action, ['letter_details','terms','guidelines','final_preview'], true)) {
$ctx = loadAllocationLetterContext($pdo, $allocId, $companyId, $isSuperAdmin);
if (empty($ctx['client'])) {
$_SESSION['error_msg'] = 'Allocation not found.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$openPreviewInNewTab = isset($_GET['open_preview']) && (string)$_GET['open_preview'] !== '0';
$steps = [
'letter_details' => 'Letter Details',
'terms' => 'Terms & Conditions',
'guidelines' => 'Building Guidelines',
'final_preview' => 'Final Preview',
];
$stepNav = '<div class="d-flex flex-wrap gap-2">';
foreach ($steps as $k => $label) {
$active = $k === $action ? 'btn btn-primary' : 'btn btn-outline-primary';
$stepNav .= '<a class="' . $active . '" href="allocation-letters.php?view=' . urlencode($viewMode) . '&action=' . urlencode($k) . '&allocation_id=' . (int)$allocId . '">' . htmlspecialchars($label) . '</a>';
}
$stepNav .= '</div>';
$backHref = 'allocation-letters.php?view=' . urlencode($viewMode) . '&allocation_id=' . (int)$allocId;
require_once __DIR__ . '/includes/header.php';
echo '<div class="container-fluid px-4 py-4">';
echo '<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">';
echo '<div><h1 class="h4 mb-1">Allocation Letter — Draft Builder</h1><div class="text-muted small">Allocation #' . str_pad((string)$allocId, 6, '0', STR_PAD_LEFT) . '</div></div>';
echo '<div class="d-flex flex-wrap gap-2"><a class="btn btn-outline-secondary" href="' . htmlspecialchars($allocationLettersBaseHref) . '">Back to List</a><a class="btn btn-outline-dark" href="' . htmlspecialchars($backHref) . '">Back to Row</a></div>';
echo '</div>';
echo $stepNav;
echo '<div class="row g-3 mt-2">';
echo '<div class="col-12 col-lg-8">';
if ($action === 'letter_details') {
$p = $ctx['property'];
$c = $ctx['client'];
$m = $ctx['meta'];
$passport = allocationLetterInlineImage((string)($c['passport_url'] ?? ''));
$b = $ctx['computed'];
echo '<div class="card shadow-sm border-0"><div class="card-body">';
echo '<h5 class="mb-3">Client</h5>';
echo '<form method="post" enctype="multipart/form-data" action="allocation-letters.php?view=' . urlencode($viewMode) . '&action=letter_details&allocation_id=' . (int)$allocId . '">';
echo '<input type="hidden" name="form_action" value="save_letter_details">';
echo '<input type="hidden" name="allocation_id" value="' . (int)$allocId . '">';
echo '<div class="row g-3">';
echo '<div class="col-12 col-md-8">';
echo '<label class="form-label">Full Name</label><input class="form-control" name="full_name" value="' . htmlspecialchars((string)($c['full_name'] ?? '')) . '">';
echo '</div>';
echo '<div class="col-12 col-md-4">';
echo '<label class="form-label">Phone</label><input class="form-control" name="phone" value="' . htmlspecialchars((string)($c['phone'] ?? '')) . '">';
echo '</div>';
echo '<div class="col-12 col-md-6">';
echo '<label class="form-label">Email</label><input class="form-control" name="email" value="' . htmlspecialchars((string)($c['email'] ?? '')) . '">';
echo '</div>';
echo '<div class="col-12">';
echo '<label class="form-label">Address</label><textarea class="form-control" name="address" rows="3">' . htmlspecialchars((string)($c['address'] ?? '')) . '</textarea>';
echo '</div>';
echo '<div class="col-12">';
echo '<label class="form-label">Passport Photo URL</label><input class="form-control" name="passport_url" value="' . htmlspecialchars((string)($c['passport_url'] ?? '')) . '">';
echo '</div>';
echo '<div class="col-12">';
echo '<label class="form-label">Passport Upload</label><input class="form-control" type="file" name="passport_file" accept="image/*">';
echo '</div>';
echo '</div>';
echo '<hr class="my-4">';
echo '<h5 class="mb-3">Property</h5>';
echo '<div class="row g-3">';
echo '<div class="col-12 col-md-6"><label class="form-label">Estate Name</label><input class="form-control" name="estate_name" value="' . htmlspecialchars((string)($p['estate_name'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-6"><label class="form-label">Property Name</label><input class="form-control" name="property_name" value="' . htmlspecialchars((string)($p['property_name'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">Plot Number</label><input class="form-control" name="plot_number" value="' . htmlspecialchars((string)($p['plot_number'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">SQM</label><input class="form-control" name="sqm" value="' . htmlspecialchars((string)($p['sqm'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">Building Use</label><input class="form-control" name="property_type" value="' . htmlspecialchars((string)($p['property_type'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-6"><label class="form-label">House Type</label><input class="form-control" name="house_type" value="' . htmlspecialchars((string)($p['house_type'] ?? '')) . '"></div>';
echo '</div>';
echo '<hr class="my-4">';
echo '<h5 class="mb-3">Meta</h5>';
echo '<div class="row g-3">';
echo '<div class="col-12 col-md-4"><label class="form-label">Allocation Date</label><input class="form-control" name="allocation_date" value="' . htmlspecialchars((string)($m['allocation_date'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">File Number</label><input class="form-control" name="file_number" value="' . htmlspecialchars((string)($m['file_number'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">Reference Number</label><input class="form-control" name="reference_number" value="' . htmlspecialchars((string)($m['reference_number'] ?? '')) . '"></div>';
echo '<div class="col-12 col-md-6"><label class="form-label">Prepared By</label><input class="form-control" name="prepared_by" value="' . htmlspecialchars((string)($m['prepared_by'] ?? '')) . '"></div>';
echo '<div class="col-12"><label class="form-label">Notes</label><textarea class="form-control" name="notes" rows="3">' . htmlspecialchars((string)($m['notes'] ?? '')) . '</textarea></div>';
echo '</div>';
echo '<div class="d-flex justify-content-between align-items-center mt-4 flex-wrap gap-2">';
echo '<div class="d-flex gap-2 flex-wrap">';
echo '<button class="btn btn-primary" type="submit" name="next_action" value="">Save Letter Details</button>';
echo '<button class="btn btn-outline-primary" type="submit" name="next_action" value="terms">Save & Next: Terms</button>';
echo '</div>';
echo '<button class="btn btn-outline-success" type="submit" name="next_action" value="preview">Save & Preview Draft</button>';
echo '</div>';
echo '</form>';
echo '</div></div>';
echo '<div class="card shadow-sm border-0 mt-3"><div class="card-body">';
echo '<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">';
echo '<h5 class="mb-0">Billing Summary</h5>';
echo '<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#billingModal">Edit Billing</button>';
echo '</div>';
echo '<div class="text-muted small mb-2">Billing summary is read-only here. Use Edit Billing to change amounts.</div>';
echo '<div class="row g-2">';
echo '<div class="col-12 col-md-4"><div class="p-3 border rounded"><div class="text-muted small">Sub Total</div><div class="fw-bold" id="billingSubTotalText">' . allocationLetterMoney((float)($b['sub_total'] ?? 0)) . '</div></div></div>';
echo '<div class="col-12 col-md-4"><div class="p-3 border rounded"><div class="text-muted small">VAT</div><div class="fw-bold" id="billingVatText">' . allocationLetterMoney((float)($b['vat'] ?? 0)) . '</div></div></div>';
echo '<div class="col-12 col-md-4"><div class="p-3 border rounded bg-success bg-opacity-10 border-success"><div class="text-success small">Grand Total</div><div class="fw-bold text-success" id="billingGrandTotalText">' . allocationLetterMoney((float)($b['grand_total'] ?? 0)) . '</div></div></div>';
echo '</div>';
echo '</div></div>';
echo '<div class="modal fade" id="billingModal" tabindex="-1" aria-hidden="true">';
echo ' <div class="modal-dialog modal-lg modal-dialog-scrollable">';
echo ' <form class="modal-content" id="billingForm">';
echo ' <div class="modal-header">';
echo ' <div><h5 class="modal-title mb-0">Edit Billing</h5><div class="text-muted small">Allocation #' . str_pad((string)$allocId, 6, '0', STR_PAD_LEFT) . '</div></div>';
echo ' <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>';
echo ' </div>';
echo ' <div class="modal-body">';
echo ' <input type="hidden" name="form_action" value="save_billing">';
echo ' <input type="hidden" name="xhr" value="1">';
echo ' <input type="hidden" name="allocation_id" value="' . (int)$allocId . '">';
echo ' <div class="row g-3">';
echo ' <div class="col-12 col-md-6"><label class="form-label">Land Cost</label><input type="number" step="0.01" min="0" class="form-control" name="land_cost" id="billLandCost"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">VAT %</label><input type="number" step="0.01" min="0" class="form-control" name="vat_percent" id="billVatPercent"></div>';
echo ' <div class="col-12"><label class="form-label">Infrastructure Charge Mode</label><div class="d-flex gap-3 flex-wrap">';
echo ' <div class="form-check"><input class="form-check-input" type="radio" name="infra_mode" id="infraModePercent" value="percent" checked><label class="form-check-label" for="infraModePercent">Percentage of base cost</label></div>';
echo ' <div class="form-check"><input class="form-check-input" type="radio" name="infra_mode" id="infraModeFixed" value="fixed"><label class="form-check-label" for="infraModeFixed">Fixed amount</label></div>';
echo ' </div></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Infrastructure %</label><input type="number" step="0.01" min="0" class="form-control" name="infra_percent" id="billInfraPercent"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Infrastructure Amount</label><input type="number" step="0.01" min="0" class="form-control" name="infra_amount" id="billInfraAmount"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Excavation Fee</label><input type="number" step="0.01" min="0" class="form-control" name="excavation_fee" id="billExcavationFee"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Construction Supervision</label><input type="number" step="0.01" min="0" class="form-control" name="construction_supervision" id="billSupervision"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Approval Fee</label><input type="number" step="0.01" min="0" class="form-control" name="approval_fee" id="billApprovalFee"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Application Form Fee</label><input type="number" step="0.01" min="0" class="form-control" name="application_form_fee" id="billApplicationFee"></div>';
echo ' <div class="col-12"><div class="fw-bold small text-uppercase text-muted">Optional Items (show in letter only if enabled)</div></div>';
echo ' <div class="col-12 col-md-6"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="include_fence" id="includeFence"><label class="form-check-label" for="includeFence">Fence / Gate</label></div><input type="number" step="0.01" min="0" class="form-control" name="fence_gate_cost" id="billFenceCost" placeholder="Fence / Gate cost"></div>';
echo ' <div class="col-12 col-md-6"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="include_carcass" id="includeCarcass"><label class="form-check-label" for="includeCarcass">Carcass</label></div><input type="number" step="0.01" min="0" class="form-control" name="carcass_cost" id="billCarcassCost" placeholder="Carcass cost"></div>';
echo ' <div class="col-12 col-md-6"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="include_exterior_finishing" id="includeExterior"><label class="form-check-label" for="includeExterior">Exterior Finishing</label></div><input type="number" step="0.01" min="0" class="form-control" name="exterior_finishing_cost" id="billExteriorCost" placeholder="Exterior finishing cost"></div>';
echo ' <div class="col-12 col-md-6"><div class="form-check mb-2"><input class="form-check-input" type="checkbox" name="include_dpc" id="includeDpc"><label class="form-check-label" for="includeDpc">DPC</label></div><input type="number" step="0.01" min="0" class="form-control" name="dpc_cost" id="billDpcCost" placeholder="DPC cost"></div>';
echo ' <div class="col-12 col-md-6"><label class="form-label">Payment Plan (months)</label><input type="number" step="1" min="1" class="form-control" name="payment_plan_months" id="paymentPlanMonths"></div>';
echo ' </div>';
echo ' <div class="alert alert-danger mt-3 d-none" id="billingError"></div>';
echo ' <div class="alert alert-success mt-3 d-none" id="billingSuccess"></div>';
echo ' </div>';
echo ' <div class="modal-footer">';
echo ' <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>';
echo ' <button type="submit" class="btn btn-primary" id="billingSaveBtn">Save Billing</button>';
echo ' </div>';
echo ' </form>';
echo ' </div>';
echo '</div>';
} elseif ($action === 'terms') {
$b = $ctx['billing'];
$t = $ctx['sections']['terms'] ?? [];
$page2 = (string)($ctx['assets']['page2'] ?? '');
$refund = (string)($t['refund_charge_percent'] ?? '');
echo '<div class="card shadow-sm border-0"><div class="card-body">';
echo '<h5 class="mb-3">Terms & Conditions (Variables)</h5>';
echo '<form method="post" action="allocation-letters.php?view=' . urlencode($viewMode) . '&action=terms&allocation_id=' . (int)$allocId . '">';
echo '<input type="hidden" name="form_action" value="save_terms"><input type="hidden" name="allocation_id" value="' . (int)$allocId . '">';
echo '<div class="row g-3">';
echo '<div class="col-12 col-md-4"><label class="form-label">Payment Duration (months)</label><input class="form-control" type="number" min="1" step="1" name="payment_plan_months" value="' . htmlspecialchars((string)($b['payment_plan_months'] ?? 3)) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">VAT %</label><input class="form-control" type="number" min="0" step="0.01" name="vat_percent" value="' . htmlspecialchars((string)($b['vat_percent'] ?? 0)) . '"></div>';
echo '<div class="col-12 col-md-4"><label class="form-label">Refund Charge %</label><input class="form-control" type="number" min="0" step="0.01" name="refund_charge_percent" value="' . htmlspecialchars($refund) . '"></div>';
echo '<div class="col-12">';
echo '<label class="form-label">Infrastructure Charge</label>';
$mode = strtolower((string)($b['infra_mode'] ?? 'percent'));
$isFixed = $mode === 'fixed';
echo '<div class="d-flex gap-3 flex-wrap">';
echo '<div class="form-check"><input class="form-check-input" type="radio" name="infra_mode" value="percent" ' . ($isFixed ? '' : 'checked') . '><label class="form-check-label">Percent</label></div>';
echo '<div class="form-check"><input class="form-check-input" type="radio" name="infra_mode" value="fixed" ' . ($isFixed ? 'checked' : '') . '><label class="form-check-label">Fixed</label></div>';
echo '</div>';
echo '</div>';
echo '<div class="col-12 col-md-6"><label class="form-label">Infrastructure %</label><input class="form-control" type="number" min="0" step="0.01" name="infra_percent" value="' . htmlspecialchars((string)($b['infra_percent'] ?? 0)) . '"></div>';
echo '<div class="col-12 col-md-6"><label class="form-label">Infrastructure Amount</label><input class="form-control" type="number" min="0" step="0.01" name="infra_amount" value="' . htmlspecialchars((string)($b['infra_amount'] ?? 0)) . '"></div>';
echo '</div>';
echo '<div class="d-flex justify-content-between align-items-center mt-4 flex-wrap gap-2">';
echo '<a class="btn btn-outline-primary" href="allocation-letters.php?view=' . urlencode($viewMode) . '&action=letter_details&allocation_id=' . (int)$allocId . '">Back: Details</a>';
echo '<div class="d-flex gap-2 flex-wrap">';
echo '<button class="btn btn-primary" type="submit" name="next_action" value="">Save Terms</button>';
echo '<button class="btn btn-outline-primary" type="submit" name="next_action" value="guidelines">Save & Next: Guidelines</button>';
echo '</div>';
echo '<button class="btn btn-outline-success" type="submit" name="next_action" value="preview">Save & Preview Draft</button>';
echo '</div>';
echo '</form>';
echo '</div></div>';
if ($page2 !== '') {
echo '<div class="card shadow-sm border-0 mt-3"><div class="card-body">';
echo '<h6 class="text-muted mb-2">Current Terms Document (Page 2 asset)</h6>';
$src = htmlspecialchars($page2);
if (preg_match('/\.pdf$/i', $page2)) {
echo '<iframe src="' . $src . '" style="width:100%;height:560px;border:1px solid #e5e7eb;border-radius:10px;"></iframe>';
} else {
echo '<img src="' . $src . '" alt="Terms" style="width:100%;border:1px solid #e5e7eb;border-radius:10px;">';
}
echo '</div></div>';
}
} elseif ($action === 'guidelines') {
$p = $ctx['property'];
$g = $ctx['sections']['guidelines'] ?? [];
$notes = (string)($g['notes'] ?? '');
$page2 = (string)($ctx['assets']['page2'] ?? '');
echo '<div class="card shadow-sm border-0"><div class="card-body">';
echo '<h5 class="mb-3">Building Guidelines</h5>';
echo '<div class="row g-3 mb-2">';
echo '<div class="col-12 col-md-6"><div class="p-3 border rounded"><div class="text-muted small">Estate Name</div><div class="fw-bold">' . htmlspecialchars((string)($p['estate_name'] ?? '')) . '</div></div></div>';
echo '<div class="col-12 col-md-6"><div class="p-3 border rounded"><div class="text-muted small">Plot Number</div><div class="fw-bold">' . htmlspecialchars((string)($p['plot_number'] ?? '')) . '</div></div></div>';
echo '</div>';
echo '<form method="post" action="allocation-letters.php?view=' . urlencode($viewMode) . '&action=guidelines&allocation_id=' . (int)$allocId . '">';
echo '<input type="hidden" name="form_action" value="save_guidelines"><input type="hidden" name="allocation_id" value="' . (int)$allocId . '">';
echo '<label class="form-label">Notes (optional)</label><textarea class="form-control" name="guidelines_notes" rows="4">' . htmlspecialchars($notes) . '</textarea>';
echo '<div class="d-flex justify-content-between align-items-center mt-4 flex-wrap gap-2">';
echo '<a class="btn btn-outline-primary" href="allocation-letters.php?view=' . urlencode($viewMode) . '&action=terms&allocation_id=' . (int)$allocId . '">Back: Terms</a>';
echo '<div class="d-flex gap-2 flex-wrap">';
echo '<button class="btn btn-primary" type="submit" name="next_action" value="">Save Guidelines</button>';
echo '<button class="btn btn-outline-success" type="submit" name="next_action" value="preview">Save & Preview Draft</button>';
echo '</div>';
echo '</div>';
echo '</form>';
echo '</div></div>';
if ($page2 !== '') {
echo '<div class="card shadow-sm border-0 mt-3"><div class="card-body">';
echo '<h6 class="text-muted mb-2">Guidelines Document (Page 2 asset)</h6>';
$src = htmlspecialchars($page2);
if (preg_match('/\.pdf$/i', $page2)) {
echo '<iframe src="' . $src . '" style="width:100%;height:560px;border:1px solid #e5e7eb;border-radius:10px;"></iframe>';
} else {
echo '<img src="' . $src . '" alt="Guidelines" style="width:100%;border:1px solid #e5e7eb;border-radius:10px;">';
}
echo '</div></div>';
}
} elseif ($action === 'final_preview') {
$previewHref = 'allocation-letters.php?view=' . urlencode($viewMode) . '&action=preview&allocation_id=' . (int)$allocId . '&force=1';
echo '<div class="alert alert-info">Opening letter preview in a new tab...</div>';
echo '<a class="btn btn-primary" target="_blank" rel="noopener" href="' . htmlspecialchars($previewHref, ENT_QUOTES) . '">Open Preview</a>';
echo '<script>(function(){try{window.open(' . json_encode($previewHref) . ', "_blank", "noopener");}catch(e){}})();</script>';
echo '</div>';
require_once __DIR__ . '/includes/footer.php';
exit;
}
echo '</div>';
echo '<div class="col-12 col-lg-4">';
echo '<div class="card shadow-sm border-0"><div class="card-body">';
echo '<div class="fw-bold mb-2">Quick Summary</div>';
echo '<div class="small text-muted mb-1">Client</div><div class="fw-bold mb-2">' . htmlspecialchars((string)($ctx['client']['full_name'] ?? '')) . '</div>';
echo '<div class="small text-muted mb-1">Estate</div><div class="fw-bold mb-2">' . htmlspecialchars((string)($ctx['property']['estate_name'] ?? '')) . '</div>';
echo '<div class="small text-muted mb-1">Plot</div><div class="fw-bold mb-2">' . htmlspecialchars((string)($ctx['property']['plot_number'] ?? '')) . '</div>';
echo '<div class="small text-muted mb-1">Grand Total</div><div class="fw-bold text-success">' . allocationLetterMoney((float)($ctx['computed']['grand_total'] ?? 0)) . '</div>';
echo '</div></div>';
echo '</div>';
echo '</div>';
echo '</div>';
echo '<script>(function(){';
echo 'const openPreview=' . ($openPreviewInNewTab ? 'true' : 'false') . ';';
echo 'if(openPreview){';
echo ' try{window.open(' . json_encode('allocation-letters.php?view=' . $viewMode . '&action=preview&allocation_id=' . (int)$allocId . '&force=1') . ',"_blank","noopener");}catch(e){}';
echo ' try{const u=new URL(window.location.href);u.searchParams.delete("open_preview");window.history.replaceState(null,"",u.toString());}catch(e){}';
echo '}';
echo 'const initBilling=' . json_encode($ctx['billing'] ?? []) . ';';
echo 'const initComputed=' . json_encode($ctx['computed'] ?? []) . ';';
echo 'function moneyNumber(v){const n=Number(v);return Number.isFinite(n)?n:0;}';
echo 'function syncInfraInputs(){const fixed=document.getElementById("infraModeFixed");const percent=document.getElementById("billInfraPercent");const amount=document.getElementById("billInfraAmount");if(!fixed||!percent||!amount)return;const isFixed=fixed.checked;percent.disabled=isFixed;amount.disabled=!isFixed;}';
echo 'function populateBilling(){';
echo ' const lc=document.getElementById("billLandCost"); if(lc) lc.value=moneyNumber(initBilling.land_cost ?? initComputed.land_cost ?? 0);';
echo ' const vat=document.getElementById("billVatPercent"); if(vat) vat.value=moneyNumber(initBilling.vat_percent ?? initComputed.vat_percent ?? 7.5);';
echo ' const mode=String(initBilling.infra_mode || "percent").toLowerCase();';
echo ' const fixed=document.getElementById("infraModeFixed"); const pct=document.getElementById("infraModePercent");';
echo ' if(fixed&&pct){fixed.checked=(mode==="fixed");pct.checked=(mode!=="fixed");}';
echo ' const ip=document.getElementById("billInfraPercent"); if(ip) ip.value=moneyNumber(initBilling.infra_percent ?? 20);';
echo ' const ia=document.getElementById("billInfraAmount"); if(ia) ia.value=moneyNumber(initBilling.infra_amount ?? 0);';
echo ' const ex=document.getElementById("billExcavationFee"); if(ex) ex.value=moneyNumber(initBilling.excavation_fee ?? 0);';
echo ' const sup=document.getElementById("billSupervision"); if(sup) sup.value=moneyNumber(initBilling.construction_supervision ?? 0);';
echo ' const ap=document.getElementById("billApprovalFee"); if(ap) ap.value=moneyNumber(initBilling.approval_fee ?? 0);';
echo ' const af=document.getElementById("billApplicationFee"); if(af) af.value=moneyNumber(initBilling.application_form_fee ?? 0);';
echo ' const f=document.getElementById("includeFence"); if(f) f.checked=Number(initBilling.include_fence ?? 0)===1;';
echo ' const c=document.getElementById("includeCarcass"); if(c) c.checked=Number(initBilling.include_carcass ?? 0)===1;';
echo ' const e=document.getElementById("includeExterior"); if(e) e.checked=Number(initBilling.include_exterior_finishing ?? initBilling.include_exterior_finishing_cost ?? 0)===1;';
echo ' const d=document.getElementById("includeDpc"); if(d) d.checked=Number(initBilling.include_dpc ?? 0)===1;';
echo ' const fc=document.getElementById("billFenceCost"); if(fc) fc.value=moneyNumber(initBilling.fence_gate_cost ?? 0);';
echo ' const cc=document.getElementById("billCarcassCost"); if(cc) cc.value=moneyNumber(initBilling.carcass_cost ?? 0);';
echo ' const ec=document.getElementById("billExteriorCost"); if(ec) ec.value=moneyNumber(initBilling.exterior_finishing_cost ?? 0);';
echo ' const dc=document.getElementById("billDpcCost"); if(dc) dc.value=moneyNumber(initBilling.dpc_cost ?? 0);';
echo ' const pm=document.getElementById("paymentPlanMonths"); if(pm) pm.value=String(Math.max(1, Number(initBilling.payment_plan_months ?? 3) || 3));';
echo ' syncInfraInputs();';
echo '}';
echo 'function refreshBillingSummary(){';
echo ' fetch("ajax_get_allocation_details.php?id=" + encodeURIComponent(' . json_encode((string)$allocId) . '), {credentials:"same-origin"})';
echo ' .then(r=>r.json()).then(function(data){';
echo ' if(!data||!data.success) return;';
echo ' const cb=data.cost_breakdown||{};';
echo ' const fmt=function(v){const n=Number(v)||0; return "₦"+n.toLocaleString(undefined,{minimumFractionDigits:0,maximumFractionDigits:2});};';
echo ' const st=document.getElementById("billingSubTotalText"); if(st&&cb.sub_total!=null) st.textContent=fmt(cb.sub_total);';
echo ' const vt=document.getElementById("billingVatText"); if(vt&&cb.vat!=null) vt.textContent=fmt(cb.vat);';
echo ' const gt=document.getElementById("billingGrandTotalText"); if(gt&&cb.grand_total!=null) gt.textContent=fmt(cb.grand_total);';
echo ' }).catch(function(){});';
echo '}';
echo 'document.addEventListener("DOMContentLoaded", function(){';
echo ' populateBilling();';
echo ' document.querySelectorAll("input[name=infra_mode]").forEach(function(r){r.addEventListener("change", syncInfraInputs);});';
echo ' const form=document.getElementById("billingForm");';
echo ' if(form){form.addEventListener("submit", async function(ev){';
echo ' ev.preventDefault();';
echo ' const err=document.getElementById("billingError"); const ok=document.getElementById("billingSuccess");';
echo ' if(err){err.classList.add("d-none"); err.textContent="";}';
echo ' if(ok){ok.classList.add("d-none"); ok.textContent="";}';
echo ' const btn=document.getElementById("billingSaveBtn"); const original=btn?btn.innerHTML:"";';
echo ' if(btn){btn.disabled=true; btn.innerHTML="Saving...";}';
echo ' try{';
echo ' const fd=new FormData(form);';
echo ' const res=await fetch("allocation-letters.php", {method:"POST", body:fd, credentials:"same-origin"});';
echo ' const json=await res.json().catch(function(){return null;});';
echo ' if(!json||!json.success){const msg=(json&&json.error)?json.error:"Failed to save."; if(err){err.textContent=msg; err.classList.remove("d-none");} return;}';
echo ' if(ok){ok.textContent="Billing saved."; ok.classList.remove("d-none");}';
echo ' refreshBillingSummary();';
echo ' try{const url=' . json_encode('allocation-letters.php?view=' . $viewMode . '&action=preview&allocation_id=' . (int)$allocId . '&force=1') . '; window.open(url,"_blank","noopener");}catch(e){}';
echo ' try{const modalEl=document.getElementById("billingModal"); if(modalEl && window.bootstrap && window.bootstrap.Modal){ const bs=window.bootstrap.Modal.getOrCreateInstance(modalEl); bs.hide(); }}catch(e){}';
echo ' }catch(ex){ if(err){err.textContent="Failed to save."; err.classList.remove("d-none");} }finally{ if(btn){btn.disabled=false; btn.innerHTML=original;} }';
echo ' });}';
echo '});';
echo '})();</script>';
require_once __DIR__ . '/includes/footer.php';
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'send_for_approval') {
$isAjax = (string)($_POST['xhr'] ?? '') === '1';
if (!$canSendToExecutive) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'message' => '', 'error' => 'Permission denied.']);
exit;
}
$_SESSION['error_msg'] = 'Permission denied.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$sendAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($sendAllocId <= 0) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'message' => '', 'error' => 'Invalid allocation selected for approval.']);
exit;
}
$_SESSION['error_msg'] = 'Invalid allocation selected for approval.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$okMsg = '';
$errMsg = '';
try {
$startedTransaction = method_exists($pdo, 'inTransaction') ? !$pdo->inTransaction() : true;
if ($startedTransaction) {
$pdo->beginTransaction();
}
$generatedPattern = __DIR__ . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'documents' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'Allocation_Letter_' . $sendAllocId . '_*';
$hasDocuments = false;
try { $hasDocuments = $pdo->query("SHOW TABLES LIKE 'documents'")->rowCount() > 0; } catch (Throwable $e) {}
$generatedFiles = glob($generatedPattern) ?: [];
$hasLetterDraftFile = !empty($generatedFiles);
$billingUpdatedAt = '';
try {
if (function_exists('tableHasColumn') && tableHasColumn('allocation_billing', 'updated_at')) {
$stBu = $pdo->prepare("SELECT updated_at FROM allocation_billing WHERE allocation_id = ? LIMIT 1");
$stBu->execute([$sendAllocId]);
$billingUpdatedAt = (string)($stBu->fetchColumn() ?: '');
}
} catch (Throwable $e) {}
if ($billingUpdatedAt !== '' && $hasLetterDraftFile) {
$latestDraftTime = 0;
foreach ($generatedFiles as $fp) {
$t = @filemtime($fp);
if ($t && $t > $latestDraftTime) { $latestDraftTime = $t; }
}
$billTime = strtotime($billingUpdatedAt);
if ($billTime && $latestDraftTime > 0 && $billTime > $latestDraftTime) {
$hasLetterDraftFile = false;
}
}
$hasLetterDraftRecord = false;
if ($hasDocuments) {
$docSql = "SELECT COUNT(*) FROM documents WHERE 1=1";
$docParams = [];
if (colx('documents','type')) { $docSql .= " AND type = ?"; $docParams[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$docSql .= " AND allocation_id = ?";
$docParams[] = $sendAllocId;
} else {
$docSql .= " AND file_path LIKE ?";
$docParams[] = '%Allocation_Letter_' . $sendAllocId . '_%';
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) { $docSql .= " AND (company_id = ? OR company_id IS NULL)"; $docParams[] = $companyId; }
$stDoc = $pdo->prepare($docSql);
$stDoc->execute($docParams);
$hasLetterDraftRecord = ((int)$stDoc->fetchColumn()) > 0;
}
$hasLetterDraft = $hasLetterDraftFile || $hasLetterDraftRecord;
if (!$hasLetterDraft) {
$generatedId = 0;
try {
$generator = new DocGenerator($pdo);
$generatedId = (int)$generator->generateAllocationLetter($sendAllocId, $uid);
} catch (Throwable $generatorError) {
$generatedId = 0;
}
$generatedFiles = glob($generatedPattern) ?: [];
$hasLetterDraftFile = !empty($generatedFiles);
if ($hasDocuments && !$hasLetterDraftRecord) {
$docSql = "SELECT COUNT(*) FROM documents WHERE 1=1";
$docParams = [];
if (colx('documents','type')) { $docSql .= " AND type = ?"; $docParams[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$docSql .= " AND allocation_id = ?";
$docParams[] = $sendAllocId;
} else {
$docSql .= " AND file_path LIKE ?";
$docParams[] = '%Allocation_Letter_' . $sendAllocId . '_%';
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) { $docSql .= " AND (company_id = ? OR company_id IS NULL)"; $docParams[] = $companyId; }
$stDoc = $pdo->prepare($docSql);
$stDoc->execute($docParams);
$hasLetterDraftRecord = ((int)$stDoc->fetchColumn()) > 0;
}
$hasLetterDraft = $hasLetterDraftFile || $hasLetterDraftRecord || $generatedId > 0;
if (!$hasLetterDraft) {
throw new RuntimeException('Unable to generate the allocation letter draft.');
}
}
if ($hasDocuments && $hasLetterDraftRecord && colx('documents','status')) {
$documentQueueStatus = pickColumnToken($pdo, 'documents', 'status', ['pending_executive_approval', 'review', 'pending', 'approved', 'draft']);
$set = ["status = ?"];
$params = [$documentQueueStatus];
if (colx('documents','updated_at')) {
$set[] = "updated_at = ?";
$params[] = date('Y-m-d H:i:s');
}
$sqlDocUpdate = "UPDATE documents SET " . implode(', ', $set) . " WHERE 1=1";
if (colx('documents','type')) { $sqlDocUpdate .= " AND type = ?"; $params[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$sqlDocUpdate .= " AND allocation_id = ?";
$params[] = $sendAllocId;
} else {
$sqlDocUpdate .= " AND file_path LIKE ?";
$params[] = '%Allocation_Letter_' . $sendAllocId . '_%';
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) {
$sqlDocUpdate .= " AND (company_id = ? OR company_id IS NULL)";
$params[] = $companyId;
}
$updDoc = $pdo->prepare($sqlDocUpdate);
$updDoc->execute($params);
}
if (colx('allocations','status')) {
$allocationQueueStatus = pickColumnToken($pdo, 'allocations', 'status', ['pending_executive_approval', 'pending']);
$setAlloc = ["status = ?"];
$paramsAlloc = [$allocationQueueStatus];
if (colx('allocations','updated_at')) {
$setAlloc[] = "updated_at = ?";
$paramsAlloc[] = date('Y-m-d H:i:s');
}
$sqlAlloc = "UPDATE allocations SET " . implode(', ', $setAlloc) . " WHERE id = ?";
$paramsAlloc[] = $sendAllocId;
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) {
$sqlAlloc .= " AND (company_id = ? OR company_id IS NULL)";
$paramsAlloc[] = $companyId;
}
$updAlloc = $pdo->prepare($sqlAlloc);
$updAlloc->execute($paramsAlloc);
}
try {
if (function_exists('allocationLetterDataUpsert')) {
allocationLetterDataUpsert($pdo, (int)$sendAllocId, [
'status' => 'pending_chairman_approval',
'sent_to_chairman_at' => date('Y-m-d H:i:s'),
'chairman_decision' => null,
'chairman_comment' => null,
'chairman_decided_by' => null,
'chairman_decided_at' => null,
]);
}
} catch (Throwable $e) {}
if ($startedTransaction && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) {
$pdo->commit();
}
if (function_exists('logActivity') && $uid > 0) {
try {
logActivity($uid, 'ALLOCATION_SENT_FOR_EXECUTIVE_APPROVAL', json_encode(['allocation_id' => $sendAllocId, 'company_id' => $companyId]));
} catch (Throwable $e) {}
}
try {
if (function_exists('ap_user_ids_by_roles') && function_exists('sendNotification')) {
$ids = ap_user_ids_by_roles($pdo, ['chairman_ceo','executive'], $companyId ? (int)$companyId : null);
foreach ($ids as $eid) {
sendNotification((int)$eid, 'allocation_letter_review', 'Allocation #' . (int)$sendAllocId . ' is pending executive approval.', $pdo);
}
}
} catch (Throwable $e) {}
$okMsg = 'Allocation letter sent to the executive dashboard for approval.';
} catch (Throwable $e) {
try {
if (!empty($startedTransaction) && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) {
$pdo->rollBack();
}
} catch (Throwable $rollbackError) {}
error_log('Allocation letter send_for_approval failed for allocation #' . $sendAllocId . ': ' . $e->getMessage());
$errMsg = 'Failed to send allocation letter for approval.';
}
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => $errMsg === '', 'message' => $okMsg, 'error' => $errMsg]);
exit;
}
if ($errMsg !== '') {
$_SESSION['error_msg'] = $errMsg;
} else {
$_SESSION['success_msg'] = $okMsg;
}
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'submit_draft') {
$isAjax = (string)($_POST['xhr'] ?? '') === '1';
if (!$isAdminOfficer && !$isSuperAdmin) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'message' => '', 'error' => 'Permission denied.']);
exit;
}
$_SESSION['error_msg'] = 'Permission denied.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Invalid allocation selected.']);
exit;
}
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$ok = false;
$err = '';
try {
$startedTransaction = method_exists($pdo, 'inTransaction') ? !$pdo->inTransaction() : true;
if ($startedTransaction) { $pdo->beginTransaction(); }
$st = $pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?");
$st->execute(['pending_head_admin_review', $targetAllocId]);
try {
if (function_exists('allocationLetterDataUpsert')) {
allocationLetterDataUpsert($pdo, (int)$targetAllocId, [
'status' => 'pending_head_admin_review',
'submitted_to_head_admin_at' => date('Y-m-d H:i:s'),
]);
}
} catch (Throwable $e) {}
if ($startedTransaction && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) { $pdo->commit(); }
$ok = true;
try {
if (function_exists('ap_user_ids_by_roles') && function_exists('sendNotification')) {
$ids = ap_user_ids_by_roles($pdo, ['head_admin','admin'], $companyId ? (int)$companyId : null);
foreach ($ids as $hid) {
sendNotification((int)$hid, 'allocation_letter_draft_submitted', 'Allocation #' . (int)$targetAllocId . ' draft is ready for review.', $pdo);
}
}
} catch (Throwable $e) {}
} catch (Throwable $e) {
$err = 'Failed to submit draft.';
try { if (!empty($startedTransaction) && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) { $pdo->rollBack(); } } catch (Throwable $x) {}
}
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => $ok, 'error' => $ok ? '' : $err]);
exit;
}
$_SESSION[$ok ? 'success_msg' : 'error_msg'] = $ok ? 'Draft submitted to Head Admin.' : $err;
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'return_for_correction') {
$isAjax = (string)($_POST['xhr'] ?? '') === '1';
if (!$isHeadAdmin && !$isSuperAdmin) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'message' => '', 'error' => 'Permission denied.']);
exit;
}
$_SESSION['error_msg'] = 'Permission denied.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Invalid allocation selected.']);
exit;
}
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$note = trim((string)($_POST['note'] ?? ''));
$ok = false;
$err = '';
try {
$startedTransaction = method_exists($pdo, 'inTransaction') ? !$pdo->inTransaction() : true;
if ($startedTransaction) { $pdo->beginTransaction(); }
$st = $pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?");
$st->execute(['returned_for_correction', $targetAllocId]);
try {
if (function_exists('allocationLetterDataUpsert')) {
allocationLetterDataUpsert($pdo, (int)$targetAllocId, [
'status' => 'returned_for_correction',
'head_admin_comment' => $note,
'returned_for_correction_at' => date('Y-m-d H:i:s'),
]);
}
} catch (Throwable $e) {}
if ($startedTransaction && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) { $pdo->commit(); }
$ok = true;
try {
if (function_exists('ap_user_ids_by_roles') && function_exists('sendNotification')) {
$ids = ap_user_ids_by_roles($pdo, ['admin_officer'], $companyId ? (int)$companyId : null);
foreach ($ids as $oid) {
sendNotification((int)$oid, 'allocation_letter_returned', 'Allocation #' . (int)$targetAllocId . ' was returned for correction.' . ($note !== '' ? (' Note: ' . $note) : ''), $pdo);
}
}
} catch (Throwable $e) {}
} catch (Throwable $e) {
$err = 'Failed to return for correction.';
try { if (!empty($startedTransaction) && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) { $pdo->rollBack(); } } catch (Throwable $x) {}
}
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => $ok, 'error' => $ok ? '' : $err]);
exit;
}
$_SESSION[$ok ? 'success_msg' : 'error_msg'] = $ok ? 'Returned for correction.' : $err;
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'save_billing' && $canManageApproval) {
$isAjax = (string)($_POST['xhr'] ?? '') === '1';
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Invalid allocation selected.']);
exit;
}
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$roleNormSave = strtolower((string)$role);
$ok = false;
$err = '';
try {
if (function_exists('ensureAllocationBillingTable')) { ensureAllocationBillingTable($pdo); }
$payload = [
'land_cost' => parseMoneyValue($_POST['land_cost'] ?? 0),
'infra_mode' => strtolower(trim((string)($_POST['infra_mode'] ?? 'percent'))),
'infra_percent' => parseMoneyValue($_POST['infra_percent'] ?? 0),
'infra_amount' => parseMoneyValue($_POST['infra_amount'] ?? 0),
'excavation_fee' => parseMoneyValue($_POST['excavation_fee'] ?? 0),
'construction_supervision' => parseMoneyValue($_POST['construction_supervision'] ?? 0),
'approval_fee' => parseMoneyValue($_POST['approval_fee'] ?? 0),
'application_form_fee' => parseMoneyValue($_POST['application_form_fee'] ?? 0),
'fence_gate_cost' => parseMoneyValue($_POST['fence_gate_cost'] ?? 0),
'carcass_cost' => parseMoneyValue($_POST['carcass_cost'] ?? 0),
'exterior_finishing_cost' => parseMoneyValue($_POST['exterior_finishing_cost'] ?? 0),
'dpc_cost' => parseMoneyValue($_POST['dpc_cost'] ?? 0),
'include_fence' => isset($_POST['include_fence']) ? 1 : 0,
'include_carcass' => isset($_POST['include_carcass']) ? 1 : 0,
'include_exterior_finishing' => isset($_POST['include_exterior_finishing']) ? 1 : 0,
'include_dpc' => isset($_POST['include_dpc']) ? 1 : 0,
'vat_percent' => parseMoneyValue($_POST['vat_percent'] ?? 0),
'payment_plan_months' => max(1, (int)($_POST['payment_plan_months'] ?? 3)),
];
if (!in_array($payload['infra_mode'], ['percent','fixed'], true)) { $payload['infra_mode'] = 'percent'; }
$cols = [
'allocation_id','company_id',
'land_cost','infra_mode','infra_percent','infra_amount',
'excavation_fee','construction_supervision','approval_fee','application_form_fee',
'fence_gate_cost','carcass_cost','exterior_finishing_cost','dpc_cost',
'include_fence','include_carcass','include_exterior_finishing','include_dpc',
'vat_percent','payment_plan_months','updated_by','updated_at'
];
$vals = [
$targetAllocId, $companyId,
$payload['land_cost'], $payload['infra_mode'], $payload['infra_percent'], $payload['infra_amount'],
$payload['excavation_fee'], $payload['construction_supervision'], $payload['approval_fee'], $payload['application_form_fee'],
$payload['fence_gate_cost'], $payload['carcass_cost'], $payload['exterior_finishing_cost'], $payload['dpc_cost'],
$payload['include_fence'], $payload['include_carcass'], $payload['include_exterior_finishing'], $payload['include_dpc'],
$payload['vat_percent'], $payload['payment_plan_months'],
$uid, date('Y-m-d H:i:s')
];
$placeholders = rtrim(str_repeat('?,', count($cols)), ',');
$updates = [];
foreach (array_slice($cols, 2) as $c) { $updates[] = $c . " = VALUES(" . $c . ")"; }
$sql = "INSERT INTO allocation_billing (" . implode(',', $cols) . ") VALUES ($placeholders) ON DUPLICATE KEY UPDATE " . implode(',', $updates);
$st = $pdo->prepare($sql);
$st->execute($vals);
$ok = true;
if ($isAdminOfficer && colx('allocations','status')) {
try {
$stCur = $pdo->prepare("SELECT status FROM allocations WHERE id = ? LIMIT 1");
$stCur->execute([$targetAllocId]);
$cur = strtolower(trim((string)($stCur->fetchColumn() ?: '')));
if ($cur === '' || in_array($cur, ['pending','draft_prepared','returned_for_correction'], true)) {
$pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?")->execute(['draft_prepared', $targetAllocId]);
}
} catch (Throwable $e) {}
}
try {
if (function_exists('computeAllocationBilling') && function_exists('allocationLetterDataUpsert')) {
$computed = computeAllocationBilling($payload);
$statusDraft = 'draft';
$versionNo = 1;
try {
if (function_exists('allocationLetterDataGet')) {
$existing = allocationLetterDataGet($pdo, (int)$targetAllocId);
$cur = strtolower((string)($existing['status'] ?? ''));
$versionNo = (int)($existing['version_no'] ?? 1);
if ($cur !== '' && !in_array($cur, ['draft','rejected','changes_requested'], true)) {
$statusDraft = $cur;
}
if (in_array($cur, ['chairman_approved','sent_to_client','viewed_by_client','accepted_by_client','completed'], true)) {
$versionNo = max(1, $versionNo) + 1;
}
}
} catch (Throwable $e) {}
allocationLetterDataUpsert($pdo, $targetAllocId, [
'status' => $statusDraft,
'version_no' => $versionNo,
'land_cost' => (float)($computed['land_cost'] ?? $payload['land_cost'] ?? 0),
'infrastructure' => (float)($computed['infrastructure_charge'] ?? 0),
'excavation' => (float)($computed['excavation_fee'] ?? 0),
'supervision' => (float)($computed['construction_supervision'] ?? 0),
'approval_fee' => (float)($computed['approval_fee'] ?? 0),
'vat' => (float)($computed['vat'] ?? 0),
'total' => (float)($computed['grand_total'] ?? 0),
]);
}
} catch (Throwable $e) {}
try {
$pdo->query("DESCRIBE audit_logs");
$cols = [];
$vals = [];
if (colx('audit_logs','entity_type')) { $cols[] = 'entity_type'; $vals[] = 'allocation_billing'; }
if (colx('audit_logs','entity_id')) { $cols[] = 'entity_id'; $vals[] = $targetAllocId; }
if (colx('audit_logs','action')) { $cols[] = 'action'; $vals[] = 'allocation_billing_update'; }
if (colx('audit_logs','reason')) { $cols[] = 'reason'; $vals[] = json_encode($payload); }
if (colx('audit_logs','details') && !in_array('reason', $cols, true)) { $cols[] = 'details'; $vals[] = json_encode($payload); }
if (colx('audit_logs','changed_by')) { $cols[] = 'changed_by'; $vals[] = $uid; }
if (colx('audit_logs','user_id') && !in_array('changed_by', $cols, true)) { $cols[] = 'user_id'; $vals[] = $uid; }
if (colx('audit_logs','ip_address')) { $cols[] = 'ip_address'; $vals[] = $_SERVER['REMOTE_ADDR'] ?? ''; }
if (colx('audit_logs','created_at')) { $cols[] = 'created_at'; $vals[] = date('Y-m-d H:i:s'); }
if (!empty($cols)) {
$ph = rtrim(str_repeat('?,', count($cols)), ',');
$pdo->prepare("INSERT INTO audit_logs (" . implode(',', $cols) . ") VALUES ($ph)")->execute($vals);
}
} catch (Throwable $e) {}
} catch (Throwable $e) {
$ok = false;
$err = 'Failed to save billing.';
}
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => $ok, 'error' => $err]);
exit;
}
if (!$ok) { $_SESSION['error_msg'] = $err; } else { $_SESSION['success_msg'] = 'Billing saved.'; }
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'save_letter_details' && $canManageApproval) {
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$passportUrl = trim((string)($_POST['passport_url'] ?? ''));
if (isset($_FILES['passport_file']) && (int)($_FILES['passport_file']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
$file = $_FILES['passport_file'];
$ext = strtolower(pathinfo((string)($file['name'] ?? ''), PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg','jpeg','png','webp'], true)) {
$_SESSION['error_msg'] = 'Invalid passport file type. Use JPG, PNG, or WEBP.';
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=letter_details&allocation_id=' . (int)$targetAllocId);
exit;
}
$dirRel = 'uploads/letters/passports';
$dirAbs = __DIR__ . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'letters' . DIRECTORY_SEPARATOR . 'passports';
if (!is_dir($dirAbs)) { @mkdir($dirAbs, 0777, true); }
$safe = 'alloc_' . (int)$targetAllocId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$destAbs = $dirAbs . DIRECTORY_SEPARATOR . $safe;
if (@move_uploaded_file((string)$file['tmp_name'], $destAbs)) {
if ($ext === 'webp' && function_exists('imagecreatefromwebp') && function_exists('imagejpeg')) {
$im = @imagecreatefromwebp($destAbs);
if ($im) {
$safeJpg = preg_replace('/\.webp$/i', '.jpg', $safe);
$destJpgAbs = $dirAbs . DIRECTORY_SEPARATOR . $safeJpg;
$okJpg = @imagejpeg($im, $destJpgAbs, 92);
if (function_exists('imagedestroy')) { @imagedestroy($im); }
if ($okJpg && is_file($destJpgAbs)) {
@unlink($destAbs);
$passportUrl = $dirRel . '/' . $safeJpg;
} else {
$passportUrl = $dirRel . '/' . $safe;
}
} else {
$passportUrl = $dirRel . '/' . $safe;
}
} else {
$passportUrl = $dirRel . '/' . $safe;
}
}
}
$data = [
'full_name' => trim((string)($_POST['full_name'] ?? '')),
'phone' => trim((string)($_POST['phone'] ?? '')),
'email' => trim((string)($_POST['email'] ?? '')),
'address' => (string)($_POST['address'] ?? ''),
'passport_url' => $passportUrl,
'estate_name' => trim((string)($_POST['estate_name'] ?? '')),
'property_name' => trim((string)($_POST['property_name'] ?? '')),
'plot_number' => trim((string)($_POST['plot_number'] ?? '')),
'sqm' => trim((string)($_POST['sqm'] ?? '')),
'property_type' => trim((string)($_POST['property_type'] ?? '')),
'house_type' => trim((string)($_POST['house_type'] ?? '')),
'allocation_date' => trim((string)($_POST['allocation_date'] ?? '')),
'file_number' => trim((string)($_POST['file_number'] ?? '')),
'reference_number' => trim((string)($_POST['reference_number'] ?? '')),
'prepared_by' => trim((string)($_POST['prepared_by'] ?? '')),
'notes' => trim((string)($_POST['notes'] ?? '')),
];
$ok = function_exists('allocationLetterSectionsSave') ? allocationLetterSectionsSave($pdo, $targetAllocId, $companyId, 'details', $data, $uid) : false;
try {
if ($ok) {
$sets = [];
$vals = [];
if (colx('allocations','plot_number')) { $sets[] = "plot_number = ?"; $vals[] = ($data['plot_number'] !== '' ? $data['plot_number'] : null); }
if (colx('allocations','plot_size')) { $sets[] = "plot_size = ?"; $vals[] = ($data['sqm'] !== '' ? $data['sqm'] : null); }
if (colx('allocations','building_use')) { $sets[] = "building_use = ?"; $vals[] = ($data['property_type'] !== '' ? $data['property_type'] : null); }
if (colx('allocations','house_type')) { $sets[] = "house_type = ?"; $vals[] = ($data['house_type'] !== '' ? $data['house_type'] : null); }
if (colx('allocations','allocation_date') && $data['allocation_date'] !== '') { $sets[] = "allocation_date = ?"; $vals[] = $data['allocation_date']; }
if (colx('allocations','building_number')) { $sets[] = "building_number = CASE WHEN (building_number IS NULL OR TRIM(building_number) = '') THEN ? ELSE building_number END"; $vals[] = ($data['plot_number'] !== '' ? $data['plot_number'] : null); }
if (colx('allocations','space_size')) { $sets[] = "space_size = CASE WHEN (space_size IS NULL OR TRIM(space_size) = '') THEN ? ELSE space_size END"; $vals[] = ($data['sqm'] !== '' ? $data['sqm'] : null); }
if (!empty($sets)) {
$sql = "UPDATE allocations SET " . implode(', ', $sets) . ", updated_at = NOW() WHERE id = ?";
$vals[] = (int)$targetAllocId;
$pdo->prepare($sql)->execute($vals);
}
}
} catch (Throwable $e) {}
try {
if (function_exists('allocationLetterDataUpsert')) {
$ctxLite = loadAllocationLetterContext($pdo, $targetAllocId, $companyId, $isSuperAdmin);
$clientId = (int)($ctxLite['client']['id'] ?? 0);
$statusDraft = 'draft';
$versionNo = 1;
try {
if (function_exists('allocationLetterDataGet')) {
$existing = allocationLetterDataGet($pdo, (int)$targetAllocId);
$cur = strtolower((string)($existing['status'] ?? ''));
$versionNo = (int)($existing['version_no'] ?? 1);
if ($cur !== '' && !in_array($cur, ['draft','rejected','changes_requested'], true)) {
$statusDraft = $cur;
}
if (in_array($cur, ['chairman_approved','sent_to_client','viewed_by_client','accepted_by_client','completed'], true)) {
$versionNo = max(1, $versionNo) + 1;
}
}
} catch (Throwable $e) {}
allocationLetterDataUpsert($pdo, $targetAllocId, [
'client_id' => $clientId > 0 ? $clientId : null,
'status' => $statusDraft,
'version_no' => $versionNo,
'phone' => $data['phone'] !== '' ? $data['phone'] : null,
'email' => $data['email'] !== '' ? $data['email'] : null,
'address' => $data['address'] !== '' ? $data['address'] : null,
'passport' => $passportUrl !== '' ? $passportUrl : null,
'plot_no' => $data['plot_number'] !== '' ? $data['plot_number'] : null,
'sqm' => $data['sqm'] !== '' ? $data['sqm'] : null,
'house_type' => $data['house_type'] !== '' ? $data['house_type'] : null,
'prepared_by' => $data['prepared_by'] !== '' ? $data['prepared_by'] : null,
'notes' => $data['notes'] !== '' ? $data['notes'] : null,
]);
}
} catch (Throwable $e) {}
if ($ok && $isAdminOfficer && colx('allocations','status')) {
try {
$stCur = $pdo->prepare("SELECT status FROM allocations WHERE id = ? LIMIT 1");
$stCur->execute([(int)$targetAllocId]);
$cur = strtolower(trim((string)($stCur->fetchColumn() ?: '')));
if ($cur === '' || in_array($cur, ['pending','draft_prepared','returned_for_correction'], true)) {
$pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?")->execute(['draft_prepared', (int)$targetAllocId]);
}
} catch (Throwable $e) {}
}
if ($ok) { $_SESSION['success_msg'] = 'Letter details saved.'; }
else { $_SESSION['error_msg'] = 'Failed to save letter details.'; }
$nextAction = strtolower(trim((string)($_POST['next_action'] ?? '')));
if ($ok && $nextAction === 'terms') {
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=terms&allocation_id=' . (int)$targetAllocId);
exit;
}
if ($ok && $nextAction === 'preview') {
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=letter_details&allocation_id=' . (int)$targetAllocId . '&open_preview=1');
exit;
}
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=letter_details&allocation_id=' . (int)$targetAllocId);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'save_terms' && $canManageApproval) {
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$infraMode = strtolower(trim((string)($_POST['infra_mode'] ?? 'percent')));
if (!in_array($infraMode, ['percent','fixed'], true)) { $infraMode = 'percent'; }
$infraPercent = (float)parseMoneyValue($_POST['infra_percent'] ?? 0);
$infraAmount = (float)parseMoneyValue($_POST['infra_amount'] ?? 0);
$vatPercent = (float)parseMoneyValue($_POST['vat_percent'] ?? 0);
$months = max(1, (int)($_POST['payment_plan_months'] ?? 3));
$refundCharge = (float)parseMoneyValue($_POST['refund_charge_percent'] ?? 0);
$ctx = loadAllocationLetterContext($pdo, $targetAllocId, $companyId, $isSuperAdmin);
$landCost = (float)parseMoneyValue($ctx['billing']['land_cost'] ?? 0);
if ($landCost <= 0) {
$landCost = (float)parseMoneyValue(($ctx['computed']['land_cost'] ?? 0));
}
if ($landCost <= 0) { $landCost = 0.0; }
try {
if (function_exists('ensureAllocationBillingTable')) { ensureAllocationBillingTable($pdo); }
$now = date('Y-m-d H:i:s');
$sql = "INSERT INTO allocation_billing (allocation_id, company_id, land_cost, infra_mode, infra_percent, infra_amount, vat_percent, payment_plan_months, updated_by, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
company_id = VALUES(company_id),
infra_mode = VALUES(infra_mode),
infra_percent = VALUES(infra_percent),
infra_amount = VALUES(infra_amount),
vat_percent = VALUES(vat_percent),
payment_plan_months = VALUES(payment_plan_months),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)";
$pdo->prepare($sql)->execute([(int)$targetAllocId, $companyId !== null ? (int)$companyId : null, $landCost, $infraMode, $infraPercent, $infraAmount, $vatPercent, $months, $uid, $now]);
} catch (Throwable $e) {}
$termsData = ['refund_charge_percent' => $refundCharge];
$ok = function_exists('allocationLetterSectionsSave') ? allocationLetterSectionsSave($pdo, $targetAllocId, $companyId, 'terms', $termsData, $uid) : false;
try {
if (function_exists('allocationLetterDataUpsert')) {
$statusDraft = 'draft';
$versionNo = 1;
try {
if (function_exists('allocationLetterDataGet')) {
$existing = allocationLetterDataGet($pdo, (int)$targetAllocId);
$cur = strtolower((string)($existing['status'] ?? ''));
$versionNo = (int)($existing['version_no'] ?? 1);
if ($cur !== '' && !in_array($cur, ['draft','rejected','changes_requested'], true)) {
$statusDraft = $cur;
}
if (in_array($cur, ['chairman_approved','sent_to_client','viewed_by_client','accepted_by_client','completed'], true)) {
$versionNo = max(1, $versionNo) + 1;
}
}
} catch (Throwable $e) {}
allocationLetterDataUpsert($pdo, (int)$targetAllocId, ['status' => $statusDraft, 'version_no' => $versionNo]);
}
} catch (Throwable $e) {}
if ($ok && $isAdminOfficer && colx('allocations','status')) {
try {
$stCur = $pdo->prepare("SELECT status FROM allocations WHERE id = ? LIMIT 1");
$stCur->execute([(int)$targetAllocId]);
$cur = strtolower(trim((string)($stCur->fetchColumn() ?: '')));
if ($cur === '' || in_array($cur, ['pending','draft_prepared','returned_for_correction'], true)) {
$pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?")->execute(['draft_prepared', (int)$targetAllocId]);
}
} catch (Throwable $e) {}
}
if ($ok) { $_SESSION['success_msg'] = 'Terms saved.'; }
else { $_SESSION['error_msg'] = 'Failed to save terms.'; }
$nextAction = strtolower(trim((string)($_POST['next_action'] ?? '')));
if ($ok && $nextAction === 'guidelines') {
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=guidelines&allocation_id=' . (int)$targetAllocId);
exit;
}
if ($ok && $nextAction === 'preview') {
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=terms&allocation_id=' . (int)$targetAllocId . '&open_preview=1');
exit;
}
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=terms&allocation_id=' . (int)$targetAllocId);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'save_guidelines' && $canManageApproval) {
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$guidelinesData = [
'notes' => trim((string)($_POST['guidelines_notes'] ?? '')),
];
$ok = function_exists('allocationLetterSectionsSave') ? allocationLetterSectionsSave($pdo, $targetAllocId, $companyId, 'guidelines', $guidelinesData, $uid) : false;
try {
if (function_exists('allocationLetterDataUpsert')) {
$statusDraft = 'draft';
$versionNo = 1;
try {
if (function_exists('allocationLetterDataGet')) {
$existing = allocationLetterDataGet($pdo, (int)$targetAllocId);
$cur = strtolower((string)($existing['status'] ?? ''));
$versionNo = (int)($existing['version_no'] ?? 1);
if ($cur !== '' && !in_array($cur, ['draft','rejected','changes_requested'], true)) {
$statusDraft = $cur;
}
if (in_array($cur, ['chairman_approved','sent_to_client','viewed_by_client','accepted_by_client','completed'], true)) {
$versionNo = max(1, $versionNo) + 1;
}
}
} catch (Throwable $e) {}
allocationLetterDataUpsert($pdo, (int)$targetAllocId, ['status' => $statusDraft, 'version_no' => $versionNo]);
}
} catch (Throwable $e) {}
if ($ok && $isAdminOfficer && colx('allocations','status')) {
try {
$stCur = $pdo->prepare("SELECT status FROM allocations WHERE id = ? LIMIT 1");
$stCur->execute([(int)$targetAllocId]);
$cur = strtolower(trim((string)($stCur->fetchColumn() ?: '')));
if ($cur === '' || in_array($cur, ['pending','draft_prepared','returned_for_correction'], true)) {
$pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?")->execute(['draft_prepared', (int)$targetAllocId]);
}
} catch (Throwable $e) {}
}
if ($ok) { $_SESSION['success_msg'] = 'Building guidelines saved.'; }
else { $_SESSION['error_msg'] = 'Failed to save building guidelines.'; }
$nextAction = strtolower(trim((string)($_POST['next_action'] ?? '')));
if ($ok && $nextAction === 'preview') {
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=guidelines&allocation_id=' . (int)$targetAllocId . '&open_preview=1');
exit;
}
header('Location: allocation-letters.php?view=' . urlencode($viewMode) . '&action=guidelines&allocation_id=' . (int)$targetAllocId);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'save_draft' && $canManageApproval) {
$isAjax = (string)($_POST['xhr'] ?? '') === '1';
$targetAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($targetAllocId <= 0) {
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Invalid allocation selected.']);
exit;
}
$_SESSION['error_msg'] = 'Invalid allocation selected.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$ok = false;
try {
if (function_exists('allocationLetterDataUpsert')) {
$existing = function_exists('allocationLetterDataGet') ? allocationLetterDataGet($pdo, (int)$targetAllocId) : [];
$cur = strtolower((string)($existing['status'] ?? ''));
$status = ($cur !== '' && !in_array($cur, ['draft','rejected','changes_requested'], true)) ? $cur : 'draft';
$ok = allocationLetterDataUpsert($pdo, (int)$targetAllocId, ['status' => $status]);
}
} catch (Throwable $e) { $ok = false; }
if ($ok && $isAdminOfficer && colx('allocations','status')) {
try {
$stCur = $pdo->prepare("SELECT status FROM allocations WHERE id = ? LIMIT 1");
$stCur->execute([(int)$targetAllocId]);
$cur2 = strtolower(trim((string)($stCur->fetchColumn() ?: '')));
if ($cur2 === '' || in_array($cur2, ['pending','draft_prepared','returned_for_correction'], true)) {
$pdo->prepare("UPDATE allocations SET status = ?, updated_at = NOW() WHERE id = ?")->execute(['draft_prepared', (int)$targetAllocId]);
}
} catch (Throwable $e) {}
}
if ($isAjax) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => $ok]);
exit;
}
$_SESSION[$ok ? 'success_msg' : 'error_msg'] = $ok ? 'Draft saved.' : 'Failed to save draft.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'save_acknowledgement' && $canManageApproval) {
$_SESSION['error_msg'] = 'Client acknowledgement is completed only in the Client Portal.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['form_action'] ?? '') === 'release_to_client') {
if (!$canReleaseToClient) {
$_SESSION['error_msg'] = 'Permission denied.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
$releaseAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
if ($releaseAllocId <= 0) {
$_SESSION['error_msg'] = 'Invalid allocation selected for release.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
try {
$startedTransaction = method_exists($pdo, 'inTransaction') ? !$pdo->inTransaction() : true;
if ($startedTransaction) {
$pdo->beginTransaction();
}
$st = $pdo->prepare("SELECT status, user_id FROM allocations WHERE id = ?" . ((!$isSuperAdmin && $companyId && colx('allocations','company_id')) ? " AND company_id = ?" : "") . " LIMIT 1");
$paramsSt = [$releaseAllocId];
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) { $paramsSt[] = $companyId; }
$st->execute($paramsSt);
$allocRow = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$curStatus = strtolower((string)($allocRow['status'] ?? ''));
$allocUserId = (int)($allocRow['user_id'] ?? 0);
if (!in_array($curStatus, ['executive_approved', 'approved'], true)) {
throw new RuntimeException('Allocation must be executive approved before releasing to client.');
}
try {
if (class_exists('DocGenerator')) {
$gen = new DocGenerator($pdo);
$gen->generateAllocationLetter($releaseAllocId, $uid);
}
} catch (Throwable $e) {}
$hasDocuments = false;
try { $hasDocuments = $pdo->query("SHOW TABLES LIKE 'documents'")->rowCount() > 0; } catch (Throwable $e) {}
if ($hasDocuments && colx('documents','status')) {
$releasedToken = pickColumnToken($pdo, 'documents', 'status', ['released', 'issued', 'signed', 'approved']);
$set = ["status = ?"];
$params = [$releasedToken];
if (colx('documents','updated_at')) {
$set[] = "updated_at = ?";
$params[] = date('Y-m-d H:i:s');
}
if (colx('documents','released_at')) {
$set[] = "released_at = ?";
$params[] = date('Y-m-d H:i:s');
}
$sqlDocUpdate = "UPDATE documents SET " . implode(', ', $set) . " WHERE 1=1";
if (colx('documents','type')) { $sqlDocUpdate .= " AND type = ?"; $params[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$sqlDocUpdate .= " AND allocation_id = ?";
$params[] = $releaseAllocId;
} else {
$sqlDocUpdate .= " AND file_path LIKE ?";
$params[] = '%Allocation_Letter_' . $releaseAllocId . '_%';
}
if ($allocUserId > 0 && colx('documents','user_id')) {
$sqlDocUpdate .= " AND user_id = ?";
$params[] = $allocUserId;
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) {
$sqlDocUpdate .= " AND company_id = ?";
$params[] = $companyId;
}
$updDoc = $pdo->prepare($sqlDocUpdate);
$updDoc->execute($params);
}
if (colx('allocations','status')) {
$allocationDoneStatus = 'released_to_client';
$setAlloc = ["status = ?"];
$paramsAlloc = [$allocationDoneStatus];
if (colx('allocations','updated_at')) {
$setAlloc[] = "updated_at = ?";
$paramsAlloc[] = date('Y-m-d H:i:s');
}
if (colx('allocations','letter_issued_at')) {
$setAlloc[] = "letter_issued_at = ?";
$paramsAlloc[] = date('Y-m-d H:i:s');
}
$sqlAlloc = "UPDATE allocations SET " . implode(', ', $setAlloc) . " WHERE id = ?";
$paramsAlloc[] = $releaseAllocId;
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) {
$sqlAlloc .= " AND company_id = ?";
$paramsAlloc[] = $companyId;
}
$updAlloc = $pdo->prepare($sqlAlloc);
$updAlloc->execute($paramsAlloc);
}
try {
if (function_exists('allocationLetterDataUpsert')) {
allocationLetterDataUpsert($pdo, (int)$releaseAllocId, [
'status' => 'sent_to_client',
'sent_to_client_at' => date('Y-m-d H:i:s'),
]);
}
} catch (Throwable $e) {}
if ($startedTransaction && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) {
$pdo->commit();
}
if (function_exists('logActivity') && $uid > 0) {
try {
logActivity($uid, 'ALLOCATION_LETTER_RELEASED_TO_CLIENT', json_encode(['allocation_id' => $releaseAllocId, 'company_id' => $companyId]));
} catch (Throwable $e) {}
}
try {
if ($allocUserId > 0) {
$mailer = __DIR__ . '/includes/mailer.php';
if (is_file($mailer)) { require_once $mailer; }
if (function_exists('sendNotification')) {
sendNotification($allocUserId, 'allocation_letter_released', 'Your allocation letter has been released to your portal and is now available for download.', $pdo);
}
}
} catch (Throwable $e) {}
$_SESSION['success_msg'] = 'Allocation letter released to the client dashboard.';
} catch (Throwable $e) {
try {
if (!empty($startedTransaction) && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) {
$pdo->rollBack();
}
} catch (Throwable $rollbackError) {}
$_SESSION['error_msg'] = 'Failed to release allocation letter to the client dashboard.';
}
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['exec_action']) && $canReviewExecutiveQueue) {
$decisionAllocId = isset($_POST['allocation_id']) ? (int)$_POST['allocation_id'] : 0;
$decision = strtolower(trim((string)($_POST['decision'] ?? '')));
$comment = trim((string)($_POST['comment'] ?? ''));
if ($decisionAllocId <= 0 || !in_array($decision, ['approve','reject','request_changes'], true)) {
$_SESSION['error_msg'] = 'Invalid executive decision request.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
if ($comment === '') {
$_SESSION['error_msg'] = 'Comment is required.';
header('Location: ' . $allocationLettersBaseHref);
exit;
}
try {
$startedTransaction = method_exists($pdo, 'inTransaction') ? !$pdo->inTransaction() : true;
if ($startedTransaction) {
$pdo->beginTransaction();
}
$st = $pdo->prepare("SELECT status, user_id FROM allocations WHERE id = ?" . ((!$isSuperAdmin && $companyId && colx('allocations','company_id')) ? " AND company_id = ?" : "") . " LIMIT 1");
$paramsSt = [$decisionAllocId];
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) { $paramsSt[] = $companyId; }
$st->execute($paramsSt);
$allocRow = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$curStatus = strtolower((string)($allocRow['status'] ?? ''));
if (!in_array($curStatus, ['pending_executive_approval'], true)) {
throw new RuntimeException('Allocation is not awaiting executive review.');
}
$allocUserId = (int)($allocRow['user_id'] ?? 0);
if ($decision === 'approve') {
$newStatus = 'executive_approved';
} elseif ($decision === 'request_changes') {
$newStatus = 'returned_for_correction';
} else {
$newStatus = pickColumnToken($pdo, 'allocations', 'status', ['rejected', 'declined', 'revoked']);
}
$setAlloc = ["status = ?"];
$paramsAlloc = [$newStatus];
if (colx('allocations','updated_at')) {
$setAlloc[] = "updated_at = ?";
$paramsAlloc[] = date('Y-m-d H:i:s');
}
if (colx('allocations','exec_comment')) {
$setAlloc[] = "exec_comment = ?";
$paramsAlloc[] = $comment;
}
if (colx('allocations','exec_decided_by')) {
$setAlloc[] = "exec_decided_by = ?";
$paramsAlloc[] = $uid;
}
if (colx('allocations','exec_decided_at')) {
$setAlloc[] = "exec_decided_at = ?";
$paramsAlloc[] = date('Y-m-d H:i:s');
}
$sqlAlloc = "UPDATE allocations SET " . implode(', ', $setAlloc) . " WHERE id = ?";
$paramsAlloc[] = $decisionAllocId;
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) {
$sqlAlloc .= " AND company_id = ?";
$paramsAlloc[] = $companyId;
}
$updAlloc = $pdo->prepare($sqlAlloc);
$updAlloc->execute($paramsAlloc);
$hasDocuments = false;
try { $hasDocuments = $pdo->query("SHOW TABLES LIKE 'documents'")->rowCount() > 0; } catch (Throwable $e) {}
if ($hasDocuments && colx('documents','status')) {
$docStatus = $decision === 'approve'
? pickColumnToken($pdo, 'documents', 'status', ['approved', 'issued', 'signed', 'pending'])
: ($decision === 'request_changes'
? pickColumnToken($pdo, 'documents', 'status', ['draft', 'pending'])
: pickColumnToken($pdo, 'documents', 'status', ['rejected', 'draft', 'pending']));
$set = ["status = ?"];
$params = [$docStatus];
if (colx('documents','updated_at')) {
$set[] = "updated_at = ?";
$params[] = date('Y-m-d H:i:s');
}
if (colx('documents','review_comment')) {
$set[] = "review_comment = ?";
$params[] = $comment;
}
if (colx('documents','reviewed_by')) {
$set[] = "reviewed_by = ?";
$params[] = $uid;
}
if (colx('documents','reviewed_at')) {
$set[] = "reviewed_at = ?";
$params[] = date('Y-m-d H:i:s');
}
$sqlDocUpdate = "UPDATE documents SET " . implode(', ', $set) . " WHERE 1=1";
if (colx('documents','type')) { $sqlDocUpdate .= " AND type = ?"; $params[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$sqlDocUpdate .= " AND allocation_id = ?";
$params[] = $decisionAllocId;
} else {
$sqlDocUpdate .= " AND file_path LIKE ?";
$params[] = '%Allocation_Letter_' . $decisionAllocId . '_%';
}
if ($allocUserId > 0 && colx('documents','user_id')) {
$sqlDocUpdate .= " AND user_id = ?";
$params[] = $allocUserId;
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) {
$sqlDocUpdate .= " AND company_id = ?";
$params[] = $companyId;
}
$updDoc = $pdo->prepare($sqlDocUpdate);
$updDoc->execute($params);
}
try {
if (function_exists('allocationLetterDataUpsert')) {
$status = $decision === 'approve' ? 'chairman_approved' : ($decision === 'request_changes' ? 'changes_requested' : 'rejected');
allocationLetterDataUpsert($pdo, (int)$decisionAllocId, [
'status' => $status,
'chairman_decision' => $decision,
'chairman_comment' => $comment,
'chairman_decided_by' => $uid > 0 ? $uid : null,
'chairman_decided_at' => date('Y-m-d H:i:s'),
]);
}
} catch (Throwable $e) {}
if ($startedTransaction && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) {
$pdo->commit();
}
if (function_exists('logActivity') && $uid > 0) {
try {
logActivity($uid, 'EXEC_ALLOCATION_LETTER_DECISION', json_encode(['allocation_id' => $decisionAllocId, 'decision' => $decision, 'company_id' => $companyId]));
} catch (Throwable $e) {}
}
$_SESSION['success_msg'] = $decision === 'approve' ? 'Allocation letter approved.' : ($decision === 'request_changes' ? 'Changes requested for this allocation letter.' : 'Allocation letter rejected.');
try {
if (function_exists('ap_user_ids_by_roles') && function_exists('sendNotification')) {
$ids = ap_user_ids_by_roles($pdo, ['head_admin','admin'], $companyId ? (int)$companyId : null);
foreach ($ids as $hid) {
sendNotification((int)$hid, 'allocation_letter_exec_decision', 'Executive decision for Allocation #' . (int)$decisionAllocId . ': ' . strtoupper($decision) . '.', $pdo);
}
}
} catch (Throwable $e) {}
} catch (Throwable $e) {
try {
if (!empty($startedTransaction) && method_exists($pdo, 'inTransaction') && $pdo->inTransaction()) {
$pdo->rollBack();
}
} catch (Throwable $rollbackError) {}
$_SESSION['error_msg'] = 'Failed to record executive decision.';
}
header('Location: ' . $allocationLettersBaseHref . '&status=' . urlencode($decision === 'approve' ? 'approved' : 'all') . '&allocation_id=' . (int)$decisionAllocId);
exit;
}
if ($action === 'offer_docx' && $allocId > 0) {
$user = [];
$formData = [];
$allocation = [];
$property = [];
$nm = $email = $phone = $addr = '';
$code = ''; $size = ''; $purpose = ''; $preferredProperty = ''; $offeredAmount = 0.0;
$orgClient = false;
try {
$st = $pdo->prepare("SELECT * FROM allocations WHERE id = ?" . ((!$isSuperAdmin && $companyId && colx('allocations','company_id')) ? " AND company_id = ?" : ""));
$params = [$allocId];
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) { $params[] = $companyId; }
$st->execute($params);
$allocation = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$uid2 = (int)($allocation['user_id'] ?? 0);
$pid2 = (int)($allocation['property_id'] ?? 0);
if ($uid2 > 0) {
$su = $pdo->prepare("SELECT * FROM users WHERE id = ? LIMIT 1");
$su->execute([$uid2]);
$user = $su->fetch(PDO::FETCH_ASSOC) ?: [];
$sf = $pdo->prepare("SELECT form_data FROM client_forms WHERE client_id = ? ORDER BY updated_at DESC, created_at DESC LIMIT 1");
try { $sf->execute([$uid2]); } catch (Throwable $eFD) {}
$row = $sf->fetch(PDO::FETCH_ASSOC) ?: [];
if (!empty($row['form_data'])) {
$tmp = json_decode($row['form_data'], true);
if (is_array($tmp)) { $formData = $tmp; }
}
$nm = $user['name'] ?? ($user['full_name'] ?? (($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')));
$email = $user['email'] ?? ($formData['email'] ?? '');
$phone = $user['phone'] ?? ($formData['phone'] ?? '');
$addr = $user['address'] ?? ($formData['address'] ?? ($formData['residential_address'] ?? ''));
$orgClient = !empty($formData['company_name'] ?? '');
}
if ($pid2 > 0) {
$sp = $pdo->prepare("SELECT * FROM properties WHERE id = ? LIMIT 1");
$sp->execute([$pid2]);
$property = $sp->fetch(PDO::FETCH_ASSOC) ?: [];
$code = $property['code'] ?? ($property['plot_code'] ?? (string)($property['id'] ?? ''));
$size = (string)($property['plot_size'] ?? ($property['area_sqm'] ?? ''));
$purpose = (string)($property['purpose'] ?? ($formData['purpose'] ?? 'RESIDENTIAL'));
$preferredProperty = (string)($formData['preferred_property'] ?? ($property['type'] ?? ''));
}
// Compute offered/total amount from form or deals/payments
if (!empty($formData['offered_amount'])) {
$offeredAmount = (float)preg_replace('/[^\d.]/', '', (string)$formData['offered_amount']);
}
if ($offeredAmount <= 0) {
try {
if (colx('deals','allocation_id')) {
$valCol = colx('deals','final_value') ? 'final_value' : (colx('deals','deal_value') ? 'deal_value' : (colx('deals','value') ? 'value' : null));
if ($valCol) {
$qd = $pdo->prepare("SELECT {$valCol} AS v FROM deals WHERE allocation_id = ? ORDER BY id DESC LIMIT 1");
$qd->execute([$allocId]); $offeredAmount = (float)($qd->fetchColumn() ?: 0.0);
}
}
} catch (Throwable $eD) {}
}
if ($offeredAmount <= 0 && colx('payments','allocation_id')) {
try {
$cmpClause = ''; $cmpParams = [];
if ($companyId && colx('payments','company_id')) { $cmpClause = " AND company_id = ?"; $cmpParams[] = $companyId; }
$qp = $pdo->prepare("SELECT COALESCE(SUM(amount),0) FROM payments WHERE allocation_id = ? AND status IN ('approved','paid')" . $cmpClause);
$qp->execute(array_merge([$allocId], $cmpParams)); $offeredAmount = (float)($qp->fetchColumn() ?: 0.0);
} catch (Throwable $eP) {}
}
} catch (Throwable $e) {}
// Determine template: organization vs individual
$tplSettingKey = $orgClient ? 'offer_letter_org_template_url' : 'offer_letter_ind_template_url';
$tplUrl = function_exists('getSetting') ? (getSetting($tplSettingKey, '') ?: '') : '';
// Fallback to user-provided examples if not configured
if ($tplUrl === '') {
$tplUrl = $orgClient
? 'https://aibenproperties.com/wp-content/uploads/2026/03/DTERRANOVA-CITY-LIMITED-PROVISIONAL-OFFER-LETTER-15-HECTARES-N.docx'
: 'https://aibenproperties.com/wp-content/uploads/2026/03/AGHOGHO-OKEKE-OFFER-LETTER-250sqm-N.docx';
}
$lettersDir = __DIR__ . '/uploads/letters';
if (!is_dir($lettersDir)) { @mkdir($lettersDir, 0777, true); }
$tplLocal = $lettersDir . ($orgClient ? '/offer_org_template.docx' : '/offer_ind_template.docx');
// Ensure local template exists
if (!file_exists($tplLocal)) {
if (stripos($tplUrl, 'http') === 0) {
$bin = @file_get_contents($tplUrl);
if ($bin !== false) { @file_put_contents($tplLocal, $bin); }
} else {
$src = __DIR__ . '/' . ltrim($tplUrl, '/');
if (file_exists($src)) { @copy($src, $tplLocal); }
}
// Inject placeholders as in letter-templates.php
try {
$zip = new ZipArchive();
if (file_exists($tplLocal) && $zip->open($tplLocal) === true) {
$xml = $zip->getFromName('word/document.xml');
if ($xml !== false) {
if ($orgClient) {
$xml = str_replace('DTERRANOVA CITY LIMITED', '{{COMPANY_NAME}}', $xml);
$xml = str_replace('DTERRANOVA', '{{COMPANY_NAME}}', $xml);
$xml = preg_replace('/(?:₦|NGN)?\s*\d[\d,]*(?:\.\d+)?/u', '{{OFFERED_AMOUNT}}', $xml, 1);
$xml = preg_replace('/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s+\d{4}\b/u', '{{DATE}}', $xml, 1);
$xml = preg_replace('/Name:\s*[^<\n]*/i', 'Name: {{CLIENT_NAME}}', $xml);
$xml = preg_replace('/Phone:\s*[^<\n]*/i', 'Phone: {{PHONE}}', $xml);
$xml = preg_replace('/Email:\s*[^<\n]*/i', 'Email: {{EMAIL}}', $xml);
$xml = preg_replace('/Address:\s*[^<\n]*/i', 'Address: {{CLIENT_ADDRESS}}', $xml);
$xml = preg_replace('/PLOT NO:\s*[^<\n]*/i', 'PLOT NO: {{PLOT_NO}}', $xml);
$xml = preg_replace('/PLOT SIZE:\s*[^<\n]*/i', 'PLOT SIZE: {{PLOT_SIZE}}', $xml);
$xml = preg_replace('/PURPOSE:\s*[^<\n]*/i', 'PURPOSE: {{PURPOSE}}', $xml);
$xml = preg_replace('/BUILDING TYPE:\s*[^<\n]*/i', 'BUILDING TYPE: {{BUILDING_TYPE}}', $xml);
$xml = preg_replace('/AMOUNT:\s*[^<\n]*/i', 'AMOUNT: {{AMOUNT}}', $xml);
} else {
$xml = str_replace('AGHOGHO OKEKE', '{{CLIENT_NAME}}', $xml);
$xml = str_replace('0803 438 0669', '{{PHONE}}', $xml);
$xml = str_replace('aghogho.bobokonedo@gmail.com', '{{EMAIL}}', $xml);
$xml = str_replace('B20B Chessville Corte Estate, Plot 65 Kafe District, Life Camp, FCT Abuja.', '{{CLIENT_ADDRESS}}', $xml);
$xml = str_replace('HUTU Prestige Recreational Resort Estate', '{{ESTATE_NAME}}', $xml);
$xml = str_replace('Cadastral Zone E09, Lugbe South District, Abuja-FCT', '{{ESTATE_LOCATION}}', $xml);
$xml = str_replace('NOT READY', '{{PLOT_NO}}', $xml);
$xml = str_replace('250SQM', '{{PLOT_SIZE}}', $xml);
$xml = str_replace('RESIDENTIAL', '{{PURPOSE}}', $xml);
$xml = str_replace('4 BEDROOMS TERRACE DUPLEX WITH BQ', '{{BUILDING_TYPE}}', $xml);
$xml = preg_replace('/(?:₦|NGN)?\s*\d[\d,]*(?:\.\d+)?/u', '{{OFFERED_AMOUNT}}', $xml, 1);
$xml = preg_replace('/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s+\d{4}\b/u', '{{DATE}}', $xml, 1);
$xml = preg_replace('/Name:\s*[^<\n]*/i', 'Name: {{CLIENT_NAME}}', $xml);
$xml = preg_replace('/Phone:\s*[^<\n]*/i', 'Phone: {{PHONE}}', $xml);
$xml = preg_replace('/Email:\s*[^<\n]*/i', 'Email: {{EMAIL}}', $xml);
$xml = preg_replace('/Address:\s*[^<\n]*/i', 'Address: {{CLIENT_ADDRESS}}', $xml);
$xml = preg_replace('/PLOT NO:\s*[^<\n]*/i', 'PLOT NO: {{PLOT_NO}}', $xml);
$xml = preg_replace('/PLOT SIZE:\s*[^<\n]*/i', 'PLOT SIZE: {{PLOT_SIZE}}', $xml);
$xml = preg_replace('/PURPOSE:\s*[^<\n]*/i', 'PURPOSE: {{PURPOSE}}', $xml);
$xml = preg_replace('/BUILDING TYPE:\s*[^<\n]*/i', 'BUILDING TYPE: {{BUILDING_TYPE}}', $xml);
$xml = preg_replace('/AMOUNT:\s*[^<\n]*/i', 'AMOUNT: {{AMOUNT}}', $xml);
}
$zip->addFromString('word/document.xml', $xml);
}
$zip->close();
}
} catch (Throwable $eX) {}
}
// Prepare output docx by replacing placeholders with real values
$companyName = function_exists('getSetting') ? (getSetting('company_name', 'Aiben Properties') ?: 'Aiben Properties') : 'Aiben Properties';
$estateName = (string)($property['title'] ?? ($property['name'] ?? ''));
$estateLocation = (string)($property['address'] ?? ($property['location'] ?? ''));
$repls = [
'{{COMPANY_NAME}}' => $companyName,
'{{CLIENT_NAME}}' => trim($nm),
'{{PHONE}}' => $phone,
'{{EMAIL}}' => $email,
'{{CLIENT_ADDRESS}}' => $addr,
'{{ESTATE_NAME}}' => $estateName,
'{{ESTATE_LOCATION}}' => $estateLocation,
'{{PLOT_NO}}' => $code,
'{{PLOT_SIZE}}' => $size,
'{{PURPOSE}}' => $purpose,
'{{BUILDING_TYPE}}' => $preferredProperty,
'{{AMOUNT}}' => number_format((float)$offeredAmount, 2),
'{{OFFERED_AMOUNT}}' => number_format((float)$offeredAmount, 2),
'{{DATE}}' => date('F j, Y'),
];
$outDir = __DIR__ . '/uploads/documents/generated';
if (!is_dir($outDir)) { @mkdir($outDir, 0777, true); }
$outPath = $outDir . '/Offer_Letter_' . (int)$allocId . '_' . time() . '.docx';
@copy($tplLocal, $outPath);
$ok = false;
try {
$zip = new ZipArchive();
if ($zip->open($outPath) === true) {
$xml = $zip->getFromName('word/document.xml');
if ($xml !== false) {
$xml = strtr($xml, $repls);
$zip->addFromString('word/document.xml', $xml);
$ok = true;
}
$zip->close();
}
} catch (Throwable $eZ) {}
if ($ok) {
$rel = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', $outPath);
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
header('Content-Disposition: inline; filename="' . basename($outPath) . '"');
header('Content-Length: ' . filesize($outPath));
readfile($outPath);
exit;
} else {
header('Content-Type: text/plain; charset=UTF-8');
http_response_code(500);
echo "Failed to generate Offer Letter DOCX.";
exit;
}
}
if ($action === 'preview' && $allocId > 0) {
$force = isset($_GET['force']) && (string)$_GET['force'] === '1';
$download = isset($_GET['download']) && (string)$_GET['download'] === '1';
$previewMissingNice = [];
try {
if ($canManageApproval && function_exists('allocationLetterDataGet')) {
$ld = allocationLetterDataGet($pdo, (int)$allocId);
$missing = [];
if (trim((string)($ld['phone'] ?? '')) === '') { $missing[] = 'phone'; }
if (trim((string)($ld['email'] ?? '')) === '') { $missing[] = 'email'; }
if (trim((string)($ld['address'] ?? '')) === '') { $missing[] = 'address'; }
if (trim((string)($ld['plot_no'] ?? '')) === '') { $missing[] = 'plot_no'; }
if (trim((string)($ld['sqm'] ?? '')) === '') { $missing[] = 'sqm'; }
if (trim((string)($ld['house_type'] ?? '')) === '') { $missing[] = 'house_type'; }
if (trim((string)($ld['prepared_by'] ?? '')) === '') { $missing[] = 'prepared_by'; }
if (!empty($missing) && function_exists('allocationLetterDataUpsert')) {
$autoPatched = false;
$ctxAuto = loadAllocationLetterContext($pdo, (int)$allocId, $companyId, $isSuperAdmin);
$preparedByFallback = trim((string)($_SESSION['user_name'] ?? $_SESSION['name'] ?? $_SESSION['full_name'] ?? ''));
if ($preparedByFallback === '' && $uid > 0) {
try {
$stU = $pdo->prepare("SELECT name, full_name, first_name, last_name FROM users WHERE id = ? LIMIT 1");
$stU->execute([$uid]);
$urow = $stU->fetch(PDO::FETCH_ASSOC) ?: [];
$preparedByFallback = trim((string)($urow['name'] ?? $urow['full_name'] ?? (($urow['first_name'] ?? '') . ' ' . ($urow['last_name'] ?? ''))));
} catch (Throwable $e) {}
}
$cAuto = is_array($ctxAuto['client'] ?? null) ? $ctxAuto['client'] : [];
$pAuto = is_array($ctxAuto['property'] ?? null) ? $ctxAuto['property'] : [];
$mAuto = is_array($ctxAuto['meta'] ?? null) ? $ctxAuto['meta'] : [];
$candidates = [
'phone' => trim((string)($cAuto['phone'] ?? '')),
'email' => trim((string)($cAuto['email'] ?? '')),
'address' => trim((string)($cAuto['address'] ?? '')),
'plot_no' => trim((string)($pAuto['plot_number'] ?? '')),
'sqm' => trim((string)($pAuto['sqm'] ?? '')),
'house_type' => trim((string)($pAuto['house_type'] ?? '')),
'prepared_by' => trim((string)($mAuto['prepared_by'] ?? $preparedByFallback)),
];
$patch = [];
foreach ($missing as $k) {
if (($candidates[$k] ?? '') !== '') {
$patch[$k] = $candidates[$k];
}
}
if (!empty($patch)) {
allocationLetterDataUpsert($pdo, (int)$allocId, $patch);
$autoPatched = true;
$ld = allocationLetterDataGet($pdo, (int)$allocId);
$missing = [];
if (trim((string)($ld['phone'] ?? '')) === '') { $missing[] = 'phone'; }
if (trim((string)($ld['email'] ?? '')) === '') { $missing[] = 'email'; }
if (trim((string)($ld['address'] ?? '')) === '') { $missing[] = 'address'; }
if (trim((string)($ld['plot_no'] ?? '')) === '') { $missing[] = 'plot_no'; }
if (trim((string)($ld['sqm'] ?? '')) === '') { $missing[] = 'sqm'; }
if (trim((string)($ld['house_type'] ?? '')) === '') { $missing[] = 'house_type'; }
if (trim((string)($ld['prepared_by'] ?? '')) === '') { $missing[] = 'prepared_by'; }
}
if ($autoPatched) { $force = true; }
}
if (!empty($missing)) {
$labels = [
'phone' => 'Phone',
'email' => 'Email',
'address' => 'Address',
'plot_no' => 'Plot No',
'sqm' => 'SQM',
'house_type' => 'House Type',
'prepared_by' => 'Prepared By',
];
$nice = [];
foreach ($missing as $k) { $nice[] = $labels[$k] ?? $k; }
$previewMissingNice = $nice;
}
}
} catch (Throwable $e) {}
$previewDocPath = '';
$previewDocId = 0;
$previewPublicId = '';
$generatedDir = __DIR__ . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'documents' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR;
$generatedPattern = $generatedDir . 'Allocation_Letter_' . $allocId . '_*';
try {
$hasDocuments = $pdo->query("SHOW TABLES LIKE 'documents'")->rowCount() > 0;
if ($hasDocuments) {
$sqlPreviewDoc = "SELECT id, file_path" . (colx('documents','public_id') ? ", public_id" : "") . " FROM documents WHERE 1=1";
$paramsPreviewDoc = [];
if (colx('documents','type')) { $sqlPreviewDoc .= " AND type = ?"; $paramsPreviewDoc[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$sqlPreviewDoc .= " AND allocation_id = ?";
$paramsPreviewDoc[] = $allocId;
} else {
$sqlPreviewDoc .= " AND file_path LIKE ?";
$paramsPreviewDoc[] = '%Allocation_Letter_' . $allocId . '_%';
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) {
$sqlPreviewDoc .= " AND company_id = ?";
$paramsPreviewDoc[] = $companyId;
}
$sqlPreviewDoc .= " ORDER BY id DESC LIMIT 1";
$stPreviewDoc = $pdo->prepare($sqlPreviewDoc);
$stPreviewDoc->execute($paramsPreviewDoc);
$rowPreviewDoc = $stPreviewDoc->fetch(PDO::FETCH_ASSOC) ?: [];
$previewDocId = (int)($rowPreviewDoc['id'] ?? 0);
$previewDocPath = (string)($rowPreviewDoc['file_path'] ?? '');
$previewPublicId = (string)($rowPreviewDoc['public_id'] ?? '');
}
} catch (Throwable $e) {}
try {
$generatedMatches = glob($generatedPattern) ?: [];
if (!empty($generatedMatches)) {
usort($generatedMatches, static function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
foreach ($generatedMatches as $matchPath) {
if (preg_match('/\.html?$/i', $matchPath)) {
$previewDocPath = $matchPath;
break;
}
}
if ($previewDocPath === '' && !empty($generatedMatches[0])) {
$previewDocPath = (string)$generatedMatches[0];
}
}
} catch (Throwable $e) {}
$billingUpdatedAt = '';
try {
if (function_exists('tableHasColumn') && tableHasColumn('allocation_billing', 'updated_at')) {
$stBu = $pdo->prepare("SELECT updated_at FROM allocation_billing WHERE allocation_id = ? LIMIT 1");
$stBu->execute([$allocId]);
$billingUpdatedAt = (string)($stBu->fetchColumn() ?: '');
}
} catch (Throwable $e) {}
if (!$force && $previewDocPath !== '') {
$previewLocalPath = $previewDocPath;
if (!preg_match('/^(?:[a-z]+:)?\/\//i', $previewLocalPath) && !preg_match('/^[A-Za-z]:[\\\\\\/]/', $previewLocalPath)) {
$isAbsUnix = (strlen($previewLocalPath) > 0 && ($previewLocalPath[0] === '/' || $previewLocalPath[0] === '\\'));
if (!$isAbsUnix) {
$previewLocalPath = __DIR__ . DIRECTORY_SEPARATOR . ltrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $previewLocalPath), DIRECTORY_SEPARATOR);
}
}
$docTime = is_file($previewLocalPath) ? (int)@filemtime($previewLocalPath) : 0;
if ($billingUpdatedAt !== '') {
$billTime = strtotime($billingUpdatedAt);
if ($billTime && $docTime > 0 && $billTime > $docTime) {
$force = true;
}
}
if (!$force && $docTime > 0) {
try {
$stAu = $pdo->prepare("SELECT updated_at FROM allocations WHERE id = ? LIMIT 1");
$stAu->execute([(int)$allocId]);
$allocUpdatedAt = (string)($stAu->fetchColumn() ?: '');
$allocTime = $allocUpdatedAt !== '' ? strtotime($allocUpdatedAt) : 0;
if ($allocTime && $allocTime > $docTime) {
$force = true;
}
} catch (Throwable $e) {}
}
if (!$force && $docTime > 0) {
try {
$tplFiles = [
__DIR__ . '/includes/doc_templates.php',
__DIR__ . '/includes/doc_generator.php',
];
$latestTplTime = 0;
foreach ($tplFiles as $tf) {
$t = is_file($tf) ? (int)@filemtime($tf) : 0;
if ($t > $latestTplTime) { $latestTplTime = $t; }
}
if ($latestTplTime > 0 && $latestTplTime > $docTime) {
$force = true;
}
} catch (Throwable $e) {}
}
}
if ($force) {
$previewDocPath = '';
$previewDocId = 0;
$previewPublicId = '';
}
if ($previewDocPath === '') {
try {
$generator = new DocGenerator($pdo);
$generator->generateAllocationLetter($allocId, $uid);
} catch (Throwable $e) {}
try {
$hasDocuments = $pdo->query("SHOW TABLES LIKE 'documents'")->rowCount() > 0;
if ($hasDocuments) {
$sqlPreviewDoc = "SELECT id, file_path" . (colx('documents','public_id') ? ", public_id" : "") . " FROM documents WHERE 1=1";
$paramsPreviewDoc = [];
if (colx('documents','type')) { $sqlPreviewDoc .= " AND type = ?"; $paramsPreviewDoc[] = 'allocation_letter'; }
if (colx('documents','allocation_id')) {
$sqlPreviewDoc .= " AND allocation_id = ?";
$paramsPreviewDoc[] = $allocId;
} else {
$sqlPreviewDoc .= " AND file_path LIKE ?";
$paramsPreviewDoc[] = '%Allocation_Letter_' . $allocId . '_%';
}
if (!$isSuperAdmin && $companyId && colx('documents','company_id')) {
$sqlPreviewDoc .= " AND company_id = ?";
$paramsPreviewDoc[] = $companyId;
}
$sqlPreviewDoc .= " ORDER BY id DESC LIMIT 1";
$stPreviewDoc = $pdo->prepare($sqlPreviewDoc);
$stPreviewDoc->execute($paramsPreviewDoc);
$rowPreviewDoc = $stPreviewDoc->fetch(PDO::FETCH_ASSOC) ?: [];
$previewDocId = (int)($rowPreviewDoc['id'] ?? 0);
$previewDocPath = (string)($rowPreviewDoc['file_path'] ?? '');
$previewPublicId = (string)($rowPreviewDoc['public_id'] ?? '');
}
} catch (Throwable $e) {}
try {
$generatedMatches = glob($generatedPattern) ?: [];
if (!empty($generatedMatches)) {
usort($generatedMatches, static function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
foreach ($generatedMatches as $matchPath) {
if (preg_match('/\.html?$/i', $matchPath)) {
$previewDocPath = $matchPath;
break;
}
}
if ($previewDocPath === '' && !empty($generatedMatches[0])) {
$previewDocPath = (string)$generatedMatches[0];
}
}
} catch (Throwable $e) {}
}
if ($previewDocPath !== '') {
$makeVerifyUrl = static function (string $publicId): string {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = (string)($_SERVER['HTTP_HOST'] ?? '');
$base = $host !== '' ? ($scheme . '://' . $host) : '';
$base = rtrim($base, '/');
return ($base !== '' ? $base : '') . '/verify.php?doc=' . rawurlencode($publicId);
};
$ensurePublicId = function () use ($pdo, $previewDocId, $previewPublicId, $allocId): string {
$pid = trim((string)$previewPublicId);
if ($pid !== '') { return $pid; }
if (!colx('documents','public_id') || $previewDocId <= 0) { return ''; }
$prefix = 'AIBEN-AL-';
for ($i = 0; $i < 25; $i++) {
$candidate = $prefix . str_pad((string)random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$st = $pdo->prepare("SELECT COUNT(*) FROM documents WHERE public_id = ? LIMIT 1");
$st->execute([$candidate]);
if ((int)$st->fetchColumn() === 0) {
$pdo->prepare("UPDATE documents SET public_id = ? WHERE id = ?")->execute([$candidate, $previewDocId]);
return $candidate;
}
}
$candidate = $prefix . str_pad((string)$allocId, 6, '0', STR_PAD_LEFT);
try { $pdo->prepare("UPDATE documents SET public_id = ? WHERE id = ?")->execute([$candidate, $previewDocId]); } catch (Throwable $e) {}
return $candidate;
};
$ensureQrData = static function (string $publicId, string $verifyUrl): array {
$relDir = 'uploads/qr';
$absDir = __DIR__ . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qr';
if (!is_dir($absDir)) { @mkdir($absDir, 0755, true); }
$absPath = $absDir . DIRECTORY_SEPARATOR . $publicId . '.png';
$relPath = $relDir . '/' . $publicId . '.png';
if (!file_exists($absPath) || filesize($absPath) < 10) {
$lib = __DIR__ . '/includes/phpqrcode/qrlib.php';
if (file_exists($lib)) { require_once $lib; }
if (class_exists('QRcode')) {
try { QRcode::png($verifyUrl, $absPath, 'M', 5, 2); } catch (Throwable $e) {}
}
}
$dataUri = '';
if (file_exists($absPath)) {
$bin = @file_get_contents($absPath);
if ($bin !== false) { $dataUri = 'data:image/png;base64,' . base64_encode($bin); }
}
return ['qr_rel_path' => $relPath, 'qr_abs_path' => $absPath, 'qr_data_uri' => $dataUri];
};
$publicId = $ensurePublicId();
$verifyUrl = $publicId !== '' ? $makeVerifyUrl($publicId) : '';
$qr = ($publicId !== '' && $verifyUrl !== '') ? $ensureQrData($publicId, $verifyUrl) : ['qr_data_uri' => ''];
$previewLocalPath = $previewDocPath;
if (!preg_match('/^(?:[a-z]+:)?\/\//i', $previewLocalPath) && !preg_match('/^[A-Za-z]:[\\\\\\/]/', $previewLocalPath)) {
$previewLocalPath = __DIR__ . DIRECTORY_SEPARATOR . ltrim(str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $previewLocalPath), DIRECTORY_SEPARATOR);
}
if ($download && is_file($previewLocalPath) && preg_match('/\.pdf$/i', $previewLocalPath)) {
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . basename($previewLocalPath) . '"');
header('Content-Length: ' . filesize($previewLocalPath));
readfile($previewLocalPath);
exit;
}
if (is_file($previewLocalPath) && preg_match('/\.html?$/i', $previewLocalPath)) {
$html = (string)@file_get_contents($previewLocalPath);
if ($html !== '') {
try {
$base = function_exists('buildAbsBaseUrl') ? buildAbsBaseUrl() : '/';
$root = realpath(__DIR__);
if ($root !== false && $root !== '') {
$rootNorm = str_replace('\\', '/', (string)$root);
$rootNormEnc = str_replace(' ', '%20', $rootNorm);
$baseNorm = rtrim((string)$base, '/') . '/';
$html = str_replace('file:///' . rtrim($rootNormEnc, '/') . '/', $baseNorm, $html);
$html = str_replace('file:///' . rtrim($rootNorm, '/') . '/', $baseNorm, $html);
$html = str_replace(rtrim($rootNormEnc, '/') . '/', $baseNorm, $html);
$html = str_replace(rtrim($rootNorm, '/') . '/', $baseNorm, $html);
$inlineProtected = static function (string $src) use ($baseNorm, $root): string {
$s = trim($src);
if ($s === '' || stripos($s, 'data:image/') === 0) { return $src; }
$rel = '';
$abs = '';
if (stripos($s, 'file:///') === 0) {
$p = substr($s, 8);
$p = str_replace('%20', ' ', $p);
$abs = $p;
} elseif (preg_match('/^[A-Za-z]:[\\\\\\/]/', $s)) {
$abs = $s;
} elseif (preg_match('/^https?:\\/\\//i', $s)) {
if (stripos($s, $baseNorm) === 0) {
$rel = ltrim(substr($s, strlen($baseNorm)), '/');
} else {
return $src;
}
} else {
$rel = ltrim($s, '/');
}
if ($abs === '' && $rel !== '') {
$abs = rtrim((string)$root, "\\/") . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $rel);
}
if ($abs === '' || !is_file($abs) || filesize($abs) < 10) { return $src; }
$bin = @file_get_contents($abs);
if (!is_string($bin) || strlen($bin) < 10) { return $src; }
$mime = 'image/png';
if (function_exists('mime_content_type')) {
$mt = @mime_content_type($abs);
if (is_string($mt) && $mt !== '') { $mime = $mt; }
} elseif (function_exists('finfo_open')) {
try {
$f = @finfo_open(FILEINFO_MIME_TYPE);
if ($f) {
$mt = @finfo_file($f, $abs);
@finfo_close($f);
if (is_string($mt) && $mt !== '') { $mime = $mt; }
}
} catch (Throwable $e) {}
}
return 'data:' . $mime . ';base64,' . base64_encode($bin);
};
$html = preg_replace_callback('~src=(["\'])([^"\']*uploads/signatures/[^"\']+)\\1~i', static function ($m) use ($inlineProtected) {
$new = $inlineProtected((string)$m[2]);
return 'src=' . $m[1] . htmlspecialchars($new, ENT_QUOTES) . $m[1];
}, $html) ?: $html;
}
} catch (Throwable $e) {}
try {
$sealOverride = '<style data-seal-fix="1">.seal-img{left:-8mm !important;z-index:1 !important;}.sig-img{left:-8mm !important;top:8mm !important;z-index:10 !important;}.sig-lead{position:relative !important;top:14mm !important;font-size:17px !important;font-weight:700 !important;}.sig-name{font-size:17px !important;font-weight:700 !important;}</style>';
if (stripos($html, 'data-seal-fix="1"') === false) {
$html = preg_replace('~</head>~i', $sealOverride . '</head>', $html, 1) ?: ($sealOverride . $html);
}
} catch (Throwable $e) {}
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
echo $html;
exit;
}
}
$frameSrc = $previewDocPath;
if (preg_match('/^[A-Za-z]:[\\\\\\/]/', $frameSrc)) {
$frameSrc = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', $frameSrc);
$frameSrc = str_replace(DIRECTORY_SEPARATOR, '/', $frameSrc);
} elseif (strlen($frameSrc) > 0 && $frameSrc[0] === '/') {
$baseDir = rtrim(str_replace('\\', '/', (string)__DIR__), '/') . '/';
$fsNorm = str_replace('\\', '/', (string)$frameSrc);
if (strpos($fsNorm, $baseDir) === 0) {
$frameSrc = ltrim(substr($fsNorm, strlen($baseDir)), '/');
}
}
$frameSrcRaw = trim((string)$frameSrc);
if ($frameSrcRaw !== '') {
$isHttp = preg_match('/^https?:\\/\\//i', $frameSrcRaw) === 1;
$isServerAbs = (strlen($frameSrcRaw) > 0 && $frameSrcRaw[0] === '/' && preg_match('~^/(home|var|tmp|usr)/~i', $frameSrcRaw));
$ok = false;
if ($isHttp) {
$curHost = strtolower((string)($_SERVER['HTTP_HOST'] ?? ''));
$linkHost = '';
if (preg_match('~^https?://([^/]+)~i', $frameSrcRaw, $m)) { $linkHost = strtolower((string)$m[1]); }
$ok = ($curHost !== '' && $linkHost === $curHost);
} elseif (!$isServerAbs) {
$ok = true;
}
if ($ok) {
$loc = $frameSrcRaw;
$v = 0;
if (isset($previewLocalPath) && is_string($previewLocalPath) && $previewLocalPath !== '' && is_file($previewLocalPath)) {
$v = (int)@filemtime($previewLocalPath);
}
if ($v <= 0 && isset($publicId) && is_string($publicId) && $publicId !== '') {
$v = (int)(crc32($publicId) & 0x7fffffff);
}
if ($v > 0) {
$loc .= (strpos($loc, '?') === false ? '?' : '&') . 'v=' . $v;
}
header('Location: ' . $loc);
exit;
}
}
$frameSrc = htmlspecialchars($frameSrc);
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Allocation Letter Preview</title><meta name="viewport" content="width=device-width, initial-scale=1"></head><body style="margin:0;background:#f3f4f6;">'
. '<iframe src="' . $frameSrc . '" style="width:100%;height:100vh;border:0;background:#fff;"></iframe>'
. '</body></html>';
exit;
}
$clientName = '';
$clientAddr = '';
$clientPhone = '';
$clientEmail = '';
$passportUrl = '';
$propertyTitle = '';
$propertyCode = '';
$plotSize = '';
$estateLocation = '';
$buildingUse = 'Residential';
$houseType = '';
$totalPrice = 0.0;
$currency = '₦';
$signatureUrl = '';
$signatoryName = '';
try {
$officialChairmanSignatureUrl = 'https://aibenproperties.com/wp-content/uploads/2026/03/chairmans-signature.png';
$signatoryName = function_exists('getSetting') ? (getSetting('allocation_signatory_name','') ?: '') : '';
if ($signatoryName === '' && function_exists('getSetting')) {
$signatoryName = getSetting('chairman_name','') ?: '';
}
if ($signatoryName === '' && isset($companyId) && $companyId) {
try {
$sn = $pdo->prepare("SELECT COALESCE(chairman_name,'') FROM companies WHERE id = ? LIMIT 1");
$sn->execute([(int)$companyId]);
$signatoryName = (string)($sn->fetchColumn() ?: '');
} catch (Throwable $eCN) {}
}
if ($signatoryName === '') { $signatoryName = 'Authorized Signatory'; }
$signatureUrl = $officialChairmanSignatureUrl;
} catch (Throwable $e) {}
try {
$ctxPreview = loadAllocationLetterContext($pdo, (int)$allocId, $companyId, $isSuperAdmin);
$cPrev = is_array($ctxPreview['client'] ?? null) ? $ctxPreview['client'] : [];
$pPrev = is_array($ctxPreview['property'] ?? null) ? $ctxPreview['property'] : [];
$bPrev = is_array($ctxPreview['computed'] ?? null) ? $ctxPreview['computed'] : [];
$billPrev = is_array($ctxPreview['billing'] ?? null) ? $ctxPreview['billing'] : [];
$clientName = (string)($cPrev['full_name'] ?? '');
$clientAddr = (string)($cPrev['address'] ?? '');
$clientPhone = (string)($cPrev['phone'] ?? '');
$clientEmail = (string)($cPrev['email'] ?? '');
$passportUrl = (string)($cPrev['passport_url'] ?? '');
$propertyTitle = (string)($pPrev['estate_name'] ?? ($pPrev['property_name'] ?? ''));
$propertyCode = (string)($pPrev['plot_number'] ?? '');
$plotSize = (string)($pPrev['sqm'] ?? '');
$buildingUse = (string)($pPrev['property_type'] ?? $buildingUse);
$houseType = (string)($pPrev['house_type'] ?? '');
$totalPrice = (float)($bPrev['grand_total'] ?? ($billPrev['land_cost'] ?? 0));
} catch (Throwable $e) {}
$previewData = [
'client_name' => $clientName !== '' ? $clientName : '—',
'client_address' => $clientAddr ?: '',
'client_phone' => $clientPhone ?: '',
'client_email' => $clientEmail ?: '',
'property_title' => $propertyTitle !== '' ? $propertyTitle : ('Allocation #' . (int)$allocId),
'property_code' => $propertyCode !== '' ? $propertyCode : ('ALC-'.$allocId),
'plot_size' => $plotSize !== '' ? $plotSize : '—',
'space_size' => ($plotSize !== '' ? $plotSize . ' SQM' : '—'),
'building_use' => $buildingUse !== '' ? $buildingUse : '—',
'house_type' => $houseType,
'estate_location' => $estateLocation,
'allocation_id' => $allocId,
'reference' => 'AL-' . str_pad($allocId, 6, '0', STR_PAD_LEFT),
'currency' => $currency,
'total_price' => number_format($totalPrice, 2),
'passport_url' => $passportUrl,
'signature_url' => $signatureUrl,
'signatory_name' => $signatoryName,
'is_preview' => true,
'render_target' => 'browser'
];
$html = getAllocationLetterTemplateAiben($previewData);
if ($canManageDraft && $viewMode === 'admin') {
$pdfPreviewHref = 'allocation-letter-pdf.php?action=preview&allocation_id=' . (int)$allocId . '®en=1';
$pdfDownloadHref = 'allocation-letter-pdf.php?action=download&allocation_id=' . (int)$allocId . '®en=1';
$toolbar = '<div class="preview-toolbar" data-preview-ui="1" style="position:sticky;top:0;z-index:9999;background:#ffffff;border-bottom:1px solid #e5e7eb;padding:14px 18px;display:flex;gap:10px;align-items:center;justify-content:space-between;">'
. '<div style="font-family:Arial,sans-serif;font-size:14px;font-weight:700;color:#111827;">Draft preview ready for admin review</div>'
. '<div style="display:flex;gap:10px;align-items:center;">'
. '<a target="_blank" rel="noopener" href="' . htmlspecialchars($pdfPreviewHref, ENT_QUOTES) . '" style="text-decoration:none;padding:10px 14px;border:1px solid #93c5fd;border-radius:10px;color:#1d4ed8;font:600 13px Arial,sans-serif;background:#eff6ff;">Preview PDF</a>'
. '<a target="_blank" rel="noopener" href="' . htmlspecialchars($pdfDownloadHref, ENT_QUOTES) . '" style="text-decoration:none;padding:10px 14px;border:1px solid #16a34a;border-radius:10px;color:#ffffff;font:600 13px Arial,sans-serif;background:#16a34a;">Download PDF</a>'
. '<a href="' . htmlspecialchars($allocationLettersBaseHref, ENT_QUOTES) . '" style="text-decoration:none;padding:10px 14px;border:1px solid #d1d5db;border-radius:10px;color:#111827;font:600 13px Arial,sans-serif;">Back</a>';
if ($canSendToExecutive) {
$toolbar .= '<form method="post" action="allocation-letters.php" onsubmit="return confirm(\'Send this allocation letter to the executive dashboard for approval?\');" style="margin:0;">'
. '<input type="hidden" name="form_action" value="send_for_approval">'
. '<input type="hidden" name="view" value="' . htmlspecialchars($viewMode, ENT_QUOTES) . '">'
. '<input type="hidden" name="allocation_id" value="' . (int)$allocId . '">'
. '<button type="submit" style="border:0;background:#111827;color:#fff;padding:10px 14px;border-radius:10px;font:600 13px Arial,sans-serif;cursor:pointer;">Send for Approval</button>'
. '</form>';
}
$toolbar .= ''
. '</div>'
. '</div>';
$html = preg_replace('/<body([^>]*)>/i', '<body$1>' . $toolbar, $html, 1) ?: $toolbar . $html;
}
header('Content-Type: text/html; charset=UTF-8');
echo $html;
exit;
}
if ($action === 'details' && $allocId > 0) {
$user = [];
$formData = [];
$allocation = [];
$property = [];
$avatar = '';
try {
$st = $pdo->prepare("SELECT * FROM allocations WHERE id = ?" . ((!$isSuperAdmin && $companyId && colx('allocations','company_id')) ? " AND company_id = ?" : ""));
$params = [$allocId];
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) { $params[] = $companyId; }
$st->execute($params);
$allocation = $st->fetch(PDO::FETCH_ASSOC) ?: [];
$uid2 = (int)($allocation['user_id'] ?? 0);
$pid2 = (int)($allocation['property_id'] ?? 0);
if ($uid2 > 0) {
$su = $pdo->prepare("SELECT * FROM users WHERE id = ? LIMIT 1");
$su->execute([$uid2]);
$user = $su->fetch(PDO::FETCH_ASSOC) ?: [];
if (function_exists('getClientAvatarUrl')) { $avatar = getClientAvatarUrl($pdo, $uid2); }
$sf = $pdo->prepare("SELECT form_data FROM client_forms WHERE client_id = ? ORDER BY updated_at DESC, created_at DESC LIMIT 1");
$sf->execute([$uid2]);
$row = $sf->fetch(PDO::FETCH_ASSOC);
if ($row && !empty($row['form_data'])) {
$tmp = json_decode($row['form_data'], true);
if (is_array($tmp)) { $formData = $tmp; }
}
}
if ($pid2 > 0) {
$sp = $pdo->prepare("SELECT * FROM properties WHERE id = ? LIMIT 1");
$sp->execute([$pid2]);
$property = $sp->fetch(PDO::FETCH_ASSOC) ?: [];
}
} catch (Throwable $e) {}
header('Content-Type: text/html; charset=UTF-8');
$nm = $user['name'] ?? ($user['full_name'] ?? (($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')));
$email = $user['email'] ?? ($formData['email'] ?? '');
$phone = $user['phone'] ?? ($formData['phone'] ?? '');
$addr = $user['address'] ?? ($formData['address'] ?? ($formData['residential_address'] ?? ''));
$company = $formData['company_name'] ?? '';
$idDoc = $formData['id_document_path'] ?? '';
$passport = $formData['passport_photo_path'] ?? $avatar;
$title = $property['title'] ?? ($property['name'] ?? ('Property #'.($property['id'] ?? '')));
$code = $property['code'] ?? ($property['plot_code'] ?? '');
$size = $property['plot_size'] ?? ($property['area_sqm'] ?? '');
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Client Details</title><meta name="viewport" content="width=device-width, initial-scale=1">';
echo '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">';
echo '<style>body{font-family:Inter,Poppins,Arial,sans-serif;background:#f5f7fb;margin:0;padding:24px} .wrap{max-width:980px;margin:0 auto} .card{background:#fff;border:0;border-radius:16px;box-shadow:0 8px 24px rgba(0,0,0,.06);padding:22px;margin-bottom:18px} .hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px} .title{font-size:22px;font-weight:800;margin:0} .grid{display:grid;grid-template-columns:1fr 1fr;gap:16px} .avatar{width:140px;height:140px;border-radius:12px;object-fit:cover;background:#e6e8ef} .row{display:flex;gap:12px;align-items:center;margin:6px 0} .label{color:#6b7280;min-width:140px;font-weight:600} .val{color:#111827;font-weight:700} .badge{display:inline-block;padding:6px 10px;border-radius:9999px;font-size:12px;border:1px solid #e5e7eb;background:#fff;color:#111827} .muted{color:#6b7280} .btn{display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;text-decoration:none;color:#111827} .btn:hover{background:#f8fafc} .media{display:flex;gap:16px;align-items:flex-start} .section{font-weight:700;margin:6px 0 8px 0} .two{display:grid;grid-template-columns:1fr 1fr;gap:8px} .kv{display:flex;justify-content:space-between;border:1px solid #e5e7eb;border-radius:10px;padding:10px} .kv .k{color:#6b7280} .kv .v{font-weight:700} .actions{display:flex;gap:8px}</style></head><body>';
echo '<div class="wrap">';
echo '<div class="hdr"><h1 class="title">Client Application Details</h1><div class="actions"><a class="btn" href="#" onclick="window.print();return false;"><i class=\"fa-solid fa-print\"></i>Print</a></div></div>';
echo '<div class="card"><div class="media">';
if (!empty($passport)) { echo '<img class="avatar" src="'.htmlspecialchars($passport).'" alt="Passport">'; }
echo '<div>';
echo '<div class="row"><div class="label">Name</div><div class="val">'.htmlspecialchars(trim($nm)).'</div></div>';
echo '<div class="row"><div class="label">Email</div><div class="val">'.htmlspecialchars($email).'</div></div>';
echo '<div class="row"><div class="label">Phone</div><div class="val">'.htmlspecialchars($phone).'</div></div>';
echo '<div class="row"><div class="label">Address</div><div class="val">'.htmlspecialchars($addr).'</div></div>';
if ($company) { echo '<div class="row"><div class="label">Company</div><div class="val">'.htmlspecialchars($company).'</div></div>'; }
if (!empty($idDoc)) { echo '<div class="row"><div class="label">ID Document</div><div class="val"><a class="btn" href="'.htmlspecialchars($idDoc).'" target="_blank"><i class=\"fa-solid fa-id-card\"></i>View</a></div></div>'; }
echo '</div></div></div>';
echo '<div class="card"><div class="section">Allocation & Property</div><div class="two">';
echo '<div class="kv"><div class="k">Allocation ID</div><div class="v">#'.(int)$allocId.'</div></div>';
echo '<div class="kv"><div class="k">Allocation Date</div><div class="v">'.htmlspecialchars($allocation['allocation_date'] ?? '').'</div></div>';
echo '<div class="kv"><div class="k">Property</div><div class="v">'.htmlspecialchars($title).'</div></div>';
echo '<div class="kv"><div class="k">Plot/Code</div><div class="v">'.htmlspecialchars($code).'</div></div>';
echo '<div class="kv"><div class="k">Plot Size</div><div class="v">'.htmlspecialchars((string)$size).'</div></div>';
echo '<div class="kv"><div class="k">Status</div><div class="v"><span class="badge">'.htmlspecialchars($allocation['status'] ?? 'N/A').'</span></div></div>';
echo '</div></div>';
echo '<div class="card"><div class="section">Form Fields</div>';
if (!empty($formData)) {
echo '<div class="grid">';
$shown = 0;
foreach ($formData as $k => $v) {
if (is_array($v)) continue;
$val = is_scalar($v) ? (string)$v : '';
if ($val === '' || strlen($k) > 60) continue;
$label = ucwords(str_replace(['_','-'],' ', $k));
echo '<div class="kv"><div class="k">'.htmlspecialchars($label).'</div><div class="v">'.htmlspecialchars($val).'</div></div>';
$shown++;
}
if ($shown === 0) { echo '<div class="muted">No structured form fields found.</div>'; }
echo '</div>';
} else {
echo '<div class="muted">No saved client form found.</div>';
}
echo '</div>';
echo '</div></body></html>';
exit;
}
if ($action === 'preview_pdf' && $allocId > 0) {
header('Content-Type: application/pdf');
try {
$coverRel = function_exists('getSetting') ? (getSetting('allocation_letter_cover_path', '') ?? '') : '';
if ($coverRel) {
$root = __DIR__;
$coverAbs = $root . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $coverRel);
if (file_exists($coverAbs)) {
// If FPDI exists, we could overlay minimal fields; otherwise just stream the cover PDF
if (class_exists('\\setasign\\Fpdi\\Fpdi')) {
$dir = __DIR__ . '/uploads/documents/generated';
if (!is_dir($dir)) { @mkdir($dir, 0777, true); }
$pdfPath = $dir . '/Allocation_Cover_Preview_' . $allocId . '_' . time() . '.pdf';
if (!class_exists('\\setasign\\Fpdi\\Fpdi')) {
// Fallback: stream the original cover file if FPDI is not available
readfile($coverAbs);
exit;
}
if (!class_exists('\setasign\Fpdi\Fpdi')) {
// Fallback: stream the original cover file if FPDI is not available
readfile($coverAbs);
exit;
}
$fpdi = new \setasign\Fpdi\Fpdi();
$fpdi->AddPage();
$fpdi->setSourceFile($coverAbs);
$tplId = $fpdi->importPage(1);
$fpdi->useTemplate($tplId, 0, 0, 210, 297);
$fpdi->Output($pdfPath, 'F');
readfile($pdfPath);
exit;
} else {
readfile($coverAbs);
exit;
}
}
}
} catch (Throwable $e) {}
http_response_code(404);
echo '%PDF-1.4% Allocation Cover Not Found';
exit;
}
include __DIR__ . '/includes/header.php';
$rows = [];
try {
$hasUsers = $pdo->query("SHOW TABLES LIKE 'users'")->rowCount() > 0;
$hasProps = $pdo->query("SHOW TABLES LIKE 'properties'")->rowCount() > 0;
$hasEstates = $pdo->query("SHOW TABLES LIKE 'estates'")->rowCount() > 0;
$hasDealsSubmit = $pdo->query("SHOW TABLES LIKE 'deals_submit'")->rowCount() > 0;
$hasPayments = $pdo->query("SHOW TABLES LIKE 'payments'")->rowCount() > 0;
$nameExpr = $hasUsers && colx('users','name') ? 'u.name' : ($hasUsers && colx('users','full_name') ? 'u.full_name' : "NULL");
$pTitleExpr = $hasProps && colx('properties','title') ? 'p.title' : ($hasProps && colx('properties','name') ? 'p.name' : "NULL");
$allocPriceExpr = "0";
if (colx('allocations','total_price')) { $allocPriceExpr = "a.total_price"; }
elseif (colx('allocations','total_amount')) { $allocPriceExpr = "a.total_amount"; }
elseif (colx('allocations','amount')) { $allocPriceExpr = "a.amount"; }
elseif (colx('allocations','price')) { $allocPriceExpr = "a.price"; }
elseif (colx('allocations','final_price')) { $allocPriceExpr = "a.final_price"; }
elseif (colx('allocations','sale_price')) { $allocPriceExpr = "a.sale_price"; }
$plotCol = "NULL";
if (colx('allocations','plot_number')) { $plotCol = "a.plot_number"; }
elseif (colx('allocations','unit_number')) { $plotCol = "a.unit_number"; }
elseif (colx('allocations','plot_no')) { $plotCol = "a.plot_no"; }
elseif (colx('allocations','property_code')) { $plotCol = "a.property_code"; }
$plotSizeCol = "NULL";
if (colx('allocations','plot_size')) { $plotSizeCol = "a.plot_size"; }
elseif ($hasProps && colx('properties','plot_size')) { $plotSizeCol = "p.plot_size"; }
elseif ($hasProps && colx('properties','area_sqm')) { $plotSizeCol = "p.area_sqm"; }
$estateJoin = " LEFT JOIN (SELECT NULL AS id, NULL AS name) e ON 1=0";
if ($hasEstates) {
if (colx('allocations','estate_id')) {
$estateJoin = " LEFT JOIN estates e ON a.estate_id = e.id";
} elseif ($hasProps && colx('properties','estate_id')) {
$estateJoin = " LEFT JOIN estates e ON p.estate_id = e.id";
}
}
$dealJoin = " LEFT JOIN (SELECT NULL AS id, NULL AS amount_offered) d ON 1=0";
if ($hasDealsSubmit && colx('allocations','deal_id') && colx('deals_submit','amount_offered')) {
$dealJoin = " LEFT JOIN deals_submit d ON a.deal_id = d.id";
}
$paidExpr = "0";
if ($hasPayments) {
$paidMatchParts = [];
if (colx('payments','allocation_id')) { $paidMatchParts[] = "allocation_id = a.id"; }
if (colx('payments','deal_id') && colx('allocations','deal_id')) { $paidMatchParts[] = "(a.deal_id IS NOT NULL AND deal_id = a.deal_id)"; }
$paidExpr = $paidMatchParts
? "(SELECT COALESCE(SUM(amount), 0) FROM payments WHERE status IN ('approved','paid') AND (" . implode(' OR ', $paidMatchParts) . "))"
: "0";
}
$sql = "SELECT a.id AS allocation_id, a.user_id, a.property_id, a.status, a.allocation_date,
e.name AS estate_name,
{$plotCol} AS plot_number,
{$plotSizeCol} AS plot_size,
COALESCE(NULLIF({$allocPriceExpr},0), " . ($hasProps && colx('properties','price') ? "p.price" : "0") . ", d.amount_offered, 0) AS total_price,
{$paidExpr} AS total_paid";
if ($nameExpr !== "NULL") { $sql .= ", {$nameExpr} AS client_name"; }
if ($pTitleExpr !== "NULL") { $sql .= ", {$pTitleExpr} AS property_title"; }
$sql .= " FROM allocations a";
if ($nameExpr !== "NULL" && $hasUsers) { $sql .= " LEFT JOIN users u ON a.user_id = u.id"; }
if ($pTitleExpr !== "NULL" && $hasProps) { $sql .= " LEFT JOIN properties p ON a.property_id = p.id"; }
$sql .= $estateJoin;
$sql .= $dealJoin;
$sql .= " WHERE 1=1";
$params = [];
if (colx('allocations','status')) {
if ($viewMode === 'executive') {
$execAll = ['pending_executive_approval','executive_approved','chairman_approved','released_to_client','sent_to_client','completed','rejected'];
if ($statusPreset === 'pending') {
$sql .= " AND a.status IN ('pending_executive_approval')";
} elseif ($statusPreset === 'approved') {
$sql .= " AND a.status IN ('executive_approved','chairman_approved')";
} elseif ($statusPreset === 'completed') {
$sql .= " AND a.status IN ('released_to_client','sent_to_client','completed')";
} elseif ($statusPreset === 'rejected') {
$sql .= " AND a.status IN ('rejected')";
} else {
$sql .= " AND a.status IN ('" . implode("','", $execAll) . "')";
}
} else {
$sql .= " AND (a.status IN ('pending','pending_executive_approval','pending_chairman_approval','pending_head_admin_review','draft_prepared','returned_for_correction','changes_requested','approved','admin_approved','executive_approved','chairman_approved','released','released_to_client','sent_to_client','completed','rejected') OR a.status IS NULL)";
}
}
if (!$isSuperAdmin && $companyId && colx('allocations','company_id')) { $sql .= " AND (a.company_id = ? OR a.company_id IS NULL OR a.company_id = 0)"; $params[] = $companyId; }
$sql .= " ORDER BY a.id DESC LIMIT 50";
$st = $pdo->prepare($sql); $st->execute($params);
$rows = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {}
$isExecutiveViewer = $viewMode === 'executive';
$canFinalizeApproval = in_array($roleNorm, ['chairman_ceo','super_admin'], true);
$pageTitle = $isExecutiveViewer ? 'Allocation Letters — Executive Review' : 'Allocation Letters — Admin Workflow';
$pageSubtitle = $isExecutiveViewer
? 'Review, approve, and finalize property allocation documents.'
: 'Generate, preview, and send allocation letters to the executive dashboard.';
$primaryNavHref = $isExecutiveViewer ? 'executive-dashboard.php' : 'management-dashboard.php';
$primaryNavLabel = $isExecutiveViewer ? 'Executive Dashboard' : 'Management Dashboard';
$secondaryNavHref = $isExecutiveViewer ? 'allocation-letters.php?view=executive&status=pending' : $allocationLettersBaseHref;
$secondaryNavLabel = $isExecutiveViewer ? 'Pending Approvals' : 'Workflow Overview';
$tableTitle = $isExecutiveViewer ? 'Executive Allocation Letter Queue' : 'Admin Allocation Letter Workflow';
$statusUiMap = [
'pending_executive_approval' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'pending_chairman_approval' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'pending_head_admin_review' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'draft_prepared' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'returned_for_correction' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'changes_requested' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'admin_approved' => ['key' => 'approved', 'label' => 'Approved', 'tone' => 'approved'],
'chairman_approved' => ['key' => 'approved', 'label' => 'Approved', 'tone' => 'approved'],
'released' => ['key' => 'completed', 'label' => 'Completed', 'tone' => 'completed'],
'released_to_client' => ['key' => 'completed', 'label' => 'Completed', 'tone' => 'completed'],
'sent_to_client' => ['key' => 'completed', 'label' => 'Completed', 'tone' => 'completed'],
'pending' => ['key' => 'pending', 'label' => 'Pending Review', 'tone' => 'pending'],
'approved' => ['key' => 'approved', 'label' => 'Approved', 'tone' => 'approved'],
'executive_approved' => ['key' => 'approved', 'label' => 'Approved', 'tone' => 'approved'],
'completed' => ['key' => 'completed', 'label' => 'Completed', 'tone' => 'completed'],
'rejected' => ['key' => 'rejected', 'label' => 'Rejected', 'tone' => 'rejected'],
];
$dashboardCounts = ['pending' => 0, 'approved' => 0, 'completed' => 0, 'total' => count($rows)];
foreach ($rows as &$row) {
$rowStatus = strtolower((string)($row['status'] ?? ''));
$statusMeta = $statusUiMap[$rowStatus] ?? ['key' => 'other', 'label' => ucwords(str_replace('_', ' ', $rowStatus ?: 'unknown')), 'tone' => 'neutral'];
$row['status_key'] = $statusMeta['key'];
$row['status_label'] = $statusMeta['label'];
$row['status_tone'] = $statusMeta['tone'];
$row['is_pending_review'] = $statusMeta['key'] === 'pending';
if (isset($dashboardCounts[$statusMeta['key']])) {
$dashboardCounts[$statusMeta['key']]++;
}
}
unset($row);
?>
<style>
.executive-letters-page{
--exec-bg:#f4f7fb;
--exec-surface:#ffffff;
--exec-border:#e6edf5;
--exec-border-strong:#d7e3f2;
--exec-text:#112136;
--exec-muted:#6b7a90;
--exec-blue:#2563eb;
--exec-green:#16a34a;
--exec-red:#dc2626;
--exec-yellow:#eab308;
--exec-yellow-soft:#fff8db;
--exec-shadow:0 20px 45px rgba(15,23,42,.08);
color:var(--exec-text);
}
.executive-letters-page .exec-breadcrumb{
display:flex;
align-items:center;
gap:10px;
margin-bottom:18px;
color:var(--exec-muted);
font-size:.92rem;
font-weight:600;
}
.executive-letters-page .exec-breadcrumb a{
color:var(--exec-muted);
text-decoration:none;
}
.executive-letters-page .exec-breadcrumb a:hover{
color:var(--exec-blue);
}
.executive-letters-page .exec-shell{
display:grid;
gap:24px;
}
.executive-letters-page .exec-hero{
background:linear-gradient(135deg,#0f172a 0%, #1e3a5f 58%, #285d8f 100%);
color:#fff;
border:0;
border-radius:20px;
box-shadow:var(--exec-shadow);
overflow:hidden;
}
.executive-letters-page .exec-hero .card-body{
padding:28px;
}
.executive-letters-page .exec-hero-title{
font-size:2rem;
font-weight:800;
margin:0 0 8px;
color:#fff;
}
.executive-letters-page .exec-hero-subtitle{
margin:0;
color:rgba(255,255,255,.78);
max-width:720px;
}
.executive-letters-page .exec-nav-pills{
display:flex;
flex-wrap:wrap;
gap:10px;
justify-content:flex-end;
}
.executive-letters-page .exec-nav-pill{
display:inline-flex;
align-items:center;
gap:8px;
padding:10px 14px;
border-radius:999px;
background:rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.16);
color:#fff;
text-decoration:none;
font-weight:600;
transition:all .2s ease;
}
.executive-letters-page .exec-nav-pill:hover,
.executive-letters-page .exec-nav-pill.active{
background:rgba(255,255,255,.16);
color:#fff;
transform:translateY(-1px);
}
.executive-letters-page .exec-stat-grid{
display:grid;
grid-template-columns:repeat(4, minmax(0,1fr));
gap:18px;
}
.executive-letters-page .exec-stat-card{
background:var(--exec-surface);
border:1px solid var(--exec-border);
border-radius:16px;
box-shadow:0 14px 28px rgba(15,23,42,.05);
padding:20px;
}
.executive-letters-page a.exec-stat-card{
color:inherit;
text-decoration:none;
display:block;
}
.executive-letters-page a.exec-stat-card:hover{
border-color:var(--exec-border-strong);
box-shadow:0 18px 34px rgba(15,23,42,.08);
transform:translateY(-1px);
}
.executive-letters-page .exec-stat-label{
display:flex;
align-items:center;
gap:10px;
margin-bottom:14px;
color:var(--exec-muted);
font-weight:700;
font-size:.86rem;
text-transform:uppercase;
letter-spacing:.06em;
}
.executive-letters-page .exec-stat-icon{
width:38px;
height:38px;
border-radius:12px;
display:inline-flex;
align-items:center;
justify-content:center;
font-size:1rem;
}
.executive-letters-page .exec-stat-card.pending .exec-stat-icon{ background:rgba(234,179,8,.16); color:#a16207; }
.executive-letters-page .exec-stat-card.approved .exec-stat-icon{ background:rgba(37,99,235,.14); color:#1d4ed8; }
.executive-letters-page .exec-stat-card.completed .exec-stat-icon{ background:rgba(22,163,74,.14); color:#15803d; }
.executive-letters-page .exec-stat-card.total .exec-stat-icon{ background:rgba(15,23,42,.08); color:#0f172a; }
.executive-letters-page .exec-stat-value{
font-size:2rem;
font-weight:800;
line-height:1;
margin-bottom:6px;
}
.executive-letters-page .exec-stat-meta{
color:var(--exec-muted);
font-size:.92rem;
}
.executive-letters-page .exec-board{
background:var(--exec-surface);
border:1px solid var(--exec-border);
border-radius:18px;
box-shadow:var(--exec-shadow);
overflow:hidden;
}
.executive-letters-page .exec-board *{
max-width:100%;
}
.side-drawer-backdrop{
position:fixed;
inset:0;
background:rgba(15,23,42,.45);
opacity:0;
pointer-events:none;
transition:opacity .2s ease;
z-index:1049;
}
.side-drawer-backdrop.show{
opacity:1;
pointer-events:auto;
}
.side-drawer{
position:fixed;
top:0;
right:0;
width:560px;
max-width:100%;
height:100vh;
background:#ffffff;
box-shadow:0 30px 70px rgba(15,23,42,.25);
transform:translateX(104%);
transition:transform .22s ease;
z-index:1050;
display:flex;
flex-direction:column;
border-left:1px solid rgba(226,232,240,.85);
}
.side-drawer.open{
transform:translateX(0);
}
.side-drawer #drawerContent{
overflow:auto;
-webkit-overflow-scrolling:touch;
}
.side-drawer .sticky-bottom{
position:sticky;
bottom:0;
}
.executive-letters-page .exec-board-head{
padding:22px 24px 16px;
border-bottom:1px solid var(--exec-border);
}
.executive-letters-page .exec-board-title{
display:flex;
align-items:center;
justify-content:space-between;
gap:16px;
margin-bottom:16px;
}
.executive-letters-page .exec-board-title h2{
margin:0;
font-size:1.15rem;
font-weight:800;
}
.executive-letters-page .exec-board-title p{
margin:6px 0 0;
color:var(--exec-muted);
}
.executive-letters-page .exec-live-count{
display:inline-flex;
align-items:center;
gap:8px;
padding:10px 14px;
border-radius:999px;
background:#eef4ff;
color:#1d4ed8;
font-weight:700;
font-size:.9rem;
}
.executive-letters-page .exec-toolbar{
display:grid;
grid-template-columns:minmax(220px,1.7fr) minmax(180px,.9fr) minmax(170px,.8fr);
gap:12px;
}
.executive-letters-page .exec-control{
position:relative;
}
.executive-letters-page .exec-control i{
position:absolute;
top:50%;
left:14px;
transform:translateY(-50%);
color:#94a3b8;
}
.executive-letters-page .exec-input,
.executive-letters-page .exec-select{
width:100%;
min-height:40px;
border:1px solid var(--exec-border-strong);
border-radius:12px;
padding:10px 14px;
background:#fff;
color:var(--exec-text);
font-weight:600;
transition:border-color .2s ease, box-shadow .2s ease;
}
.executive-letters-page .exec-control .exec-input{
padding-left:40px;
}
.executive-letters-page .exec-input:focus,
.executive-letters-page .exec-select:focus{
border-color:#9dbaf5;
box-shadow:0 0 0 4px rgba(37,99,235,.12);
outline:0;
}
.executive-letters-page .exec-table-wrap{
padding:8px 10px 14px;
}
.executive-letters-page .exec-table{
margin:0;
border-collapse:separate;
border-spacing:0 10px;
}
.executive-letters-page .exec-table thead th{
border:0;
color:var(--exec-muted);
font-size:.78rem;
text-transform:uppercase;
letter-spacing:.08em;
font-weight:800;
padding:0 16px 8px;
}
.executive-letters-page .exec-table tbody tr{
background:#fff;
box-shadow:0 10px 20px rgba(15,23,42,.04);
transition:transform .18s ease, box-shadow .18s ease, background-color .18s ease;
cursor:pointer;
}
.executive-letters-page .exec-table tbody tr.is-focused{
background:rgba(37,99,235,.08);
box-shadow:inset 4px 0 0 var(--exec-blue), 0 12px 22px rgba(37,99,235,.12);
}
.executive-letters-page .exec-table tbody tr:hover{
transform:translateY(-1px);
box-shadow:0 16px 28px rgba(15,23,42,.07);
}
.executive-letters-page .exec-table tbody tr.pending-review{
background:var(--exec-yellow-soft);
box-shadow:inset 4px 0 0 #f0b90b, 0 10px 20px rgba(234,179,8,.12);
}
.executive-letters-page .exec-table tbody td{
border:0;
padding:14px 16px;
vertical-align:middle;
}
.executive-letters-page .exec-table tbody tr td:first-child{
border-radius:14px 0 0 14px;
}
.executive-letters-page .exec-table tbody tr td:last-child{
border-radius:0 14px 14px 0;
}
.executive-letters-page .exec-entity-title{
font-weight:800;
color:var(--exec-text);
}
.executive-letters-page .exec-entity-meta{
color:var(--exec-muted);
font-size:.88rem;
margin-top:4px;
}
.executive-letters-page .exec-badge{
display:inline-flex;
align-items:center;
justify-content:center;
padding:7px 12px;
border-radius:999px;
font-size:.78rem;
font-weight:800;
letter-spacing:.02em;
}
.executive-letters-page .exec-badge.pending{ background:rgba(234,179,8,.18); color:#9a6700; }
.executive-letters-page .exec-badge.approved{ background:rgba(37,99,235,.12); color:#1d4ed8; }
.executive-letters-page .exec-badge.completed{ background:rgba(22,163,74,.14); color:#15803d; }
.executive-letters-page .exec-badge.rejected{ background:rgba(220,38,38,.12); color:#b91c1c; }
.executive-letters-page .exec-badge.neutral{ background:#eef2f7; color:#475569; }
.executive-letters-page .exec-actions{
display:flex;
flex-wrap:wrap;
gap:8px;
justify-content:flex-start;
}
.executive-letters-page .exec-btn{
display:inline-flex;
align-items:center;
justify-content:center;
gap:8px;
min-height:38px;
padding:9px 12px;
border-radius:12px;
border:1px solid transparent;
font-size:.86rem;
font-weight:700;
text-decoration:none;
transition:all .2s ease;
}
.executive-letters-page .exec-btn:hover{
transform:translateY(-1px);
}
.executive-letters-page .exec-btn-secondary{
background:#fff;
border-color:var(--exec-border-strong);
color:#1f2937;
}
.executive-letters-page .exec-btn-secondary:hover{
background:#f8fbff;
color:#0f172a;
}
.executive-letters-page .exec-btn-approve{
background:rgba(22,163,74,.12);
color:#15803d;
border-color:rgba(22,163,74,.2);
}
.executive-letters-page .exec-btn-approve:hover{
background:rgba(22,163,74,.18);
color:#166534;
}
.executive-letters-page .exec-btn-reject{
background:rgba(220,38,38,.1);
color:#b91c1c;
border-color:rgba(220,38,38,.16);
}
.executive-letters-page .exec-btn-reject:hover{
background:rgba(220,38,38,.16);
color:#991b1b;
}
.executive-letters-page .exec-btn-primary-dark{
background:#0f172a;
color:#fff;
}
.executive-letters-page .exec-btn-primary-dark:hover{
background:#111f38;
color:#fff;
}
.executive-letters-page .exec-btn[disabled]{
opacity:.45;
pointer-events:none;
transform:none;
}
.executive-letters-page .exec-empty{
display:grid;
place-items:center;
gap:12px;
padding:60px 20px 70px;
text-align:center;
color:var(--exec-muted);
}
.executive-letters-page .exec-empty i{
width:64px;
height:64px;
border-radius:20px;
display:inline-flex;
align-items:center;
justify-content:center;
background:#eef4ff;
color:#2563eb;
font-size:1.4rem;
}
.executive-letters-page .exec-empty strong{
display:block;
color:var(--exec-text);
font-size:1.02rem;
}
.executive-letters-page .exec-alert{
margin:0;
border:0;
border-radius:14px;
box-shadow:0 10px 20px rgba(15,23,42,.05);
}
.executive-letters-page .exec-modal .modal-content{
border:0;
border-radius:18px;
box-shadow:0 24px 55px rgba(15,23,42,.18);
}
.executive-letters-page .exec-modal .modal-header,
.executive-letters-page .exec-modal .modal-footer{
border-color:#edf2f7;
padding:18px 22px;
}
.executive-letters-page .exec-modal .modal-body{
padding:20px 22px;
}
.executive-letters-page .exec-modal .form-control{
border-radius:14px;
border:1px solid var(--exec-border-strong);
min-height:46px;
}
.executive-letters-page .exec-modal .form-control:focus{
border-color:#9dbaf5;
box-shadow:0 0 0 4px rgba(37,99,235,.12);
}
@media (max-width: 1199.98px){
.executive-letters-page .exec-stat-grid{
grid-template-columns:repeat(2, minmax(0,1fr));
}
}
@media (max-width: 991.98px){
.executive-letters-page .exec-toolbar{
grid-template-columns:1fr;
gap:10px;
}
.executive-letters-page .exec-nav-pills{
justify-content:flex-start;
}
.executive-letters-page .exec-input,
.executive-letters-page .exec-select{
min-height:38px;
padding:9px 12px;
border-radius:12px;
}
}
@media (max-width: 1024px){
.executive-letters-page .exec-hero .card-body{
padding:22px;
}
.executive-letters-page .exec-hero-title{
font-size:1.55rem;
}
.executive-letters-page .exec-stat-grid{
grid-template-columns:1fr;
}
.executive-letters-page .exec-table thead{
display:none;
}
.executive-letters-page .exec-table,
.executive-letters-page .exec-table tbody,
.executive-letters-page .exec-table tr,
.executive-letters-page .exec-table td{
display:block;
width:100%;
}
.executive-letters-page .exec-table tbody tr{
padding:8px 0;
}
.executive-letters-page .exec-table tbody td{
padding:10px 16px;
}
.executive-letters-page .exec-table tbody tr td:first-child,
.executive-letters-page .exec-table tbody tr td:last-child{
border-radius:0;
}
.executive-letters-page .exec-actions{
display:grid;
grid-template-columns:1fr;
}
.executive-letters-page .exec-btn{
width:100%;
justify-content:center;
}
.side-drawer{
width:100%;
max-width:100%;
}
.side-drawer .p-4{
padding:16px !important;
}
}
</style>
<div class="container-fluid px-4 py-4">
<div class="executive-letters-page">
<div class="exec-breadcrumb">
<a href="<?= htmlspecialchars($primaryNavHref) ?>">Dashboard</a>
<i class="fa-solid fa-chevron-right"></i>
<span>Allocation Letters</span>
</div>
<div class="exec-shell">
<div class="card exec-hero">
<div class="card-body">
<div class="row g-4 align-items-center">
<div class="col-xl-8">
<h1 class="exec-hero-title"><?= htmlspecialchars($pageTitle) ?></h1>
<p class="exec-hero-subtitle"><?= htmlspecialchars($pageSubtitle) ?></p>
</div>
<div class="col-xl-4">
<div class="exec-nav-pills">
<a href="<?= htmlspecialchars($primaryNavHref) ?>" class="exec-nav-pill">
<i class="fa-solid fa-gauge-high"></i>
<span><?= htmlspecialchars($primaryNavLabel) ?></span>
</a>
<a href="<?= htmlspecialchars($secondaryNavHref) ?>" class="exec-nav-pill">
<i class="fa-solid fa-list-check"></i>
<span><?= htmlspecialchars($secondaryNavLabel) ?></span>
</a>
<a href="<?= htmlspecialchars($allocationLettersBaseHref) ?>" class="exec-nav-pill active">
<i class="fa-solid fa-file-signature"></i>
<span>Allocation Letters</span>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="exec-stat-grid">
<a class="exec-stat-card pending" href="allocation-letters.php?view=executive&status=pending">
<div class="exec-stat-label"><span class="exec-stat-icon"><i class="fa-solid fa-hourglass-half"></i></span>Pending Review</div>
<div class="exec-stat-value"><?= number_format((int)$dashboardCounts['pending']) ?></div>
<div class="exec-stat-meta">Priority items needing chairman attention</div>
</a>
<a class="exec-stat-card approved" href="allocation-letters.php?view=executive&status=approved">
<div class="exec-stat-label"><span class="exec-stat-icon"><i class="fa-solid fa-circle-check"></i></span>Approved</div>
<div class="exec-stat-value"><?= number_format((int)$dashboardCounts['approved']) ?></div>
<div class="exec-stat-meta">Allocation letters already cleared for workflow progression</div>
</a>
<a class="exec-stat-card completed" href="allocation-letters.php?view=executive&status=completed">
<div class="exec-stat-label"><span class="exec-stat-icon"><i class="fa-solid fa-flag-checkered"></i></span>Completed</div>
<div class="exec-stat-value"><?= number_format((int)$dashboardCounts['completed']) ?></div>
<div class="exec-stat-meta">Documents fully closed and issued</div>
</a>
<a class="exec-stat-card total" href="allocation-letters.php?view=executive&status=all">
<div class="exec-stat-label"><span class="exec-stat-icon"><i class="fa-solid fa-layer-group"></i></span>Total Queue</div>
<div class="exec-stat-value"><?= number_format((int)$dashboardCounts['total']) ?></div>
<div class="exec-stat-meta">Executive review records in the current dashboard view</div>
</a>
</div>
<?php if (!empty($_SESSION['error_msg'])): ?>
<div class="alert alert-warning exec-alert"><?= htmlspecialchars($_SESSION['error_msg']); unset($_SESSION['error_msg']); ?></div>
<?php endif; ?>
<?php if (!empty($_SESSION['success_msg'])): ?>
<div class="alert alert-success exec-alert"><?= htmlspecialchars($_SESSION['success_msg']); unset($_SESSION['success_msg']); ?></div>
<?php endif; ?>
<div class="exec-board">
<div class="exec-board-head">
<div class="exec-board-title">
<div>
<h2><?= htmlspecialchars($tableTitle) ?></h2>
<p>Search the queue, filter decisions, and move quickly on high-priority allocation letters.</p>
</div>
<div class="exec-live-count">
<i class="fa-solid fa-list-ul"></i>
<span><span id="visibleAllocationCount"><?= number_format((int)$dashboardCounts['total']) ?></span> items visible</span>
</div>
</div>
<div class="exec-toolbar">
<div class="exec-control">
<i class="fa-solid fa-magnifying-glass"></i>
<input type="search" id="allocationSearchInput" class="exec-input" placeholder="Search by client name or allocation ID">
</div>
<div>
<select id="allocationStatusFilter" class="exec-select">
<option value="all">All Statuses</option>
<option value="pending">Pending Review</option>
<option value="approved">Approved</option>
<option value="completed">Completed</option>
<option value="rejected">Rejected</option>
</select>
</div>
<div>
<input type="date" id="allocationDateFilter" class="exec-input">
</div>
</div>
</div>
<?php if (!empty($rows)): ?>
<div class="exec-table-wrap">
<div class="table-responsive">
<table class="table exec-table align-middle" id="executiveAllocationTable">
<thead>
<tr>
<th>Allocation ID</th>
<th>Estate + Plot</th>
<th>Client</th>
<th>Status</th>
<th>Payment</th>
<th>Progress</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<?php
$rawStatus = strtolower((string)($r['status'] ?? ''));
$canSendForApproval = $canSendToExecutive && !in_array($rawStatus, ['pending_executive_approval','executive_approved','completed','released','released_to_client'], true);
$canDecide = ($r['status_key'] ?? '') === 'pending';
$clientLabel = $r['client_name'] ?? ('Client #'.(int)($r['user_id'] ?? 0));
$propertyLabel = $r['property_title'] ?? ('Property #'.(int)($r['property_id'] ?? 0));
$estateLabel = trim((string)($r['estate_name'] ?? '')) !== '' ? (string)$r['estate_name'] : $propertyLabel;
$plotBits = [];
if (!empty($r['plot_number'])) { $plotBits[] = 'Plot ' . (string)$r['plot_number']; }
if (!empty($r['plot_size'])) { $plotBits[] = (string)$r['plot_size']; }
$plotLine = $plotBits ? implode(' • ', $plotBits) : '';
$price = (float)($r['total_price'] ?? 0);
$paid = (float)($r['total_paid'] ?? 0);
$balance = max(0.0, $price - $paid);
$percent = $price > 0 ? min(100, (int)round(($paid / $price) * 100)) : 0;
$barTone = $percent >= 100 ? 'success' : (($percent > 0) ? 'warning' : 'danger');
$fmtPaid = function_exists('formatCurrency') ? formatCurrency($paid) : ('₦' . number_format($paid, 2));
$fmtPrice = function_exists('formatCurrency') ? formatCurrency($price) : ('₦' . number_format($price, 2));
$fmtBal = function_exists('formatCurrency') ? formatCurrency($balance) : ('₦' . number_format($balance, 2));
$searchBlob = strtolower(trim((string)$clientLabel . ' ' . (string)$propertyLabel . ' ' . (string)$estateLabel . ' ' . (string)$plotLine . ' #' . (int)$r['allocation_id'] . ' ' . (int)$r['allocation_id']));
$rowDate = '';
if (!empty($r['allocation_date'])) {
try {
$rowDate = date('Y-m-d', strtotime((string)$r['allocation_date']));
} catch (Throwable $e) {
$rowDate = '';
}
}
?>
<tr class="<?= !empty($r['is_pending_review']) ? 'pending-review' : '' ?><?= $focusedAllocationId === (int)$r['allocation_id'] ? ' is-focused' : '' ?>" data-allocation-id="<?= (int)$r['allocation_id'] ?>" data-search="<?= htmlspecialchars($searchBlob) ?>" data-status="<?= htmlspecialchars((string)($r['status_key'] ?? 'other')) ?>" data-date="<?= htmlspecialchars($rowDate) ?>" data-detail-url="allocation-letters.php?view=<?= urlencode($viewMode) ?>&action=details&allocation_id=<?= (int)$r['allocation_id'] ?>">
<td>
<div class="exec-entity-title">#<?= (int)$r['allocation_id'] ?></div>
<div class="exec-entity-meta"><?= !empty($r['allocation_date']) ? date('M j, Y', strtotime((string)$r['allocation_date'])) : 'Date not available' ?></div>
</td>
<td>
<div class="exec-entity-title"><?= htmlspecialchars($estateLabel) ?></div>
<div class="exec-entity-meta"><?= htmlspecialchars($plotLine !== '' ? $plotLine : $propertyLabel) ?></div>
<?php if ($plotLine !== '' && $propertyLabel !== $estateLabel): ?>
<div class="exec-entity-meta"><?= htmlspecialchars($propertyLabel) ?></div>
<?php endif; ?>
</td>
<td>
<div class="exec-entity-title"><?= htmlspecialchars($clientLabel) ?></div>
<div class="exec-entity-meta">Client allocation profile</div>
</td>
<td>
<span class="exec-badge <?= htmlspecialchars((string)($r['status_tone'] ?? 'neutral')) ?>"><?= htmlspecialchars((string)($r['status_label'] ?? '-')) ?></span>
</td>
<td>
<div class="exec-entity-title"><?= htmlspecialchars($fmtPaid) ?></div>
<div class="exec-entity-meta">of <?= htmlspecialchars($fmtPrice) ?> • Bal <?= htmlspecialchars($fmtBal) ?></div>
</td>
<td style="min-width:140px;">
<div class="d-flex justify-content-between small text-muted mb-1">
<span><?= (int)$percent ?>%</span>
</div>
<div class="progress" style="height:6px; background:#eef2f7;">
<div class="progress-bar bg-<?= htmlspecialchars($barTone) ?>" role="progressbar" style="width: <?= (int)$percent ?>%"></div>
</div>
</td>
<td>
<div class="exec-actions">
<a class="exec-btn exec-btn-secondary" target="_blank" href="allocation-letters.php?view=<?= urlencode($viewMode) ?>&action=preview&allocation_id=<?= (int)$r['allocation_id'] ?>&force=1">
<i class="fa-regular fa-eye"></i>
<span><?= $isExecutiveViewer ? 'Preview' : 'Preview Draft' ?></span>
</a>
<?php if (!$isExecutiveViewer): ?>
<button type="button" class="exec-btn exec-btn-secondary" onclick="openAllocationDetailsDrawer(<?= (int)$r['allocation_id'] ?>); event.stopPropagation();">
<i class="fa-solid fa-circle-info"></i>
<span>Allocation Details</span>
</button>
<a class="exec-btn exec-btn-secondary" target="_blank" href="allocation-letters.php?view=<?= urlencode($viewMode) ?>&action=details&allocation_id=<?= (int)$r['allocation_id'] ?>">
<i class="fa-solid fa-file-lines"></i>
<span>Letter Details</span>
</a>
<?php endif; ?>
<?php if ($isExecutiveViewer): ?>
<?php if ($canDecide): ?>
<button type="button" class="exec-btn exec-btn-approve" data-bs-toggle="modal" data-bs-target="#execDecisionModal" data-id="<?= (int)$r['allocation_id'] ?>" data-decision="approve" data-client="<?= htmlspecialchars((string)$clientLabel) ?>">
<i class="fa-solid fa-circle-check"></i>
<span>Approve</span>
</button>
<button type="button" class="exec-btn exec-btn-secondary" data-bs-toggle="modal" data-bs-target="#execDecisionModal" data-id="<?= (int)$r['allocation_id'] ?>" data-decision="request_changes" data-client="<?= htmlspecialchars((string)$clientLabel) ?>">
<i class="fa-solid fa-rotate-left"></i>
<span>Request Changes</span>
</button>
<button type="button" class="exec-btn exec-btn-reject" data-bs-toggle="modal" data-bs-target="#execDecisionModal" data-id="<?= (int)$r['allocation_id'] ?>" data-decision="reject" data-client="<?= htmlspecialchars((string)$clientLabel) ?>">
<i class="fa-solid fa-ban"></i>
<span>Reject</span>
</button>
<?php endif; ?>
<?php else: ?>
<?php if ($canManageApproval): ?>
<form action="generate_document.php" method="POST" target="_blank" class="d-inline">
<input type="hidden" name="doc_type" value="allocation_letter">
<input type="hidden" name="target_id" value="<?= (int)$r['allocation_id'] ?>">
<button type="submit" class="exec-btn exec-btn-secondary">
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Generate Draft</span>
</button>
</form>
<?php endif; ?>
<?php if ($canSendForApproval): ?>
<form method="post" class="d-inline" onsubmit="return confirm('Send this allocation letter to the executive dashboard for approval?');">
<input type="hidden" name="form_action" value="send_for_approval">
<input type="hidden" name="view" value="<?= htmlspecialchars($viewMode) ?>">
<input type="hidden" name="allocation_id" value="<?= (int)$r['allocation_id'] ?>">
<button type="submit" class="exec-btn exec-btn-primary-dark">
<i class="fa-solid fa-paper-plane"></i>
<span>Send for Approval</span>
</button>
</form>
<?php elseif ($rawStatus === 'pending_executive_approval'): ?>
<button type="button" class="exec-btn exec-btn-secondary" disabled>
<i class="fa-solid fa-hourglass-half"></i>
<span>Pending Executive Review</span>
</button>
<?php elseif ($rawStatus === 'executive_approved'): ?>
<form method="post" class="d-inline" onsubmit="return confirm('Send this approved allocation letter to the client dashboard?');">
<input type="hidden" name="form_action" value="release_to_client">
<input type="hidden" name="view" value="<?= htmlspecialchars($viewMode) ?>">
<input type="hidden" name="allocation_id" value="<?= (int)$r['allocation_id'] ?>">
<button type="submit" class="exec-btn exec-btn-approve">
<i class="fa-solid fa-paper-plane"></i>
<span>Send to Client Dashboard</span>
</button>
</form>
<?php elseif (in_array($rawStatus, ['completed','released_to_client'], true)): ?>
<button type="button" class="exec-btn exec-btn-secondary" disabled>
<i class="fa-solid fa-check-double"></i>
<span>Released to Client</span>
</button>
<?php endif; ?>
<?php endif; ?>
<?php if (colx('allocations','letter_issued_at')): ?>
<form method="post" action="ajax_update_document_status.php" class="d-inline">
<input type="hidden" name="entity" value="allocation_letter">
<input type="hidden" name="allocation_id" value="<?= (int)$r['allocation_id'] ?>">
<input type="hidden" name="action" value="mark_issued">
<button class="exec-btn exec-btn-secondary" type="submit">
<i class="fa-solid fa-check-double"></i>
<span>Mark Issued</span>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<div class="exec-empty" id="allocationEmptyState" <?= !empty($rows) ? 'hidden' : '' ?>>
<i class="fa-solid fa-file-circle-check"></i>
<div>
<strong>No allocation letters pending review</strong>
<div>No new executive review items match the current queue or filter selection.</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="side-drawer-backdrop" id="drawerBackdrop" onclick="closeAllocationDetailsDrawer()"></div>
<div class="side-drawer" id="sideDrawer">
<div class="p-4 border-bottom d-flex justify-content-between align-items-center bg-white sticky-top">
<div>
<h5 class="mb-1 fw-bold text-contrast">Allocation Details</h5>
<span class="badge bg-light text-dark border me-2" id="detailId">#Loading...</span>
<span id="detailStatus"></span>
</div>
<button type="button" class="btn-close" onclick="closeAllocationDetailsDrawer()"></button>
</div>
<div class="p-4 flex-grow-1" id="drawerContent">
<div class="text-center py-5"><div class="spinner-border text-navy" role="status"></div></div>
</div>
<div class="p-4 border-top bg-light mt-auto sticky-bottom">
<div class="d-grid gap-2 d-md-flex justify-content-md-end" id="drawerActions">
<button class="btn btn-outline-secondary" onclick="closeAllocationDetailsDrawer()">Close</button>
</div>
</div>
</div>
<div class="modal fade" id="billingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<form class="modal-content" id="billingForm">
<div class="modal-header">
<div>
<h5 class="modal-title mb-0">Edit Billing</h5>
<div class="text-muted small" id="billingModalSub">Allocation #—</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="form_action" value="save_billing">
<input type="hidden" name="xhr" value="1">
<input type="hidden" name="allocation_id" id="billingAllocationId" value="">
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label" id="billLandCostLabel">Land Cost</label>
<input type="number" step="0.01" min="0" class="form-control" name="land_cost" id="billLandCost">
</div>
<div class="col-12 col-md-6">
<label class="form-label">VAT %</label>
<input type="number" step="0.01" min="0" class="form-control" name="vat_percent" id="billVatPercent">
</div>
<div class="col-12">
<label class="form-label">Infrastructure Charge Mode</label>
<div class="d-flex gap-3 flex-wrap">
<div class="form-check">
<input class="form-check-input" type="radio" name="infra_mode" id="infraModePercent" value="percent" checked>
<label class="form-check-label" for="infraModePercent">Percentage of base cost</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="infra_mode" id="infraModeFixed" value="fixed">
<label class="form-check-label" for="infraModeFixed">Fixed amount</label>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Infrastructure %</label>
<input type="number" step="0.01" min="0" class="form-control" name="infra_percent" id="billInfraPercent">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Infrastructure Amount</label>
<input type="number" step="0.01" min="0" class="form-control" name="infra_amount" id="billInfraAmount">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Excavation Fee</label>
<input type="number" step="0.01" min="0" class="form-control" name="excavation_fee" id="billExcavationFee">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Construction Supervision</label>
<input type="number" step="0.01" min="0" class="form-control" name="construction_supervision" id="billSupervision">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Approval Fee</label>
<input type="number" step="0.01" min="0" class="form-control" name="approval_fee" id="billApprovalFee">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Application Form Fee</label>
<input type="number" step="0.01" min="0" class="form-control" name="application_form_fee" id="billApplicationFee">
</div>
<div class="col-12">
<div class="fw-bold small text-uppercase text-muted">Optional Items (show in letter only if enabled)</div>
</div>
<div class="col-12 col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="include_fence" id="includeFence">
<label class="form-check-label" for="includeFence">Fence / Gate</label>
</div>
<input type="number" step="0.01" min="0" class="form-control" name="fence_gate_cost" id="billFenceCost" placeholder="Fence / Gate cost">
</div>
<div class="col-12 col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="include_carcass" id="includeCarcass">
<label class="form-check-label" for="includeCarcass">Carcass</label>
</div>
<input type="number" step="0.01" min="0" class="form-control" name="carcass_cost" id="billCarcassCost" placeholder="Carcass cost">
</div>
<div class="col-12 col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="include_exterior_finishing" id="includeExterior">
<label class="form-check-label" for="includeExterior">Exterior Finishing</label>
</div>
<input type="number" step="0.01" min="0" class="form-control" name="exterior_finishing_cost" id="billExteriorCost" placeholder="Exterior finishing cost">
</div>
<div class="col-12 col-md-6">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="include_dpc" id="includeDpc">
<label class="form-check-label" for="includeDpc">DPC</label>
</div>
<input type="number" step="0.01" min="0" class="form-control" name="dpc_cost" id="billDpcCost" placeholder="DPC cost">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Payment Plan</label>
<select class="form-select" id="paymentPlanPreset">
<option value="3">3 months</option>
<option value="6">6 months</option>
<option value="custom">Custom</option>
</select>
<input type="number" step="1" min="1" class="form-control mt-2" name="payment_plan_months" id="paymentPlanMonths" value="3">
</div>
<div class="col-12 col-md-6">
<label class="form-label">Grand Total (auto)</label>
<input type="text" class="form-control" id="billGrandTotal" value="₦0.00" readonly>
<div class="text-muted small mt-1">Subtotal and VAT are calculated automatically.</div>
</div>
</div>
<div class="alert alert-danger mt-3 d-none" id="billingError"></div>
<div class="alert alert-success mt-3 d-none" id="billingSuccess">Saved.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" id="billingSaveBtn">Save Billing</button>
</div>
</form>
</div>
</div>
<?php if ($isExecutiveViewer): ?>
<div class="modal fade executive-letters-page exec-modal" id="execDecisionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form method="post" action="<?= htmlspecialchars($allocationLettersBaseHref . '&status=' . urlencode($statusPreset)) ?>" class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title mb-1" id="execDecisionTitle">Confirm Decision</h5>
<div class="text-muted small" id="execDecisionSubtitle">Review the selected allocation action before continuing.</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="exec_action" value="1">
<input type="hidden" name="allocation_id" id="execDecisionAllocationId">
<input type="hidden" name="decision" id="execDecisionValue">
<div class="alert alert-light border mb-3" id="execDecisionContext">You are about to confirm an executive decision for this allocation letter.</div>
<div class="mb-3">
<label class="form-label fw-semibold">Comment</label>
<textarea name="comment" class="form-control" rows="4" required placeholder="Provide a clear approval or rejection reason"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="exec-btn exec-btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="exec-btn exec-btn-primary-dark" id="execDecisionSubmit">
<i class="fa-solid fa-circle-check"></i>
<span>Confirm Action</span>
</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<script>
(function(){
const CAN_EDIT_BILLING = <?= $canManageApproval && $viewMode === 'admin' ? 'true' : 'false' ?>;
function getStatusBadgeHTML(status) {
const map = {
pending: { icon: 'fa-clock', cls: 'status-badge-pending', label: 'Pending' },
pending_executive_approval: { icon: 'fa-clock', cls: 'status-badge-pending', label: 'Pending' },
approved: { icon: 'fa-check-circle', cls: 'status-badge-approved', label: 'Approved' },
executive_approved: { icon: 'fa-check-circle', cls: 'status-badge-approved', label: 'Approved' },
allocated: { icon: 'fa-check-double', cls: 'status-badge-paid', label: 'Allocated' },
finalized: { icon: 'fa-lock', cls: 'status-badge-approved', label: 'Finalized' },
completed: { icon: 'fa-lock', cls: 'status-badge-approved', label: 'Completed' },
revoked: { icon: 'fa-ban', cls: 'status-badge-rejected', label: 'Revoked' },
rejected: { icon: 'fa-circle-xmark', cls: 'status-badge-rejected', label: 'Rejected' }
};
const key = String(status || '').toLowerCase().replace(/\s+/g, '_');
const cfg = map[key];
if (!cfg) return `<span class="badge bg-secondary text-white">${String(status || 'Unknown')}</span>`;
return `<span class="status-badge ${cfg.cls}"><i class="fa-solid ${cfg.icon}"></i> ${cfg.label}</span>`;
}
function fmtMoney(v) {
const n = Number(v) || 0;
return '₦' + n.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
window.openAllocationDetailsDrawer = function(id, openBilling) {
const allocId = Number(id) || 0;
if (!allocId) return;
const shouldOpenBilling = Boolean(openBilling);
const backdrop = document.getElementById('drawerBackdrop');
const drawer = document.getElementById('sideDrawer');
const content = document.getElementById('drawerContent');
const detailId = document.getElementById('detailId');
const detailStatus = document.getElementById('detailStatus');
if (detailId) detailId.textContent = '#Loading...';
if (detailStatus) detailStatus.innerHTML = '';
if (content) content.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-navy" role="status"></div></div>';
if (backdrop) backdrop.classList.add('show');
if (drawer) drawer.classList.add('open');
document.body.style.overflow = 'hidden';
try { if (drawer) drawer.setAttribute('aria-hidden', 'false'); } catch(e) {}
fetch('ajax_get_allocation_details.php?id=' + encodeURIComponent(String(allocId)), { credentials: 'same-origin' })
.then(function(res){ return res.json(); })
.then(function(data){
if (!data || !data.success) {
const err = (data && data.error) ? data.error : 'Failed to load details.';
if (content) content.innerHTML = '<div class="alert alert-danger">' + String(err) + '</div>';
return;
}
renderAllocationDetails(data, { openBilling: shouldOpenBilling });
})
.catch(function(){
if (content) content.innerHTML = '<div class="alert alert-danger">Failed to load details.</div>';
});
};
window.closeAllocationDetailsDrawer = function() {
const backdrop = document.getElementById('drawerBackdrop');
const drawer = document.getElementById('sideDrawer');
if (backdrop) backdrop.classList.remove('show');
if (drawer) drawer.classList.remove('open');
document.body.style.overflow = '';
try { if (drawer) drawer.setAttribute('aria-hidden', 'true'); } catch(e) {}
setTimeout(function(){
const content = document.getElementById('drawerContent');
const detailId = document.getElementById('detailId');
const detailStatus = document.getElementById('detailStatus');
if (content) content.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-navy" role="status"></div></div>';
if (detailId) detailId.textContent = '#...';
if (detailStatus) detailStatus.innerHTML = '';
}, 300);
};
document.addEventListener('keydown', function(e){
if (e && e.key === 'Escape') {
const drawer = document.getElementById('sideDrawer');
if (drawer && drawer.classList.contains('open')) {
window.closeAllocationDetailsDrawer();
}
}
});
function renderAllocationDetails(data, opts) {
const openBilling = Boolean(opts && opts.openBilling);
const alloc = data.allocation || {};
const payments = Array.isArray(data.payments) ? data.payments : [];
const docs = Array.isArray(data.documents) ? data.documents : [];
const history = Array.isArray(data.edit_history) ? data.edit_history : [];
const timeline = Array.isArray(data.timeline) ? data.timeline : [];
const summary = data.payment_summary || {};
const cost = data.cost_breakdown || {};
const letter = (data && typeof data.letter_data === 'object' && data.letter_data) ? data.letter_data : {};
const statusKey = String(alloc.status || '').toLowerCase().replace(/\s+/g, '_');
const total = Number(summary.total_price ?? alloc.property_price ?? 0) || 0;
const paid = Number(summary.total_paid ?? 0) || 0;
const outstanding = Number(summary.outstanding ?? (total - paid)) || 0;
const percent = total > 0 ? Math.min(100, Math.round((paid / total) * 100)) : 0;
const barTone = percent >= 100 ? 'success' : (percent > 0 ? 'warning' : 'danger');
const detailId = document.getElementById('detailId');
const detailStatus = document.getElementById('detailStatus');
if (detailId) detailId.textContent = '#' + String(alloc.id || '').padStart(6, '0');
if (detailStatus) detailStatus.innerHTML = getStatusBadgeHTML(statusKey);
const avatar = alloc.client_photo_b64 || alloc.client_photo || '';
const clientInitial = String(alloc.client_name || 'C').charAt(0).toUpperCase();
const clientPhotoHtml = avatar
? `<img src="${avatar}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;">`
: `<div class="avatar-ring" style="width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;">${clientInitial}</div>`;
const propTitle = String(alloc.property_title || '—');
const propAddr = String(alloc.property_address || alloc.location || '—');
const unit = alloc.unit_number ? String(alloc.unit_number) : '';
const plot = alloc.plot_number ? String(alloc.plot_number) : '';
const buildingNumber = alloc.building_number ? String(alloc.building_number) : '';
const houseType = alloc.house_type ? String(alloc.house_type) : '';
const buildingUse = alloc.building_use ? String(alloc.building_use) : '';
const areaSqm = alloc.area_sqm ? String(alloc.area_sqm) : (alloc.space_size ? String(alloc.space_size) : '');
const decisionKeyRaw = String(letter.chairman_decision || alloc.exec_decision || '').toLowerCase();
const decisionKey = (decisionKeyRaw === 'approve' || decisionKeyRaw === 'approved') ? 'approved'
: (decisionKeyRaw === 'request_changes' || decisionKeyRaw === 'changes_requested' || decisionKeyRaw === 'returned_for_correction') ? 'changes_requested'
: (decisionKeyRaw === 'reject' || decisionKeyRaw === 'rejected' || decisionKeyRaw === 'declined' || decisionKeyRaw === 'revoked') ? 'rejected'
: '';
const decisionLabel = decisionKey === 'approved' ? 'Approved'
: (decisionKey === 'changes_requested' ? 'Changes Requested' : (decisionKey === 'rejected' ? 'Rejected' : ''));
const decisionAtRaw = letter.chairman_decided_at || alloc.exec_decided_at || '';
const decisionAt = decisionAtRaw ? new Date(decisionAtRaw).toLocaleString() : '';
const decisionComment = String(letter.chairman_comment || alloc.exec_comment || alloc.review_comment || '').trim();
const feedbackBadge = decisionKey ? getStatusBadgeHTML(decisionKey) : '';
const feedbackHtml = (decisionComment || decisionLabel)
? `
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Executive Feedback</h6>
<div class="card border p-3 rounded-3">
<div class="d-flex justify-content-between align-items-start">
<div>
${decisionLabel ? `<div class="fw-bold">${decisionLabel}</div>` : `<div class="fw-bold">Feedback</div>`}
${decisionAt ? `<div class="text-muted small">${decisionAt}</div>` : ``}
</div>
<div>${feedbackBadge}</div>
</div>
${decisionComment ? `<div class="mt-2 small" style="white-space:pre-wrap;">${decisionComment.replace(/</g,'<').replace(/>/g,'>')}</div>` : `<div class="mt-2 small text-muted fst-italic">No note provided.</div>`}
</div>
</div>
`
: '';
const paymentsRows = payments.length
? payments.slice(0, 10).map(function(p){
const dt = p.date ? new Date(p.date).toLocaleDateString() : '';
const method = p.method || p.payment_method || 'Transfer';
const amt = fmtMoney(p.amount);
const st = getStatusBadgeHTML(p.status || '');
return `<tr><td class="text-muted">${dt}</td><td>${method}</td><td class="text-end fw-bold">${amt}</td><td class="text-end">${st}</td></tr>`;
}).join('')
: '<tr><td colspan="4" class="text-muted fst-italic">No payments recorded</td></tr>';
const docsRows = docs.length
? docs.slice(0, 10).map(function(d){
const t = d.type || 'document';
const title = d.title || '';
const st = getStatusBadgeHTML(d.status || '');
const open = d.file_path ? `<a href="${d.file_path}" target="_blank" class="btn btn-sm btn-light border">Open</a>` : '';
return `<tr><td class="text-muted">${t}</td><td class="fw-bold">${title}</td><td class="text-end">${st}</td><td class="text-end">${open}</td></tr>`;
}).join('')
: '<tr><td colspan="4" class="text-muted fst-italic">No documents linked</td></tr>';
const histRows = history.length
? history.slice(0, 10).map(function(h){
const who = h.edited_by_name || (h.edited_by ? ('Admin #' + h.edited_by) : '—');
const summaryTxt = h.summary || h.edit_note || '';
const when = h.edited_at ? new Date(h.edited_at).toLocaleString() : '';
return `<tr><td class="text-muted">${who}</td><td>${summaryTxt}</td><td class="text-end">${when}</td></tr>`;
}).join('')
: '<tr><td colspan="3" class="text-muted fst-italic">No edits recorded</td></tr>';
const timelineRows = timeline.length
? timeline.map(function(step){
const dt = step.date ? new Date(step.date).toLocaleDateString() : '--';
const role = step.role || '';
const label = step.step || '';
const cls = step.status || '';
return `<div class="timeline-item ${cls}"><div class="timeline-dot"></div><div class="d-flex justify-content-between"><span class="fw-bold small">${label}</span><span class="text-muted small" style="font-size:0.7rem;">${dt}</span></div><div class="small text-muted">${role}</div></div>`;
}).join('')
: '<div class="text-muted small fst-italic">No timeline available</div>';
const costHtml = `
<div class="row g-2 text-center">
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Land</div>
<div class="fw-bold small">${fmtMoney(cost.land_cost)}</div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Infrastructure</div>
<div class="fw-bold small">${fmtMoney(cost.infrastructure_charge ?? cost.infra_lease)}</div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">VAT</div>
<div class="fw-bold small">${fmtMoney(cost.vat)}</div>
</div>
</div>
<div class="col-12 mt-2">
<div class="p-2 border rounded bg-success bg-opacity-10 border-success">
<div class="small text-success">Grand Total</div>
<div class="fw-bold small text-success">${fmtMoney(cost.grand_total)}</div>
</div>
</div>
</div>
`;
const html = `
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Client</h6>
<div class="card border p-3 rounded-3">
<div class="d-flex align-items-center">
<div class="me-3">${clientPhotoHtml}</div>
<div>
<div class="fw-bold">${String(alloc.client_name || '—')}</div>
<div class="text-muted small">${String(alloc.client_email || '')}</div>
<div class="text-muted small">${String(alloc.client_phone || '')}</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Property</h6>
<div class="card border p-3 rounded-3">
<div class="d-flex justify-content-between mb-2">
<div class="fw-bold">${propTitle}</div>
<div class="fw-bold">${fmtMoney(alloc.property_price || total)}</div>
</div>
<div class="text-muted small">${propAddr}</div>
<div class="row g-2 mt-2 small">
${buildingNumber ? `<div class="col-6"><span class="text-muted">Unit/Plot:</span> <span class="fw-semibold">${buildingNumber}</span></div>` : ''}
${houseType ? `<div class="col-6"><span class="text-muted">Type:</span> <span class="fw-semibold">${houseType}</span></div>` : ''}
${buildingUse ? `<div class="col-6"><span class="text-muted">Use:</span> <span class="fw-semibold">${buildingUse}</span></div>` : ''}
${areaSqm ? `<div class="col-6"><span class="text-muted">Size:</span> <span class="fw-semibold">${areaSqm}</span></div>` : ''}
${(unit || plot) && !buildingNumber ? `<div class="col-12"><span class="text-muted">Unit:</span> <span class="fw-semibold">${unit || plot}</span></div>` : ''}
</div>
</div>
</div>
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Allocation</h6>
<div class="card border p-3 rounded-3">
<div class="row g-2 small">
<div class="col-6"><span class="text-muted">Agent:</span> <span class="fw-semibold">${String(alloc.agent_name || '—')}</span></div>
<div class="col-6"><span class="text-muted">Status:</span> <span class="fw-semibold">${String(alloc.status || '—')}</span></div>
<div class="col-6"><span class="text-muted">Created:</span> <span class="fw-semibold">${alloc.created_at ? new Date(alloc.created_at).toLocaleString() : '—'}</span></div>
<div class="col-6"><span class="text-muted">Updated:</span> <span class="fw-semibold">${alloc.updated_at ? new Date(alloc.updated_at).toLocaleString() : '—'}</span></div>
</div>
</div>
</div>
${feedbackHtml}
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Cost Breakdown</h6>
<div class="card border p-3 rounded-3">${costHtml}</div>
</div>
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Payment</h6>
<div class="card border p-3 rounded-3">
<div class="row g-2 text-center">
<div class="col-4">
<div class="p-2 border rounded">
<div class="small text-muted">Total</div>
<div class="fw-bold small">${fmtMoney(total)}</div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded bg-success bg-opacity-10 border-success">
<div class="small text-success">Paid</div>
<div class="fw-bold small text-success">${fmtMoney(paid)}</div>
</div>
</div>
<div class="col-4">
<div class="p-2 border rounded bg-danger bg-opacity-10 border-danger">
<div class="small text-danger">Balance</div>
<div class="fw-bold small text-danger">${fmtMoney(outstanding)}</div>
</div>
</div>
</div>
<div class="mt-2">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Payment Progress</small>
<small class="fw-bold">${percent}%</small>
</div>
<div class="progress-premium">
<div class="progress-bar-premium progress-bar-${barTone}" role="progressbar" style="width:${percent}%"></div>
</div>
</div>
<div class="mt-3">
<small class="fw-bold">Recent Transactions</small>
<table class="table table-sm table-borderless small mt-1">
<tbody>${paymentsRows}</tbody>
</table>
</div>
</div>
</div>
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Documents</h6>
<div class="card border p-2 rounded-3">
<table class="table table-sm table-borderless small mb-0">
<tbody>${docsRows}</tbody>
</table>
</div>
</div>
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Edit History</h6>
<div class="card border p-2 rounded-3">
<table class="table table-sm table-borderless small mb-0">
<thead><tr><th>Admin</th><th>Change</th><th class="text-end">Time</th></tr></thead>
<tbody>${histRows}</tbody>
</table>
</div>
</div>
<div class="mb-4">
<h6 class="text-uppercase text-muted small fw-bold mb-3">Workflow Timeline</h6>
<div class="ps-2">${timelineRows}</div>
</div>
`;
const content = document.getElementById('drawerContent');
if (content) content.innerHTML = html;
const actions = document.getElementById('drawerActions');
if (actions) {
const base =
`<a class="btn btn-outline-dark" href="allocation-details.php?id=${encodeURIComponent(String(alloc.id || ''))}" target="_blank" rel="noopener"><i class="fa-solid fa-up-right-from-square me-2"></i>Open allocation details</a>`
+ `<button class="btn btn-outline-secondary" onclick="closeAllocationDetailsDrawer()">Close</button>`;
if (CAN_EDIT_BILLING) {
const st = String((alloc && alloc.status) ? alloc.status : '').toLowerCase();
const canSendForApproval = !['pending_executive_approval','pending','review','executive_approved','completed','released','finalized'].includes(st);
const canReleaseToClient = ['executive_approved','approved'].includes(st);
actions.innerHTML =
`<form method="post" action="allocation-letters.php" class="d-inline" style="margin:0;">
<input type="hidden" name="form_action" value="save_draft">
<input type="hidden" name="view" value="admin">
<input type="hidden" name="allocation_id" value="${encodeURIComponent(String(alloc.id || ''))}">
<button type="submit" class="btn btn-outline-dark"><i class="fa-solid fa-floppy-disk me-2"></i>Save Draft</button>
</form>`
+ `<a class="btn btn-outline-dark" href="allocation-letters.php?view=admin&action=letter_details&allocation_id=${encodeURIComponent(String(alloc.id || ''))}"><i class="fa-solid fa-file-pen me-2"></i>Letter Details</a>`
+ `<button class="btn btn-outline-primary" id="openBillingBtn"><i class="fa-solid fa-pen-to-square me-2"></i>Edit Billing</button>`
+ (canSendForApproval ? `<form method="post" action="allocation-letters.php" class="d-inline" style="margin:0;" onsubmit="return confirm('Send this allocation letter to the Chairman/Executive for approval?');">
<input type="hidden" name="form_action" value="send_for_approval">
<input type="hidden" name="view" value="admin">
<input type="hidden" name="allocation_id" value="${encodeURIComponent(String(alloc.id || ''))}">
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-paper-plane me-2"></i>Send to Chairman Approval</button>
</form>` : ``)
+ (canReleaseToClient ? `<form method="post" action="allocation-letters.php" class="d-inline" style="margin:0;" onsubmit="return confirm('Send this approved allocation letter to the Client Dashboard?');">
<input type="hidden" name="form_action" value="release_to_client">
<input type="hidden" name="view" value="admin">
<input type="hidden" name="allocation_id" value="${encodeURIComponent(String(alloc.id || ''))}">
<button type="submit" class="btn btn-success"><i class="fa-solid fa-paper-plane me-2"></i>Send to Client Dashboard</button>
</form>` : ``)
+ `<a class="btn btn-outline-success" target="_blank" href="allocation-letters.php?view=admin&action=preview&allocation_id=${encodeURIComponent(String(alloc.id || ''))}&force=1"><i class="fa-solid fa-eye me-2"></i>Preview Draft</a>`
+ `<a class="btn btn-outline-primary" target="_blank" href="allocation-letter-pdf.php?action=preview&allocation_id=${encodeURIComponent(String(alloc.id || ''))}®en=1"><i class="fa-solid fa-file-pdf me-2"></i>Preview PDF</a>`
+ `<a class="btn btn-success" target="_blank" href="allocation-letter-pdf.php?action=download&allocation_id=${encodeURIComponent(String(alloc.id || ''))}"><i class="fa-solid fa-download me-2"></i>Download PDF</a>`
+ `<a class="btn btn-outline-secondary" href="allocation-letters.php?view=admin"><i class="fa-solid fa-list me-2"></i>Back to List</a>`
+ base;
} else {
actions.innerHTML = base;
}
}
if (CAN_EDIT_BILLING) {
const btn = document.getElementById('openBillingBtn');
if (btn) {
btn.onclick = function() {
openBillingModal(alloc, data.billing_settings || {}, data.cost_breakdown || {});
};
}
}
if (CAN_EDIT_BILLING && openBilling) {
openBillingModal(alloc, data.billing_settings || {}, data.cost_breakdown || {});
}
}
function moneyNumber(v) {
const n = Number(v);
return isFinite(n) ? n : 0;
}
function computeBillingFromForm() {
const land = moneyNumber(document.getElementById('billLandCost').value);
const vatPct = moneyNumber(document.getElementById('billVatPercent').value);
const infraMode = document.getElementById('infraModeFixed').checked ? 'fixed' : 'percent';
const infraPct = moneyNumber(document.getElementById('billInfraPercent').value);
const infraAmt = moneyNumber(document.getElementById('billInfraAmount').value);
const excavation = moneyNumber(document.getElementById('billExcavationFee').value);
const supervision = moneyNumber(document.getElementById('billSupervision').value);
const approval = moneyNumber(document.getElementById('billApprovalFee').value);
const appForm = moneyNumber(document.getElementById('billApplicationFee').value);
const incFence = document.getElementById('includeFence').checked;
const incCarcass = document.getElementById('includeCarcass').checked;
const incExterior = document.getElementById('includeExterior').checked;
const incDpc = document.getElementById('includeDpc').checked;
const fence = incFence ? moneyNumber(document.getElementById('billFenceCost').value) : 0;
const carcass = incCarcass ? moneyNumber(document.getElementById('billCarcassCost').value) : 0;
const exterior = incExterior ? moneyNumber(document.getElementById('billExteriorCost').value) : 0;
const dpc = incDpc ? moneyNumber(document.getElementById('billDpcCost').value) : 0;
const infraCharge = infraMode === 'fixed' ? infraAmt : ((infraPct / 100) * land);
const sub = land + infraCharge + excavation + supervision + approval + appForm + fence + carcass + exterior + dpc;
const vat = (vatPct / 100) * sub;
const grand = sub + vat;
return { grand };
}
function syncInfraInputs() {
const isFixed = document.getElementById('infraModeFixed').checked;
document.getElementById('billInfraPercent').disabled = isFixed;
document.getElementById('billInfraAmount').disabled = !isFixed;
}
function updateGrandTotalPreview() {
const r = computeBillingFromForm();
const el = document.getElementById('billGrandTotal');
if (el) el.value = fmtMoney(r.grand);
}
function openBillingModal(alloc, billing, cost) {
const modalEl = document.getElementById('billingModal');
const bsModal = modalEl ? bootstrap.Modal.getOrCreateInstance(modalEl) : null;
const allocId = Number(alloc.id || 0) || 0;
document.getElementById('billingAllocationId').value = String(allocId);
const sub = document.getElementById('billingModalSub');
if (sub) sub.textContent = 'Allocation #' + String(allocId).padStart(6, '0');
const bu = String(alloc.property_type || alloc.building_use || alloc.buildingUse || alloc.type || '').toLowerCase();
const landLabel = (bu.includes('land'))
? 'Land Cost'
: ((bu.includes('commercial') || bu.includes('shop') || bu.includes('suite') || bu.includes('office')) ? 'Unit Cost' : 'Property Cost');
const lcLabel = document.getElementById('billLandCostLabel');
if (lcLabel) lcLabel.textContent = landLabel;
document.getElementById('billLandCost').value = moneyNumber(billing.land_cost ?? cost.land_cost ?? alloc.property_price ?? 0);
document.getElementById('billVatPercent').value = moneyNumber(billing.vat_percent ?? cost.vat_percent ?? 7.5);
const mode = String(billing.infra_mode || 'percent').toLowerCase();
document.getElementById('infraModeFixed').checked = mode === 'fixed';
document.getElementById('infraModePercent').checked = mode !== 'fixed';
document.getElementById('billInfraPercent').value = moneyNumber(billing.infra_percent ?? 20);
document.getElementById('billInfraAmount').value = moneyNumber(billing.infra_amount ?? 0);
document.getElementById('billExcavationFee').value = moneyNumber(billing.excavation_fee ?? 0);
document.getElementById('billSupervision').value = moneyNumber(billing.construction_supervision ?? 0);
document.getElementById('billApprovalFee').value = moneyNumber(billing.approval_fee ?? 0);
document.getElementById('billApplicationFee').value = moneyNumber(billing.application_form_fee ?? 0);
document.getElementById('includeFence').checked = Number(billing.include_fence ?? 0) === 1;
document.getElementById('includeCarcass').checked = Number(billing.include_carcass ?? 0) === 1;
document.getElementById('includeExterior').checked = Number(billing.include_exterior_finishing ?? 0) === 1;
document.getElementById('includeDpc').checked = Number(billing.include_dpc ?? 0) === 1;
document.getElementById('billFenceCost').value = moneyNumber(billing.fence_gate_cost ?? 0);
document.getElementById('billCarcassCost').value = moneyNumber(billing.carcass_cost ?? 0);
document.getElementById('billExteriorCost').value = moneyNumber(billing.exterior_finishing_cost ?? 0);
document.getElementById('billDpcCost').value = moneyNumber(billing.dpc_cost ?? 0);
const months = Math.max(1, Number(billing.payment_plan_months ?? 3) || 3);
document.getElementById('paymentPlanMonths').value = String(months);
const preset = (months === 3 || months === 6) ? String(months) : 'custom';
document.getElementById('paymentPlanPreset').value = preset;
document.getElementById('paymentPlanMonths').disabled = preset !== 'custom';
const err = document.getElementById('billingError');
const ok = document.getElementById('billingSuccess');
if (err) { err.classList.add('d-none'); err.textContent = ''; }
if (ok) { ok.classList.add('d-none'); }
syncInfraInputs();
updateGrandTotalPreview();
if (bsModal) bsModal.show();
}
const billingForm = document.getElementById('billingForm');
if (billingForm) {
const inputs = billingForm.querySelectorAll('input,select');
inputs.forEach(function(el){
el.addEventListener('input', function(){
if (el.id === 'paymentPlanPreset') return;
if (el.name === 'infra_mode') { syncInfraInputs(); }
updateGrandTotalPreview();
});
el.addEventListener('change', function(){
if (el.id === 'paymentPlanPreset') {
const v = el.value;
const monthsEl = document.getElementById('paymentPlanMonths');
if (v === 'custom') { monthsEl.disabled = false; }
else { monthsEl.disabled = true; monthsEl.value = v; }
updateGrandTotalPreview();
}
if (el.name === 'infra_mode') { syncInfraInputs(); updateGrandTotalPreview(); }
if (el.type === 'checkbox') { updateGrandTotalPreview(); }
});
});
billingForm.addEventListener('submit', async function(e){
e.preventDefault();
const err = document.getElementById('billingError');
const ok = document.getElementById('billingSuccess');
if (err) { err.classList.add('d-none'); err.textContent = ''; }
if (ok) { ok.classList.add('d-none'); }
const btn = document.getElementById('billingSaveBtn');
const original = btn ? btn.innerHTML : '';
if (btn) { btn.disabled = true; btn.innerHTML = 'Saving...'; }
try {
const fd = new FormData(billingForm);
const res = await fetch('allocation-letters.php', { method: 'POST', body: fd, credentials: 'same-origin' });
const json = await res.json().catch(function(){ return null; });
if (!json || !json.success) {
const msg = (json && json.error) ? json.error : 'Failed to save.';
if (err) { err.textContent = msg; err.classList.remove('d-none'); }
return;
}
const allocId = document.getElementById('billingAllocationId').value;
if (ok) {
ok.textContent = 'Saved. Opening letter preview...';
ok.classList.remove('d-none');
}
if (allocId) {
const data = await fetch('ajax_get_allocation_details.php?id=' + encodeURIComponent(String(allocId)), { credentials: 'same-origin' })
.then(function(r){ return r.json(); }).catch(function(){ return null; });
if (data && data.success) {
renderAllocationDetails(data);
}
try {
const modalEl = document.getElementById('billingModal');
const bsModal = modalEl ? bootstrap.Modal.getOrCreateInstance(modalEl) : null;
if (bsModal) { bsModal.hide(); }
} catch (e) {}
try {
const url = 'allocation-letters.php?view=admin&action=preview&allocation_id=' + encodeURIComponent(String(allocId)) + '&force=1';
window.open(url, '_blank');
} catch (e) {}
}
} catch (ex) {
if (err) { err.textContent = 'Failed to save.'; err.classList.remove('d-none'); }
} finally {
if (btn) { btn.disabled = false; btn.innerHTML = original; }
}
});
}
const searchInput = document.getElementById('allocationSearchInput');
const statusFilter = document.getElementById('allocationStatusFilter');
const dateFilter = document.getElementById('allocationDateFilter');
const table = document.getElementById('executiveAllocationTable');
const emptyState = document.getElementById('allocationEmptyState');
const visibleCounter = document.getElementById('visibleAllocationCount');
const rows = table ? Array.from(table.querySelectorAll('tbody tr')) : [];
const statusPreset = <?= json_encode($statusPreset) ?>;
const focusedAllocationId = <?= json_encode($focusedAllocationId) ?>;
const autoOpen = <?= json_encode(isset($_GET['auto_open']) && (string)$_GET['auto_open'] !== '0') ?>;
const autoBilling = <?= json_encode(isset($_GET['auto_billing']) && (string)$_GET['auto_billing'] !== '0') ?>;
function applyFilters() {
const term = (searchInput ? searchInput.value : '').trim().toLowerCase();
const status = statusFilter ? statusFilter.value : 'all';
const date = dateFilter ? dateFilter.value : '';
let visible = 0;
rows.forEach(function(row){
const rowSearch = (row.getAttribute('data-search') || '').toLowerCase();
const rowStatus = row.getAttribute('data-status') || '';
const rowDate = row.getAttribute('data-date') || '';
const matchesTerm = !term || rowSearch.indexOf(term) !== -1;
const matchesStatus = status === 'all' || rowStatus === status;
const matchesDate = !date || rowDate === date;
const show = matchesTerm && matchesStatus && matchesDate;
row.hidden = !show;
if (show) { visible += 1; }
});
if (visibleCounter) {
visibleCounter.textContent = visible.toLocaleString();
}
if (emptyState) {
emptyState.hidden = visible !== 0;
}
}
if (statusFilter && statusPreset) {
statusFilter.value = statusPreset;
}
if (searchInput) { searchInput.addEventListener('input', applyFilters); }
if (statusFilter) { statusFilter.addEventListener('change', applyFilters); }
if (dateFilter) { dateFilter.addEventListener('change', applyFilters); }
applyFilters();
if (focusedAllocationId) {
const focusedRow = rows.find(function(row){
return String(row.getAttribute('data-allocation-id') || '') === String(focusedAllocationId);
});
if (focusedRow && !focusedRow.hidden) {
setTimeout(function(){
focusedRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 120);
}
if (autoOpen) {
setTimeout(function(){
window.openAllocationDetailsDrawer(focusedAllocationId, autoBilling);
}, 220);
}
}
rows.forEach(function(row){
row.addEventListener('click', function(event){
if (event.target.closest('a, button, form, input, textarea, select, label')) {
return;
}
const allocId = row.getAttribute('data-allocation-id');
if (allocId) {
window.openAllocationDetailsDrawer(allocId);
}
});
});
const decisionModal = document.getElementById('execDecisionModal');
if (decisionModal) {
const titleEl = document.getElementById('execDecisionTitle');
const subtitleEl = document.getElementById('execDecisionSubtitle');
const contextEl = document.getElementById('execDecisionContext');
const allocationInput = document.getElementById('execDecisionAllocationId');
const decisionInput = document.getElementById('execDecisionValue');
const submitBtn = document.getElementById('execDecisionSubmit');
decisionModal.addEventListener('show.bs.modal', function (event) {
const trigger = event.relatedTarget;
if (!trigger) { return; }
const allocationId = trigger.getAttribute('data-id') || '';
const decision = trigger.getAttribute('data-decision') || 'approve';
const client = trigger.getAttribute('data-client') || 'this client';
const approveMode = decision === 'approve';
const requestMode = decision === 'request_changes';
if (allocationInput) allocationInput.value = allocationId;
if (decisionInput) decisionInput.value = decision;
if (titleEl) titleEl.textContent = approveMode ? 'Approve Allocation Letter' : (requestMode ? 'Request Changes' : 'Reject Allocation Letter');
if (subtitleEl) subtitleEl.textContent = approveMode
? 'This decision moves the document forward in the executive workflow.'
: (requestMode ? 'This returns the allocation letter to Admin for corrections.' : 'This decision sends the allocation back for corrective action.');
if (contextEl) {
const variant = approveMode ? 'alert-success' : (requestMode ? 'alert-warning' : 'alert-danger');
contextEl.className = 'alert mb-3 ' + variant;
contextEl.textContent = (approveMode ? 'Approve' : (requestMode ? 'Request changes for' : 'Reject')) + ' allocation #' + allocationId + ' for ' + client + '.';
}
if (submitBtn) {
submitBtn.className = 'exec-btn ' + (approveMode ? 'exec-btn-approve' : (requestMode ? 'exec-btn-secondary' : 'exec-btn-reject'));
submitBtn.innerHTML = approveMode
? '<i class="fa-solid fa-circle-check"></i><span>Confirm Approval</span>'
: (requestMode ? '<i class="fa-solid fa-rotate-left"></i><span>Request Changes</span>' : '<i class="fa-solid fa-ban"></i><span>Confirm Rejection</span>');
}
});
const modalForm = decisionModal.querySelector('form');
if (modalForm) {
let submitting = false;
modalForm.addEventListener('submit', async function(event){
if (submitting) { return; }
event.preventDefault();
const decision = (decisionInput && decisionInput.value) ? decisionInput.value : 'approve';
const requestMode = decision === 'request_changes';
const title = decision === 'approve' ? 'Approve Allocation Letter' : (requestMode ? 'Request Changes' : 'Reject Allocation Letter');
const message = decision === 'approve'
? 'Are you sure you want to approve this allocation letter?'
: (requestMode ? 'Request changes for this allocation letter?' : 'Are you sure you want to reject this allocation letter?');
const variant = decision === 'approve' ? 'success' : (requestMode ? 'warning' : 'danger');
const ok = window.showConfirm ? await window.showConfirm({ title, message, confirmText: 'Yes, continue', variant }) : window.confirm(message);
if (!ok) { return false; }
submitting = true;
modalForm.submit();
});
}
}
document.querySelectorAll('.js-finalize-allocation').forEach(function(button){
button.addEventListener('click', async function(event){
event.preventDefault();
const allocationId = button.getAttribute('data-id') || '';
const previewUrl = button.getAttribute('data-preview-url') || '';
if (!allocationId) { return; }
const ok = window.showConfirm
? await window.showConfirm({ title: 'Finalize Allocation Letter', message: 'This will approve, sign, and release the allocation letter to the client portal.', confirmText: 'Finalize now', variant: 'success' })
: window.confirm('Finalize and release this allocation letter?');
if (!ok) { return; }
const originalHtml = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i><span>Finalizing...</span>';
try {
const response = await fetch('start-approval.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'allocation_id=' + encodeURIComponent(allocationId)
});
const result = await response.json().catch(function(){ return null; });
if (!result || !result.success) {
const blockers = result && Array.isArray(result.blockers) && result.blockers.length
? '\n - ' + result.blockers.join('\n - ')
: '';
window.alert('Final approval was blocked.' + blockers);
return;
}
if (previewUrl) {
window.open(previewUrl, '_blank', 'noopener');
}
window.location.reload();
} catch (error) {
window.alert('Final approval failed.');
} finally {
button.disabled = false;
button.innerHTML = originalHtml;
}
});
});
})();
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>