<?php
require_once 'auth.php';
include 'db.php';
requireLogin($conn);
$role = $_SESSION['role'] ?? 'user';
$user_department = currentUserDepartment($conn);
$draft_id = (int) ($_GET['id'] ?? 0);
$is_edit = $draft_id > 0;
$current_document_type = 'Memo Kelulusan';
if (!$is_edit) {
if (!currentUserCan($conn, 'draft_letter', 'create')) {
echo "<script>alert('Anda tidak mempunyai kebenaran untuk menyediakan surat.'); window.location='draft_letters.php';</script>";
exit();
}
}
$folder_sql = "SELECT f.id, f.folder_name, f.folder_code, f.department,
(SELECT ref_no FROM documents d WHERE d.folder_id = f.id AND d.deleted_at IS NULL ORDER BY d.id DESC LIMIT 1) AS latest_ref_no,
(SELECT COUNT(*) FROM documents d WHERE d.folder_id = f.id AND d.deleted_at IS NULL) AS document_count
FROM folders f
WHERE f.deleted_at IS NULL AND f.closed_at IS NULL AND COALESCE(f.folder_type, 'office') <> 'sendirian'";
$folder_sql .= " AND f.department = ?";
$folder_sql .= " ORDER BY f.department, f.folder_name";
$folder_stmt = mysqli_prepare($conn, $folder_sql);
mysqli_stmt_bind_param($folder_stmt, "s", $user_department);
mysqli_stmt_execute($folder_stmt);
$folder_rows = mysqli_fetch_all(mysqli_stmt_get_result($folder_stmt), MYSQLI_ASSOC);
$folders = [];
foreach ($folder_rows as $folder_row) {
$folder_row['next_ref_no'] = generateNextDocumentReference($folder_row['folder_code'] ?? '');
$folder_row['next_file_numbering'] = (string) ((int) ($folder_row['document_count'] ?? 0) + 1);
$folders[(int) $folder_row['id']] = $folder_row;
}
if (!$is_edit && empty($folders)) {
echo "<script>alert('Tiada fail office aktif ditemui untuk bahagian anda. Sila wujudkan fail dahulu.'); window.location='list_folder.php';</script>";
exit();
}
$document_types = [$current_document_type];
$letter_templates = getLetterTemplates($conn);
$memo_position_options = getMemoPositionOptions();
$departments_list = getDepartmentList();
$status_labels = getDraftLetterStatusList();
$current_user_id = (int) ($_SESSION['user_id'] ?? 0);
$department_head_users = getDepartmentHeadOfficerUsers($conn);
$routing_users = getMemoRoutingUsers($conn);
$routing_user_count_by_position = [];
$routing_user_profiles_by_position = [];
foreach ($routing_users as $routing_user) {
$routing_position = trim((string) ($routing_user['routing_position'] ?? ''));
if ($routing_position !== '') {
$routing_user_count_by_position[$routing_position] = ($routing_user_count_by_position[$routing_position] ?? 0) + 1;
$routing_user_profiles_by_position[$routing_position][] = [
'id' => (int) ($routing_user['id'] ?? 0),
'name' => trim((string) ($routing_user['fullname'] ?? '')),
'position' => trim((string) ($routing_user['position'] ?? $routing_position)),
];
}
}
function formatRoutingRegisteredUsersLabel($position, $profiles_by_position) {
$profiles = $profiles_by_position[$position] ?? [];
if (empty($profiles)) {
return 'Belum ada pengguna berdaftar';
}
$names = array_values(array_filter(array_map(function ($profile) {
return trim((string) ($profile['name'] ?? ''));
}, $profiles)));
return !empty($names) ? implode(', ', $names) : 'Belum ada pengguna berdaftar';
}
$current_user_row = null;
$current_user_stmt = mysqli_prepare($conn, "SELECT id, fullname, short_name, email, department, position FROM users WHERE id = ? LIMIT 1");
if ($current_user_stmt) {
mysqli_stmt_bind_param($current_user_stmt, "i", $current_user_id);
mysqli_stmt_execute($current_user_stmt);
$current_user_row = mysqli_fetch_assoc(mysqli_stmt_get_result($current_user_stmt));
}
$current_user_is_head_officer = isHeadOfficerPosition($current_user_row['position'] ?? '');
$attachment_map = [];
$draft = null;
$is_department_officer_revision_editor = false;
if ($is_edit) {
$draft_sql = "SELECT ld.*, f.folder_name, f.folder_code, f.department AS folder_department, d.id AS document_exists,
d.file_numbering,
creator.short_name AS created_by_short_name,
creator.fullname AS created_by_fullname,
(SELECT COUNT(*) FROM documents d2 WHERE d2.folder_id = ld.folder_id AND d2.deleted_at IS NULL AND d2.id <= ld.document_id) AS computed_file_numbering
FROM letter_drafts ld
INNER JOIN folders f ON f.id = ld.folder_id
LEFT JOIN documents d ON d.id = ld.document_id AND d.deleted_at IS NULL
LEFT JOIN users creator ON creator.id = ld.created_by
WHERE ld.id = ? AND ld.deleted_at IS NULL
LIMIT 1";
$draft_stmt = mysqli_prepare($conn, $draft_sql);
mysqli_stmt_bind_param($draft_stmt, "i", $draft_id);
mysqli_stmt_execute($draft_stmt);
$draft = mysqli_fetch_assoc(mysqli_stmt_get_result($draft_stmt));
if (!$draft) {
header("Location: draft_letters.php?error=not_found");
exit();
}
if (($draft['approval_status'] ?? '') === 'lulus') {
header("Location: draft_letter_print.php?id={$draft_id}");
exit();
}
$is_draft_creator = (int) ($draft['created_by'] ?? 0) === $current_user_id;
$is_department_officer_revision_editor = ($draft['approval_status'] ?? '') === 'sedang_dipinda_ketua_bahagian'
&& (int) ($draft['approval_stage'] ?? 0) === 1
&& (int) ($draft['department_officer_user_id'] ?? 0) === $current_user_id;
if (!currentUserCan($conn, 'draft_letter', 'update') && !$is_department_officer_revision_editor) {
echo "<script>alert('Anda tidak mempunyai kebenaran untuk mengubah surat ini.'); window.location='draft_letters.php';</script>";
exit();
}
$is_private_draft = ($draft['status'] ?? 'draf') === 'draf' || ($draft['approval_status'] ?? 'draft') === 'draft';
if ($is_private_draft && !$is_draft_creator && $role !== 'superadmin' && !$is_department_officer_revision_editor) {
header("Location: draft_letters.php?error=unauthorized");
exit();
}
if (($draft['approval_status'] ?? '') === 'sedang_dipinda_ketua_bahagian' && !$is_department_officer_revision_editor) {
header("Location: draft_letter_print.php?id={$draft_id}&support_error=revision_locked");
exit();
}
if (!isSameDepartmentDb($conn, $draft['department'] ?? '', $user_department)) {
header("Location: draft_letters.php?error=unauthorized");
exit();
}
if (!isset($folders[(int) $draft['folder_id']])) {
$folders[(int) $draft['folder_id']] = [
'id' => (int) $draft['folder_id'],
'folder_name' => $draft['folder_name'] ?? 'Fail',
'folder_code' => $draft['folder_code'] ?? '',
'department' => $draft['department'] ?? '',
'latest_ref_no' => $draft['ref_no'] ?? '',
'next_ref_no' => $draft['ref_no'] ?? '',
'next_file_numbering' => $draft['file_numbering'] ?? '',
];
}
$doc_map = getDocumentAttachmentsMap($conn, [['id' => (int) ($draft['document_id'] ?? 0)]]);
$attachment_map = $doc_map[(int) ($draft['document_id'] ?? 0)] ?? [];
}
$default_folder = reset($folders);
$selected_folder_id = $is_edit ? (int) ($draft['folder_id'] ?? 0) : (int) ($_GET['folder_id'] ?? ($default_folder['id'] ?? 0));
if (!isset($folders[$selected_folder_id]) && $default_folder) {
$selected_folder_id = (int) $default_folder['id'];
}
$selected_folder = $folders[$selected_folder_id] ?? $default_folder;
$current_user_signature_path = getUserSignaturePath($conn, (int) ($_SESSION['user_id'] ?? 0));
$selected_department_head_user = getDepartmentHeadOfficerByDepartment($conn, $selected_folder['department'] ?? ($current_user_row['department'] ?? ($_SESSION['department'] ?? '')));
$excluded_approval_route_positions = [];
if ($selected_department_head_user) {
$selected_head_routing_position = getMemoRoutingPositionForUser($selected_department_head_user);
if ($selected_head_routing_position !== '') {
$excluded_approval_route_positions[] = $selected_head_routing_position;
}
}
$prepared_by_short_name = trim((string) ($is_edit
? ($draft['created_by_short_name'] ?? ($draft['created_by_fullname'] ?? ''))
: ($current_user_row['short_name'] ?? ($_SESSION['short_name'] ?? ($_SESSION['fullname'] ?? '')))
));
$form_values = [
'folder_id' => $selected_folder_id,
'template_id' => (int) ($draft['template_id'] ?? ($letter_templates[0]['template_id'] ?? 0)),
'subject' => $draft['subject'] ?? '',
'letter_date' => $draft['letter_date'] ?? date('Y-m-d'),
'doc_type' => $draft['doc_type'] ?? $current_document_type,
'recipient_name' => $draft['recipient_name'] ?? '',
'recipient_organization' => $draft['recipient_organization'] ?? '',
'recipient_address' => $draft['recipient_address'] ?? '',
'attention_to' => $draft['attention_to'] ?? '',
'routing_positions' => decodeMemoRoutingPositions($draft['routing_positions'] ?? ''),
'greeting' => $draft['greeting'] ?? 'Tuan/Puan,',
'letter_body' => $draft['letter_body'] ?? '',
'letter_pages' => decodeDraftLetterPages($draft['letter_pages'] ?? '', $draft['letter_body'] ?? ''),
'approval_page_body' => $draft['approval_page_body'] ?? '',
'approval_route_bodies' => decodeDraftApprovalRouteBodies($draft['approval_route_bodies'] ?? ''),
'page_number_position' => in_array($draft['page_number_position'] ?? '', ['none', 'bottom_center', 'bottom_right'], true)
? $draft['page_number_position']
: 'none',
'page_header_text' => $draft['page_header_text'] ?? '',
'page_footer_text' => $draft['page_footer_text'] ?? '',
'closing_text' => $draft['closing_text'] ?? '',
'signatory_name' => $draft['signatory_name'] ?? ($selected_department_head_user['fullname'] ?? ($_SESSION['fullname'] ?? '')),
'signatory_position' => $draft['signatory_position'] ?? ($selected_department_head_user['position'] ?? getMemoDepartmentHeadPosition($selected_folder['department'] ?? ($current_user_row['department'] ?? ($_SESSION['department'] ?? '')))),
'signatory_department' => $draft['signatory_department'] ?? ($selected_department_head_user['department'] ?? ($selected_folder['department'] ?? ($_SESSION['department'] ?? ''))),
'signatory_signature_path' => $draft['signatory_signature_path'] ?? '',
'status' => $draft['status'] ?? 'siap',
'support_user_id' => (int) ($draft['support_user_id'] ?? 0),
'approval_flow' => 'two_level',
'department_officer_user_id' => (int) ($draft['department_officer_user_id'] ?? 0),
];
if ($selected_department_head_user && (!$is_edit || empty($form_values['signatory_signature_path']))) {
$form_values['signatory_name'] = $selected_department_head_user['fullname'] ?? $form_values['signatory_name'];
$form_values['signatory_department'] = $selected_department_head_user['department'] ?? $form_values['signatory_department'];
$form_values['signatory_position'] = getDisplayPositionForUser($selected_department_head_user, $form_values['signatory_department']) ?: $form_values['signatory_position'];
$form_values['department_officer_user_id'] = (int) ($selected_department_head_user['id'] ?? 0);
}
$form_values['signatory_position'] = getDisplayPositionForDepartment($form_values['signatory_department'], $form_values['signatory_position']) ?: $form_values['signatory_position'];
$routing_candidate_positions = $memo_position_options['Ketua Bahagian'] ?? [];
foreach ([$form_values['recipient_organization'], $form_values['attention_to']] as $selected_memo_position) {
if ($selected_memo_position !== '' && !in_array($selected_memo_position, $routing_candidate_positions, true)) {
$routing_candidate_positions[] = $selected_memo_position;
}
}
foreach ($form_values['routing_positions'] as $saved_routing_position) {
if ($saved_routing_position !== '' && !in_array($saved_routing_position, $routing_candidate_positions, true)) {
$routing_candidate_positions[] = $saved_routing_position;
}
}
$routing_candidate_positions = array_values(array_filter($routing_candidate_positions, function ($position) use ($excluded_approval_route_positions) {
return !in_array($position, $excluded_approval_route_positions, true);
}));
$form_values['routing_positions'] = array_values(array_filter($form_values['routing_positions'], function ($position) use ($excluded_approval_route_positions) {
return !in_array($position, $excluded_approval_route_positions, true);
}));
$routing_order_by_position = [];
foreach ($form_values['routing_positions'] as $routing_index => $routing_position) {
$routing_order_by_position[$routing_position] = $routing_index + 1;
}
$routing_order_limit = max(1, count(array_unique(array_column($routing_users, 'routing_position'))));
$display_file_numbering = ($is_edit && (($draft['status'] ?? 'draf') === 'siap'))
? (($draft['file_numbering'] ?? '') !== '' && ($draft['file_numbering'] ?? '') !== 'X' && ($draft['file_numbering'] ?? '') !== ($draft['ref_no'] ?? '')
? ($draft['file_numbering'] ?? '')
: ($draft['computed_file_numbering'] ?? ''))
: 'X';
$current_ref_display = formatDraftLetterReferenceDisplay(
$form_values['signatory_department'] ?: ($selected_folder['department'] ?? ''),
$is_edit ? ($draft['ref_no'] ?? '') : ($selected_folder['next_ref_no'] ?? ''),
$display_file_numbering
);
$current_ref_display = preg_replace('/\s+JLD\.\s*(\d+)\s*\(([^)]+)\)$/i', ' Jld. $1 ($2)', $current_ref_display);
$selected_template = null;
foreach ($letter_templates as $template_row) {
if ((int) ($template_row['template_id'] ?? 0) === (int) $form_values['template_id']) {
$selected_template = $template_row;
break;
}
}
if (!$is_edit && $selected_template && trim(implode('', array_map('strip_tags', $form_values['letter_pages']))) === '') {
$initial_template_html = replaceLetterTemplatePlaceholders($selected_template['template_content'] ?? '', [
'reference_no' => $current_ref_display,
'date' => formatMalayMemoDate($form_values['letter_date']),
'receiver_name' => $form_values['recipient_organization'],
'receiver_department' => 'Majlis Agama Islam Negeri Pulau Pinang',
'subject' => $form_values['subject'],
'letter_content' => '',
'sender_name' => $form_values['signatory_name'],
'sender_position' => $form_values['signatory_position'],
'sender_department' => $form_values['signatory_department'],
]);
if (trim(strip_tags($initial_template_html)) !== '') {
$form_values['letter_body'] = $initial_template_html;
$form_values['letter_pages'] = [$initial_template_html];
}
}
$preparer_is_signed = !empty($form_values['signatory_signature_path']) && file_exists(__DIR__ . '/' . $form_values['signatory_signature_path']);
$error_message = '';
if (isset($_GET['error'])) {
if ($_GET['error'] === 'validation') {
$error_detail = (string) ($_GET['msg'] ?? '');
if ($error_detail === 'preparer_signature_required') {
$error_message = 'Sila klik Tandatangan Penyedia sebelum menghantar surat.';
} elseif ($error_detail === 'preparer_signature_missing') {
$error_message = 'Tandatangan penyedia belum didaftarkan dalam profil pengguna.';
} else {
$error_message = 'Sila lengkapkan semua maklumat surat yang wajib.';
}
} elseif ($_GET['error'] === 'folder') {
$error_message = 'Fail yang dipilih tidak sah atau tidak lagi aktif.';
} elseif ($_GET['error'] === 'folder_closed') {
$error_message = 'Fail yang dipilih telah ditutup. Surat baharu tidak boleh diwujudkan dalam fail tersebut.';
} elseif ($_GET['error'] === 'document_missing') {
$error_message = 'Rekod dokumen berkaitan tidak ditemui. Sila semak semula draf ini.';
} elseif ($_GET['error'] === 'unauthorized') {
$error_message = 'Anda tidak mempunyai kebenaran untuk tindakan ini.';
} elseif ($_GET['error'] === 'system' && isset($_GET['msg'])) {
$error_message = 'Ralat sistem: ' . urldecode((string) $_GET['msg']);
} elseif ($_GET['error'] === 'attachment_not_found') {
$error_message = 'Lampiran sokongan tidak dijumpai.';
} elseif ($_GET['error'] === 'attachment_delete_failed') {
$error_message = 'Lampiran sokongan gagal dipadam.';
}
}
$success_message = '';
if (isset($_GET['success']) && $_GET['success'] === 'attachment_deleted') {
$success_message = 'Lampiran sokongan berjaya dipadam.';
}
function sanitizeDraftEditorInlineStyle($style) {
$safe_styles = [];
$has_small_font = false;
foreach (explode(';', (string) $style) as $declaration) {
$parts = explode(':', $declaration, 2);
if (count($parts) !== 2) {
continue;
}
$property = strtolower(trim($parts[0]));
$value = trim($parts[1]);
if ($property === 'text-align' && preg_match('/^(left|center|right|justify)$/i', $value)) {
$safe_styles[] = 'text-align: ' . strtolower($value);
} elseif (in_array($property, ['height', 'min-height', 'width', 'min-width'], true) && preg_match('/^(?:0|[0-9]+(?:\.[0-9]+)?(?:cm|mm|pt|px))$/i', $value)) {
$safe_styles[] = $property . ': ' . strtolower($value);
} elseif ($property === 'display' && in_array(strtolower($value), ['block', 'flex'], true)) {
$safe_styles[] = 'display: ' . strtolower($value);
} elseif ($property === 'align-items' && strtolower($value) === 'center') {
$safe_styles[] = 'align-items: center';
} elseif ($property === 'justify-content' && in_array(strtolower($value), ['center', 'flex-start'], true)) {
$safe_styles[] = 'justify-content: ' . strtolower($value);
} elseif ($property === 'margin' && preg_match('/^(?:0|[0-9]+(?:\.[0-9]+)?(?:cm|mm|pt|px))$/i', $value)) {
$safe_styles[] = 'margin: ' . strtolower($value);
} elseif (in_array($property, ['margin-top', 'margin-bottom'], true) && preg_match('/^(?:0|[0-9]+(?:\.[0-9]+)?(?:cm|mm|pt|px))$/i', $value)) {
$safe_styles[] = $property . ': ' . strtolower($value);
} elseif ($property === 'font-size' && preg_match('/^8(?:px|pt)$/i', $value)) {
$safe_styles[] = 'font-size: 8px';
$has_small_font = true;
} elseif (in_array($property, ['line-height', 'font-size', 'font-family'], true)) {
continue;
}
}
$safe_styles[] = 'font-family: Arial, Helvetica, sans-serif';
if (!$has_small_font) {
$safe_styles[] = 'font-size: 16px';
}
$safe_styles[] = 'line-height: 1.5';
return implode('; ', array_unique($safe_styles));
}
function applyDraftBodyTypography($html) {
$html = trim((string) $html);
if ($html === '') {
return '';
}
return preg_replace_callback('/<(p|div|li|td|th|blockquote|h3|h4|span)(\s[^>]*)?>/i', function ($matches) {
$tag = $matches[1];
$attrs = $matches[2] ?? '';
if (preg_match('/style\s*=\s*(["\'])(.*?)\1/i', $attrs, $style_match)) {
$quote = $style_match[1];
$safe_style = sanitizeDraftEditorInlineStyle($style_match[2]);
$attrs = preg_replace(
'/style\s*=\s*(["\']).*?\1/i',
'style=' . $quote . htmlspecialchars($safe_style, ENT_QUOTES) . $quote,
$attrs,
1
);
} else {
$attrs .= ' style="font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5"';
}
return '<' . $tag . $attrs . '>';
}, $html);
}
function normalizeQuillListHtml($html) {
$html = (string) $html;
if (stripos($html, 'data-list') === false && stripos($html, 'ql-ui') === false) {
return $html;
}
$html = preg_replace('/<span\b(?=[^>]*\bclass\s*=\s*(["\'])[^"\']*\bql-ui\b[^"\']*\1)[^>]*>\s*<\/span>/i', '', $html);
return preg_replace_callback('/<ol\b([^>]*)>(.*?)<\/ol>/is', function ($matches) {
$content = $matches[2];
if (stripos($content, 'data-list') === false) {
return $matches[0];
}
preg_match_all('/<li\b([^>]*)>(.*?)<\/li>/is', $content, $items, PREG_SET_ORDER);
if (empty($items)) {
return $matches[0];
}
$html_parts = [];
$current_type = '';
foreach ($items as $item) {
$attrs = $item[1] ?? '';
$item_type = preg_match('/\bdata-list\s*=\s*(["\'])bullet\1/i', $attrs) ? 'ul' : 'ol';
$attrs = preg_replace('/\s*data-list\s*=\s*(["\']).*?\1/i', '', $attrs);
$attrs = preg_replace('/\s*data-list\s*=\s*[^\s>]+/i', '', $attrs);
if ($current_type !== $item_type) {
if ($current_type !== '') {
$html_parts[] = '</' . $current_type . '>';
}
$current_type = $item_type;
$html_parts[] = '<' . $current_type . '>';
}
$html_parts[] = '<li' . $attrs . '>' . $item[2] . '</li>';
}
if ($current_type !== '') {
$html_parts[] = '</' . $current_type . '>';
}
return implode('', $html_parts);
}, $html);
}
function renderDraftEditorBody($body) {
$body = trim((string) $body);
$default_intro_html = '<p>YBhg. Dato&rsquo;,</p><p><br></p><p>Dengan hormatnya saya diarah merujuk perkara di atas.</p><p><br></p><p>2. &nbsp;&nbsp;&nbsp;&nbsp;</p>';
$default_body = applyDraftBodyTypography($default_intro_html);
if ($body === '' || preg_match('/^\s*(?:<p[^>]*>\s*)?-?\s*(?:<\/p>)?\s*$/i', $body)) {
return $default_body;
}
$allowed_tags = '<p><br><strong><b><em><i><u><ol><ul><li><table><thead><tbody><tr><th><td><colgroup><col><blockquote><h3><h4><div><span>';
$body = preg_replace('/(<div\b[^>]*class\s*=\s*(["\'])generated-closing-lock\s+signature-block\2)\s+signature-block(?:"|\')/i', '$1', $body);
$body = normalizeQuillListHtml($body);
$sanitized = strip_tags($body, $allowed_tags);
$sanitized = preg_replace('/\s+on[a-z]+\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $sanitized);
$sanitized = preg_replace_callback('/\s+style\s*=\s*(["\'])(.*?)\1/i', function ($matches) {
$safe_style = sanitizeDraftEditorInlineStyle($matches[2]);
return $safe_style !== '' ? ' style="' . htmlspecialchars($safe_style, ENT_QUOTES) . '"' : '';
}, $sanitized);
$sanitized = preg_replace('/\s+style\s*=\s*(?!["\'])[^\s>]+/i', '', $sanitized);
$sanitized = preg_replace('/\s+id\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $sanitized);
$sanitized = preg_replace_callback('/\s+class\s*=\s*(["\'])(.*?)\1/i', function ($matches) {
$classes = preg_split('/\s+/', trim((string) $matches[2]));
$allowed_classes = array_values(array_intersect($classes, ['generated-closing-lock', 'signature-block', 'prepared-by-line']));
return !empty($allowed_classes) ? ' class="' . htmlspecialchars(implode(' ', $allowed_classes), ENT_QUOTES) . '"' : '';
}, $sanitized);
$sanitized = preg_replace_callback('/\s+class\s*=\s*(?!["\'])([^\s>]+)/i', function ($matches) {
$classes = preg_split('/\s+/', trim((string) $matches[1]));
$allowed_classes = array_values(array_intersect($classes, ['generated-closing-lock', 'signature-block', 'prepared-by-line']));
return !empty($allowed_classes) ? ' class="' . htmlspecialchars(implode(' ', $allowed_classes), ENT_QUOTES) . '"' : '';
}, $sanitized);
$sanitized = preg_replace('/<div\b(?=[^>]*data-generated-closing\s*=\s*(["\'])1\1)(?![^>]*\sclass\s*=)/i', '<div class="generated-closing-lock signature-block"', $sanitized);
$sanitized = preg_replace('/javascript\s*:/i', '', $sanitized);
if (strip_tags($body) === $body) {
$paragraphs = preg_split("/\r?\n\s*\r?\n/", str_replace(["\r\n", "\r"], "\n", $body));
$html = '';
foreach ($paragraphs as $paragraph) {
$paragraph = trim($paragraph);
if ($paragraph !== '') {
$html .= '<p style="font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5">' . nl2br(htmlspecialchars($paragraph)) . '</p>';
}
}
$sanitized = $html;
}
$sanitized = preg_replace('/^\s*<p[^>]*>\s*-\s*<\/p>\s*/i', '', $sanitized);
$sanitized = preg_replace('/^\s*<p[^>]*>\s*(?:<br\s*\/?>|&nbsp;|\s)*\s*<\/p>\s*/i', '', $sanitized);
$sanitized = preg_replace('/<p\b([^>]*)>\s*3\.\s*(?:&nbsp;|\s|<br\s*\/?>)*<\/p>/i', '', $sanitized);
$plain_text = trim(html_entity_decode(strip_tags($sanitized), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
if (stripos($plain_text, 'Dengan hormatnya saya diarah merujuk perkara di atas') === 0) {
$sanitized = applyDraftBodyTypography('<p>YBhg. Dato&rsquo;,</p><p><br></p>') . $sanitized;
}
$sanitized = preg_replace_callback(
'/(<p\b[^>]*>[\s\S]*?YBhg\.\s*Dato[\s\S]*?<\/p>)(?:\s*<p\b[^>]*>\s*(?:<br\s*\/?>|&nbsp;|\s)*<\/p>){0,10}\s*(<p\b[^>]*>[\s\S]*?Dengan hormatnya saya diarah merujuk perkara di atas\.[\s\S]*?<\/p>)(?:\s*<p\b[^>]*>\s*(?:<br\s*\/?>|&nbsp;|\s)*<\/p>){0,10}\s*(<p\b[^>]*>[\s\S]*?2\.\s*(?:&nbsp;|\s)*<\/p>)/i',
function ($matches) {
return '<p>YBhg. Dato&rsquo;,</p><p><br></p>'
. '<p>Dengan hormatnya saya diarah merujuk perkara di atas.</p><p><br></p>'
. $matches[3];
},
$sanitized
);
$sanitized = applyDraftBodyTypography($sanitized);
return $sanitized !== '' ? $sanitized : $default_body;
}
function formatMalayMemoDate($date) {
$timestamp = strtotime((string) $date);
if (!$timestamp) {
return '-';
}
$months = [
1 => 'Januari', 2 => 'Februari', 3 => 'Mac', 4 => 'April',
5 => 'Mei', 6 => 'Jun', 7 => 'Julai', 8 => 'Ogos',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Disember',
];
return date('d', $timestamp) . ' ' . $months[(int) date('n', $timestamp)] . ' ' . date('Y', $timestamp);
}
function renderMemoPositionSelectOptions($memo_position_options, $selected_value) {
$selected_value = trim((string) $selected_value);
$selected_found = false;
foreach ($memo_position_options as $group_label => $positions) {
echo '<optgroup label="' . htmlspecialchars($group_label) . '">';
foreach ($positions as $position) {
$selected = $selected_value === $position ? ' selected' : '';
if ($selected !== '') {
$selected_found = true;
}
echo '<option value="' . htmlspecialchars($position) . '"' . $selected . '>' . htmlspecialchars($position) . '</option>';
}
echo '</optgroup>';
}
if ($selected_value !== '' && !$selected_found) {
echo '<option value="' . htmlspecialchars($selected_value) . '" selected>' . htmlspecialchars($selected_value) . '</option>';
}
}
function splitMemoPositionValues($value) {
if (is_array($value)) {
$items = $value;
} else {
$items = preg_split('/\r\n|\r|\n/', (string) $value);
}
$normalized = [];
foreach ($items as $item) {
$item = trim((string) $item);
if ($item !== '' && !in_array($item, $normalized, true)) {
$normalized[] = $item;
}
}
return $normalized;
}
function renderMemoPositionCheckboxDropdown($memo_position_options, $field_id, $field_label, $selected_values) {
$selected_values = splitMemoPositionValues($selected_values);
$selected_lookup = array_fill_keys($selected_values, true);
$input_name = $field_id . '_checked[]';
?>
<div class="memo-check-dropdown" data-target-select="<?= htmlspecialchars($field_id) ?>">
<button type="button" class="memo-check-toggle">
<span class="memo-check-summary">Pilih tambahan</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="memo-check-menu" aria-label="<?= htmlspecialchars($field_label) ?>">
<?php foreach ($memo_position_options as $group_label => $positions): ?>
<div class="memo-check-group"><?= htmlspecialchars($group_label) ?></div>
<?php foreach ($positions as $position): ?>
<label class="memo-check-option">
<input
type="checkbox"
name="<?= htmlspecialchars($input_name) ?>"
value="<?= htmlspecialchars($position) ?>"
<?= isset($selected_lookup[$position]) ? 'checked' : '' ?>
>
<span><?= htmlspecialchars($position) ?></span>
</label>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
</div>
<?php
}
function renderMemoDisplayValue($value) {
$positions = splitMemoPositionValues($value);
if (empty($positions)) {
return '-';
}
$rows = [];
foreach ($positions as $position) {
$rows[] = htmlspecialchars($position) . '<br>Majlis Agama Islam Negeri Pulau Pinang';
}
return implode('<br>', $rows);
}
?>
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $is_edit ? 'Ubah ' . htmlspecialchars($current_document_type) : 'Sediakan ' . htmlspecialchars($current_document_type) ?> | MAINPP WORKSPACE</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/vendor/quill/quill.snow.css">
<?php renderAppShellAssets(); ?>
<style>
:root {
--primary: #0077b6; --primary-dark: #005f99; --secondary: #00b4d8;
--superadmin: #6c5ce7; --superadmin-dark: #5b4cdb;
--bg-light: #f8fbfd; --bg-card: #ffffff; --text-main: #1a3a5c;
--text-muted: #6c757d; --border: #e0e8f0; --sidebar-width: 240px;
--success: #1a9955; --danger: #dc3545; --warning: #ffc107;
--draft: #0f766e;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', 'Inter', Tahoma, Geneva, Verdana, sans-serif; }
body { background: linear-gradient(135deg, #e0f7fa 0%, #f1f8e9 100%); min-height: 100vh; display: flex; color: var(--text-main); }
.sidebar { width: var(--sidebar-width); background: linear-gradient(180deg, #fff 0%, #f8fbfd 100%); border-right: 1px solid var(--border); position: fixed; height: 100vh; padding: 20px 0; overflow-y: auto; overflow-x: hidden; z-index: 1000; box-shadow: 2px 0 10px rgba(0,0,0,0.03); transition: width 0.3s ease; display:flex; flex-direction:column; }
.sidebar.collapsed { width: 70px; }
.sidebar-header { text-align: center; padding: 0 20px 20px; border-bottom: 1px solid var(--border); margin-bottom: 20px; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-header h3, .sidebar.collapsed .sidebar-header p, .sidebar.collapsed .menu-text { display: none; }
.sidebar-header img { width: 50px; margin-bottom: 10px; transition: margin 0.3s ease; }
.sidebar.collapsed .sidebar-header img { margin: 0 auto 10px; }
.sidebar-header h3 { font-size: 1rem; font-weight: 700; color: var(--primary); margin-bottom: 4px; }
.sidebar-header p { font-size: 0.75rem; color: var(--text-muted); }
.role-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; margin-left: 8px; }
.role-badge.user { background: #e6f4ff; color: var(--primary); }
.role-badge.admin { background: #d4edda; color: var(--success); }
.role-badge.superadmin { background: linear-gradient(90deg, var(--superadmin), var(--superadmin-dark)); color: #fff; }
.sidebar-menu { list-style: none; padding: 0 10px; display:flex; flex-direction:column; flex:1; }
.sidebar-section { padding: 10px 18px 6px; color: var(--text-muted); font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.sidebar-spacer { margin-top: auto; }
.sidebar-menu li a { display: flex; align-items: center; gap: 12px; padding: 12px 18px; color: var(--text-main); text-decoration: none; border-radius: 8px; margin-bottom: 4px; transition: all 0.2s ease; font-size: 0.9rem; white-space: nowrap; }
.sidebar-menu li a:hover, .sidebar-menu li a.active { background: linear-gradient(90deg, var(--primary), var(--secondary)); color: #fff; }
.sidebar-menu li a i { width: 20px; text-align: center; font-size: 1.1rem; }
.sidebar-menu .dropdown { position: relative; }
.sidebar-menu .dropdown-toggle { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 12px 18px; color: var(--text-main); background: none; border: none; border-radius: 8px; cursor: pointer; font-size: 0.9rem; text-align: left; transition: all 0.2s ease; }
.sidebar-menu .dropdown-toggle:hover, .sidebar-menu .dropdown-toggle.active { background: linear-gradient(90deg, var(--primary), var(--secondary)); color: #fff; }
.sidebar-menu .dropdown-toggle i.chevron { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); font-size: 0.8rem; }
.sidebar-menu .dropdown.active .dropdown-toggle i.chevron { transform: rotate(180deg); }
.sidebar-menu .dropdown-menu { display: flex; flex-direction: column; padding-left: 42px; margin-top: 4px; max-height: 0; opacity: 0; overflow: hidden; transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s ease; }
.sidebar-menu .dropdown.active .dropdown-menu { max-height: 260px; opacity: 1; }
.sidebar-menu .dropdown-menu a { padding: 8px 18px; color: var(--text-main); text-decoration: none; border-radius: 6px; font-size: 0.85rem; display: flex; align-items: center; gap: 8px; opacity: 0; transform: translateY(-5px); transition: opacity 0.2s ease, transform 0.2s ease; }
.sidebar-menu .dropdown.active .dropdown-menu a { opacity: 1; transform: translateY(0); }
.sidebar-menu .dropdown-menu a:hover, .sidebar-menu .dropdown-menu a.active { background: #e6f4ff; color: var(--primary); }
.sidebar.collapsed + .main { margin-left: 70px; }
.sidebar-toggle { position: absolute; right: -12px; top: 20px; width: 24px; height: 24px; background: var(--primary); color: #fff; border: none; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; box-shadow: 0 2px 6px rgba(0,0,0,0.15); transition: transform 0.3s ease; z-index: 1001; }
.sidebar.collapsed .sidebar-toggle { transform: rotate(180deg); }
.main { margin-left: var(--sidebar-width); flex: 1; padding: 25px 30px; transition: margin-left 0.3s ease; }
.top-bar { background: var(--bg-card); padding: 15px 25px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.04); margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); }
.top-bar h2 { color: var(--primary-dark); font-size: 1.35rem; font-weight: 850; display: flex; align-items: center; gap: 10px; }
        .top-bar h2 i { color: var(--secondary); }
.user-info { display: flex; align-items: center; gap: 15px; font-size: 0.9rem; color: var(--text-muted); }
.user-info span { color: var(--primary); font-weight: 600; }
.user-info a { color: var(--danger); text-decoration: none; font-weight: 500; display: flex; align-items: center; gap: 5px; }
.user-info a:hover { text-decoration: underline; }
.shell { display: block; max-width: 1220px; margin: 0 auto; }
.form-card, .side-card { background: var(--bg-card); border-radius: 16px; box-shadow: 0 8px 25px rgba(15, 23, 42, 0.06); border: 1px solid var(--border); }
.form-card { padding: 26px; }
.form-header { margin-bottom: 22px; }
.form-header h1 { font-size: 1.3rem; color: var(--primary); margin-bottom: 6px; }
.form-header p { color: var(--text-muted); font-size: 0.9rem; line-height: 1.6; }
.alert { background: #f8d7da; color: #721c24; padding: 12px 14px; border-radius: 10px; margin-bottom: 18px; border-left: 4px solid var(--danger); }
.draft-stack { display: flex; flex-direction: column; }
.draft-stack > .signatory-control-group,
.draft-stack > .department-officer-control,
.draft-stack > .attachment-control,
.draft-stack > .preparer-sign-panel { order: 43; }
.draft-stack > .generate-closing-btn { order: 44; align-self: flex-start; }
.draft-stack > .btn-row {
order: 50;
justify-content: flex-end;
align-items: center;
padding-top: 14px;
border-top: 1px solid var(--border);
margin-top: 22px;
}
.draft-stack > .letter-editor-section { order: 30; }
.draft-stack > .approval-editor-group { order: 40; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 15px; }
.form-group { margin-bottom: 18px; }
.form-group label { display: block; margin-bottom: 6px; color: var(--text-main); font-weight: 700; font-size: 0.9rem; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 11px 14px; border: 1px solid var(--border); border-radius: 10px; font-size: 0.95rem; background: #fff; box-sizing: border-box; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: var(--primary); outline: none; box-shadow: 0 0 0 3px rgba(0,119,182,0.15); }
.form-group textarea { min-height: 130px; resize: vertical; }
.hint { font-size: 0.78rem; color: var(--text-muted); margin-top: 5px; line-height: 1.5; }
.routing-checklist { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: #f8fbfd; }
.routing-option { display: grid; grid-template-columns: 22px minmax(0, 1fr) 110px; align-items: center; gap: 9px; padding: 9px 10px; border: 1px solid #dbe7ef; border-radius: 6px; background: #fff; cursor: pointer; }
.routing-option input { width: 16px; height: 16px; margin: 0; }
.routing-option span { display: block; min-width: 0; font-size: 0.86rem; font-weight: 600; line-height: 1.35; }
.routing-option small { display: block; color: var(--text-muted); font-size: 0.72rem; margin-top: 3px; }
.routing-option .routing-source { color: var(--primary); }
.routing-order-select { width: 100%; padding: 7px 8px; border: 1px solid var(--border); border-radius: 6px; background: #fff; font-size: 0.82rem; }
.routing-order-select:disabled { background: #eef2f6; color: #94a3b8; }
.routing-option.is-unavailable { opacity: 0.58; cursor: not-allowed; }
.approval-disabled { opacity: 0.55; pointer-events: none; }
.approval-editor-group { margin-top: 28px; }
.approval-pages-container { display: block; }
.approval-page-preview { position: relative; width: 21cm; min-width: 21cm; max-width: 21cm; height: 29.7cm; min-height: 29.7cm; max-height: 29.7cm; overflow: hidden; margin: 14px auto 0; background: #fff; border: 1px solid #d7e2ea; box-shadow: 0 12px 30px rgba(15, 23, 42, 0.10); color: #111; font-family: Arial, Helvetica, sans-serif; }
.approval-page-inner { height: 100%; padding: 2.54cm 1.8cm; overflow: hidden; }
.approval-route-preview { height: 100%; overflow: hidden; }
.approval-page-label { font-size: 16px; font-weight: 700; margin-bottom: 28px; }
.approval-recipient-body:empty::before { content: attr(data-placeholder); color: #94a3b8; font-weight: 400; }
.approval-recipient-body { min-height: 0; font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5; outline: none; margin-bottom: 6px; }
.approval-recipient-body p { margin: 0; font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5; }
.approval-recipient-body div { font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5; }
.approval-decision-options { display: flex; flex-wrap: wrap; gap: 18px; margin: 6px 0 4px; }
.approval-decision-option { display: inline-flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 700; color: #111; }
.approval-decision-box { width: 14px; height: 14px; border: 1.6px solid #444; border-radius: 2px; display: inline-block; flex: 0 0 14px; background: #fff; }
.approval-route-block { font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5; padding-top: 6px; page-break-inside: avoid; }
.approval-route-block + .approval-route-block { border-top: 1px solid #111; margin-top: 26px; padding-top: 20px; }
.approval-route-title { font-weight: 700; text-decoration: underline; margin: 6px 0 14px; line-height: 1.5; }
.approval-comment-lines { display: grid; gap: calc(1.5em - 1px); margin-bottom: 18px; }
.approval-comment-lines span { display: block; border-bottom: 1px solid #777; height: 1px; }
.approval-route-sign-row { display: grid; grid-template-columns: 195px 285px; justify-content: space-between; align-items: start; width: 100%; }
.approval-route-date { padding-top: 0; line-height: 1.5; white-space: nowrap; }
.approval-route-signature { width: 285px; min-height: 76px; }
.approval-route-sign-line { border-bottom: 1px dotted #555; height: 18px; margin: 0 0 5px; }
.approval-route-signature strong, .approval-route-signature span { display: block; line-height: 1.5; }
.approval-decision-option { line-height: 1.5; }
@media (max-width: 1200px) {
.routing-checklist { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.routing-checklist { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.routing-option { grid-template-columns: 22px minmax(0, 1fr); }
.routing-order-select { grid-column: 2; }
.page-sheet-actions {
top: 56px;
right: 8px;
}
}
.readonly-box { padding: 12px 14px; border: 1px dashed #b9d8ea; border-radius: 12px; background: #f9fdff; }
.readonly-box strong { display: block; color: var(--primary-dark); }
.readonly-box span { color: var(--text-muted); font-size: 0.82rem; }
.reference-readonly-box { min-height: 39px; display: flex; align-items: center; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 22px; }
.btn-row-left { margin-right: auto; }
.btn { padding: 12px 20px; border: none; border-radius: 10px; text-decoration: none; cursor: pointer; font-size: 0.92rem; display: inline-flex; align-items: center; gap: 8px; font-weight: 700; }
.btn-primary { background: linear-gradient(90deg, var(--draft), var(--secondary)); color: #fff; }
.btn-secondary { background: #eef4f8; color: var(--primary-dark); }
.btn-warning { background: #f59e0b; color: #fff; }
.btn-danger { background: var(--danger); color: #fff; }
.btn-sm { padding: 7px 12px; font-size: 0.78rem; border-radius: 8px; }
.btn[disabled] { opacity: 0.55; cursor: not-allowed; }
.preparer-sign-panel { margin-top: 18px; border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; background: #f8fbfd; display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.preparer-sign-panel strong { display: block; color: var(--primary-dark); margin-bottom: 4px; }
.preparer-sign-panel span { color: var(--text-muted); font-size: 0.86rem; }
.preparer-sign-panel.signed { background: #ecfdf3; border-color: #bbf7d0; }
.preparer-sign-panel.missing { background: #fff7ed; border-color: #fed7aa; }
.form-card.compact-letter-form { padding: 16px 18px; }
.compact-letter-form .form-header { margin-bottom: 10px; }
.compact-letter-form .form-header h1 { font-size: 1.12rem; margin-bottom: 0; }
.compact-letter-form .draft-stack { gap: 0; }
.compact-letter-form .grid {
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 8px 12px;
}
.compact-letter-form .letter-primary-grid {
grid-template-columns: 170px 190px minmax(320px, 1fr);
}
.compact-letter-form .form-group { margin-bottom: 9px; }
.compact-letter-form .form-group label {
margin-bottom: 3px;
font-size: 0.8rem;
line-height: 1.2;
}
.compact-letter-form .form-group input,
.compact-letter-form .form-group select,
.compact-letter-form .form-group textarea {
padding: 8px 10px;
border-radius: 8px;
font-size: 0.86rem;
}
.compact-letter-form .readonly-box {
padding: 8px 10px;
border-radius: 9px;
}
.compact-letter-form .readonly-box strong {
line-height: 1.25;
font-size: 0.86rem;
}
.compact-letter-form .readonly-box span,
.compact-letter-form .hint {
font-size: 0.72rem;
line-height: 1.25;
}
.compact-letter-form .routing-checklist {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
padding: 8px;
}
.compact-letter-form .routing-option {
grid-template-columns: 18px minmax(0, 1fr) 86px;
gap: 6px;
padding: 7px 8px;
border-radius: 6px;
}
.compact-letter-form .routing-option input {
width: 14px;
height: 14px;
}
.compact-letter-form .routing-option span {
font-size: 0.72rem;
line-height: 1.16;
}
.compact-letter-form .routing-option small {
font-size: 0.63rem;
margin-top: 2px;
line-height: 1.5;
}
.compact-letter-form .routing-order-select {
padding: 5px 6px;
font-size: 0.7rem;
border-radius: 6px;
}
.compact-letter-form .attachment-control input[type="file"] {
padding: 7px 10px;
}
.compact-letter-form .preparer-sign-panel {
margin-top: 8px;
padding: 10px 12px;
border-radius: 10px;
gap: 10px;
}
.compact-letter-form .preparer-sign-panel strong {
margin-bottom: 2px;
font-size: 0.86rem;
}
.compact-letter-form .preparer-sign-panel span {
font-size: 0.76rem;
line-height: 1.25;
}
.compact-letter-form .btn {
padding: 9px 14px;
border-radius: 8px;
font-size: 0.84rem;
}
.compact-letter-form .letter-editor-section {
position: static;
background: var(--bg-card);
padding-top: 4px;
box-shadow: none;
border-radius: 12px;
}
.compact-letter-form .word-editor-shell {
margin-top: 2px;
display: grid;
grid-template-columns: max-content max-content max-content max-content 220px;
column-gap: 16px;
grid-template-rows: auto auto;
max-height: none;
}
.compact-letter-form .word-toolbar-section {
padding: 6px 8px;
min-width: 0;
}
.compact-letter-form .toolbar-section-label {
margin-bottom: 4px;
font-size: 0.66rem;
}
.compact-letter-form .word-toolbar {
gap: 5px;
flex-wrap: nowrap;
}
.compact-letter-form .word-toolbar button {
width: 30px;
height: 30px;
border-radius: 7px;
}
.compact-letter-form .word-toolbar select {
height: 30px;
padding: 0 7px;
border-radius: 7px;
font-size: 0.76rem;
width: auto;
}
.compact-letter-form .word-toolbar .font-family-select { width: 190px; min-width: 190px; }
.compact-letter-form .word-toolbar .font-size-select { width: 68px; min-width: 68px; }
.compact-letter-form #table_rows,
.compact-letter-form #table_cols { width: 92px; min-width: 92px; }
.compact-letter-form .word-toolbar select.locked-control,
.compact-letter-form input.locked-control[readonly] {
background: #eef2f6;
color: #64748b;
cursor: not-allowed;
}
.compact-letter-form .page-controls-section {
padding: 6px 8px;
grid-column: auto;
min-width: 0;
align-self: stretch;
align-items: flex-start;
}
.compact-letter-form #letter_pages_container {
grid-column: 1 / -1;
min-height: 0;
max-height: none;
}
@media (max-width: 980px) {
.compact-letter-form .word-editor-shell {
grid-template-columns: 1fr 1fr;
}
.compact-letter-form #letter_pages_container {
grid-column: 1 / -1;
}
}
@media (max-width: 1180px) {
.compact-letter-form .routing-checklist { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
.compact-letter-form .routing-checklist { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.compact-letter-form .letter-primary-grid { grid-template-columns: 1fr 1fr; }
.compact-letter-form .subject-field { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
.form-card.compact-letter-form { padding: 14px; }
.compact-letter-form .grid { grid-template-columns: 1fr; }
.compact-letter-form .letter-primary-grid { grid-template-columns: 1fr; }
.compact-letter-form .routing-checklist { grid-template-columns: 1fr; }
.compact-letter-form .routing-option { grid-template-columns: 18px minmax(0, 1fr); }
}
.side-card { padding: 20px; }
.memo-check-dropdown {
position: relative;
margin-top: 6px;
}
.memo-check-toggle {
width: 100%;
min-height: 34px;
border: 1px solid var(--border);
border-radius: 8px;
background: #f8fbfd;
color: var(--text-main);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 34px 8px 14px;
cursor: pointer;
font-size: 0.82rem;
font-weight: 700;
text-align: left;
position: relative;
}
.memo-check-toggle::after {
display: none;
}
.memo-check-toggle > i {
display: block;
position: absolute;
right: 13px;
top: calc(50% - 4px);
color: #111827;
font-size: 0.72rem;
line-height: 1;
transform: translateY(-50%);
pointer-events: none;
}
.memo-check-menu {
position: absolute;
z-index: 80;
top: calc(100% + 5px);
left: 0;
right: 0;
max-height: 260px;
overflow: auto;
display: none;
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.14);
padding: 8px;
}
.memo-check-dropdown.open .memo-check-menu { display: block; }
.memo-check-group {
padding: 7px 8px 4px;
color: var(--text-muted);
font-size: 0.68rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.memo-check-option {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
gap: 8px;
align-items: start;
padding: 7px 8px;
border-radius: 7px;
cursor: pointer;
font-size: 0.78rem;
line-height: 1.25;
}
.memo-check-option:hover { background: #eef8ff; }
.memo-check-option input { width: 15px; height: 15px; margin-top: 1px; }
.side-card h3 { color: var(--primary); margin-bottom: 12px; font-size: 1rem; }
.side-card p { color: var(--text-muted); line-height: 1.6; font-size: 0.88rem; }
.summary-list { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.summary-item { background: #f8fbfd; border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; }
.summary-item label { display: block; font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 6px; }
.summary-item strong { color: var(--primary-dark); }
.attachment-list { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.attachment-item { padding: 10px 12px; border-radius: 10px; background: #f8fbfd; border: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.attachment-item a { color: var(--primary); text-decoration: none; font-weight: 600; }
.attachment-item a:hover { text-decoration: underline; }
.selected-attachment-list { display: none; flex-direction: column; gap: 8px; margin-top: 10px; }
.selected-attachment-list.active { display: flex; }
.selected-attachment-title { font-size: 0.82rem; font-weight: 700; color: var(--primary-dark); margin-top: 12px; }
.attachment-meta { color: var(--text-muted); font-size: 0.78rem; margin-left: 6px; }
.attachment-remove-btn { border: none; background: #fee2e2; color: var(--danger); border-radius: 8px; padding: 7px 10px; cursor: pointer; font-weight: 700; display: inline-flex; align-items: center; gap: 6px; }
.attachment-remove-btn:hover { background: #fecaca; }
.word-editor-shell {
border: 1px solid rgba(255,255,255,0.62);
border-radius: 18px;
overflow: visible;
background: rgba(255,255,255,0.52);
box-shadow: 0 22px 54px rgba(2,62,95,0.12);
backdrop-filter: blur(22px) saturate(1.28);
-webkit-backdrop-filter: blur(22px) saturate(1.28);
}
#letter_pages_container {
max-height: none;
min-height: 0;
overflow: visible;
padding: 8px 64px 18px;
background: rgba(238,245,248,0.42);
border-top: 1px solid rgba(255,255,255,0.52);
scroll-behavior: smooth;
--letter-page-zoom: 1;
}
.toolbar-section-label {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 6px;
display: block;
}
/* Page Controls Section */
.page-controls-section {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: rgba(255,255,255,0.34);
border-bottom: 1px solid rgba(255,255,255,0.48);
flex-wrap: wrap;
}
.page-control-left {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.compact-letter-form .page-control-left {
align-items: stretch;
flex-direction: column;
gap: 0;
width: 100%;
}
.generate-closing-btn {
border: 1px solid rgba(0, 116, 179, 0.22);
background: #ffffff;
color: var(--primary);
border-radius: 10px;
padding: 9px 13px;
font-weight: 700;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 6px 14px rgba(0, 116, 179, 0.08);
}
.generate-closing-btn:hover {
background: #eef8ff;
}
.page-control-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.page-controls span {
color: var(--text-muted);
font-size: 0.84rem;
font-weight: 700;
}
#page_count_label {
font-size: 0.66rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 4px;
line-height: 1.2;
display: block;
}
.page-controls button {
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
color: var(--primary-dark);
padding: 8px 12px;
cursor: pointer;
font-weight: 700;
display: inline-flex;
align-items: center;
gap: 7px;
transition: all 0.2s ease;
}
.page-controls button:hover {
background: #e6f4ff;
border-color: var(--primary);
}
.page-controls select {
height: 36px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0 10px;
background: #fff;
color: var(--text-main);
font-size: 0.84rem;
cursor: pointer;
}
.page-controls .delete-current-page-btn {
color: #b42318;
border-color: #f5c2c7;
}
.page-controls .delete-current-page-btn:hover {
background: #fde8e8;
border-color: #b42318;
}
.memo-page-preview {
position: relative;
background: #fff;
width: 21cm;
min-width: 21cm;
max-width: 21cm;
height: 29.7cm;
min-height: 29.7cm;
max-height: 29.7cm;
overflow: hidden;
margin: 18px auto;
padding: 2.54cm 1.8cm;
box-shadow: 0 4px 18px rgba(15, 23, 42, 0.12);
border: 1px solid #d8e2ea;
color: #111827;
font-family: Arial, Helvetica, sans-serif;
display: flex;
flex-direction: column;
zoom: 1;
}
.memo-page-preview,
.approval-page-preview {
line-height: 1.5;
}
.memo-page-preview :not(i),
.approval-page-preview :not(i) {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
}
.memo-page-preview .memo-editor-fixed,
.memo-page-preview .memo-editor-fixed td,
.memo-page-preview .memo-editor-footer,
.memo-page-preview .memo-editor-footer > div,
.memo-page-preview .memo-label-grid td,
.approval-page-preview .approval-route-block,
.approval-page-preview .approval-route-block p,
.approval-page-preview .approval-route-block div,
.approval-page-preview .approval-route-signature strong,
.approval-page-preview .approval-route-signature span,
.approval-page-preview .approval-route-title,
.approval-page-preview .approval-decision-option {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
}
.memo-page-preview.is-extra-page {
height: 29.7cm;
min-height: 29.7cm;
max-height: 29.7cm;
}
.page-sheet-actions {
position: absolute;
top: 2.54cm;
right: -46px;
z-index: 5;
display: flex;
flex-direction: column;
gap: 6px;
}
.page-sheet-actions button {
width: 34px;
height: 34px;
border: 1px solid #b9d8ea;
border-radius: 7px;
background: #fff;
color: var(--primary-dark);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 7px rgba(15, 23, 42, 0.10);
}
.page-sheet-actions button:hover { background: #e6f4ff; border-color: var(--primary); }
.page-sheet-actions .page-delete-btn { color: #b42318; border-color: #f5c2c7; }
.page-sheet-actions .page-delete-btn:hover { background: #fde8e8; border-color: #b42318; }
.page-sheet-actions button:disabled { opacity: 0.42; cursor: not-allowed; background: #f1f5f9; }
.memo-custom-header {
position: absolute;
top: 30px;
left: 2.54cm;
right: 2.54cm;
min-height: 26px;
color: #475569;
font-size: 16px;
line-height: 1.5;
text-align: center;
white-space: pre-wrap;
}
.memo-custom-header:focus,
.memo-custom-footer:focus {
outline: 2px dashed var(--primary);
outline-offset: 4px;
background: rgba(0, 119, 182, 0.04);
}
.memo-custom-header,
.memo-custom-footer {
cursor: text;
}
.memo-custom-header p,
.memo-custom-footer p {
margin: 0;
}
.memo-custom-footer {
position: absolute;
bottom: 30px;
left: 2.54cm;
right: 2.54cm;
min-height: 26px;
color: #475569;
font-size: 16px;
line-height: 1.5;
text-align: center;
white-space: pre-wrap;
}
.memo-page-number {
position: absolute;
bottom: 38px;
min-width: 32px;
color: #475569;
font-size: 16px;
line-height: 1;
text-align: center;
user-select: none;
}
.memo-page-number.bottom_center {
left: 50%;
transform: translateX(-50%);
}
.memo-page-number.bottom_right {
right: 2.54cm;
}
.memo-page-number.none {
display: none;
}
.approval-page-preview .memo-page-number {
bottom: 38px;
}
.memo-editor-fixed {
user-select: none;
}
.memo-logo {
text-align: center;
margin-bottom: 2px;
}
.memo-logo img {
width: 130px;
height: auto;
object-fit: contain;
}
.memo-title {
text-align: center;
letter-spacing: 0;
font-weight: 700;
font-size: 29px;
margin-bottom: 12px;
padding-left: 0;
}
        .memo-page-preview .memo-editor-fixed .memo-title {
            font-size: 29px !important;
        }
.memo-ref-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 13px;
margin-bottom: 16px;
}
.memo-ref-table td {
border: 1px solid #111;
padding: 1mm 2mm;
vertical-align: top;
line-height: 1.2;
}
        .memo-ref-table *,
        .memo-ref-table td {
            font-size: 13px !important;
        }
        .memo-ref-left {
width: 48%;
font-weight: 400;
line-height: 1.2;
}
.memo-ref-right {
width: 52%;
font-weight: 400;
}
.memo-ref-line {
display: grid;
grid-template-columns: 58px 8px minmax(0, 1fr); column-gap: 8px;
align-items: start;
white-space: nowrap;
}
.memo-ref-label {
display: block;
}
.memo-ref-colon {
display: block;
text-align: center;
}
        .memo-ref-value {
min-width: 0;
white-space: nowrap;
}
.memo-label-grid {
width: 100%;
border-collapse: collapse;
margin-bottom: 12px;
font-size: 16px;
}
.memo-label-grid td {
padding: 4px 0;
vertical-align: top;
}
.memo-label {
width: 92px;
font-weight: 400;
letter-spacing: 0.02em;
}
.memo-colon {
width: 18px;
}
.memo-value {
font-weight: 400;
}
.memo-subject {
font-weight: 700;
text-transform: uppercase;
}
.word-editor {
min-height: 24px;
max-height: var(--editor-max-height, 260px);
overflow: hidden;
outline: none;
line-height: 1.5;
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
color: #111827;
flex: 0 0 auto;
}
.word-editor p {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
}
.word-editor div,
.word-editor li,
.word-editor span {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
}
.word-editor ol, .word-editor ul {
margin: 0 0 12px 24px;
padding-left: 18px;
}
.word-editor table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin: 10px 0 14px;
}
.word-editor td, .word-editor th {
border: 1px solid #111;
padding: 6px 8px;
min-width: 80px;
}
.word-editor th {
background: #f1f5f9;
font-weight: 700;
}
.word-editor:empty:before {
content: "Tulis kandungan memo di sini...";
color: #9ca3af;
}
.quill-letter-host {
border: 0 !important;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
flex: 0 0 auto;
height: auto;
}
.quill-letter-host .ql-container {
height: auto;
min-height: 24px;
}
.quill-letter-host .ql-editor {
padding: 0;
min-height: 24px;
height: auto;
max-height: var(--editor-max-height, 260px);
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #111827;
}
.quill-letter-host .ql-editor p {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 1.5;
}
.word-toolbar-section {
border-bottom: 1px solid rgba(255,255,255,0.48);
padding: 10px 12px;
background: rgba(255,255,255,0.34);
backdrop-filter: blur(18px) saturate(1.2);
-webkit-backdrop-filter: blur(18px) saturate(1.2);
}
.toolbar-section-label {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 6px;
display: block;
}
.word-toolbar {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.word-toolbar button {
width: 34px;
height: 34px;
border: 1px solid var(--border);
border-radius: 8px;
background: #f8fbfd;
color: var(--primary-dark);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
transition: all 0.2s ease;
}
.word-toolbar button:hover {
background: #e6f4ff;
color: var(--primary);
border-color: var(--primary);
}
.word-toolbar select {
height: 34px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0 8px;
background: #fff;
color: var(--text-main);
font-size: 0.82rem;
cursor: pointer;
}
.word-toolbar select:hover { border-color: var(--primary); }
.word-toolbar .font-family-select { min-width: 150px; }
.word-toolbar .font-size-select { width: 82px; }
.memo-editor-footer {
user-select: none;
margin-top: 14px;
padding-top: 0;
line-height: 1.5;
font-size: 16px;
flex: 0 0 auto;
}
.closing-pledge {
margin: 18px 0 14px;
line-height: 1.5;
}
.closing-pledge strong {
display: block;
font-weight: 800;
letter-spacing: 0.02em;
}
.signature-block,
.generated-closing-lock {
user-select: none;
cursor: default;
display: block;
break-inside: avoid;
page-break-inside: avoid;
white-space: normal;
}
.signature-block *,
.generated-closing-lock * {
user-select: none;
white-space: normal;
}
.memo-sign-space {
height: 58px;
}
.signature-preview-img {
max-width: 150px;
max-height: 58px;
display: block;
margin-bottom: 4px;
object-fit: contain;
}
.prepared-by-line {
display: block;
margin-top: 0;
font-family: Arial, Helvetica, sans-serif;
font-size: 8px !important;
line-height: 1;
font-weight: 400;
}
.page-footer {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 0.8rem;
margin-top: 30px;
}
:root {
--primary: #0077b6;
--primary-dark: #005f99;
--secondary: #00b4d8;
--deep-blue: #023e5f;
--bg-light: rgba(245, 250, 253, 0.72);
--bg-card: rgba(255,255,255,0.68);
--text-main: #163447;
--text-muted: #2d5367;
--border: rgba(255,255,255,0.48);
--sidebar-width: 280px;
--glass: rgba(255,255,255,0.58);
--glass-strong: rgba(255,255,255,0.76);
--shadow-soft: 0 24px 70px rgba(2,62,95,0.16);
}
body {
background:
linear-gradient(135deg, rgba(245,250,253,0.90), rgba(222,246,252,0.76)),
url('assets/auth-bg.png') center/cover no-repeat fixed;
color: var(--text-main);
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(circle at 18% 18%, rgba(0,180,216,0.22), transparent 28%),
radial-gradient(circle at 86% 12%, rgba(0,119,182,0.18), transparent 28%),
linear-gradient(120deg, rgba(255,255,255,0.28), rgba(255,255,255,0.08));
pointer-events: none;
z-index: 0;
}
body::after {
content: '';
position: fixed;
width: 520px;
height: 520px;
right: -180px;
bottom: -220px;
background: rgba(0,180,216,0.18);
filter: blur(8px);
transform: rotate(18deg);
border-radius: 42px;
pointer-events: none;
z-index: 0;
}
.sidebar {
background: linear-gradient(180deg, rgba(2,62,95,0.94), rgba(0,119,182,0.84));
border-right: 1px solid rgba(255,255,255,0.22);
box-shadow: 18px 0 44px rgba(2,38,60,0.20);
backdrop-filter: blur(18px) saturate(1.25);
}
.sidebar-header {
background: transparent !important;
border-bottom: 1px solid rgba(255,255,255,0.16);
}
.sidebar-header img {
width: 58px;
filter: drop-shadow(0 8px 14px rgba(0,0,0,0.22));
}
.sidebar-header h3 { color: #fff; font-weight: 800; }
.sidebar-header p { color: rgba(255,255,255,0.72); }
.sidebar-section { color: rgba(255,255,255,0.56); }
.sidebar-menu li a,
.sidebar-menu .dropdown-toggle {
color: rgba(255,255,255,0.88);
}
.sidebar-menu li a:hover,
.sidebar-menu li a.active,
.sidebar-menu .dropdown-toggle:hover,
.sidebar-menu .dropdown-toggle.active {
background: rgba(255,255,255,0.18);
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18);
}
.sidebar-menu .dropdown-menu a {
color: rgba(255,255,255,0.78);
}
.sidebar-menu .dropdown-menu a:hover,
.sidebar-menu .dropdown-menu a.active {
background: rgba(255,255,255,0.16);
color: #fff;
}
.sidebar-toggle {
right: 10px;
top: 28px;
width: 28px;
height: 28px;
background: rgba(255,255,255,0.18);
border: 1px solid rgba(255,255,255,0.22);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.main {
padding: 28px 32px;
position: relative;
z-index: 1;
}
.top-bar,
.form-card,
.side-card,
.info-panel,
.summary-card,
.header-footer-panel {
background: var(--glass);
border: 1px solid var(--border);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(24px) saturate(1.35);
}
.top-bar {
border-radius: 18px;
padding: 16px 24px;
}
.top-bar h2,
.form-header h1,
.section-title {
color: var(--primary-dark);
font-weight: 800;
}
.form-card,
.side-card {
border-radius: 22px;
}
.form-group input,
.form-group select,
.form-group textarea,
.readonly-box,
.routing-option,
.attachment-control,
.preparer-sign-panel {
background: rgba(255,255,255,0.74);
border-color: rgba(255,255,255,0.62);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: rgba(0,180,216,0.82);
box-shadow: 0 0 0 4px rgba(0,180,216,0.16);
}
.word-editor-shell,
.page-controls-section,
.word-toolbar-section {
background: rgba(255,255,255,0.46);
border-color: rgba(255,255,255,0.62);
box-shadow: 0 18px 44px rgba(2,62,95,0.10);
backdrop-filter: blur(22px) saturate(1.28);
-webkit-backdrop-filter: blur(22px) saturate(1.28);
}
.word-toolbar button,
.page-controls button,
.page-controls select,
.word-toolbar select,
.word-toolbar input {
background: rgba(255,255,255,0.78);
border-color: rgba(0,119,182,0.18);
}
.compact-letter-form .letter-editor-section {
background: rgba(255,255,255,0.30);
border: 1px solid rgba(255,255,255,0.52);
box-shadow: 0 22px 54px rgba(2,62,95,0.10);
backdrop-filter: blur(22px) saturate(1.28);
-webkit-backdrop-filter: blur(22px) saturate(1.28);
}
.compact-letter-form .word-editor-shell {
background: rgba(255,255,255,0.36);
border-color: rgba(255,255,255,0.56);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.72), 0 18px 44px rgba(2,62,95,0.10);
backdrop-filter: blur(24px) saturate(1.3);
-webkit-backdrop-filter: blur(24px) saturate(1.3);
}
.compact-letter-form .word-toolbar-section,
.compact-letter-form .page-controls-section {
background: rgba(255,255,255,0.22);
border-color: rgba(255,255,255,0.44);
box-shadow: inset 1px 0 0 rgba(255,255,255,0.34);
backdrop-filter: blur(20px) saturate(1.25);
-webkit-backdrop-filter: blur(20px) saturate(1.25);
}
.compact-letter-form #letter_pages_container {
background: rgba(238,245,248,0.24);
border-color: rgba(255,255,255,0.42);
position: relative;
z-index: 1;
pointer-events: auto;
}
#page_number_position {
background: rgba(255,255,255,0.86);
border: 1px solid rgba(0,119,182,0.20);
border-radius: 7px;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.82), 0 8px 20px rgba(2,62,95,0.06);
color: var(--text-main);
font-size: 0.76rem;
height: 30px;
min-width: 0;
width: 210px;
max-width: 100%;
padding: 0 7px;
}
.compact-letter-form .page-controls-section {
align-items: flex-start;
padding: 6px 8px;
}
.compact-letter-form .generate-closing-btn {
margin-top: 2px;
padding: 7px 12px;
align-self: flex-start;
}
.compact-letter-form .memo-page-preview,
.compact-letter-form .approval-page-preview,
.compact-letter-form .letter-page-editor,
.compact-letter-form .quill-letter-host,
.compact-letter-form .quill-letter-host .ql-editor {
position: relative;
z-index: 2;
pointer-events: auto;
user-select: text;
-webkit-user-select: text;
}
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--secondary)); }
.btn-secondary { background: rgba(255,255,255,0.76); color: var(--primary-dark); border: 1px solid rgba(0,119,182,0.18); }
.page-footer { color: var(--text-muted); }
@media (max-width: 1100px) {
.shell { grid-template-columns: 1fr; }
}
@media (max-width: 992px) {
.sidebar { width: 70px; }
.sidebar .sidebar-header h3, .sidebar .sidebar-header p, .sidebar .menu-text, .sidebar .role-badge, .sidebar .dropdown-menu, .sidebar .sidebar-section { display: none; }
.main { margin-left: 70px; }
.sidebar-toggle { display: none; }
}
@media (max-width: 640px) {
.header-footer-panel { grid-template-columns: 1fr; }
.page-controls-section { flex-direction: column; align-items: stretch; }
.page-control-left, .page-control-right { justify-content: space-between; }
}
.sidebar {
overflow: hidden;
}
.sidebar-header {
flex: 0 0 auto;
}
.sidebar-menu {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 0;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: #8f8f8f transparent;
}
.sidebar-menu::-webkit-scrollbar {
width: 10px;
}
.sidebar-menu::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-menu::-webkit-scrollbar-thumb {
background: #8f8f8f;
border-radius: 999px;
border: 2px solid transparent;
background-clip: content-box;
}
.sidebar-menu::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}

        /* Button hover animation */
        .main .btn,
        .main .tab-btn,
        .main .action-btn,
        .main .btn-action,
        .main .filter-btn,
        .main .button,
        .main .generate-closing-btn,
        .main .attachment-remove-btn,
        .main .page-add-btn,
        .main .page-delete-btn,
        .main .delete-current-page-btn,
        .main .form-actions a,
        .main .form-actions button,
        .main .filter-actions a,
        .main .filter-actions button,
        .main .document-command a,
        .main .document-command button,
        .main table a[class*="btn"],
        .main table button[class*="btn"],
        .main input[type="submit"],
        .main input[type="button"] {
            transition: transform 0.2s ease, filter 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease, border-color 0.2s ease !important;
            will-change: transform;
        }

        .main .btn:hover:not(:disabled),
        .main .tab-btn:hover:not(:disabled),
        .main .action-btn:hover:not(:disabled),
        .main .btn-action:hover:not(:disabled),
        .main .filter-btn:hover:not(:disabled),
        .main .button:hover:not(:disabled),
        .main .generate-closing-btn:hover:not(:disabled),
        .main .attachment-remove-btn:hover:not(:disabled),
        .main .page-add-btn:hover:not(:disabled),
        .main .page-delete-btn:hover:not(:disabled),
        .main .delete-current-page-btn:hover:not(:disabled),
        .main .form-actions a:hover,
        .main .form-actions button:hover:not(:disabled),
        .main .filter-actions a:hover,
        .main .filter-actions button:hover:not(:disabled),
        .main .document-command a:hover,
        .main .document-command button:hover:not(:disabled),
        .main table a[class*="btn"]:hover,
        .main table button[class*="btn"]:hover:not(:disabled),
        .main input[type="submit"]:hover:not(:disabled),
        .main input[type="button"]:hover:not(:disabled) {
            transform: translateY(-1px);
            filter: brightness(1.04);
            box-shadow: 0 14px 28px rgba(0, 119, 182, 0.18);
        }

        .main .btn:active:not(:disabled),
        .main .tab-btn:active:not(:disabled),
        .main .action-btn:active:not(:disabled),
        .main .btn-action:active:not(:disabled),
        .main .filter-btn:active:not(:disabled),
        .main .button:active:not(:disabled),
        .main .generate-closing-btn:active:not(:disabled),
        .main .attachment-remove-btn:active:not(:disabled),
        .main .page-add-btn:active:not(:disabled),
        .main .page-delete-btn:active:not(:disabled),
        .main .delete-current-page-btn:active:not(:disabled),
        .main .form-actions a:active,
        .main .form-actions button:active:not(:disabled),
        .main .filter-actions a:active,
        .main .filter-actions button:active:not(:disabled),
        .main .document-command a:active,
        .main .document-command button:active:not(:disabled),
        .main table a[class*="btn"]:active,
        .main table button[class*="btn"]:active:not(:disabled),
        .main input[type="submit"]:active:not(:disabled),
        .main input[type="button"]:active:not(:disabled) {
            transform: translateY(0);
            filter: brightness(0.98);
        }</style>
</head>
<body class="role-<?= htmlspecialchars($role) ?>">
<aside class="sidebar" id="sidebar">
<button class="sidebar-toggle" id="sidebarToggle" title="Tutup/Buka Menu">
<i class="fas fa-chevron-left"></i>
</button>
<div class="sidebar-header">
<img src="mainpp.png" alt="Logo MAINPP">
<h3>MAINPP WORKSPACE</h3>
</div>
<ul class="sidebar-menu">
<?php renderUnifiedSidebarMenu($conn, $role, basename(__FILE__)); ?>
</ul>
</aside>
<main class="main">
<div class="top-bar">
<h2><i class="fas fa-file-signature"></i> <?= $is_edit ? 'Ubah ' . htmlspecialchars($current_document_type) : 'Sediakan ' . htmlspecialchars($current_document_type) ?></h2>
<?php renderTopBarUserMenu($conn, $role, basename(__FILE__)); ?>
</div>
<?php
renderBreadcrumb([
['label' => 'Dashboard', 'url' => 'dashboard.php'],
['label' => 'Modul Surat', 'url' => 'draft_letters.php'],
['label' => 'Sediakan Surat', 'url' => 'draft_letter_form.php'],
['label' => ($is_edit ? 'Ubah ' : '') . $current_document_type, 'url' => basename(__FILE__) . ($is_edit ? '?id=' . $draft_id : ''), 'current' => true]
]);
?>
<div class="shell">
<div class="form-card compact-letter-form">
<div class="form-header">
<h1><?= $is_edit ? 'Ubah ' . htmlspecialchars($current_document_type) : htmlspecialchars($current_document_type) . ' Baharu' ?></h1>
</div>
<?php if ($error_message !== ''): ?>
<div class="alert"><?= htmlspecialchars($error_message) ?></div>
<?php endif; ?>
<?php if ($success_message !== ''): ?>
<div class="alert" style="background:#e6fff0;color:var(--success);border-color:#b8ebcd;"><?= htmlspecialchars($success_message) ?></div>
<?php endif; ?>
<form method="POST" action="process_draft_letter.php?action=<?= $is_edit ? 'update' : 'add' ?>" enctype="multipart/form-data" class="draft-stack">
<?php if ($is_edit): ?>
<input type="hidden" name="id" value="<?= $draft_id ?>">
<input type="hidden" name="folder_id" value="<?= (int) $form_values['folder_id'] ?>">
<?php endif; ?>
<div class="grid">
<div class="form-group">
<label>Fail Office <span style="color:var(--danger);">*</span></label>
<?php if ($is_edit): ?>
<div class="readonly-box">
<strong><?= htmlspecialchars(($selected_folder['folder_name'] ?? '-') . ' (' . ($selected_folder['folder_code'] ?? '-') . ')') ?></strong>
<span><?= htmlspecialchars($selected_folder['department'] ?? '-') ?></span>
</div>
<?php else: ?>
<select name="folder_id" id="folder_id" required>
<option value="">-- Pilih Fail Office --</option>
<?php foreach ($folders as $folder): ?>
<option
value="<?= (int) $folder['id'] ?>"
data-folder-code="<?= htmlspecialchars($folder['folder_code']) ?>"
data-folder-name="<?= htmlspecialchars($folder['folder_name']) ?>"
data-department="<?= htmlspecialchars($folder['department']) ?>"
data-next-ref="<?= htmlspecialchars($folder['next_ref_no']) ?>"
data-next-file-numbering="<?= htmlspecialchars($folder['next_file_numbering']) ?>"
<?= (int) $form_values['folder_id'] === (int) $folder['id'] ? 'selected' : '' ?>
>
<?= htmlspecialchars($folder['folder_name'] . ' - ' . $folder['folder_code']) ?>
</option>
<?php endforeach; ?>
</select>
<div class="hint">Hanya fail office aktif dipaparkan kerana rujukan surat rasmi akan dijana secara automatik.</div>
<?php endif; ?>
</div>
<div class="form-group">
<label>No. Rujukan</label>
<div class="readonly-box reference-readonly-box">
<strong id="ref_preview"><?= htmlspecialchars($current_ref_display) ?></strong>
</div>
<div class="hint"><?= ($is_edit && (($draft['status'] ?? 'draf') === 'siap')) ? 'Rujukan kekal selepas surat diwujudkan.' : 'Draf akan dipaparkan sebagai X. Nombor rasmi hanya muncul selepas dihantar untuk kelulusan.' ?></div>
</div>
</div>
<input type="hidden" name="template_id" id="template_id" value="<?= (int) $form_values['template_id'] ?>">
<div class="grid letter-primary-grid">
<div class="form-group">
<label>Tarikh Surat <span style="color:var(--danger);">*</span></label>
<input type="date" name="letter_date" value="<?= htmlspecialchars($form_values['letter_date']) ?>" required>
</div>
<div class="form-group">
<label>Dokumen <span style="color:var(--danger);">*</span></label>
<input type="text" name="doc_type" id="doc_type" class="locked-control" value="<?= htmlspecialchars($form_values['doc_type']) ?>" required readonly>
</div>
<div class="form-group subject-field">
<label>Perkara / Tajuk Surat <span style="color:var(--danger);">*</span></label>
<input type="text" name="subject" value="<?= htmlspecialchars($form_values['subject']) ?>" required>
</div>
</div>
<div class="grid">
<div class="form-group">
<label>Kepada <span style="color:var(--danger);">*</span></label>
<select name="recipient_organization" id="recipient_organization" required>
<option value="">-- Pilih Jawatan --</option>
<?php renderMemoPositionSelectOptions($memo_position_options, splitMemoPositionValues($form_values['recipient_organization'])[0] ?? ''); ?>
</select>
</div>
<div class="form-group">
<label>Melalui</label>
<select name="attention_to" id="attention_to">
<option value="">-- Tiada / Tidak Berkaitan --</option>
<?php renderMemoPositionSelectOptions($memo_position_options, splitMemoPositionValues($form_values['attention_to'])[0] ?? ''); ?>
</select>
</div>
</div>
<div class="form-group approval-route-group">
<label>Turutan Edaran Kelulusan <span style="color:var(--danger);">*</span></label>
<div class="routing-checklist" id="routing_positions">
<?php foreach ($routing_candidate_positions as $position_option): ?>
<?php $available_count = (int) ($routing_user_count_by_position[$position_option] ?? 0); ?>
<?php
$is_checked = isset($routing_order_by_position[$position_option]);
$source_label = $position_option === $form_values['recipient_organization']
? 'Pilihan Kepada'
: ($position_option === $form_values['attention_to'] ? 'Pilihan Melalui' : 'Ketua Bahagian');
?>
<label class="routing-option <?= $available_count === 0 ? 'is-unavailable' : '' ?>" data-position="<?= htmlspecialchars($position_option) ?>" data-available-count="<?= $available_count ?>">
<input
type="checkbox"
class="routing-checkbox"
name="routing_positions[]"
value="<?= htmlspecialchars($position_option) ?>"
<?= $is_checked ? 'checked' : '' ?>
<?= $available_count === 0 ? 'disabled' : '' ?>
>
<span>
<?= htmlspecialchars($position_option) ?>
<small class="routing-source"><?= htmlspecialchars($source_label) ?></small>
<small><?= htmlspecialchars(formatRoutingRegisteredUsersLabel($position_option, $routing_user_profiles_by_position)) ?></small>
</span>
<select
class="routing-order-select"
name="routing_order[<?= htmlspecialchars($position_option) ?>]"
aria-label="Turutan <?= htmlspecialchars($position_option) ?>"
<?= $is_checked && $available_count > 0 ? '' : 'disabled' ?>
>
<?php for ($route_number = 1; $route_number <= $routing_order_limit; $route_number++): ?>
<option value="<?= $route_number ?>" <?= (($routing_order_by_position[$position_option] ?? 0) === $route_number) ? 'selected' : '' ?>>
Pilihan <?= $route_number ?>
</option>
<?php endfor; ?>
</select>
</label>
<?php endforeach; ?>
</div>
<div class="hint">Kepada dan Melalui hanya dipaparkan pada memo. Tandakan penerima tindakan di sini dan pilih nombor 1, 2, 3 dan seterusnya untuk menentukan giliran emel, kelulusan dan tandatangan.</div>
</div>
<input type="hidden" name="recipient_name" value="">
<input type="hidden" name="recipient_address" value="">
<input type="hidden" name="greeting" value="">
<div class="form-group letter-editor-section">
<label>Isi Surat <span style="color:var(--danger);">*</span></label>
<input type="hidden" name="letter_body" id="letter_body" value="<?= htmlspecialchars($form_values['letter_body'], ENT_QUOTES) ?>">
<input type="hidden" name="letter_pages" id="letter_pages" value="<?= htmlspecialchars(json_encode($form_values['letter_pages'], JSON_UNESCAPED_UNICODE), ENT_QUOTES) ?>">
<input type="hidden" name="page_header_text" id="page_header_text" value="<?= htmlspecialchars($form_values['page_header_text'], ENT_QUOTES) ?>">
<input type="hidden" name="page_footer_text" id="page_footer_text" value="<?= htmlspecialchars($form_values['page_footer_text'], ENT_QUOTES) ?>">
<div class="word-editor-shell">
<div class="word-toolbar-section">
<span class="toolbar-section-label">Font Style</span>
<div class="word-toolbar">
<select id="font_family" class="font-family-select locked-control" title="Jenis font" disabled>
<option value="Arial" selected>Arial</option>
<option value="Calibri">Calibri</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Tahoma">Tahoma</option>
<option value="Verdana">Verdana</option>
</select>
<select id="font_size" class="font-size-select locked-control" title="Saiz font" disabled>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="14">14</option>
<option value="16" selected>16</option>
<option value="18">18</option>
<option value="20">20</option>
</select>
<button type="button" data-command="bold" title="Bold"><i class="fas fa-bold"></i></button>
<button type="button" data-command="italic" title="Italic"><i class="fas fa-italic"></i></button>
<button type="button" data-command="underline" title="Underline"><i class="fas fa-underline"></i></button>
</div>
</div>
<div class="word-toolbar-section">
<span class="toolbar-section-label">Text Allignment</span>
<div class="word-toolbar">
<button type="button" data-command="justifyLeft" title="Align Left"><i class="fas fa-align-left"></i></button>
<button type="button" data-command="justifyCenter" title="Allign Center"><i class="fas fa-align-center"></i></button>
<button type="button" data-command="justifyRight" title="Allign Right"><i class="fas fa-align-right"></i></button>
<button type="button" data-command="justifyFull" title="Justify"><i class="fas fa-align-justify"></i></button>
</div>
</div>
<div class="word-toolbar-section">
<span class="toolbar-section-label">List</span>
<div class="word-toolbar">
<button type="button" data-command="insertOrderedList" title="Number"><i class="fas fa-list-ol"></i></button>
<button type="button" data-command="insertUnorderedList" title="Bullet"><i class="fas fa-list-ul"></i></button>
</div>
</div>
<div class="word-toolbar-section">
<span class="toolbar-section-label">Table</span>
<div class="word-toolbar">
<select id="table_rows" title="Total Row">
<?php for ($row_count = 1; $row_count <= 10; $row_count++): ?>
<option value="<?= $row_count ?>" <?= $row_count === 3 ? 'selected' : '' ?>><?= $row_count ?> row</option>
<?php endfor; ?>
</select>
<select id="table_cols" title="Total Column">
<?php for ($col_count = 1; $col_count <= 8; $col_count++): ?>
<option value="<?= $col_count ?>" <?= $col_count === 2 ? 'selected' : '' ?>><?= $col_count ?> col</option>
<?php endfor; ?>
</select>
<button type="button" data-action="insertTable" title="Insert Table"><i class="fas fa-table"></i></button>
</div>
</div>
<div class="page-controls-section">
<div class="page-control-left">
<span id="page_count_label">Page Number</span>
<select name="page_number_position" id="page_number_position" title="Kedudukan nombor halaman">
<option value="none" <?= $form_values['page_number_position'] === 'none' ? 'selected' : '' ?>>Tiada Nombor Halaman</option>
<option value="bottom_center" <?= $form_values['page_number_position'] === 'bottom_center' ? 'selected' : '' ?>>Nombor: Bawah Tengah</option>
<option value="bottom_right" <?= $form_values['page_number_position'] === 'bottom_right' ? 'selected' : '' ?>>Nombor: Bawah Kanan</option>
</select>
</div>
</div>
<div id="letter_pages_container">
<?php foreach ($form_values['letter_pages'] as $page_index => $page_body): ?>
<div class="memo-page-preview <?= $page_index > 0 ? 'is-extra-page' : '' ?>" data-page-index="<?= $page_index ?>" data-page-number="<?= $page_index + 1 ?>">
<div class="page-sheet-actions" aria-label="Tindakan halaman">
<button type="button" class="page-add-btn" title="Tambah halaman selepas halaman ini"><i class="fas fa-file-circle-plus"></i></button>
<button type="button" class="page-delete-btn" title="Padam halaman ini" <?= $page_index === 0 ? 'disabled' : '' ?>><i class="fas fa-trash-alt"></i></button>
</div>
<div class="memo-custom-header" contenteditable="false" data-header-footer="header"><?= renderDraftEditorBody($form_values['page_header_text']) ?></div>
<?php if ($page_index === 0): ?>
                                <div class="memo-editor-fixed">
                                <div class="memo-logo"><img src="mainpp.png" alt="Logo MAINPP"></div>
                                <div class="memo-title" id="memo_doc_type_preview"><?= htmlspecialchars(strtoupper($form_values['doc_type'])) ?></div>
                                <table class="memo-ref-table">
                                <tr>
                                <td class="memo-ref-left" rowspan="2">
                                MAJLIS AGAMA ISLAM NEGERI PULAU PINANG,<br>
                                TINGKAT 7, 8 &amp; 9, MENARA JKP,<br>
                                128, JALAN MACALISTER,<br>
                                10400 PULAU PINANG.
                                </td>
                                <td class="memo-ref-right"><div class="memo-ref-line"><span class="memo-ref-label">Ruj Kami</span><span class="memo-ref-colon">:</span><span class="memo-ref-value" id="memo_ref_preview"><?= htmlspecialchars($current_ref_display) ?></span></div></td>
                                </tr>
                                <tr>
                                <td class="memo-ref-right"><div class="memo-ref-line"><span class="memo-ref-label">Tarikh</span><span class="memo-ref-colon">:</span><span class="memo-ref-value" id="memo_date_preview"><?= htmlspecialchars(formatMalayMemoDate($form_values['letter_date'])) ?></span></div></td>
                                </tr>
                                </table>
                                <table class="memo-label-grid">
                                <tr>
                                <td class="memo-label">DARIPADA</td>
                                <td class="memo-colon">:</td>
                                <td class="memo-value" id="memo_from_preview"><?= htmlspecialchars($form_values['signatory_position'] ?: '-') ?><br>Majlis Agama Islam Negeri Pulau Pinang</td>
                                </tr>
                                <tr>
                                <td class="memo-label">MELALUI</td>
                                <td class="memo-colon">:</td>
                                <td class="memo-value" id="memo_through_preview"><?= renderMemoDisplayValue($form_values['attention_to']) ?></td>
                                </tr>
                                <tr>
                                <td class="memo-label">KEPADA</td>
                                <td class="memo-colon">:</td>
                                <td class="memo-value" id="memo_to_preview"><?= renderMemoDisplayValue($form_values['recipient_organization']) ?></td>
                                </tr>
                                </table>
                                </div>
<?php endif; ?>
<div class="word-editor letter-page-editor" contenteditable="true"><?= renderDraftEditorBody($page_body) ?></div>
<div class="memo-custom-footer" contenteditable="false" data-header-footer="footer"><?= renderDraftEditorBody($form_values['page_footer_text']) ?></div>
<div class="memo-page-number <?= htmlspecialchars($form_values['page_number_position']) ?>"><?= $page_index + 1 ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<input type="hidden" name="closing_text" value="">
<div class="form-group approval-editor-group" id="approval_editor_group">
<label>Kandungan Halaman Kelulusan</label>
<div class="hint">Halaman ini dijana selepas memo. Sediakan kandungan berasingan untuk setiap penerima sebelum tajuk Ulasan.</div>
<input type="hidden" name="approval_page_body" id="approval_page_body" value="<?= htmlspecialchars($form_values['approval_page_body'], ENT_QUOTES) ?>">
<input type="hidden" name="approval_page_subject" value="">
<input type="hidden" name="approval_route_bodies" id="approval_route_bodies" value="<?= htmlspecialchars(json_encode($form_values['approval_route_bodies'], JSON_UNESCAPED_UNICODE), ENT_QUOTES) ?>">
<div class="approval-pages-container" id="approval_pages_container"></div>
</div>
<div class="grid signatory-control-group">
<div class="form-group">
<label>Nama Penandatangan</label>
<input type="text" name="signatory_name" value="<?= htmlspecialchars($form_values['signatory_name']) ?>">
</div>
<div class="form-group">
<label>Daripada <span style="color:var(--danger);">*</span></label>
<select name="signatory_position" id="signatory_position" required>
<option value="">-- Pilih Jawatan --</option>
<?php renderMemoPositionSelectOptions($memo_position_options, $form_values['signatory_position']); ?>
</select>
</div>
<div class="form-group">
<label>Bahagian Penandatangan</label>
<input type="text" name="signatory_department" id="signatory_department" value="<?= htmlspecialchars($form_values['signatory_department']) ?>">
</div>
</div>
<input type="hidden" name="approval_flow" value="two_level">
<input type="hidden" name="department_officer_user_id" id="department_officer_user_id" value="<?= (int) $form_values['department_officer_user_id'] ?>">
<div class="form-group attachment-control">
<label>Lampiran Sokongan</label>
<input type="file" name="file_upload[]" id="support_attachment_input" accept=".pdf,.doc,.docx,.xls,.xlsx" multiple>
<div class="hint">Boleh tambah lebih daripada satu fail. Format: PDF, DOC, DOCX, XLS, XLSX. Maksimum 10MB setiap fail.</div>
<div class="selected-attachment-title" id="selected_attachment_title" style="display:none;">Fail baru dipilih</div>
<div class="selected-attachment-list" id="selected_attachment_list"></div>
<?php if (!empty($attachment_map)): ?>
<div class="attachment-list">
<?php foreach ($attachment_map as $attachment): ?>
<div class="attachment-item">
<div>
<i class="fas fa-paperclip"></i>
<a href="download.php?attachment_id=<?= (int) ($attachment['id'] ?? 0) ?>" target="_blank">
<?= htmlspecialchars($attachment['original_name'] ?: basename($attachment['file_path'])) ?>
</a>
</div>
<?php if ($is_edit && !empty($attachment['id'])): ?>
<a
href="process_draft_letter.php?action=delete_attachment&id=<?= $draft_id ?>&attachment_id=<?= (int) $attachment['id'] ?>"
class="btn btn-danger btn-sm"
onclick="return confirm('Padam lampiran sokongan ini?')"
>
<i class="fas fa-trash-alt"></i> Padam
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php elseif ($is_edit): ?>
<div class="current-file">Tiada lampiran sokongan sedia ada.</div>
<?php endif; ?>
</div>
<input type="hidden" name="preparer_signed" id="preparer_signed" value="<?= $preparer_is_signed ? '1' : '0' ?>">
<div class="preparer-sign-panel <?= !$current_user_is_head_officer || $preparer_is_signed ? 'signed' : ($current_user_signature_path === '' ? 'missing' : '') ?>" id="preparer_sign_panel">
<div>
<strong id="preparer_sign_title">
<?php if (!$current_user_is_head_officer): ?>
Semakan ketua bahagian diperlukan
<?php else: ?>
<?= $preparer_is_signed ? 'Tandatangan penyedia telah dibuat' : 'Tandatangan penyedia diperlukan' ?>
<?php endif; ?>
</strong>
<span id="preparer_sign_hint">
<?php if (!$current_user_is_head_officer): ?>
Surat boleh dihantar kepada ketua bahagian untuk disemak dan ditandatangani.
<?php elseif ($preparer_is_signed): ?>
Surat ini sudah mempunyai tandatangan penyedia.
<?php elseif ($current_user_signature_path === ''): ?>
Tandatangan anda belum didaftarkan dalam profil pengguna.
<?php else: ?>
Klik Tandatangan Penyedia dahulu sebelum surat dihantar.
<?php endif; ?>
</span>
</div>
<?php if (!$current_user_is_head_officer): ?>
<span class="btn btn-secondary" aria-disabled="true">
<i class="fas fa-user-check"></i> Hantar Untuk Semakan
</span>
<?php else: ?>
<?php if ($current_user_signature_path !== ''): ?>
<button type="button" class="btn btn-warning" id="preparer_sign_btn" data-signature-path="<?= htmlspecialchars($current_user_signature_path) ?>">
<i class="fas fa-signature"></i> <?= $preparer_is_signed ? 'Tandatangan Semula' : 'Tandatangan Penyedia' ?>
</button>
<?php else: ?>
<a href="profile_pengguna.php" class="btn btn-secondary">
<i class="fas fa-user-pen"></i> Daftar Tandatangan
</a>
<?php endif; ?>
<?php endif; ?>
</div>
<button type="button" class="generate-closing-btn" id="generate_closing_btn">
<i class="fas fa-wand-magic-sparkles"></i> Generate Closing
</button>
<div class="btn-row">
<a href="draft_letter_form.php" class="btn btn-secondary btn-row-left">
<i class="fas fa-arrow-left"></i> Kembali
</a>
<button type="submit" class="btn btn-secondary" name="status" value="draf" id="save_draft_btn">
<i class="fas fa-save"></i> Simpan Draf
</button>
<button type="submit" class="btn btn-primary" name="status" value="siap" id="submit_draft_btn">
<i class="fas fa-paper-plane"></i> Simpan &amp; Hantar Untuk Kelulusan
</button>
</div>
</form>
</div>
</div>
<div class="page-footer">
Hakcipta Terpelihara &copy; <?= date('Y') ?> Majlis Agama Islam Negeri Pulau Pinang.
</div>
</main>
<script src="assets/vendor/quill/quill.js"></script>
<script src="assets/js/editor-table-resize.js"></script>
<script>
const sidebar = document.getElementById('sidebar');
const toggleBtn = document.getElementById('sidebarToggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => sidebar.classList.toggle('collapsed'));
}
if (window.innerWidth < 992) sidebar.classList.add('collapsed');
document.querySelectorAll('.sidebar-menu .dropdown-toggle, .dropdown-toggle').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const dropdown = this.parentElement;
const isActive = dropdown.classList.contains('active');
document.querySelectorAll('.sidebar-menu .dropdown, .dropdown').forEach(item => {
if (item !== dropdown) item.classList.remove('active');
});
dropdown.classList.toggle('active', !isActive);
});
});
function confirmLogout() {
return confirm("Adakah anda pasti ingin log keluar?");
}
const folderSelect = document.getElementById('folder_id');
const templateSelect = document.getElementById('template_id');
const refPreview = document.getElementById('ref_preview');
const signatoryDepartmentInput = document.getElementById('signatory_department');
const letterBodyInput = document.getElementById('letter_body');
const letterPagesInput = document.getElementById('letter_pages');
const letterPagesContainer = document.getElementById('letter_pages_container');
const pageCountLabel = document.getElementById('page_count_label');
const addPageBtn = document.getElementById('add_page_btn');
const pageHeaderInput = document.getElementById('page_header_text');
const pageFooterInput = document.getElementById('page_footer_text');
const deleteCurrentPageBtn = document.getElementById('delete_current_page_btn');
const pageNumberPositionSelect = document.getElementById('page_number_position');
const memoRefPreview = document.getElementById('memo_ref_preview');
const memoDatePreview = document.getElementById('memo_date_preview');
const memoFromPreview = document.getElementById('memo_from_preview');
const memoToPreview = document.getElementById('memo_to_preview');
const memoThroughPreview = document.getElementById('memo_through_preview');
const memoSignatoryPreview = document.getElementById('memo_signatory_preview');
const memoDocTypePreview = document.getElementById('memo_doc_type_preview');
const memoSignaturePreview = document.getElementById('memo_signature_preview');
const generateClosingBtn = document.getElementById('generate_closing_btn');
const letterDateInput = document.querySelector('input[name="letter_date"]');
const subjectInput = document.querySelector('input[name="subject"]');
const recipientInput = document.querySelector('select[name="recipient_organization"]');
const throughInput = document.querySelector('select[name="attention_to"]');
const routingChecklist = document.getElementById('routing_positions');
const approvalRouteGroup = document.querySelector('.approval-route-group');
const approvalEditorGroup = document.getElementById('approval_editor_group');
const routingAvailability = <?= json_encode($routing_user_count_by_position, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const routingUserProfiles = <?= json_encode($routing_user_profiles_by_position, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const routingHeadPositions = new Set(<?= json_encode(array_values($memo_position_options['Ketua Bahagian'] ?? []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>);
const excludedApprovalRoutePositions = new Set(<?= json_encode($excluded_approval_route_positions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>);
const routingOrderLimit = <?= (int) $routing_order_limit ?>;
const docTypeInput = document.querySelector('[name="doc_type"]');
const signatoryNameInput = document.querySelector('input[name="signatory_name"]');
const signatoryPositionInput = document.querySelector('select[name="signatory_position"]');
const draftBodyStyle = 'font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.5';
        const letterUndoStack = [];
        const letterRedoStack = [];
        let isRestoringLetterSnapshot = false;
        const maxLetterUndoSnapshots = 80;
const preparedByShortName = <?= json_encode($prepared_by_short_name, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const fontFamilySelect = document.getElementById('font_family');
const fontSizeSelect = document.getElementById('font_size');
const tableRowsSelect = document.getElementById('table_rows');
const tableColsSelect = document.getElementById('table_cols');
const draftForm = document.querySelector('form[action^="process_draft_letter.php"]');
const draftStatusSelect = document.getElementById('draft_status');
const preparerSignedInput = document.getElementById('preparer_signed');
const preparerSignBtn = document.getElementById('preparer_sign_btn');
const preparerSignPanel = document.getElementById('preparer_sign_panel');
const preparerSignTitle = document.getElementById('preparer_sign_title');
const preparerSignHint = document.getElementById('preparer_sign_hint');
const submitDraftBtn = document.getElementById('submit_draft_btn');
const supportAttachmentInput = document.getElementById('support_attachment_input');
const selectedAttachmentTitle = document.getElementById('selected_attachment_title');
const selectedAttachmentList = document.getElementById('selected_attachment_list');
const approvalPageBodyInput = document.getElementById('approval_page_body');
const approvalRouteBodiesInput = document.getElementById('approval_route_bodies');
const approvalPagesContainer = document.getElementById('approval_pages_container');
const currentUserIsHeadOfficer = <?= $current_user_is_head_officer ? 'true' : 'false' ?>;
const isDepartmentOfficerRevisionEditor = <?= $is_department_officer_revision_editor ? 'true' : 'false' ?>;
const currentUserSignaturePath = <?= json_encode($current_user_signature_path, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const departmentHeadsByDepartment = <?= json_encode(array_reduce($department_head_users, function ($carry, $user) {
$department = normalizeDepartmentValue($user['department'] ?? '');
if ($department !== '' && !isset($carry[$department])) {
$carry[$department] = [
'id' => (int) ($user['id'] ?? 0),
'name' => trim((string) ($user['fullname'] ?? ($user['email'] ?? ''))),
'position' => trim((string) ($user['position'] ?? 'Ketua Pegawai')),
'department' => $department,
];
}
return $carry;
}, []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const letterTemplates = <?= json_encode(array_map(static function ($template) {
return [
'id' => (int) ($template['template_id'] ?? 0),
'name' => (string) ($template['template_name'] ?? ''),
'content' => (string) ($template['template_content'] ?? ''),
];
}, $letter_templates), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
let approvalRouteBodies = <?= json_encode($form_values['approval_route_bodies'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
let activeLetterEditor = null;
let savedEditorRange = null;
let isPaginating = false;
let isRepaginating = false;
let isInsertingSignature = false;
let scheduledRepaginationFrame = null;
let scheduledRepaginationEditor = null;
let generatedClosingRestoreTimer = null;
let isRebalancingGeneratedClosing = false;
let allLetterPagesSelected = false;
let routingSequenceCounter = 0;
function isReadyToSendStatus() {
return true;
}
function updateApprovalControlsForStatus() {
const readyToSend = isReadyToSendStatus();
approvalRouteGroup?.classList.toggle('approval-disabled', !readyToSend);
approvalEditorGroup?.classList.toggle('approval-disabled', !readyToSend);
document.querySelectorAll('.routing-checkbox').forEach(checkbox => {
const availableCount = parseInt(checkbox.closest('.routing-option')?.dataset.availableCount || '1', 10);
checkbox.disabled = !readyToSend || availableCount <= 0;
});
document.querySelectorAll('.routing-order-select').forEach(select => {
const checkbox = select.closest('.routing-option')?.querySelector('.routing-checkbox');
select.disabled = !readyToSend || !checkbox || !checkbox.checked;
});
if (typeof departmentOfficerSelect !== 'undefined' && departmentOfficerSelect) {
departmentOfficerSelect.disabled = !readyToSend;
}
if (submitDraftBtn) {
submitDraftBtn.disabled = false;
}
if (preparerSignPanel) {
preparerSignPanel.classList.toggle('approval-disabled', !readyToSend);
}
}
function formatFileSize(bytes) {
const size = Number(bytes) || 0;
if (size >= 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(1) + ' MB';
}
if (size >= 1024) {
return Math.ceil(size / 1024) + ' KB';
}
return size + ' B';
}
function getCheckedMemoPositions(selectElement) {
if (!selectElement) return [];
const dropdown = document.querySelector('.memo-check-dropdown[data-target-select="' + selectElement.id + '"]');
if (!dropdown) {
return [selectElement.value.trim()].filter(Boolean);
}
return Array.from(dropdown.querySelectorAll('input[type="checkbox"]:checked'))
.map(input => input.value.trim())
.filter(Boolean);
}
function renderMemoPreviewPositions(selectElement) {
const values = getCheckedMemoPositions(selectElement);
if (!values.length) return '-';
return values.map(value => escapeHtml(value) + '<br>Majlis Agama Islam Negeri Pulau Pinang').join('<br>');
}
function syncMemoCheckboxDropdown(dropdown) {
if (!dropdown) return;
const select = document.getElementById(dropdown.dataset.targetSelect || '');
const checked = Array.from(dropdown.querySelectorAll('input[type="checkbox"]:checked'));
const values = checked.map(input => input.value.trim()).filter(Boolean);
const summary = dropdown.querySelector('.memo-check-summary');
if (summary) {
summary.textContent = values.length ? values.length + ' dipilih' : 'Pilih tambahan';
}
if (select) {
if (values.length && (!select.value || !values.includes(select.value))) {
select.value = values[0];
}
if (!values.length) {
select.value = '';
}
}
}
function selectedMemoDisplayPositions() {
return [
...getCheckedMemoPositions(recipientInput),
...getCheckedMemoPositions(throughInput),
].filter((value, index, values) => value && values.indexOf(value) === index);
}
function setSupportAttachmentFiles(files) {
if (!supportAttachmentInput) return;
const transfer = new DataTransfer();
files.forEach(file => transfer.items.add(file));
supportAttachmentInput.files = transfer.files;
}
function renderSelectedAttachments() {
if (!supportAttachmentInput || !selectedAttachmentList || !selectedAttachmentTitle) return;
const files = Array.from(supportAttachmentInput.files || []);
selectedAttachmentList.innerHTML = '';
selectedAttachmentTitle.style.display = files.length ? 'block' : 'none';
selectedAttachmentList.classList.toggle('active', files.length > 0);
files.forEach((file, index) => {
const item = document.createElement('div');
item.className = 'attachment-item';
const details = document.createElement('div');
details.innerHTML = '<i class="fas fa-paperclip"></i> ';
const name = document.createElement('strong');
name.textContent = file.name;
const meta = document.createElement('span');
meta.className = 'attachment-meta';
meta.textContent = formatFileSize(file.size);
details.appendChild(name);
details.appendChild(meta);
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'attachment-remove-btn';
removeButton.innerHTML = '<i class="fas fa-trash-alt"></i> Padam';
removeButton.addEventListener('click', () => {
const updatedFiles = Array.from(supportAttachmentInput.files || []).filter((_, fileIndex) => fileIndex !== index);
setSupportAttachmentFiles(updatedFiles);
renderSelectedAttachments();
});
item.appendChild(details);
item.appendChild(removeButton);
selectedAttachmentList.appendChild(item);
});
}
if (supportAttachmentInput) {
supportAttachmentInput.addEventListener('change', renderSelectedAttachments);
}
function getRoutingSourceLabel(position) {
if (getCheckedMemoPositions(recipientInput).includes(position)) return 'Pilihan Kepada';
if (getCheckedMemoPositions(throughInput).includes(position)) return 'Pilihan Melalui';
return 'Ketua Bahagian';
}
function nextAvailableRoutingOrder(excludedCheckbox = null) {
const used = new Set(Array.from(document.querySelectorAll('.routing-checkbox:checked')).filter(checkbox => checkbox !== excludedCheckbox).map(checkbox => {
const select = checkbox.closest('.routing-option')?.querySelector('.routing-order-select');
return select ? parseInt(select.value, 10) : 0;
}).filter(Boolean));
for (let order = 1; order <= routingOrderLimit; order++) {
if (!used.has(order)) return order;
}
return Math.min(routingOrderLimit, used.size + 1);
}
function normalizeRoutingOrder(changedSelect = null) {
const checkedRows = Array.from(document.querySelectorAll('.routing-checkbox:checked'))
.map((checkbox, index) => {
const row = checkbox.closest('.routing-option');
const select = row?.querySelector('.routing-order-select');
const sequence = parseInt(checkbox.dataset.checkedSequence || '', 10);
return { checkbox, row, select, index, sequence: Number.isFinite(sequence) ? sequence : index + 1 };
})
.filter(item => item.select);
if (checkedRows.length === 0) return;
const changedItem = changedSelect
? checkedRows.find(item => item.select === changedSelect)
: null;
const desiredOrder = changedItem
? Math.max(1, Math.min(routingOrderLimit, parseInt(changedSelect.value, 10) || 1))
: 0;
const ordered = checkedRows
.filter(item => item !== changedItem)
.sort((left, right) => {
return left.sequence - right.sequence || left.index - right.index;
});
if (changedItem) {
ordered.splice(Math.min(desiredOrder - 1, ordered.length), 0, changedItem);
}
ordered.forEach((item, index) => {
item.select.disabled = false;
item.select.value = String(Math.min(index + 1, routingOrderLimit));
item.checkbox.dataset.checkedSequence = String(index + 1);
});
routingSequenceCounter = ordered.length;
}
function addRoutingCandidate(position) {
position = (position || '').trim();
if (!routingChecklist || !position) return;
if (excludedApprovalRoutePositions.has(position)) return;
const existing = Array.from(routingChecklist.querySelectorAll('.routing-option')).find(row => row.dataset.position === position);
if (existing) {
const source = existing.querySelector('.routing-source');
if (source) source.textContent = getRoutingSourceLabel(position);
return;
}
const profiles = routingUserProfiles[position] || [];
const availableCount = parseInt(routingAvailability[position] || profiles.length || 0, 10);
const row = document.createElement('label');
row.className = 'routing-option' + (availableCount === 0 ? ' is-unavailable' : '');
row.dataset.position = position;
row.dataset.availableCount = String(availableCount);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'routing-checkbox';
checkbox.name = 'routing_positions[]';
checkbox.value = position;
checkbox.disabled = availableCount === 0;
const details = document.createElement('span');
details.appendChild(document.createTextNode(position));
const source = document.createElement('small');
source.className = 'routing-source';
source.textContent = getRoutingSourceLabel(position);
const availability = document.createElement('small');
availability.textContent = profiles.length
? profiles.map(profile => (profile.name || '').trim()).filter(Boolean).join(', ')
: 'Belum ada pengguna berdaftar';
details.appendChild(source);
details.appendChild(availability);
const orderSelect = document.createElement('select');
orderSelect.className = 'routing-order-select';
orderSelect.name = 'routing_order[' + position + ']';
orderSelect.disabled = true;
orderSelect.setAttribute('aria-label', 'Turutan ' + position);
for (let order = 1; order <= routingOrderLimit; order++) {
const option = document.createElement('option');
option.value = String(order);
option.textContent = 'Pilihan ' + order;
orderSelect.appendChild(option);
}
row.appendChild(checkbox);
row.appendChild(details);
row.appendChild(orderSelect);
routingChecklist.appendChild(row);
updateApprovalControlsForStatus();
}
function syncRoutingCandidates() {
if (!routingChecklist) return;
const selectedDisplayPositions = new Set(selectedMemoDisplayPositions());
selectedDisplayPositions.forEach(addRoutingCandidate);
routingChecklist.querySelectorAll('.routing-option').forEach(row => {
const position = row.dataset.position || '';
if (excludedApprovalRoutePositions.has(position)) {
row.remove();
return;
}
const source = row.querySelector('.routing-source');
if (source) source.textContent = getRoutingSourceLabel(position);
const checkbox = row.querySelector('.routing-checkbox');
if (!routingHeadPositions.has(position) && !selectedDisplayPositions.has(position) && checkbox && !checkbox.checked) {
row.remove();
}
});
}
if (routingChecklist) {
routingChecklist.addEventListener('change', event => {
const checkbox = event.target.closest('.routing-checkbox');
if (checkbox) {
const orderSelect = checkbox.closest('.routing-option')?.querySelector('.routing-order-select');
if (!orderSelect) return;
orderSelect.disabled = !checkbox.checked;
if (checkbox.checked) {
checkbox.dataset.checkedSequence = String(++routingSequenceCounter);
normalizeRoutingOrder();
} else {
delete checkbox.dataset.checkedSequence;
orderSelect.value = '1';
normalizeRoutingOrder();
}
}
const orderSelect = event.target.closest('.routing-order-select');
if (orderSelect) {
normalizeRoutingOrder(orderSelect);
}
updateApprovalPagePreview();
updateApprovalControlsForStatus();
});
}
function getSelectedApprovalRoutes() {
return Array.from(document.querySelectorAll('.routing-checkbox:checked')).map(checkbox => {
const row = checkbox.closest('.routing-option');
const orderSelect = row?.querySelector('.routing-order-select');
return {
position: checkbox.value,
order: orderSelect ? parseInt(orderSelect.value, 10) || 999 : 999
};
}).sort((left, right) => left.order - right.order || left.position.localeCompare(right.position));
}
function getSelectedApprovalRecipients() {
const recipients = [];
getSelectedApprovalRoutes().forEach(route => {
const profiles = routingUserProfiles[route.position] || [];
profiles.forEach(profile => recipients.push({ ...route, profile }));
});
return recipients;
}
function captureApprovalRouteBodies() {
const capturedBodies = {};
document.querySelectorAll('.approval-recipient-body[data-recipient-key]').forEach(editor => {
const key = editor.dataset.recipientKey;
capturedBodies[key] = (capturedBodies[key] || '') + editor.innerHTML.trim();
});
approvalRouteBodies = { ...approvalRouteBodies, ...capturedBodies };
}
function renderApprovalRouteBlock(route) {
const profiles = routingUserProfiles[route.position] || [];
const profile = route.profile || profiles[0] || { id: 0, name: '', position: route.position };
const name = (profile.name || '').trim();
const position = (profile.position || route.position || 'Pelulus').trim();
const recipientKey = profile.id ? 'user:' + profile.id : 'position:' + route.position;
const savedBody = approvalRouteBodies[recipientKey] || approvalRouteBodies['position:' + route.position] || '';
const decisionOptions = route.isLast
? ['Diluluskan', 'Tidak Diluluskan']
: ['Disokong', 'Tidak Disokong'];
return `
<section class="approval-route-block">
<div
class="approval-recipient-body"
contenteditable="true"
data-recipient-key="${escapeHtml(recipientKey)}"
data-placeholder="Tulis kandungan untuk penerima ini bermula di sini..."
>${savedBody}</div>
<div class="approval-decision-options">
${decisionOptions.map(label => `
<div class="approval-decision-option">
<span class="approval-decision-box" aria-hidden="true"></span>
<span>${escapeHtml(label)}</span>
</div>
`).join('')}
</div>
<div class="approval-route-title">Ulasan ${escapeHtml(route.position)} MAINPP:</div>
<div class="approval-comment-lines"><span></span><span></span></div>
<div class="approval-route-sign-row">
<div class="approval-route-date">Tarikh: .................... ..........</div>
<div class="approval-route-signature">
<div class="approval-route-sign-line"></div>
${name ? `<strong>(${escapeHtml(name.toUpperCase())})</strong>` : ''}
<span>${escapeHtml(position)}</span>
<span>Majlis Agama Islam Negeri Pulau Pinang</span>
</div>
</div>
</section>
`;
}
function getHeaderFooterHtml(type) {
const selector = type === 'footer' ? '.memo-custom-footer' : '.memo-custom-header';
const source = document.querySelector(selector);
return source ? source.innerHTML.trim() : '';
}
function syncHeaderFooterInputs() {
if (pageHeaderInput) pageHeaderInput.value = getHeaderFooterHtml('header');
if (pageFooterInput) pageFooterInput.value = getHeaderFooterHtml('footer');
}
function applyHeaderFooterToAll(type, html) {
const selector = type === 'footer' ? '.memo-custom-footer' : '.memo-custom-header';
document.querySelectorAll(selector).forEach(element => {
if (element.innerHTML !== html) {
element.innerHTML = html;
}
});
syncHeaderFooterInputs();
}
function createApprovalPreviewPage() {
const page = document.createElement('div');
page.className = 'approval-page-preview';
const headerHtml = pageHeaderInput ? pageHeaderInput.value : '';
const footerHtml = pageFooterInput ? pageFooterInput.value : '';
const position = pageNumberPositionSelect ? pageNumberPositionSelect.value : 'none';
page.innerHTML = `
<div class="memo-custom-header">${headerHtml}</div>
<div class="approval-page-inner"><div class="approval-route-preview"></div></div>
<div class="memo-custom-footer">${footerHtml}</div>
<div class="memo-page-number ${position}"></div>
`;
approvalPagesContainer.appendChild(page);
renumberApprovalPages();
return page.querySelector('.approval-route-preview');
}
function renumberApprovalPages() {
const mainPageCount = document.querySelectorAll('.memo-page-preview[data-page-index]').length;
document.querySelectorAll('.approval-page-preview').forEach((page, index) => {
const number = page.querySelector('.memo-page-number');
if (number) number.textContent = String(mainPageCount + index + 1);
});
}
function splitOversizedApprovalBlock(preview, block, route) {
if (preview.scrollHeight <= preview.clientHeight + 2) return;
const sourceBody = block.querySelector('.approval-recipient-body');
if (!sourceBody) return;
const continuationHolder = document.createElement('div');
continuationHolder.innerHTML = renderApprovalRouteBlock(route);
const continuationBlock = continuationHolder.firstElementChild;
const continuationBody = continuationBlock.querySelector('.approval-recipient-body');
continuationBody.innerHTML = '';
Array.from(block.children).forEach(child => {
if (child !== sourceBody) child.remove();
});
while (preview.scrollHeight > preview.clientHeight + 2 && sourceBody.lastChild) {
continuationBody.insertBefore(sourceBody.lastChild, continuationBody.firstChild);
}
const continuationPreview = createApprovalPreviewPage();
continuationPreview.appendChild(continuationBlock);
if (continuationPreview.scrollHeight > continuationPreview.clientHeight + 2 && continuationBody.childNodes.length > 1) {
splitOversizedApprovalBlock(continuationPreview, continuationBlock, route);
}
}
function updateApprovalPagePreview() {
captureApprovalRouteBodies();
if (approvalPagesContainer) {
const recipients = getSelectedApprovalRecipients().map((recipient, index, array) => ({
...recipient,
isLast: recipient.order === Math.max(...array.map(item => item.order || 0))
}));
approvalPagesContainer.innerHTML = '';
let currentPreview = createApprovalPreviewPage();
if (recipients.length === 0) {
currentPreview.innerHTML = '<div class="hint">Pilih penerima dalam Turutan Edaran Kelulusan untuk menjana ruangan ulasan dan tandatangan.</div>';
} else {
recipients.forEach(route => {
const holder = document.createElement('div');
holder.innerHTML = renderApprovalRouteBlock(route);
const block = holder.firstElementChild;
currentPreview.appendChild(block);
if (currentPreview.scrollHeight > currentPreview.clientHeight + 2 && currentPreview.children.length > 1) {
block.remove();
currentPreview = createApprovalPreviewPage();
currentPreview.appendChild(block);
}
splitOversizedApprovalBlock(currentPreview, block, route);
});
}
}
syncApprovalPageContent();
document.querySelectorAll('.approval-recipient-body').forEach(ensureEditorTypography);
}
function syncApprovalPageContent() {
captureApprovalRouteBodies();
if (approvalRouteBodiesInput) {
approvalRouteBodiesInput.value = JSON.stringify(approvalRouteBodies);
}
if (approvalPageBodyInput) approvalPageBodyInput.value = '';
}
function syncActiveEditorContent(editor) {
if (editor && editor.classList && editor.classList.contains('approval-recipient-body')) {
syncApprovalPageContent();
} else {
if (editor && editor.classList && editor.classList.contains('letter-page-editor') && !isPaginating && !isRepaginating && !isInsertingSignature) {
scheduleFullLetterPagination(editor);
}
syncEditorBody();
}
syncHeaderFooterInputs();
}
function getLetterEditors() {
return Array.from(document.querySelectorAll('#letter_pages_container .memo-page-preview .letter-page-editor'));
}
function getEditorQuill(editor) {
return editor && editor.__quillInstance ? editor.__quillInstance : null;
}
function registerWordLikeQuillFormats() {
if (!window.Quill || window.__wordLikeQuillFormatsRegistered) return;
const Embed = Quill.import('blots/embed');
class SoftBreakBlot extends Embed {
static create() {
return document.createElement('br');
}
static value() {
return true;
}
}
SoftBreakBlot.blotName = 'softbreak';
SoftBreakBlot.tagName = 'BR';
Quill.register(SoftBreakBlot, true);
const BlockEmbed = Quill.import('blots/block/embed');
class GeneratedClosingBlot extends BlockEmbed {
static create(value) {
const node = super.create();
node.setAttribute('data-generated-closing', '1');
node.setAttribute('data-signature-block', '1');
node.setAttribute('contenteditable', 'false');
node.classList.add('signature-block', 'generated-closing-lock');
node.innerHTML = value || '';
return node;
}
static value(node) {
return node.innerHTML || '';
}
}
GeneratedClosingBlot.blotName = 'generatedClosing';
GeneratedClosingBlot.tagName = 'div';
GeneratedClosingBlot.className = 'generated-closing-lock';
Quill.register(GeneratedClosingBlot, true);
window.__wordLikeQuillFormatsRegistered = true;
}
function getQuillLineFormats(quill, index) {
const safeIndex = Math.max(0, Math.min(index || 0, Math.max(0, quill.getLength() - 1)));
const formats = quill.getFormat(safeIndex);
return {
align: formats.align || false,
indent: formats.indent || false,
list: formats.list || false
};
}
function applyQuillLineFormats(quill, index, formats) {
Object.entries(formats || {}).forEach(([name, value]) => {
if (value) quill.formatLine(index, 1, name, value, 'silent');
});
}
function scheduleWordLikeEditorSync(editor) {
if (!editor) return;
window.requestAnimationFrame(() => {
if (!document.body.contains(editor)) return;
ensureEditorTypography(editor);
if (!isPaginating && !isRepaginating) {
scheduleFullLetterPagination(editor);
reflowEditorContentUp(editor);
}
syncActiveEditorContent(editor);
rememberEditorSelection(editor);
});
}
function scheduleEnterPagination(editor) {
if (!editor) return;
window.requestAnimationFrame(() => {
if (!document.body.contains(editor)) return;
ensureEditorTypography(editor);
refreshEditorPageHeights();
if (!isPaginating && !isRepaginating) {
repaginate(editor);
}
if (isCaretInPageBreakZone(editor) && editor.scrollHeight <= editor.clientHeight + 2) {
continueEnterToNextPage(editor);
}
syncActiveEditorContent(editor);
rememberEditorSelection(editor);
});
}

function paginateEditorAndFollowing(startEditor) {
if (!startEditor || isPaginating) return;

const startIndex = Math.max(0, getLetterEditors().indexOf(startEditor));
let pass = 0;
let hadOverflow = true;
while (hadOverflow && pass < 30) {
hadOverflow = false;
const editors = getLetterEditors();
for (let index = startIndex; index < editors.length; index++) {
const editor = editors[index];
if (!editor || !document.body.contains(editor)) continue;
refreshEditorPageHeights();
if (editor.scrollHeight > editor.clientHeight + 2) {
hadOverflow = true;
paginateEditorOverflow(editor);
}
}
pass += 1;
}
}
function repaginate(editor = null) {
const targetEditor = editor || activeLetterEditor || getLetterEditors()[0];
if (!targetEditor || isPaginating || isRepaginating) return;

isRepaginating = true;
try {
preserveQuillSelection(targetEditor, () => {
if (!document.body.contains(targetEditor)) return;
ensureEditorTypography(targetEditor);
refreshEditorPageHeights();
arrangeSignatureBlocks(targetEditor);
paginateEditorAndFollowing(targetEditor);
arrangeSignatureBlocks(targetEditor);
syncEditorBody();
});
} finally {
isRepaginating = false;
}
}
function scheduleFullLetterPagination(editor) {
if (!editor) return;
scheduledRepaginationEditor = editor;
if (scheduledRepaginationFrame !== null || isRepaginating || isInsertingSignature) return;

scheduledRepaginationFrame = window.requestAnimationFrame(() => {
const targetEditor = scheduledRepaginationEditor;
scheduledRepaginationFrame = null;
scheduledRepaginationEditor = null;
if (!targetEditor || !document.body.contains(targetEditor) || isPaginating) return;
repaginate(targetEditor);
});
}
// FIXED: Only trigger reflow when cursor is at start of editor
function scheduleBackspaceReflow(editor) {
if (!editor) return;
window.requestAnimationFrame(() => {
if (!document.body.contains(editor)) return;
// Only reflow if cursor is actually at the start
if (!isSelectionAtStartOfEditor(editor)) {
// Just sync content without reflow
syncActiveEditorContent(editor);
return;
}
const previousEditor = getPreviousLetterEditor(editor);
const reflowStart = previousEditor || editor;
reflowEditorContentUp(reflowStart);
if (previousEditor && !document.body.contains(editor)) {
focusEditorEnd(previousEditor);
}
repaginate(previousEditor || editor);
syncEditorBody();
});
}
function createWordLikeKeyboardBindings(editor) {
const resolveEditor = () => editor && editor.__quillRoot ? editor.__quillRoot : editor;
return {
wordTab: {
key: 9,
shiftKey: false,
handler(range) {
const targetEditor = resolveEditor();
const quill = getEditorQuill(targetEditor) || getEditorQuill(editor);
if (!quill || !range) return false;
const tabText = '\u2003\u2003\u2003';
pushLetterUndoSnapshot();
                        if (range.length) quill.deleteText(range.index, range.length, 'user');
quill.insertText(range.index, tabText, 'user');
quill.setSelection(range.index + tabText.length, 0, 'silent');
scheduleWordLikeEditorSync(targetEditor);
return false;
}
},
wordShiftTab: {
key: 9,
shiftKey: true,
handler(range) {
const targetEditor = resolveEditor();
const quill = getEditorQuill(targetEditor) || getEditorQuill(editor);
if (!quill || !range) return false;
const tabText = '\u2003\u2003\u2003';
pushLetterUndoSnapshot();
                        const before = quill.getText(Math.max(0, range.index - tabText.length), tabText.length);
if (before === tabText) {
quill.deleteText(range.index - tabText.length, tabText.length, 'user');
quill.setSelection(range.index - tabText.length, 0, 'silent');
}
scheduleWordLikeEditorSync(targetEditor);
return false;
}
},
wordEnter: {
key: 'Enter',
shiftKey: false,
handler(range) {
const targetEditor = resolveEditor();
const quill = getEditorQuill(targetEditor) || getEditorQuill(editor);
if (!quill || !range) return true;
const inlineFormats = quill.getFormat(range.index);
const lineFormats = getQuillLineFormats(quill, range.index);
if (range.length) quill.deleteText(range.index, range.length, 'user');
quill.insertText(range.index, '\n', inlineFormats, 'user');
applyQuillLineFormats(quill, range.index + 1, lineFormats);
quill.setSelection(range.index + 1, 0, 'silent');
scheduleEnterPagination(targetEditor);
return false;
}
},
wordShiftEnter: {
key: 'Enter',
shiftKey: true,
handler(range) {
const targetEditor = resolveEditor();
const quill = getEditorQuill(targetEditor) || getEditorQuill(editor);
if (!quill || !range) return false;
if (range.length) quill.deleteText(range.index, range.length, 'user');
quill.insertEmbed(range.index, 'softbreak', true, 'user');
quill.setSelection(range.index + 1, 0, 'silent');
scheduleWordLikeEditorSync(targetEditor);
return false;
}
},
// FIXED: Only trigger reflow when cursor is at start of editor
wordBackspace: {
key: 'Backspace',
handler(range, context) {
const targetEditor = resolveEditor();
// Check if cursor is at the start of the editor
if (!isSelectionAtStartOfEditor(targetEditor)) {
// Let Quill handle normal backspace
return true;
}
// Only trigger reflow when at start
scheduleBackspaceReflow(targetEditor);
return true;
}
},
wordDelete: {
key: 'Delete',
handler() {
window.requestAnimationFrame(() => scheduleWordLikeEditorSync(resolveEditor()));
return true;
}
}
};
}
        function handleWordLikeTab(quill, targetEditor, shiftKey) {
            if (!quill || !targetEditor) return false;
            const range = quill.getSelection(true);
            if (!range) return false;
            const tabText = '\u2003\u2003\u2003';
            pushLetterUndoSnapshot();
            if (shiftKey) {
                const start = Math.max(0, range.index - tabText.length);
                const before = quill.getText(start, tabText.length);
                if (before === tabText) {
                    quill.deleteText(start, tabText.length, 'user');
                    quill.setSelection(start, 0, 'silent');
                }
            } else {
                if (range.length) quill.deleteText(range.index, range.length, 'user');
                quill.insertText(range.index, tabText, 'user');
                quill.setSelection(range.index + tabText.length, 0, 'silent');
            }
            scheduleWordLikeEditorSync(targetEditor);
            return true;
        }

        function attachWordLikeTabHandler(editorRoot, quill) {
            if (!editorRoot || editorRoot.dataset.wordTabHandlerAttached === '1') return;
            editorRoot.dataset.wordTabHandlerAttached = '1';
            editorRoot.addEventListener('keydown', event => {
                if (event.key !== 'Tab') return;
                event.preventDefault();
                event.stopPropagation();
                handleWordLikeTab(quill, editorRoot, event.shiftKey);
            }, true);
        }
function enhanceEditorWithQuill(editor) {
if (!editor || editor.__quillInstance || !window.Quill) {
return editor;
}
registerWordLikeQuillFormats();
const initialHtml = editor.innerHTML;
editor.innerHTML = '';
const quill = new Quill(editor, {
theme: 'snow',
modules: {
toolbar: false,
table: true,
history: {
userOnly: true
},
keyboard: {
bindings: createWordLikeKeyboardBindings(editor)
}
}
});
editor.classList.add('quill-letter-host');
quill.root.classList.add('word-editor', 'letter-page-editor');
quill.root.__quillInstance = quill;
editor.__quillInstance = quill;
editor.__quillRoot = quill.root;
editor.classList.remove('word-editor', 'letter-page-editor');
editor.removeAttribute('contenteditable');
if (initialHtml.trim()) {
quill.clipboard.dangerouslyPasteHTML(0, initialHtml, 'silent');
}
attachEditorEvents(quill.root);
            attachWordLikeTabHandler(quill.root, quill);
if (window.attachResizableEditorTables) {
attachResizableEditorTables(quill.root, {
onResizeEnd(resizedEditor) {
ensureEditorTypography(resizedEditor);
syncActiveEditorContent(resizedEditor);
if (!isPaginating && !isRepaginating && !isInsertingSignature) {
scheduleFullLetterPagination(resizedEditor);
}
}
});
}
quill.on('text-change', () => {
ensureEditorTypography(quill.root);
syncActiveEditorContent(quill.root);
if (quill.root.dataset.generatedClosingHtml) {
restoreGeneratedClosingIfNeeded(quill.root, quill.root.dataset.generatedClosingHtml);
}
if (!isPaginating && !isRepaginating && !isInsertingSignature) {
scheduleFullLetterPagination(quill.root);
}
});
return quill.root;
}
function editorHasContent(editor) {
if (!editor) return false;
return editor.textContent.replace(/\s+/g, '').trim() !== '' || !!editor.querySelector('img, table');
}
function isManagedMemoFooter(node) {
return node && node.nodeType === Node.ELEMENT_NODE && node.id === 'memo_editor_footer';
}
function getEditableChildNodes(editor) {
return Array.from(editor ? editor.childNodes : []).filter(node => !isManagedMemoFooter(node));
}
function getEditorPage(editor) {
return editor ? editor.closest('.memo-page-preview') : null;
}
function getEditorHostElement(editor) {
if (!editor) return null;
return editor.closest('.quill-letter-host') || editor;
}
function getPreviousLetterEditor(editor) {
const editors = getLetterEditors();
const index = editors.indexOf(editor);
return index > 0 ? editors[index - 1] : null;
}
function getNextLetterEditor(editor) {
const editors = getLetterEditors();
const index = editors.indexOf(editor);
return index >= 0 && index < editors.length - 1 ? editors[index + 1] : null;
}
function getSelectionLetterEditor() {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return null;
const node = selection.getRangeAt(0).startContainer;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
return element ? element.closest('.letter-page-editor') : null;
}
function captureLetterSnapshot() {
const editors = getLetterEditors();
return {
pages: editors.map(editor => editor.innerHTML),
activeIndex: Math.max(0, editors.indexOf(activeLetterEditor || getSelectionLetterEditor() || editors[0]))
};
}

function serializeLetterSnapshot(snapshot) {
return JSON.stringify(snapshot ? snapshot.pages : []);
}

function pushLetterUndoSnapshot() {
if (isRestoringLetterSnapshot) return;
const snapshot = captureLetterSnapshot();
const serialized = serializeLetterSnapshot(snapshot);
const last = letterUndoStack[letterUndoStack.length - 1];
if (last && serializeLetterSnapshot(last) === serialized) return;
letterUndoStack.push(snapshot);
if (letterUndoStack.length > maxLetterUndoSnapshots) letterUndoStack.shift();
letterRedoStack.length = 0;
}

function setEditorSnapshotHtml(editor, html) {
if (!editor) return;
const safeHtml = html && html.trim() ? html : '<p style="' + draftBodyStyle + '"><br></p>';
const quill = getEditorQuill(editor);
if (quill) {
quill.setText('', 'silent');
quill.clipboard.dangerouslyPasteHTML(0, safeHtml, 'silent');
} else {
editor.innerHTML = safeHtml;
}
ensureEditorTypography(editor);
}

function restoreLetterSnapshot(snapshot, pushRedo = true) {
if (!snapshot || !Array.isArray(snapshot.pages)) return false;
isRestoringLetterSnapshot = true;
try {
if (pushRedo) letterRedoStack.push(captureLetterSnapshot());
let editors = getLetterEditors();
while (editors.length < snapshot.pages.length) {
const pages = document.querySelectorAll('.memo-page-preview');
const lastPage = pages[pages.length - 1];
addLetterPage(lastPage, false);
editors = getLetterEditors();
}
while (editors.length > Math.max(1, snapshot.pages.length)) {
const lastEditor = editors[editors.length - 1];
const page = getEditorPage(lastEditor);
if (!page || page.dataset.pageIndex === '0') break;
moveFooterBeforePageDelete(page);
page.remove();
editors = getLetterEditors();
}
editors = getLetterEditors();
editors.forEach((editor, index) => setEditorSnapshotHtml(editor, snapshot.pages[index] || ''));
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
syncEditorBody();
const focusEditor = getLetterEditors()[Math.min(snapshot.activeIndex || 0, getLetterEditors().length - 1)];
if (focusEditor) focusEditorEnd(focusEditor);
return true;
} finally {
isRestoringLetterSnapshot = false;
}
}

function undoLetterSnapshot() {
const snapshot = letterUndoStack.pop();
if (!snapshot) return false;
return restoreLetterSnapshot(snapshot, true);
}

function redoLetterSnapshot() {
const snapshot = letterRedoStack.pop();
if (!snapshot) return false;
letterUndoStack.push(captureLetterSnapshot());
return restoreLetterSnapshot(snapshot, false);
}

function handleLetterUndoRedoShortcut(event) {
const key = (event.key || '').toLowerCase();
const isModifier = event.ctrlKey || event.metaKey;
if (!isModifier) return false;
if (key === 'z' && !event.shiftKey) {
event.preventDefault();
event.stopPropagation();
undoLetterSnapshot();
return true;
}
if (key === 'y' || (key === 'z' && event.shiftKey)) {
event.preventDefault();
event.stopPropagation();
redoLetterSnapshot();
return true;
}
return false;
}
function focusEditorStart(editor) {
if (!editor) return;
editor.focus();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(true);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
activeLetterEditor = editor;
rememberEditorSelection(editor);
}
function removeExtraLetterPages() {
getLetterEditors().slice(1).forEach(editor => {
const page = getEditorPage(editor);
if (page) page.remove();
});
}
function clearAllLetterPages() {
const firstEditor = getLetterEditors()[0];
if (!firstEditor) return;
removeExtraLetterPages();
firstEditor.innerHTML = '<p style="' + draftBodyStyle + '"><br></p>';
allLetterPagesSelected = false;
activeLetterEditor = firstEditor;
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
firstEditor.focus();
const range = document.createRange();
range.selectNodeContents(firstEditor);
range.collapse(true);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
rememberEditorSelection(firstEditor);
}
function selectAllLetterPages() {
const editors = getLetterEditors();
if (!editors.length) return;
const firstEditor = editors[0];
const lastEditor = editors[editors.length - 1];
const range = document.createRange();
range.setStart(firstEditor, 0);
range.setEnd(lastEditor, lastEditor.childNodes.length);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
allLetterPagesSelected = true;
activeLetterEditor = firstEditor;
firstEditor.focus();
}
function selectionTouchesEditor(selection, editor) {
if (!selection || !editor || selection.rangeCount === 0) return false;
const range = selection.getRangeAt(0);
try {
return range.intersectsNode(editor);
} catch (error) {
return false;
}
}
function getSelectedLetterEditors() {
const selection = window.getSelection();
if (allLetterPagesSelected) return getLetterEditors();
return getLetterEditors().filter(editor => selectionTouchesEditor(selection, editor));
}
// FIXED: More accurate detection of cursor at start
function isSelectionAtStartOfEditor(editor) {
const selection = window.getSelection();
if (!editor || !selection || selection.rangeCount === 0 || !selection.isCollapsed) return false;
const range = selection.getRangeAt(0);
if (!editor.contains(range.startContainer) && range.startContainer !== editor) return false;
// Check if we're at the very beginning
const beforeRange = document.createRange();
beforeRange.setStart(editor, 0);
beforeRange.setEnd(range.startContainer, range.startOffset);
const beforeText = beforeRange.toString();
// Only return true if there's no text content before cursor
return beforeText.length === 0;
}
function focusEditorEnd(editor) {
if (!editor) return;
editor.focus();
const range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
activeLetterEditor = editor;
rememberEditorSelection(editor);
}
function getCollapsedCaretRect(range) {
if (!range) return null;
let rect = range.getBoundingClientRect();
if (rect && (rect.width || rect.height || rect.top || rect.bottom)) return rect;
const node = range.startContainer;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
return element ? element.getBoundingClientRect() : null;
}
function isCaretNearEditorBottom(editor) {
const selection = window.getSelection();
if (!editor || !selection || selection.rangeCount === 0 || !selection.isCollapsed) return false;
const range = selection.getRangeAt(0);
if (!editor.contains(range.startContainer) && range.startContainer !== editor) return false;
const caretRect = getCollapsedCaretRect(range.cloneRange());
const editorRect = editor.getBoundingClientRect();
if (!caretRect || !editorRect) return false;
return caretRect.bottom >= editorRect.bottom - 8;
}
function isCaretInPageBreakZone(editor) {
const selection = window.getSelection();
if (!editor || !selection || selection.rangeCount === 0 || !selection.isCollapsed) return false;
const range = selection.getRangeAt(0);
if (!editor.contains(range.startContainer) && range.startContainer !== editor) return false;
const caretRect = getCollapsedCaretRect(range.cloneRange());
const editorRect = editor.getBoundingClientRect();
if (!caretRect || !editorRect) return false;
const editorStyle = window.getComputedStyle(editor);
const lineHeight = parseFloat(editorStyle.lineHeight) || 16;
const maxHeight = parseFloat(editorStyle.maxHeight);
const editorLimitBottom = Number.isFinite(maxHeight) && maxHeight > 0
? editorRect.top + maxHeight
: editorRect.bottom;
return caretRect.bottom >= editorLimitBottom - Math.max(18, lineHeight + 4);
}
function continueEnterToNextPage(editor) {
if (!editor) return;
const page = getEditorPage(editor);
if (!page) return;
const nextPage = page.nextElementSibling && page.nextElementSibling.classList.contains('memo-page-preview')
? page.nextElementSibling
: null;
const nextEditor = nextPage ? nextPage.querySelector('.letter-page-editor') : getNextLetterEditor(editor);
focusEditorStart(nextEditor || addLetterPage(page));
}
function handleEditorEnterKey(editor, event) {
if (event.key !== 'Enter' || event.ctrlKey || event.altKey || event.metaKey) return;
refreshEditorPageHeights();
window.requestAnimationFrame(() => {
if (!document.body.contains(editor)) return;
refreshEditorPageHeights();
if (editor.scrollHeight > editor.clientHeight + 2) {
repaginate(editor);
window.requestAnimationFrame(() => {
const selectedEditor = getSelectionLetterEditor();
if (selectedEditor && selectedEditor !== editor) {
activeLetterEditor = selectedEditor;
rememberEditorSelection(selectedEditor);
return;
}
if (isCaretNearEditorBottom(editor) && editor.scrollHeight > editor.clientHeight + 2) {
continueEnterToNextPage(editor);
}
});
} else if (isCaretInPageBreakZone(editor)) {
continueEnterToNextPage(editor);
}
syncEditorBody();
});
}
function removeEmptyLetterPage(editor) {
const previousEditor = getPreviousLetterEditor(editor);
const page = getEditorPage(editor);
if (!page || page.dataset.pageIndex === '0') {
return previousEditor || editor;
}
moveFooterBeforePageDelete(page);
page.remove();
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
return previousEditor || getLetterEditors()[0] || null;
}
function handleEditorBackspaceKey(editor, event) {
if (event.key !== 'Backspace' || event.ctrlKey || event.altKey || event.metaKey) return;
if (!isSelectionAtStartOfEditor(editor)) {
scheduleBackspaceReflow(editor);
return;
}
const previousEditor = getPreviousLetterEditor(editor);
if (!previousEditor) return;
event.preventDefault();
if (!editorHasContent(editor)) {
const focusEditor = removeEmptyLetterPage(editor);
if (focusEditor) focusEditorEnd(focusEditor);
syncEditorBody();
return;
}
if (removeLeadingEmptyEditorUnit(editor)) {
if (!editorHasContent(editor)) {
const focusEditor = removeEmptyLetterPage(editor);
if (focusEditor) focusEditorEnd(focusEditor);
} else {
focusEditorStart(editor);
}
syncEditorBody();
return;
}
mergeEditorIntoPreviousPage(editor);
syncEditorBody();
}
function mergeEditorIntoPreviousPage(editor) {
const previousEditor = getPreviousLetterEditor(editor);
const page = getEditorPage(editor);
if (!previousEditor || !page) return false;
const hasContent = editorHasContent(editor);
while (getEditableChildNodes(editor).length > 0) {
previousEditor.appendChild(getEditableChildNodes(editor)[0]);
}
moveFooterBeforePageDelete(page);
page.remove();
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
focusEditorEnd(previousEditor);
if (hasContent) scheduleFullLetterPagination(previousEditor);
return true;
}
function deleteLastEditorUnit(editor) {
if (!editor) return false;
const deleteLastUnitFrom = container => {
for (let index = container.childNodes.length - 1; index >= 0; index--) {
const child = container.childNodes[index];
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent || '';
if (text.length > 0) {
child.textContent = text.slice(0, -1);
if (child.textContent.length === 0) child.remove();
return true;
}
child.remove();
continue;
}
if (child.nodeType !== Node.ELEMENT_NODE) {
child.remove();
continue;
}
if (child.tagName === 'BR') {
child.remove();
return true;
}
if (deleteLastUnitFrom(child)) {
if (child !== editor
&& child.childNodes.length === 0
&& !child.matches('img, table, tbody, thead, tfoot, tr, td, th')) {
child.remove();
}
return true;
}
if (!child.firstChild && !child.matches('img, table')) {
child.remove();
return true;
}
}
return false;
};
return deleteLastUnitFrom(editor);
}
function isEmptyEditorLineNode(node) {
if (!node) return false;
if (node.nodeType === Node.TEXT_NODE) {
return (node.textContent || '').replace(/\u00a0/g, ' ').trim() === '';
}
if (node.nodeType !== Node.ELEMENT_NODE) return false;
if (node.matches('img, table')) return false;
return (node.textContent || '').replace(/\u00a0/g, ' ').trim() === ''
&& !node.querySelector('img, table');
}
function removeLeadingEmptyEditorUnit(editor) {
if (!editor) return false;
while (editor.firstChild && isEmptyEditorLineNode(editor.firstChild)) {
editor.firstChild.remove();
if (!editor.firstChild) {
editor.innerHTML = '<p style="' + draftBodyStyle + '"><br></p>';
return true;
}
return true;
}
return false;
}
function removeEmptyTrailingEditorPages() {
const editors = getLetterEditors();
for (let index = editors.length - 1; index > 0; index--) {
const editor = editors[index];
if (editorHasContent(editor)) break;
const page = getEditorPage(editor);
if (page) {
moveFooterBeforePageDelete(page);
page.remove();
}
}
}
function pullNextEditorContentInto(editor) {
const nextEditor = getNextLetterEditor(editor);
if (!editor || !nextEditor || !editorHasContent(nextEditor)) return false;
let movedContent = false;
refreshEditorPageHeights();
while (getEditableChildNodes(nextEditor).length > 0) {
const nodeToMove = getEditableChildNodes(nextEditor)[0];
editor.appendChild(nodeToMove);
refreshEditorPageHeights();
if (editor.scrollHeight > editor.clientHeight + 2) {
nextEditor.insertBefore(nodeToMove, nextEditor.firstChild);
refreshEditorPageHeights();
break;
}
movedContent = true;
}
return movedContent;
}
function reflowEditorContentUp(startEditor) {
if (!startEditor || isPaginating) return;
isPaginating = true;
try {
moveFooterToLastPage();
refreshEditorPageHeights();
const editors = getLetterEditors();
const startIndex = Math.max(0, editors.indexOf(startEditor));
for (let index = startIndex; index < editors.length - 1; index++) {
const editor = editors[index];
while (pullNextEditorContentInto(editor)) {
// Keep pulling until the current page is full or the next page is empty.
}
}
removeEmptyTrailingEditorPages();
} finally {
isPaginating = false;
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
}
}
function rememberEditorSelection(editor) {
const selection = window.getSelection();
if (!editor || !selection || selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
if ((editor.contains(range.startContainer) || range.startContainer === editor)
&& (editor.contains(range.endContainer) || range.endContainer === editor)) {
savedEditorRange = range.cloneRange();
allLetterPagesSelected = false;
}
}
function normalizeEditorFontTags(editor) {
if (!editor) return;
editor.querySelectorAll('font').forEach(font => {
const span = document.createElement('span');
span.innerHTML = font.innerHTML;
font.replaceWith(span);
});
}
function ensureEditorTypography(editor) {
if (!editor) return;
normalizeEditorFontTags(editor);
getAllEditableBlocks(editor).forEach(target => {
if (target.nodeType !== Node.ELEMENT_NODE) return;
if (!target.style.fontFamily) target.style.fontFamily = 'Arial, Helvetica, sans-serif';
if (target.style.fontSize !== '8px') target.style.fontSize = '16px';
if (!target.style.lineHeight) target.style.lineHeight = '1.5';
});
if (!editor.style.fontFamily) editor.style.fontFamily = 'Arial, Helvetica, sans-serif';
if (editor.style.fontSize !== '8px') editor.style.fontSize = '16px';
if (!editor.style.lineHeight) editor.style.lineHeight = '1.5';
}
function ensureAllDocumentTypography() {
getLetterEditors().forEach(ensureEditorTypography);
document.querySelectorAll('.approval-recipient-body').forEach(ensureEditorTypography);
}
function normalizeQuillLineStylesForSave(editor) {
if (!editor) return;
editor.querySelectorAll('.ql-align-center, .ql-align-right, .ql-align-justify').forEach(block => {
if (block.classList.contains('ql-align-center')) block.style.textAlign = 'center';
if (block.classList.contains('ql-align-right')) block.style.textAlign = 'right';
if (block.classList.contains('ql-align-justify')) block.style.textAlign = 'justify';
});
}
function applyQuillToolbarCommand(editor, quill, command) {
if (!editor || !quill || !command) return false;
const range = quill.getSelection(true);
if (!range) return false;
if (typeof isQuillRangeOnSuratRasmiGeneratedLine === 'function' && isQuillRangeOnSuratRasmiGeneratedLine(quill, range)) {
return true;
}
const length = Math.max(range.length || 0, 1);
if (command === 'justifyLeft' || command === 'justifyCenter' || command === 'justifyRight' || command === 'justifyFull') {
const alignMap = {
justifyLeft: false,
justifyCenter: 'center',
justifyRight: 'right',
justifyFull: 'justify'
};
quill.formatLine(range.index, length, 'align', alignMap[command], 'user');
} else if (command === 'insertOrderedList' || command === 'insertUnorderedList') {
const listValue = command === 'insertOrderedList' ? 'ordered' : 'bullet';
const currentFormats = quill.getFormat(range.index, length);
quill.formatLine(range.index, length, 'list', currentFormats.list === listValue ? false : listValue, 'user');
} else {
return false;
}
quill.setSelection(range.index, range.length, 'silent');
normalizeQuillLineStylesForSave(editor);
rememberEditorSelection(editor);
syncActiveEditorContent(editor);
scheduleFullLetterPagination(editor);
return true;
}
function execEditorCommand(command) {
const editor = activeLetterEditor || document.querySelector('.letter-page-editor');
if (!editor || !command) return;
            pushLetterUndoSnapshot();
activeLetterEditor = editor;
const quill = getEditorQuill(editor);
if (quill && applyQuillToolbarCommand(editor, quill, command)) return;
editor.focus();
if (savedEditorRange) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(savedEditorRange);
}
document.execCommand(command, false, null);
rememberEditorSelection(editor);
syncActiveEditorContent(editor);
}
function applyStyleToSelection(styleProperty, styleValue) {
const editor = activeLetterEditor || document.querySelector('.letter-page-editor');
if (!editor || !styleProperty || !styleValue) return;
editor.focus();
if (savedEditorRange) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(savedEditorRange);
}
const selection = window.getSelection();
const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
if (!range || (!editor.contains(range.commonAncestorContainer) && range.commonAncestorContainer !== editor)) {
editor.style[styleProperty] = styleValue;
} else if (range.collapsed) {
const target = getClosestEditableBlock(editor, range.startContainer) || editor;
target.style[styleProperty] = styleValue;
} else {
getEditableBlocksInRange(editor, range).forEach(target => {
target.style[styleProperty] = styleValue;
});
}
rememberEditorSelection(editor);
syncActiveEditorContent(editor);
}
function applyEditorFontFamily() {
const family = fontFamilySelect ? fontFamilySelect.value : 'Arial';
applyStyleToSelection('fontFamily', family + ', sans-serif');
}
function applyEditorFontSize() {
const size = fontSizeSelect ? parseInt(fontSizeSelect.value, 10) : 12;
applyStyleToSelection('fontSize', (Number.isFinite(size) ? size : 16) + 'px');
}
function getEditableBlocksInRange(editor, range) {
const blockSelector = 'p, div, li, td, th, blockquote, h1, h2, h3, h4, h5, h6';
const blocks = Array.from(editor.querySelectorAll(blockSelector)).filter(element => {
if (element === editor || !editor.contains(element)) return false;
try {
return range.intersectsNode(element);
} catch (error) {
return false;
}
});
return blocks.filter(element => !blocks.some(other => other !== element && other.contains(element)));
}
function getClosestEditableBlock(editor, node) {
const blockSelector = 'p, div, li, td, th, blockquote, h1, h2, h3, h4, h5, h6';
const element = node && node.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
if (!element) return null;
const block = element.closest(blockSelector);
return block && editor.contains(block) && block !== editor ? block : null;
}
function getAllEditableBlocks(editor) {
const blocks = Array.from(editor.querySelectorAll('p, div, li, td, th, blockquote, h1, h2, h3, h4, h5, h6'))
.filter(element => element !== editor);
return blocks.length ? blocks : [editor];
}
function applyEditorIndent(editor, direction) {
if (!editor) return;
            pushLetterUndoSnapshot();
editor.focus();
const selection = window.getSelection();
const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
let targets = [];
if (range && (editor.contains(range.startContainer) || range.startContainer === editor)) {
targets = range.collapsed
? [getClosestEditableBlock(editor, range.startContainer) || editor]
: getEditableBlocksInRange(editor, range);
}
if (targets.length === 0) targets = [editor];
targets.forEach(target => {
if (target.nodeType !== Node.ELEMENT_NODE) return;
const current = parseFloat(target.style.marginLeft || window.getComputedStyle(target).marginLeft) || 0;
const next = Math.max(0, current + (direction * 36));
target.style.marginLeft = next ? next + 'px' : '';
});
rememberEditorSelection(editor);
syncActiveEditorContent(editor);
}
function ensureEditorLineSpacing(editor) {
ensureEditorTypography(editor);
}
function ensureAllDocumentLineSpacing() {
ensureAllDocumentTypography();
}
function applyEditorLineSpacing() {
const editor = activeLetterEditor || document.querySelector('.letter-page-editor');
if (!editor) return;
            pushLetterUndoSnapshot();
const selectedEditors = getSelectedLetterEditors();
if (allLetterPagesSelected || selectedEditors.length > 1) {
(selectedEditors.length ? selectedEditors : getLetterEditors()).forEach(selectedEditor => {
ensureEditorTypography(selectedEditor);
});
refreshEditorPageHeights();
getLetterEditors().forEach(selectedEditor => {
if (!isPaginating && !isRepaginating) scheduleFullLetterPagination(selectedEditor);
});
syncEditorBody();
return;
}
editor.focus();
if (savedEditorRange) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(savedEditorRange);
}
const selection = window.getSelection();
const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
let targets = [];
if (range && (editor.contains(range.startContainer) || range.startContainer === editor)) {
targets = range.collapsed
? [getClosestEditableBlock(editor, range.startContainer)].filter(Boolean)
: getEditableBlocksInRange(editor, range);
}
if (targets.length === 0) {
targets = getAllEditableBlocks(editor);
}
ensureEditorTypography(editor);
rememberEditorSelection(editor);
syncActiveEditorContent(editor);
}
function buildEditorTableHtml(rows, cols) {
const colWidth = (100 / cols).toFixed(4) + '%';
let tableHtml = '<table style="width:100%;"><colgroup>';
for (let col = 0; col < cols; col++) {
tableHtml += '<col style="width:' + colWidth + ';">';
}
tableHtml += '</colgroup><tbody>';
for (let row = 0; row < rows; row++) {
tableHtml += '<tr>';
for (let col = 0; col < cols; col++) {
tableHtml += row === 0 ? '<th>Tajuk ' + (col + 1) + '</th>' : '<td>&nbsp;</td>';
}
tableHtml += '</tr>';
}
return tableHtml + '</tbody></table><p style="' + draftBodyStyle + '"><br></p>';
}
function insertHtmlAtEditorSelection(editor, html) {
if (savedEditorRange) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(savedEditorRange);
}
const selection = window.getSelection();
let range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
if (!range || !editor.contains(range.startContainer)) {
range = document.createRange();
range.selectNodeContents(editor);
range.collapse(false);
}
range.deleteContents();
const fragment = range.createContextualFragment(html);
const lastNode = fragment.lastChild;
range.insertNode(fragment);
if (lastNode) {
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
const quill = getEditorQuill(editor);
if (quill) quill.update('user');
}
function insertEditorTable() {
const editor = activeLetterEditor || document.querySelector('.letter-page-editor');
if (!editor) return;
const rows = Math.max(1, Math.min(10, parseInt(tableRowsSelect ? tableRowsSelect.value : '3', 10) || 3));
const cols = Math.max(1, Math.min(8, parseInt(tableColsSelect ? tableColsSelect.value : '2', 10) || 2));
editor.focus();
const quill = getEditorQuill(editor);
const tableModule = quill && quill.getModule ? quill.getModule('table') : null;
if (tableModule && typeof tableModule.insertTable === 'function') {
quill.focus();
const range = quill.getSelection(true) || { index: Math.max(0, quill.getLength() - 1), length: 0 };
if (range.length) quill.deleteText(range.index, range.length, 'user');
quill.setSelection(range.index, 0, 'silent');
tableModule.insertTable(rows, cols);
quill.update('user');
} else {
insertHtmlAtEditorSelection(editor, buildEditorTableHtml(rows, cols));
}
rememberEditorSelection(editor);
syncActiveEditorContent(editor);
}
function getParagraphForNode(node, editor) {
if (!node || !editor) return null;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
if (!element) return null;
const paragraph = element.closest('p, div');
return paragraph && editor.contains(paragraph) ? paragraph : null;
}
function isEmptyDefaultNumberLine(paragraph, number) {
if (!paragraph) return false;
const text = paragraph.textContent.replace(/\u00a0/g, ' ').trim();
return text === number + '.';
}
function placeCaretAtEnd(element) {
if (!element) return;
const range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
function focusNumberTwoLine(editor, event) {
const paragraph = getParagraphForNode(event.target, editor);
if (!isEmptyDefaultNumberLine(paragraph, '2')) return;
paragraph.innerHTML = '2. &nbsp;&nbsp;&nbsp;&nbsp;';
window.requestAnimationFrame(() => {
placeCaretAtEnd(paragraph);
rememberEditorSelection(editor);
});
}
function removeEmptyNumberThreeLines(editor) {
if (!editor) return;
Array.from(editor.querySelectorAll('p, div')).forEach(paragraph => {
if (isEmptyDefaultNumberLine(paragraph, '3')) {
paragraph.remove();
}
});
}
function syncEditorBody() {
const editors = getLetterEditors();
editors.forEach(removeEmptyNumberThreeLines);
const pages = editors.map(editor => editor.innerHTML.trim()).filter(html => {
const temp = document.createElement('div');
temp.innerHTML = html;
return temp.textContent.replace(/\s+/g, '').trim() !== '';
});
if (letterBodyInput) letterBodyInput.value = pages[0] || '';
if (letterPagesInput) letterPagesInput.value = JSON.stringify(pages.length ? pages : ['']);
if (pageCountLabel) pageCountLabel.textContent = 'Page Number';
}
function renumberPages() {
document.querySelectorAll('.memo-page-preview[data-page-index]').forEach((page, index) => {
page.dataset.pageIndex = String(index);
page.dataset.pageNumber = String(index + 1);
const number = page.querySelector('.memo-page-number');
if (number) number.textContent = String(index + 1);
const deleteButton = page.querySelector('.page-delete-btn');
if (deleteButton) deleteButton.disabled = index === 0;
});
renumberApprovalPages();
syncEditorBody();
}
function updatePageNumberPosition() {
const position = pageNumberPositionSelect ? pageNumberPositionSelect.value : 'none';
document.querySelectorAll('.memo-page-number').forEach(number => {
number.classList.remove('none', 'bottom_center', 'bottom_right');
number.classList.add(position);
});
}
function updateHeaderFooterPreview() {
const headerHtml = pageHeaderInput ? pageHeaderInput.value : '';
const footerHtml = pageFooterInput ? pageFooterInput.value : '';
document.querySelectorAll('.memo-custom-header').forEach(header => {
header.innerHTML = headerHtml;
});
document.querySelectorAll('.memo-custom-footer').forEach(footer => {
footer.innerHTML = footerHtml;
});
}
function updateLetterPageFitScale() {
if (!letterPagesContainer) return;
letterPagesContainer.style.setProperty('--letter-page-zoom', '1');
}
function refreshEditorPageHeights() {
updateLetterPageFitScale();
document.querySelectorAll('.memo-page-preview').forEach(page => {
const editor = page.querySelector('.letter-page-editor');
if (!editor) return;
const pageStyle = window.getComputedStyle(page);
const paddingBottom = parseFloat(pageStyle.paddingBottom) || 0;
const pageRect = page.getBoundingClientRect();
const editorRect = editor.getBoundingClientRect();
const editorTop = Math.max(0, editorRect.top - pageRect.top);
const memoFooter = page.querySelector('#memo_editor_footer');
const memoFooterHeight = memoFooter ? memoFooter.getBoundingClientRect().height : 0;
const footerGap = memoFooter ? 14 : 0;
const pageNumber = page.querySelector('.memo-page-number:not(.none)');
const bottomGuard = pageNumber ? 24 : 12;
const maxHeight = Math.max(24, page.clientHeight - paddingBottom - editorTop - memoFooterHeight - footerGap - bottomGuard);
editor.style.setProperty('--editor-max-height', maxHeight + 'px');
editor.style.maxHeight = maxHeight + 'px';
const quillHost = editor.closest('.quill-letter-host');
if (quillHost) {
quillHost.style.setProperty('--editor-max-height', maxHeight + 'px');
quillHost.style.maxHeight = maxHeight + 'px';
const quillContainer = quillHost.querySelector('.ql-container');
if (quillContainer) {
quillContainer.style.maxHeight = maxHeight + 'px';
quillContainer.style.overflow = 'hidden';
}
}
});
}
function attachEditorEvents(editor) {
ensureEditorTypography(editor);
editor.addEventListener('beforeinput', event => {
pushLetterUndoSnapshot();
if (isGeneratedClosingInteraction(event)) {
event.preventDefault();
}
}, true);
editor.addEventListener('keydown', event => {
if (handleLetterUndoRedoShortcut(event)) return;
if (isGeneratedClosingInteraction(event)) {
event.preventDefault();
return;
}
handleEditorBackspaceKey(editor, event);
}, true);
editor.addEventListener('focus', () => {
activeLetterEditor = editor;
rememberEditorSelection(editor);
});
editor.addEventListener('beforeinput', event => {
const inputType = event.inputType || '';
if (inputType === 'insertParagraph' || inputType === 'insertLineBreak' || inputType.indexOf('delete') === 0) {
window.requestAnimationFrame(() => {
if (!document.body.contains(editor)) return;
ensureEditorTypography(editor);
syncActiveEditorContent(editor);
});
}
});
editor.addEventListener('click', event => focusNumberTwoLine(editor, event));
editor.addEventListener('mouseup', () => rememberEditorSelection(editor));
editor.addEventListener('keyup', () => rememberEditorSelection(editor));
editor.addEventListener('input', () => {
rememberEditorSelection(editor);
ensureEditorTypography(editor);
removeEmptyNumberThreeLines(editor);
if (!isPaginating && !isRepaginating) {
scheduleFullLetterPagination(editor);
}
syncEditorBody();
});
editor.addEventListener('blur', syncEditorBody);
}
function attachHeaderFooterEvents(element) {
element.addEventListener('dblclick', () => {
element.contentEditable = 'true';
element.focus();
activeLetterEditor = element;
rememberEditorSelection(element);
});
element.addEventListener('focus', () => {
if (element.contentEditable !== 'true') return;
activeLetterEditor = element;
rememberEditorSelection(element);
});
element.addEventListener('mouseup', () => rememberEditorSelection(element));
element.addEventListener('keyup', () => rememberEditorSelection(element));
element.addEventListener('input', () => {
rememberEditorSelection(element);
const type = element.dataset.headerFooter === 'footer' ? 'footer' : 'header';
applyHeaderFooterToAll(type, element.innerHTML);
});
element.addEventListener('blur', () => {
syncHeaderFooterInputs();
element.contentEditable = 'false';
});
}
if (approvalPagesContainer) {
approvalPagesContainer.addEventListener('focusin', event => {
const editor = event.target.closest('.approval-recipient-body');
if (!editor) return;
activeLetterEditor = editor;
rememberEditorSelection(editor);
});
approvalPagesContainer.addEventListener('mouseup', event => {
const editor = event.target.closest('.approval-recipient-body');
if (editor) rememberEditorSelection(editor);
});
approvalPagesContainer.addEventListener('keyup', event => {
const editor = event.target.closest('.approval-recipient-body');
if (editor) rememberEditorSelection(editor);
});
approvalPagesContainer.addEventListener('input', event => {
const editor = event.target.closest('.approval-recipient-body');
if (!editor) return;
rememberEditorSelection(editor);
ensureEditorTypography(editor);
syncApprovalPageContent();
});
approvalPagesContainer.addEventListener('blur', event => {
const editor = event.target.closest('.approval-recipient-body');
if (editor) syncApprovalPageContent();
}, true);
}
function moveFooterToLastPage() {
const footer = document.getElementById('memo_editor_footer');
const pages = document.querySelectorAll('.memo-page-preview');
const lastPage = pages[pages.length - 1];
const lastEditor = lastPage ? lastPage.querySelector('.letter-page-editor') : null;
if (footer && lastPage && lastEditor) {
const editorHost = getEditorHostElement(lastEditor);
(editorHost || lastEditor).insertAdjacentElement('afterend', footer);
}
refreshEditorPageHeights();
}
function moveFooterBeforePageDelete(page) {
const footer = document.getElementById('memo_editor_footer');
if (!footer || !page || footer.parentElement !== page) return;
const pages = Array.from(document.querySelectorAll('.memo-page-preview'));
const fallbackPage = pages.filter(item => item !== page).pop();
if (fallbackPage) {
const fallbackEditor = fallbackPage.querySelector('.letter-page-editor');
if (fallbackEditor) {
const editorHost = getEditorHostElement(fallbackEditor);
(editorHost || fallbackEditor).insertAdjacentElement('afterend', footer);
} else {
fallbackPage.appendChild(footer);
}
}
}
function addLetterPage(afterPage = null, focusEditor = true) {
if (!letterPagesContainer) return null;
const page = document.createElement('div');
page.className = 'memo-page-preview is-extra-page';
page.dataset.pageIndex = String(document.querySelectorAll('.memo-page-preview').length);
page.dataset.pageNumber = String(document.querySelectorAll('.memo-page-preview').length + 1);
const pageActions = document.createElement('div');
pageActions.className = 'page-sheet-actions';
pageActions.setAttribute('aria-label', 'Tindakan halaman');
pageActions.innerHTML = `
<button type="button" class="page-add-btn" title="Tambah halaman selepas halaman ini"><i class="fas fa-file-circle-plus"></i></button>
<button type="button" class="page-delete-btn" title="Padam halaman ini"><i class="fas fa-trash-alt"></i></button>
`;
page.appendChild(pageActions);
const customHeader = document.createElement('div');
customHeader.className = 'memo-custom-header';
customHeader.contentEditable = 'false';
customHeader.dataset.headerFooter = 'header';
customHeader.innerHTML = pageHeaderInput ? pageHeaderInput.value : '';
attachHeaderFooterEvents(customHeader);
page.appendChild(customHeader);
const editor = document.createElement('div');
editor.className = 'word-editor letter-page-editor';
editor.contentEditable = 'true';
editor.innerHTML = '<p style="' + draftBodyStyle + '"><br></p>';
page.appendChild(editor);
const customFooter = document.createElement('div');
customFooter.className = 'memo-custom-footer';
customFooter.contentEditable = 'false';
customFooter.dataset.headerFooter = 'footer';
customFooter.innerHTML = pageFooterInput ? pageFooterInput.value : '';
attachHeaderFooterEvents(customFooter);
page.appendChild(customFooter);
const pageNumber = document.createElement('div');
pageNumber.className = 'memo-page-number ' + (pageNumberPositionSelect ? pageNumberPositionSelect.value : 'none');
pageNumber.textContent = page.dataset.pageNumber;
page.appendChild(pageNumber);
if (afterPage && afterPage.parentElement === letterPagesContainer) {
afterPage.insertAdjacentElement('afterend', page);
} else {
letterPagesContainer.appendChild(page);
}
moveFooterToLastPage();
renumberPages();
const enhancedEditor = enhanceEditorWithQuill(editor);
if (focusEditor) focusEditorStart(enhancedEditor);
return enhancedEditor;
}
function addPageAfterCurrent() {
const currentPage = activeLetterEditor ? activeLetterEditor.closest('.memo-page-preview') : null;
pushLetterUndoSnapshot();
addLetterPage(currentPage);
}
if (letterPagesContainer) {
letterPagesContainer.addEventListener('click', event => {
const addButton = event.target.closest('.page-add-btn');
const deleteButton = event.target.closest('.page-delete-btn');
const page = event.target.closest('.memo-page-preview');
if (!page) return;
if (addButton) {
pushLetterUndoSnapshot();
const editor = addLetterPage(page);
if (editor) activeLetterEditor = editor;
return;
}
if (deleteButton && !deleteButton.disabled) {
pushLetterUndoSnapshot();
deleteLetterPage(page);
}
});
}
function splitOverflowTextNodeToNextPage(editor, nextEditor, node) {
const sourceText = (node.textContent || '').replace(/\u00a0/g, ' ');
if (!sourceText || sourceText.length < 2) return false;
const writeText = value => {
if (node.nodeType === Node.TEXT_NODE) {
node.textContent = value;
return;
}
node.textContent = value;
};
let low = 1;
let high = sourceText.length - 1;
let bestFit = 0;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
writeText(sourceText.slice(0, mid));
refreshEditorPageHeights();
if (editor.scrollHeight <= editor.clientHeight + 2) {
bestFit = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
if (bestFit < 1) {
writeText(sourceText);
return false;
}
let splitAt = bestFit;
const previousSpace = sourceText.lastIndexOf(' ', bestFit);
if (previousSpace > 0) splitAt = previousSpace;
const currentText = sourceText.slice(0, splitAt).trimEnd();
const overflowText = sourceText.slice(splitAt).trimStart();
if (!overflowText) {
writeText(sourceText);
return false;
}
writeText(currentText || '\u00a0');
const overflowNode = node.nodeType === Node.TEXT_NODE
? document.createTextNode(overflowText)
: node.cloneNode(false);
if (overflowNode.nodeType !== Node.TEXT_NODE) {
overflowNode.textContent = overflowText;
}
nextEditor.insertBefore(overflowNode, nextEditor.firstChild);
return true;
}
function paginateEditorOverflow(editor) {
if (!editor || isPaginating) return;
const page = editor.closest('.memo-page-preview');
if (!page) return;
isPaginating = true;
let overflowEditor = null;
let movedContent = false;
let continuePagination = true;
try {
moveFooterToLastPage();
refreshEditorPageHeights();
let nextPage = page.nextElementSibling && page.nextElementSibling.classList.contains('memo-page-preview')
? page.nextElementSibling
: null;
let nextEditor = nextPage ? nextPage.querySelector('.letter-page-editor') : null;
while (editor.scrollHeight > editor.clientHeight + 2 && getEditableChildNodes(editor).length > 0) {
if (!nextEditor) {
nextEditor = addLetterPage(page, false);
nextPage = nextEditor ? nextEditor.closest('.memo-page-preview') : null;
refreshEditorPageHeights();
}
if (!nextEditor) break;
const editableNodes = getEditableChildNodes(editor);
const nodeToMove = editableNodes[editableNodes.length - 1];
if (!nodeToMove) break;
if (editableNodes.length === 1) {
movedContent = splitOverflowTextNodeToNextPage(editor, nextEditor, nodeToMove);
if (!movedContent) {
nextEditor.insertBefore(nodeToMove, nextEditor.firstChild);
movedContent = true;
continuePagination = false;
}
refreshEditorPageHeights();
break;
} else {
nextEditor.insertBefore(nodeToMove, nextEditor.firstChild);
movedContent = true;
}
refreshEditorPageHeights();
}
if (movedContent && nextEditor && continuePagination) {
overflowEditor = nextEditor;
}
} finally {
isPaginating = false;
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
if (hasGeneratedClosing() && !isRepaginating) {
window.requestAnimationFrame(rebalanceGeneratedClosingPlacement);
}
if (overflowEditor && !isRepaginating) {
window.requestAnimationFrame(() => paginateEditorOverflow(overflowEditor));
}
}
}
function deleteLetterPage(page) {
if (!page) return;
const pages = document.querySelectorAll('.memo-page-preview');
if (pages.length <= 1 || page.dataset.pageIndex === '0') {
alert('Halaman pertama tidak boleh dipadam.');
return;
}
const editor = page.querySelector('.letter-page-editor');
const hasContent = editor && editor.textContent.replace(/\s+/g, '').trim() !== '';
if (hasContent && !confirm('Padam halaman ini beserta kandungannya?')) {
return;
}
moveFooterBeforePageDelete(page);
page.remove();
moveFooterToLastPage();
renumberPages();
}
function formatMemoHeaderReference(value) {
return String(value || '').replace(/\s+JLD\.\s*(\d+)\s*\(([^)]+)\)$/i, ' Jld. $1 ($2)');
}
function formatMemoDate(value) {
if (!value) return '-';
const date = new Date(value + 'T00:00:00');
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString('ms-MY', { day: '2-digit', month: 'short', year: 'numeric' });
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, char => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[char]));
}
function normalizeMemoSubjectText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function getMemoIntroBlockText(block) {
return (block ? block.textContent : '').replace(/\s+/g, ' ').trim();
}
function isMemoIntroReferenceBlock(block) {
return getMemoIntroBlockText(block).toLowerCase().includes('dengan hormatnya saya diarah merujuk perkara di atas');
}
function isMemoSalutationBlock(block) {
return /YBhg\.\s*Dato/i.test(getMemoIntroBlockText(block));
}
function isBlankMemoParagraph(block) {
if (!block) return false;
return block.textContent.replace(/\s+/g, '').trim() === '' && !block.querySelector('img, table');
}
function createMemoSubjectParagraph(subjectText) {
const paragraph = document.createElement('p');
paragraph.setAttribute('data-memo-subject-line', '1');
paragraph.setAttribute('contenteditable', 'false');
paragraph.setAttribute('style', draftBodyStyle);
const strong = document.createElement('strong');
strong.textContent = subjectText;
paragraph.appendChild(strong);
return paragraph;
}
function createMemoBlankParagraph() {
const paragraph = document.createElement('p');
paragraph.setAttribute('style', draftBodyStyle);
paragraph.appendChild(document.createElement('br'));
return paragraph;
}
function findMemoSubjectInsertionPoint() {
for (const editor of getLetterEditors()) {
const blocks = Array.from(editor.children);
const intro = blocks.find(block => isMemoIntroReferenceBlock(block));
if (!intro) continue;
const introIndex = blocks.indexOf(intro);
const hasSalutationBeforeIntro = blocks.slice(0, introIndex).some(block => isMemoSalutationBlock(block));
if (hasSalutationBeforeIntro || introIndex >= 0) {
return { editor, intro };
}
}
return null;
}
function normalizeMemoGeneratedLineText(value) {
return (value || '')
.replace(/\u00a0/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.toUpperCase();
}
function removeMemoSubjectLines(subjectText = '') {
document.querySelectorAll('[data-memo-subject-line="1"]').forEach(line => line.remove());
const normalizedSubject = normalizeMemoGeneratedLineText(subjectText);
if (!normalizedSubject) return;
getLetterEditors().forEach(editor => {
Array.from(editor.children).forEach(block => {
if (!block || block.nodeType !== Node.ELEMENT_NODE) return;
if (block.matches('[data-generated-closing="1"], [data-signature-block="1"]')) return;
if (block.querySelector('[data-generated-closing="1"], [data-signature-block="1"]')) return;
const blockText = normalizeMemoGeneratedLineText(block.textContent);
const looksLikeGeneratedSubject = !!block.querySelector('strong, b, u')
|| block.getAttribute('contenteditable') === 'false'
|| block.style.fontWeight === 'bold'
|| block.style.textDecoration.includes('underline');
if (blockText === normalizedSubject && looksLikeGeneratedSubject) {
block.remove();
}
});
});
}
function ensureBlankBeforeMemoIntro(editor, intro) {
if (!editor || !intro) return;
const previous = intro.previousElementSibling;
if (previous && isBlankMemoParagraph(previous)) return;
editor.insertBefore(createMemoBlankParagraph(), intro);
}
function updateMemoSubjectLineOnPage() {
const subjectText = normalizeMemoSubjectText(subjectInput ? subjectInput.value : '');
const insertionPoint = findMemoSubjectInsertionPoint();
if (!insertionPoint) return;

const { editor, intro } = insertionPoint;
const previous = intro.previousElementSibling;
if (previous && isBlankMemoParagraph(previous)) {
previous.remove();
}
removeMemoSubjectLines(subjectText);

if (subjectText) {
editor.insertBefore(createMemoSubjectParagraph(subjectText), intro);
} else {
ensureBlankBeforeMemoIntro(editor, intro);
}

const quill = getEditorQuill(editor);
if (quill) quill.update('silent');
ensureEditorTypography(editor);
syncEditorBody();
scheduleFullLetterPagination(editor);
}
function getPreparedClosingHtml() {
return getPreparedSignatureGroups().join('');
}
function getPreparedSignatureGroups() {
const signatoryName = (signatoryNameInput?.value || <?= json_encode($_SESSION['fullname'] ?? '', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>).trim();
const displayName = signatoryName ? signatoryName.toUpperCase() : '';
const signaturePath = (preparerSignedInput?.value === '1' && preparerSignBtn?.dataset.signaturePath)
? preparerSignBtn.dataset.signaturePath
: '';
const sloganStyle = draftBodyStyle + '; margin:0';
const closingNameStyle = draftBodyStyle + '; margin:0';
const signatureSpaceStyle = draftBodyStyle + '; min-height:72px; height:72px; width:150px; display:flex; align-items:center; justify-content:center; margin:0';
const signatureHtml = signaturePath
? '<p data-generated-signature="1" style="' + signatureSpaceStyle + '"><img src="' + escapeHtml(signaturePath) + '" alt="Tandatangan Digital" style="max-width:150px;max-height:58px;object-fit:contain;display:block;"></p>'
: '<p data-generated-signature="1" style="' + signatureSpaceStyle + '"><br></p>';
return [
'<p style="' + sloganStyle + '"><strong>"MALAYSIA MADANI"</strong></p>',
'<p style="' + sloganStyle + '"><strong>"BERKHIDMAT UNTUK NEGARA"</strong></p>',
'<p style="' + sloganStyle + '; margin:0 0 24px 0"><strong>"CEKAP, AKAUNTABILITI, TELUS"</strong></p>',
'<p style="' + closingNameStyle + '">Saya yang menjalankan amanah,</p>',
signatureHtml,
'<p style="' + closingNameStyle + '"><strong>(' + escapeHtml(displayName) + ')</strong><span class="prepared-by-line" style="display:block;margin-top:0;font-size:8px;line-height:1">Disediakan Oleh: ' + escapeHtml(preparedByShortName || '') + '</span></p>'
];
}
function lockGeneratedClosingBlocks() {
document.querySelectorAll('[data-generated-closing="1"]').forEach(block => {
block.setAttribute('contenteditable', 'false');
block.setAttribute('data-signature-block', '1');
block.classList.add('signature-block', 'generated-closing-lock');
block.querySelectorAll('*').forEach(child => child.setAttribute('contenteditable', 'false'));
});
}
function getSignatureBlocks() {
return Array.from(document.querySelectorAll('[data-signature-block="1"], [data-generated-closing="1"]'))
.filter((block, index, blocks) => blocks.indexOf(block) === index);
}
function editorHasMeaningfulNonSignatureContent(editor) {
if (!editor) return false;
const clone = editor.cloneNode(true);
clone.querySelectorAll('[data-signature-block="1"], [data-generated-closing="1"], [data-memo-subject-line="1"], [data-surat-rasmi-generated-line]').forEach(node => node.remove());
return clone.textContent.replace(/\s+/g, '').trim() !== '' || !!clone.querySelector('img, table');
}
function getSignatureStartEditor(fallbackEditor = null) {
const editors = getLetterEditors();
let lastContentEditor = null;
editors.forEach(editor => {
if (editorHasMeaningfulNonSignatureContent(editor)) {
lastContentEditor = editor;
}
});
return lastContentEditor || fallbackEditor || editors[0] || null;
}
function getOrCreateNextEditor(editor) {
const page = getEditorPage(editor);
let nextEditor = getNextLetterEditor(editor);
if (!nextEditor) {
nextEditor = addLetterPage(page, false);
}
return nextEditor;
}
function editorHasRoomForNode(editor, node) {
if (!editor || !node) return false;
return editor.scrollHeight <= editor.clientHeight + 2;
}
function appendSignatureBlockToEditor(editor, block) {
if (!editor || !block) return false;
editor.appendChild(block);
lockGeneratedClosingBlocks();
refreshEditorPageHeights();
return editorHasRoomForNode(editor, block);
}
function removeEmptySignaturePages() {
getLetterEditors().slice(1).forEach(editor => {
if (editorHasContent(editor)) return;
const page = getEditorPage(editor);
if (page) page.remove();
});
}
function preserveQuillSelection(editor, callback) {
const selectedEditor = getSelectionLetterEditor() || activeLetterEditor || editor;
const quill = getEditorQuill(selectedEditor);
const selection = quill ? quill.getSelection() : null;
const result = callback();
if (quill && selection && document.body.contains(selectedEditor)) {
const maxIndex = Math.max(0, quill.getLength() - 1);
const nextIndex = Math.min(selection.index, maxIndex);
const nextLength = Math.min(selection.length || 0, Math.max(0, maxIndex - nextIndex));
quill.setSelection(nextIndex, nextLength, 'silent');
}
return result;
}
function arrangeSignatureBlocks(startEditor = null) {
const blocks = getSignatureBlocks();
if (!blocks.length) return;

const targetStart = getSignatureStartEditor(startEditor);
if (!targetStart) return;

blocks.forEach(block => block.remove());
let targetEditor = targetStart;

blocks.forEach(block => {
let placed = appendSignatureBlockToEditor(targetEditor, block);
if (!placed) {
block.remove();
targetEditor = getOrCreateNextEditor(targetEditor);
if (targetEditor) {
const placedOnNextPage = appendSignatureBlockToEditor(targetEditor, block);
if (!placedOnNextPage && editorHasOnlyGeneratedClosing(targetEditor)) {
lockGeneratedClosingBlocks();
}
}
}
});

lockGeneratedClosingBlocks();
removeEmptyTrailingEditorPages();
removeEmptySignaturePages();
renumberPages();
refreshEditorPageHeights();
}
function isGeneratedClosingInteraction(event) {
if (event?.target?.closest && event.target.closest('[data-generated-closing="1"]')) {
return true;
}
if (event?.target?.closest && event.target.closest('[data-memo-subject-line="1"]')) {
return true;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return false;
const node = selection.getRangeAt(0).commonAncestorContainer;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
return !!(element && element.closest('[data-generated-closing="1"], [data-memo-subject-line="1"]'));
}
function removeGeneratedClosings() {
document.querySelectorAll('[data-generated-closing="1"]').forEach(block => {
const editor = block.closest('.letter-page-editor');
const quill = getEditorQuill(editor);
if (quill && window.Quill) {
const blot = Quill.find(block);
if (blot) {
const index = quill.getIndex(blot);
quill.deleteText(index, 1, 'silent');
return;
}
}
block.remove();
});
}
function hasGeneratedClosing() {
return !!document.querySelector('[data-generated-closing="1"]');
}
function getGeneratedClosingInsertIndex(quill) {
if (!quill) return 0;
const text = quill.getText();
const contentEnd = text.replace(/\s+$/g, '').length;
const trailingLength = Math.max(0, quill.getLength() - 1 - contentEnd);
if (trailingLength > 0) {
quill.deleteText(contentEnd, trailingLength, 'silent');
}
return contentEnd;
}
function insertGeneratedClosing(editor, html, source = 'silent') {
if (!editor || !html) return false;
const groups = Array.isArray(html) ? html.filter(Boolean) : [html].filter(Boolean);
if (!groups.length) return false;
const quill = getEditorQuill(editor);
if (quill) {
const insertAt = getGeneratedClosingInsertIndex(quill);
quill.insertText(insertAt, '\n', source);
let insertIndex = insertAt + 1;
groups.forEach(groupHtml => {
quill.insertEmbed(insertIndex, 'generatedClosing', groupHtml, source);
insertIndex += 1;
});
} else {
while (editor.lastElementChild && !editor.lastElementChild.textContent.trim() && !editor.lastElementChild.querySelector('img, table, [data-generated-closing="1"]')) {
editor.lastElementChild.remove();
}
editor.insertAdjacentHTML('beforeend', '<p style="' + draftBodyStyle + '"><br></p>' + groups.map(groupHtml => '<div data-generated-closing="1" data-signature-block="1" class="signature-block generated-closing-lock" contenteditable="false">' + groupHtml + '</div>').join(''));
}
editor.dataset.generatedClosingHtml = groups.join('');
lockGeneratedClosingBlocks();
return true;
}
function editorHasOnlyGeneratedClosing(editor) {
if (!editor) return false;
const meaningfulNodes = getEditableChildNodes(editor).filter(node => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent.replace(/\s+/g, '').trim() !== '';
}
if (node.nodeType !== Node.ELEMENT_NODE) return false;
if (node.matches('[data-generated-closing="1"]')) return false;
if (node.querySelector('[data-generated-closing="1"]')) {
const clone = node.cloneNode(true);
clone.querySelectorAll('[data-generated-closing="1"]').forEach(item => item.remove());
return clone.textContent.replace(/\s+/g, '').trim() !== '' || !!clone.querySelector('img, table');
}
return node.textContent.replace(/\s+/g, '').trim() !== '' || !!node.querySelector('img, table');
});
return meaningfulNodes.length === 0;
}
function rebalanceGeneratedClosingPlacement() {
if (isRebalancingGeneratedClosing || isPaginating) return;
const closing = document.querySelector('[data-generated-closing="1"]');
const currentEditor = closing ? closing.closest('.letter-page-editor') : null;
if (!closing || !currentEditor) {
lockGeneratedClosingBlocks();
return;
}
isRebalancingGeneratedClosing = true;
try {
arrangeSignatureBlocks(currentEditor);
} finally {
isRebalancingGeneratedClosing = false;
lockGeneratedClosingBlocks();
removeEmptyTrailingEditorPages();
renumberPages();
refreshEditorPageHeights();
syncEditorBody();
}
}
function restoreGeneratedClosingIfNeeded(editor, html) {
if (!editor || !html) return;
window.clearTimeout(generatedClosingRestoreTimer);
generatedClosingRestoreTimer = window.setTimeout(() => {
if (!document.body.contains(editor)) return;
if (editor.querySelector('[data-generated-closing="1"]')) {
lockGeneratedClosingBlocks();
rebalanceGeneratedClosingPlacement();
}
}, 0);
}
function insertSignature(editor = null) {
const editors = getLetterEditors();
const targetEditor = editor && editor.classList && editor.classList.contains('letter-page-editor')
? editor
: (activeLetterEditor && activeLetterEditor.classList.contains('letter-page-editor') ? activeLetterEditor : editors[editors.length - 1]);
if (!targetEditor) return false;

isInsertingSignature = true;
try {
removeGeneratedClosings();
insertGeneratedClosing(targetEditor, getPreparedSignatureGroups(), 'user');
} finally {
isInsertingSignature = false;
}
repaginate(targetEditor);
syncEditorBody();
return true;
}
function generatePreparedClosing() {
if (hasGeneratedClosing()) {
alert('Penutup surat sudah wujud pada kertas. Padam penutup sedia ada dahulu jika mahu jana semula.');
rebalanceGeneratedClosingPlacement();
return;
}
if (!confirm('Adakah ini line terakhir ayat anda?')) {
return;
}
pushLetterUndoSnapshot();
const editors = getLetterEditors();
const targetEditor = activeLetterEditor && activeLetterEditor.classList.contains('letter-page-editor')
? activeLetterEditor
: editors[editors.length - 1];
if (!targetEditor) return;
insertSignature(targetEditor);
activeLetterEditor = targetEditor;
}
function applySignatureToExistingClosing(signaturePath, markSigned = false) {
if (!signaturePath) return false;
const generatedClosing = document.querySelector('[data-generated-closing="1"]');
if (!generatedClosing) return false;
const signatureParagraph = document.querySelector('[data-generated-closing="1"] [data-generated-signature="1"], [data-generated-signature="1"]');
if (!signatureParagraph) return false;
let changed = false;
if (!signatureParagraph.querySelector('img')) {
signatureParagraph.innerHTML = '<img src="' + escapeHtml(signaturePath) + '" alt="Tandatangan Digital" style="max-width:150px;max-height:58px;object-fit:contain;">';
changed = true;
}
if (markSigned && preparerSignedInput) {
preparerSignedInput.value = '1';
}
if (markSigned && memoSignaturePreview && signaturePath !== '') {
memoSignaturePreview.src = signaturePath;
memoSignaturePreview.style.display = 'block';
}
if (markSigned && submitDraftBtn) {
submitDraftBtn.disabled = false;
}
if (markSigned && preparerSignPanel) {
preparerSignPanel.classList.add('signed');
preparerSignPanel.classList.remove('missing');
}
if (markSigned && preparerSignTitle) {
preparerSignTitle.textContent = 'Tandatangan penyedia telah dibuat';
}
if (markSigned && preparerSignHint) {
preparerSignHint.textContent = 'Surat ini sudah mempunyai tandatangan penyedia dan boleh dihantar.';
}
lockGeneratedClosingBlocks();
syncEditorBody();
return changed || markSigned;
}
function applyDepartmentOfficerRevisionSignatureAfterEditorReady() {
if (!isDepartmentOfficerRevisionEditor) return;
const signaturePath = (preparerSignBtn?.dataset.signaturePath || currentUserSignaturePath || '').trim();
if (!signaturePath) return;
queueMicrotask(() => {
requestAnimationFrame(() => {
applySignatureToExistingClosing(signaturePath, false);
});
});
}
function updateMemoPreview() {
if (memoDatePreview && letterDateInput) memoDatePreview.textContent = formatMemoDate(letterDateInput.value);
if (memoDocTypePreview && docTypeInput) memoDocTypePreview.textContent = (docTypeInput.value || '-').toUpperCase();
if (memoFromPreview && signatoryPositionInput) {
memoFromPreview.innerHTML = signatoryPositionInput.value.trim()
? escapeHtml(signatoryPositionInput.value.trim()) + '<br>Majlis Agama Islam Negeri Pulau Pinang'
: '-';
}
if (memoToPreview && recipientInput) {
memoToPreview.innerHTML = renderMemoPreviewPositions(recipientInput);
}
if (memoThroughPreview && throughInput) {
memoThroughPreview.innerHTML = renderMemoPreviewPositions(throughInput);
}
if (memoSignatoryPreview && signatoryNameInput) {
const signatoryName = signatoryNameInput.value.trim();
memoSignatoryPreview.textContent = signatoryName ? '(' + signatoryName.toUpperCase() + ')' : '-';
}
}
function getCurrentTemplatePlaceholderData() {
return {
reference_no: refPreview ? refPreview.textContent.trim() : '',
date: memoDatePreview ? memoDatePreview.textContent.trim() : (letterDateInput ? letterDateInput.value : ''),
receiver_name: getCheckedMemoPositions(recipientInput).join(', '),
receiver_department: 'Majlis Agama Islam Negeri Pulau Pinang',
subject: subjectInput ? subjectInput.value.trim() : '',
letter_content: '',
sender_name: signatoryNameInput ? signatoryNameInput.value.trim() : '',
sender_position: signatoryPositionInput ? signatoryPositionInput.value.trim() : '',
sender_department: signatoryDepartmentInput ? signatoryDepartmentInput.value.trim() : ''
};
}
function replaceTemplatePlaceholders(content, data) {
let output = String(content || '');
Object.keys(data).forEach(key => {
const safeValue = String(data[key] || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
output = output.split('{{' + key + '}}').join(safeValue);
});
return output;
}
function selectedLetterTemplate() {
const selectedId = templateSelect ? parseInt(templateSelect.value, 10) : 0;
return letterTemplates.find(template => template.id === selectedId) || letterTemplates[0] || null;
}
function applySelectedTemplateToEditor(force = false) {
const template = selectedLetterTemplate();
const firstEditor = document.querySelector('.letter-page-editor');
if (!template || !firstEditor) return;
const currentPlain = firstEditor.textContent.replace(/\s+/g, '').trim();
if (!force && currentPlain !== '') return;
if (force && currentPlain !== '' && !confirm('Guna template ini akan menggantikan kandungan halaman pertama. Teruskan?')) {
return;
}
clearAllLetterPages();
const targetEditor = document.querySelector('.letter-page-editor');
if (!targetEditor) return;
targetEditor.innerHTML = replaceTemplatePlaceholders(template.content, getCurrentTemplatePlaceholderData());
ensureEditorTypography(targetEditor);
activeLetterEditor = targetEditor;
moveFooterToLastPage();
renumberPages();
refreshEditorPageHeights();
syncEditorBody();
}
document.querySelectorAll('.word-toolbar button[data-command]').forEach(button => {
button.addEventListener('mousedown', event => event.preventDefault());
button.addEventListener('click', () => execEditorCommand(button.dataset.command));
});
if (fontFamilySelect) {
fontFamilySelect.value = 'Arial';
fontFamilySelect.disabled = true;
}
if (fontSizeSelect) {
fontSizeSelect.value = '16';
fontSizeSelect.disabled = true;
}
const tableButton = document.querySelector('.word-toolbar button[data-action="insertTable"]');
if (tableButton) {
tableButton.addEventListener('click', insertEditorTable);
}
if (templateSelect) {
templateSelect.addEventListener('change', () => applySelectedTemplateToEditor(true));
}
document.querySelectorAll('.memo-check-dropdown').forEach(dropdown => {
const toggle = dropdown.querySelector('.memo-check-toggle');
const select = document.getElementById(dropdown.dataset.targetSelect || '');
syncMemoCheckboxDropdown(dropdown);
if (toggle) {
toggle.addEventListener('click', event => {
event.preventDefault();
document.querySelectorAll('.memo-check-dropdown.open').forEach(openDropdown => {
if (openDropdown !== dropdown) openDropdown.classList.remove('open');
});
dropdown.classList.toggle('open');
});
}
dropdown.addEventListener('change', event => {
if (!event.target.matches('input[type="checkbox"]')) return;
syncMemoCheckboxDropdown(dropdown);
updateMemoPreview();
syncRoutingCandidates();
updateApprovalPagePreview();
});
if (select) {
select.addEventListener('change', () => {
const matchingCheckbox = Array.from(dropdown.querySelectorAll('input[type="checkbox"]')).find(input => input.value === select.value);
if (matchingCheckbox) matchingCheckbox.checked = true;
syncMemoCheckboxDropdown(dropdown);
updateMemoPreview();
syncRoutingCandidates();
updateApprovalPagePreview();
});
}
});
document.addEventListener('click', event => {
if (event.target.closest('.memo-check-dropdown')) return;
document.querySelectorAll('.memo-check-dropdown.open').forEach(dropdown => dropdown.classList.remove('open'));
});
document.querySelectorAll('.letter-page-editor').forEach(enhanceEditorWithQuill);
document.querySelectorAll('.memo-custom-header[data-header-footer], .memo-custom-footer[data-header-footer]').forEach(attachHeaderFooterEvents);
ensureAllDocumentTypography();
lockGeneratedClosingBlocks();
if (addPageBtn) addPageBtn.addEventListener('click', addPageAfterCurrent);
if (deleteCurrentPageBtn) {
deleteCurrentPageBtn.addEventListener('click', () => {
const currentPage = activeLetterEditor ? activeLetterEditor.closest('.memo-page-preview') : null;
pushLetterUndoSnapshot();
deleteLetterPage(currentPage);
});
}
if (pageNumberPositionSelect) {
pageNumberPositionSelect.addEventListener('change', updatePageNumberPosition);
}
moveFooterToLastPage();
updatePageNumberPosition();
updateHeaderFooterPreview();
syncHeaderFooterInputs();
syncEditorBody();
refreshEditorPageHeights();
applyDepartmentOfficerRevisionSignatureAfterEditorReady();
window.addEventListener('resize', refreshEditorPageHeights);
const departmentOfficerSelect = document.getElementById('department_officer_user_id');
function syncSignatoryFromDepartmentHead(department) {
const head = departmentHeadsByDepartment[department || ''];
if (!head) return;
if (signatoryNameInput && head.name) {
signatoryNameInput.value = head.name;
}
if (signatoryPositionInput && head.position) {
signatoryPositionInput.value = head.position;
}
if (signatoryDepartmentInput && head.department) {
signatoryDepartmentInput.value = head.department;
}
if (departmentOfficerSelect && departmentOfficerSelect.tagName !== 'SELECT' && head.id) {
departmentOfficerSelect.value = String(head.id);
}
if (departmentOfficerSelect && departmentOfficerSelect.tagName === 'SELECT' && head.id) {
const headOption = Array.from(departmentOfficerSelect.options).find(option => parseInt(option.value, 10) === head.id);
if (headOption && !headOption.hidden) {
departmentOfficerSelect.value = String(head.id);
}
}
updateMemoPreview();
}
function syncSignatoryFromDepartmentOfficer() {
if (!departmentOfficerSelect) return;
if (departmentOfficerSelect.tagName !== 'SELECT') return;
const selected = departmentOfficerSelect.options[departmentOfficerSelect.selectedIndex];
if (!selected || !selected.value) return;
if (signatoryNameInput && selected.dataset.name) {
signatoryNameInput.value = selected.dataset.name;
}
if (signatoryPositionInput && selected.dataset.position) {
signatoryPositionInput.value = selected.dataset.position;
}
if (signatoryDepartmentInput && selected.dataset.department) {
signatoryDepartmentInput.value = selected.dataset.department;
}
updateMemoPreview();
}
if (departmentOfficerSelect) {
departmentOfficerSelect.addEventListener('change', syncSignatoryFromDepartmentOfficer);
syncSignatoryFromDepartmentOfficer();
}
if (draftStatusSelect) {
draftStatusSelect.addEventListener('change', () => {
updateApprovalControlsForStatus();
updateApprovalPagePreview();
syncApprovalPageContent();
});
}
[letterDateInput, subjectInput, recipientInput, throughInput, docTypeInput, signatoryDepartmentInput, signatoryPositionInput, signatoryNameInput].forEach(input => {
if (input) input.addEventListener('input', updateMemoPreview);
if (input) input.addEventListener('change', updateMemoPreview);
});
if (subjectInput) {
subjectInput.addEventListener('input', updateMemoSubjectLineOnPage);
subjectInput.addEventListener('change', updateMemoSubjectLineOnPage);
}
if (approvalPagesContainer) {
approvalPagesContainer.addEventListener('input', syncApprovalPageContent);
approvalPagesContainer.addEventListener('blur', () => {
syncApprovalPageContent();
updateApprovalPagePreview();
}, true);
}
document.querySelectorAll('.routing-order-select').forEach(select => {
select.addEventListener('change', () => {
normalizeRoutingOrder(select);
updateApprovalPagePreview();
});
});
if (recipientInput) recipientInput.addEventListener('change', syncRoutingCandidates);
if (throughInput) throughInput.addEventListener('change', syncRoutingCandidates);
syncRoutingCandidates();
document.querySelectorAll('.routing-checkbox:checked').forEach(checkbox => {
const select = checkbox.closest('.routing-option')?.querySelector('.routing-order-select');
const order = select ? parseInt(select.value, 10) || 0 : 0;
checkbox.dataset.checkedSequence = String(order || ++routingSequenceCounter);
});
normalizeRoutingOrder();
updateApprovalPagePreview();
syncApprovalPageContent();
updateApprovalControlsForStatus();
updateMemoSubjectLineOnPage();
if (generateClosingBtn) {
generateClosingBtn.addEventListener('click', generatePreparedClosing);
}
if (preparerSignBtn && preparerSignedInput) {
preparerSignBtn.addEventListener('click', function () {
if (!confirm('Adakah anda pasti untuk sign surat ini?')) {
return;
}
pushLetterUndoSnapshot();
const signaturePath = this.dataset.signaturePath || '';
applySignatureToExistingClosing(signaturePath, true);
this.innerHTML = '<i class="fas fa-signature"></i> Tandatangan Semula';
});
}
if (draftForm) {
draftForm.addEventListener('submit', function (event) {
syncEditorBody();
syncHeaderFooterInputs();
syncApprovalPageContent();
const readyToSend = isReadyToSendStatus();
if (readyToSend && currentUserIsHeadOfficer && !isDepartmentOfficerRevisionEditor && preparerSignedInput && preparerSignedInput.value !== '1') {
event.preventDefault();
alert('Sila klik Tandatangan Penyedia sebelum menghantar surat.');
preparerSignPanel?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
const plainText = Array.from(document.querySelectorAll('.letter-page-editor')).map(editor => editor.textContent).join('').replace(/\s+/g, '').trim();
if (plainText === '') {
event.preventDefault();
alert('Sila isi kandungan memo terlebih dahulu.');
const firstEditor = document.querySelector('.letter-page-editor');
if (firstEditor) firstEditor.focus();
return;
}
if (readyToSend && departmentOfficerSelect && departmentOfficerSelect.required && !departmentOfficerSelect.value) {
event.preventDefault();
alert('Sila pilih Ketua Pegawai sebelum menyimpan memo.');
departmentOfficerSelect.focus();
return;
}
if (readyToSend) {
normalizeRoutingOrder();
}
const selectedRoutes = Array.from(document.querySelectorAll('.routing-checkbox:checked'));
if (readyToSend && selectedRoutes.length === 0) {
event.preventDefault();
alert('Sila pilih sekurang-kurangnya seorang penerima dalam Turutan Edaran Kelulusan.');
routingChecklist?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
const emptyApprovalBody = Array.from(document.querySelectorAll('.approval-recipient-body')).find(editor => {
return editor.textContent.replace(/\s+/g, '').trim() === '';
});
if (readyToSend && emptyApprovalBody) {
event.preventDefault();
alert('Sila isi kandungan untuk setiap penerima dalam halaman kelulusan.');
emptyApprovalBody.scrollIntoView({ behavior: 'smooth', block: 'center' });
emptyApprovalBody.focus();
return;
}
const routeOrders = selectedRoutes.map(checkbox => {
const select = checkbox.closest('.routing-option')?.querySelector('.routing-order-select');
return select ? parseInt(select.value, 10) : 0;
}).sort((left, right) => left - right);
const validSequence = routeOrders.every((order, index) => order === index + 1);
if (readyToSend && !validSequence) {
event.preventDefault();
alert('Nombor turutan mesti unik dan berturutan bermula daripada Pilihan 1.');
routingChecklist?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return;
}
});
}
const referencePreviewUsesActualNumber = <?= json_encode($is_edit && (($draft['status'] ?? 'draf') === 'siap'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
function updateFolderSummary() {
if (!folderSelect) return;
const option = folderSelect.options[folderSelect.selectedIndex];
if (!option || !option.value) return;
const department = option.dataset.department || '';
const nextRef = option.dataset.nextRef || '-';
const nextFileNumbering = option.dataset.nextFileNumbering || '';
const refDisplay = nextRef + '(' + (referencePreviewUsesActualNumber ? (nextFileNumbering || 'X') : 'X') + ')';
refPreview.textContent = refDisplay || '-';
if (memoRefPreview) memoRefPreview.textContent = formatMemoHeaderReference(refDisplay || '-');
if (signatoryDepartmentInput && !signatoryDepartmentInput.value.trim()) {
signatoryDepartmentInput.value = department || '';
}
syncDepartmentOfficerOptions(department);
syncSignatoryFromDepartmentHead(department);
updateMemoPreview();
}
function syncDepartmentOfficerOptions(department) {
if (!departmentOfficerSelect) return;
const selectedValue = departmentOfficerSelect.value;
let selectedStillVisible = false;
Array.from(departmentOfficerSelect.options).forEach(option => {
if (!option.value) {
option.hidden = false;
return;
}
const sameDepartment = !department || (option.dataset.department || '') === department;
option.hidden = !sameDepartment;
if (sameDepartment && option.value === selectedValue) {
selectedStillVisible = true;
}
});
if (selectedValue && !selectedStillVisible) {
departmentOfficerSelect.value = '';
}
if (!departmentOfficerSelect.value) {
const head = departmentHeadsByDepartment[department || ''];
if (head && head.id) {
const headOption = Array.from(departmentOfficerSelect.options).find(option => parseInt(option.value, 10) === head.id);
if (headOption && !headOption.hidden) {
departmentOfficerSelect.value = String(head.id);
}
}
}
}
if (folderSelect) {
folderSelect.addEventListener('change', updateFolderSummary);
updateFolderSummary();
}
updateMemoPreview();
</script>
<?php renderSessionTimeoutScript(); ?>
</body>
</html>
