commit dc41df7bf6d80347d9c40f7fdbbbac0d5418e3af Author: Flatlogic Bot Date: Sat Feb 28 13:31:11 2026 +0000 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e427ff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*/node_modules/ +*/build/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e2bbc23 --- /dev/null +++ b/.htaccess @@ -0,0 +1,18 @@ +DirectoryIndex index.php index.html +Options -Indexes +Options -MultiViews + +RewriteEngine On + +# 0) Serve existing files/directories as-is +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] + +# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists) +RewriteCond %{REQUEST_FILENAME}.php -f +RewriteRule ^(.+?)/?$ $1.php [L] + +# 2) Optional: strip trailing slash for non-directories (keeps .php links working) +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.+)/$ $1 [R=301,L] diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/WorkflowEngine.php b/WorkflowEngine.php new file mode 100644 index 0000000..9a20ffe --- /dev/null +++ b/WorkflowEngine.php @@ -0,0 +1,863 @@ +pdo = db(); + } + + public function getDashboardMatrix(?string $searchTerm = null, ?int $groupId = null, ?int $activeProcessDefinitionId = null, ?int $meetingFilterGroupId = null, ?string $meetingFilterDatetime = null): array { + // 1. Base query for people + $sql_people = "SELECT p.*, bg.name as bni_group_name FROM people p LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id"; + $params = []; + $where_clauses = []; + + // 2. Add filter conditions + if ($searchTerm) { + $where_clauses[] = "(p.first_name LIKE :search OR p.last_name LIKE :search OR p.company_name LIKE :search OR p.email LIKE :search)"; + $params[':search'] = '%' . $searchTerm . '%'; + } + + if ($groupId) { + $where_clauses[] = "p.bni_group_id = :group_id"; + $params[':group_id'] = $groupId; + } + + if ($activeProcessDefinitionId) { + $terminal_statuses = ['positive', 'negative', 'completed', 'error', 'inactive']; + $in_clause = implode(',', array_map([$this->pdo, 'quote'], $terminal_statuses)); + + $sql_people .= " INNER JOIN process_instances pi ON p.id = pi.person_id"; + $where_clauses[] = "pi.process_definition_id = :active_process_id AND (pi.current_status IS NOT NULL AND pi.current_status NOT IN ($in_clause))"; + $params[':active_process_id'] = $activeProcessDefinitionId; + } + + if ($meetingFilterGroupId && $meetingFilterDatetime) { + $meetingId = $this->getOrCreateMeeting($meetingFilterGroupId, $meetingFilterDatetime); + $sql_people .= " INNER JOIN meeting_attendance ma ON p.id = ma.person_id"; + $where_clauses[] = "ma.meeting_id = :meeting_id"; + $where_clauses[] = "ma.attendance_status IN ('present', 'absent', 'substitute')"; + $params[':meeting_id'] = $meetingId; + } + + if (!empty($where_clauses)) { + $sql_people .= " WHERE " . implode(" AND ", $where_clauses); + } + + $sql_people .= " ORDER BY p.last_name, p.first_name"; + + // 3. Execute query to get filtered people + $stmt_people = $this->pdo->prepare($sql_people); + $stmt_people->execute($params); + $people = $stmt_people->fetchAll(PDO::FETCH_ASSOC); + + // 4. Fetch all process definitions with their JSON + $stmt_defs = $this->pdo->prepare("SELECT id, name, definition_json, is_active FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name"); + $stmt_defs->execute(); + $process_definitions_raw = $stmt_defs->fetchAll(PDO::FETCH_ASSOC); + + $definitions = []; + $definition_map = []; + foreach ($process_definitions_raw as $def) { + $definitions[$def['id']] = [ + 'id' => $def['id'], + 'name' => $def['name'], + 'is_active' => $def['is_active'] + ]; + $definition_map[$def['id']] = !empty($def['definition_json']) ? json_decode($def['definition_json'], true) : null; + } + + // 5. Fetch instances ONLY for the filtered people + $instances = []; + $person_ids = array_column($people, 'id'); + if (!empty($person_ids)) { + $placeholders = implode(',', array_fill(0, count($person_ids), '?')); + $stmt_instances = $this->pdo->prepare("SELECT * FROM process_instances WHERE person_id IN ($placeholders)"); + $stmt_instances->execute($person_ids); + $instances_data = $stmt_instances->fetchAll(PDO::FETCH_ASSOC); + + foreach ($instances_data as $instance) { + $enriched_instance = $instance; + $def_id = $instance['process_definition_id']; + $node_id = $instance['current_node_id']; + + $definition = $definition_map[$def_id] ?? null; + + if ($definition && isset($definition['type']) && $definition['type'] === 'checklist') { + $tasks = $definition['tasks'] ?? []; + $instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : []; + $totalTasks = count($tasks); + $completedTasks = 0; + if(is_array($instanceData)) { + foreach ($tasks as $task) { + if (!empty($instanceData[$task['code']])) { + $completedTasks++; + } + } + } + + if ($totalTasks > 0 && $completedTasks === $totalTasks) { + $status = 'completed'; + } elseif ($completedTasks > 0) { + $status = 'in_progress'; + } else { + $status = 'inactive'; + } + $enriched_instance['computed_status'] = $status; + $enriched_instance['computed_reason'] = "$completedTasks/$totalTasks completed"; + $enriched_instance['computed_next_step'] = ''; + } else if ($definition && isset($definition['nodes'][$node_id])) { + $node_info = $definition['nodes'][$node_id]; + $enriched_instance['computed_status'] = $node_info['ui_hints']['status'] ?? $instance['current_status']; + $enriched_instance['computed_reason'] = $node_info['ui_hints']['reason'] ?? $instance['current_reason']; + $enriched_instance['computed_next_step'] = $node_info['ui_hints']['next_step'] ?? $instance['suggested_next_step']; + } else { + $enriched_instance['computed_status'] = $instance['current_status']; + $enriched_instance['computed_reason'] = $instance['current_reason']; + $enriched_instance['computed_next_step'] = $instance['suggested_next_step']; + } + + $instances[$instance['person_id']][$def_id] = $enriched_instance; + } + } + + // 6. Fetch ancillary data + $stmt_functions = $this->pdo->query("SELECT * FROM functions ORDER BY display_order"); + $all_functions = $stmt_functions->fetchAll(PDO::FETCH_ASSOC); + + $stmt_person_functions = $this->pdo->query("SELECT user_id, function_id FROM user_functions"); + $person_functions_map = []; + while ($row = $stmt_person_functions->fetch(PDO::FETCH_ASSOC)) { + $person_functions_map[$row['user_id']][] = $row['function_id']; + } + + $stmt_bni_groups = $this->pdo->query("SELECT * FROM bni_groups ORDER BY name"); + $bni_groups = $stmt_bni_groups->fetchAll(PDO::FETCH_ASSOC); + + // 7. Fetch Spotkania columns (upcoming meetings) + $today = date('Y-m-d H:i:s'); + $stmt_meetings = $this->pdo->prepare(" + WITH RankedMeetings AS ( + SELECT + bg.id as group_id, + bg.name as group_name, + ce.start_datetime, + ROW_NUMBER() OVER(PARTITION BY bg.id ORDER BY ce.start_datetime) as rn + FROM bni_groups bg + JOIN calendar_event_groups ceg ON bg.id = ceg.bni_group_id + JOIN calendar_events ce ON ceg.calendar_event_id = ce.id + WHERE ce.start_datetime >= :today + ) + SELECT group_id, group_name, start_datetime + FROM RankedMeetings + WHERE rn <= 3 + ORDER BY group_id, start_datetime; + "); + $stmt_meetings->execute(['today' => $today]); + $upcoming_meetings_flat = $stmt_meetings->fetchAll(PDO::FETCH_ASSOC); + + $spotkania_cols = []; + foreach ($upcoming_meetings_flat as $meeting) { + $spotkania_cols[$meeting['group_id']]['group_id'] = $meeting['group_id']; + $spotkania_cols[$meeting['group_id']]['group_name'] = $meeting['group_name']; + $spotkania_cols[$meeting['group_id']]['meetings'][] = $meeting['start_datetime']; + } + + + return [ + 'people' => $people, + 'definitions' => array_values($definitions), + 'instances' => $instances, + 'all_functions' => $all_functions, + 'person_functions_map' => $person_functions_map, + 'bni_groups' => $bni_groups, + 'spotkania_cols' => $spotkania_cols, // Add this to the return array + ]; + } + + public function startProcess(string $processCode, int $personId, int $userId): int { + $this->pdo->beginTransaction(); + try { + // 1. Find active process definition by code. + $stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE code = ? AND is_active = 1"); + $stmt_def->execute([$processCode]); + $definition = $stmt_def->fetch(PDO::FETCH_ASSOC); + + if (!$definition) { + // If no process definition is found, check if there is a definition for a checklist + $stmt_def = $this->pdo->prepare("SELECT * FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$processCode]); + $definition = $stmt_def->fetch(PDO::FETCH_ASSOC); + + if (!$definition) { + throw new WorkflowNotFoundException("Process definition with code or id '$processCode' not found."); + } + + $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; + if (empty($definition_json) || $definition_json['type'] !== 'checklist') { + throw new WorkflowNotAllowedException("Process definition with code '$processCode' not found or not a checklist."); + } + + // For checklists, there's no start_node_id, so we can proceed with instance creation + $startNodeId = null; + + } else { + $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; + if (empty($definition_json) || !isset($definition_json['start_node_id'])) { + throw new WorkflowRuleFailedException("Process definition is missing start_node_id."); + } + $startNodeId = $definition_json['start_node_id']; + } + + // 2. Create a new process instance. + $stmt_insert = $this->pdo->prepare( + "INSERT INTO process_instances (person_id, process_definition_id, current_node_id, current_status, last_activity_at) VALUES (?, ?, ?, 'in_progress', NOW())" + ); + $stmt_insert->execute([$personId, $definition['id'], $startNodeId]); + $instanceId = $this->pdo->lastInsertId(); + + // 3. Create a system event for process start. + $this->addEvent($instanceId, 'system', 'Process started.', $startNodeId, [], $userId); + + $this->pdo->commit(); + return (int)$instanceId; + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + public function getProcessState(int $instanceId): ?array { + $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE id = ?"); + $stmt->execute([$instanceId]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$instance) { + return null; + } + + $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$instance['process_definition_id']]); + $definition_json = $stmt_def->fetchColumn(); + $definition = !empty($definition_json) ? json_decode($definition_json, true) : []; + + $currentNodeId = $instance['current_node_id']; + $nodeInfo = $definition['nodes'][$currentNodeId] ?? null; + + return [ + 'instance' => $instance, + 'definition' => $definition, + 'currentNode' => $nodeInfo, + ]; + } + + public function applyTransition(int $instanceId, string $transitionId, array $inputPayload, int $userId): array { + $this->pdo->beginTransaction(); + try { + $state = $this->getProcessState($instanceId); + if (!$state) { + throw new WorkflowNotFoundException("Process instance not found."); + } + + $instance = $state['instance']; + $definition = $state['definition']; + $currentNodeId = $instance['current_node_id']; + + $transition = null; + foreach ($definition['transitions'] as $t) { + if ($t['from'] === $currentNodeId && $t['id'] === $transitionId) { + // For user-triggered transitions, we ignore the condition here. + // The UI should prevent showing buttons for transitions whose data-based conditions aren't met. + $transition = $t; + break; + } + } + + if (!$transition) { + throw new WorkflowNotAllowedException("Transition not found or not allowed from the current node."); + } + + // Apply the initial, user-triggered transition + $newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $transition, $inputPayload, $userId); + $instance['current_node_id'] = $newNodeId; // Update instance state for the loop + + // Loop for automatic transitions (router nodes) + for ($i = 0; $i < 10; $i++) { // Max 10 auto-steps to prevent infinite loops + $autoTransition = $this->findAutomaticTransition($instance, $definition); + if ($autoTransition) { + // Automatic transitions have no user payload + $newNodeId = $this->applySingleTransition($instanceId, $instance, $definition, $autoTransition, [], $userId); + $instance['current_node_id'] = $newNodeId; // Update for next iteration + } else { + break; // No more automatic transitions found + } + } + + $this->pdo->commit(); + + // Refetch the final state of the instance to return the correct status + $finalState = $this->getProcessState($instanceId)['instance']; + $finalNodeInfo = $definition['nodes'][$finalState['current_node_id']] ?? null; + + return [ + 'instanceId' => $instanceId, + 'currentNodeId' => $finalState['current_node_id'], + 'currentStatus' => $finalNodeInfo['ui_hints']['status'] ?? $finalState['current_status'], + 'currentReason' => $finalNodeInfo['ui_hints']['reason'] ?? $finalState['current_reason'], + 'suggestedNextStep' => $finalNodeInfo['ui_hints']['next_step'] ?? $finalState['suggested_next_step'], + 'lastActivityAt' => $finalState['last_activity_at'], + ]; + + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + private function applySingleTransition(int $instanceId, array &$instance, array $definition, array $transition, array $inputPayload, int $userId): string + { + $newNodeId = $transition['to']; + $newNodeInfo = $definition['nodes'][$newNodeId] ?? null; + + $newStatus = $newNodeInfo['ui_hints']['status'] ?? 'in_progress'; + $newReason = $newNodeInfo['ui_hints']['reason'] ?? ''; + $newNextStep = $newNodeInfo['ui_hints']['next_step'] ?? ''; + + $stmt_update = $this->pdo->prepare( + "UPDATE process_instances SET current_node_id = ?, current_status = ?, current_reason = ?, suggested_next_step = ?, last_activity_at = NOW() WHERE id = ?" + ); + $stmt_update->execute([ + $newNodeId, + $newStatus, + $newReason, + $newNextStep, + $instanceId + ]); + + $message = $inputPayload['message'] ?? $transition['name']; + $this->addEvent($instanceId, 'transition_applied', $message, $newNodeId, $inputPayload, $userId); + + if (isset($transition['actions'])) { + foreach ($transition['actions'] as $action) { + if ($action['type'] === 'start_process') { + $this->executeStartProcessAction($instance['person_id'], $action, $userId); + } elseif ($action['type'] === 'set_data') { + // Pass the instance by reference to be updated with new data + $this->executeSetDataAction($instanceId, $instance, $action, $inputPayload); + } + } + } + + return $newNodeId; + } + + private function findAutomaticTransition(array $instance, array $definition): ?array + { + $currentNodeId = $instance['current_node_id']; + foreach ($definition['transitions'] as $transition) { + if ($transition['from'] === $currentNodeId) { + // An automatic transition MUST have a condition. + if (isset($transition['condition'])) { + if ($this->checkTransitionCondition($transition, $instance)) { + return $transition; + } + } + } + } + return null; + } + + public function addNote(int $instanceId, string $message, int $userId): bool { + $state = $this->getProcessState($instanceId); + if (!$state) { + throw new WorkflowNotFoundException("Process instance #$instanceId not found."); + } + $currentNodeId = $state['instance']['current_node_id']; + $payload = ['message' => $message]; + $this->addEvent($instanceId, 'note', $message, $currentNodeId, $payload, $userId); + return true; + } + + public function applyManualStatus(int $instanceId, string $status, string $reasonOrNote, int $userId): bool { + $this->pdo->beginTransaction(); + try { + $state = $this->getProcessState($instanceId); + if (!$state) { + throw new WorkflowNotFoundException("Process instance #$instanceId not found."); + } + + $stmt_update = $this->pdo->prepare( + "UPDATE process_instances SET current_status = ?, current_reason = ?, last_activity_at = NOW() WHERE id = ?" + ); + $stmt_update->execute([$status, $reasonOrNote, $instanceId]); + + $currentNodeId = $state['instance']['current_node_id']; + $message = "Status manually set to '$status'."; + if (!empty($reasonOrNote)) { + $message .= " Reason: $reasonOrNote"; + } + + $this->addEvent($instanceId, 'manual_status_change', $message, $currentNodeId, ['status' => $status, 'reason' => $reasonOrNote], $userId); + + $this->pdo->commit(); + return true; + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + public function bulkAddNotes(array $notes): array + { + $results = []; + foreach ($notes as $note) { + try { + $this->addNote((int)$note['instance_id'], $note['message'], (int)$note['user_id']); + $results[] = ['instance_id' => $note['instance_id'], 'success' => true]; + } catch (Exception $e) { + $results[] = ['instance_id' => $note['instance_id'], 'success' => false, 'error' => $e->getMessage()]; + } + } + return $results; + } + + public function bulkManualStatus(array $statuses): array + { + $results = []; + foreach ($statuses as $status) { + try { + $this->applyManualStatus((int)$status['instance_id'], $status['status'], $status['reason'] ?? '', (int)$status['user_id']); + $results[] = ['instance_id' => $status['instance_id'], 'success' => true]; + } catch (Exception $e) { + $results[] = ['instance_id' => $status['instance_id'], 'success' => false, 'error' => $e->getMessage()]; + } + } + return $results; + } + + public function updateChecklistStatus(int $instanceId, string $taskCode, bool $isChecked, int $userId): array + { + $this->pdo->beginTransaction(); + try { + // Get current data_json + $stmt = $this->pdo->prepare("SELECT data_json, process_definition_id FROM process_instances WHERE id = ?"); + $stmt->execute([$instanceId]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$instance) { + throw new WorkflowNotFoundException("Process instance #$instanceId not found."); + } + + $data = $instance['data_json'] ? json_decode($instance['data_json'], true) : []; + + // Update the specific task status + $data[$taskCode] = $isChecked; + $newDataJson = json_encode($data); + + // Save new data_json and update timestamp + $stmt = $this->pdo->prepare("UPDATE process_instances SET data_json = ?, last_activity_at = CURRENT_TIMESTAMP WHERE id = ?"); + $stmt->execute([$newDataJson, $instanceId]); + + // Add an event for the checklist update + $message = "Checklist task '$taskCode' marked as " . ($isChecked ? 'complete' : 'incomplete') . "."; + $this->addEvent($instanceId, 'checklist_update', $message, null, ['task' => $taskCode, 'checked' => $isChecked], $userId); + + // Calculate progress + $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$instance['process_definition_id']]); + $definitionJson = $stmt_def->fetchColumn(); + $definition = json_decode($definitionJson, true); + $totalTasks = count($definition['tasks'] ?? []); + $completedTasks = count(array_filter($data)); + + $this->pdo->commit(); + + return [ + 'success' => true, + 'progress' => [ + 'completed' => $completedTasks, + 'total' => $totalTasks + ], + 'lastActivityAt' => date('d/m/y') + ]; + + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + + private function addEvent(int $instanceId, string $eventType, string $message, ?string $nodeId, array $payload, int $userId): void { + $stmt = $this->pdo->prepare( + "INSERT INTO process_events (process_instance_id, event_type, message, node_id, payload_json, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())" + ); + $stmt->execute([$instanceId, $eventType, $message, $nodeId, json_encode($payload), $userId]); + } + + public function getOrCreateInstanceByDefId(int $personId, int $processDefinitionId, int $userId, array $context = []): ?array { + if (!is_int($processDefinitionId) || $processDefinitionId <= 0) { + throw new InvalidArgumentException("processDefinitionId must be a positive integer."); + } + if (!is_int($personId) || $personId <= 0) { + throw new InvalidArgumentException("personId must be a positive integer."); + } + + $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?"); + $stmt->execute([$personId, $processDefinitionId]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$instance) { + $stmt_def = $this->pdo->prepare("SELECT definition_json, code, is_active FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$processDefinitionId]); + $definition = $stmt_def->fetch(PDO::FETCH_ASSOC); + + if (!$definition) { + throw new WorkflowNotFoundException("Process definition #$processDefinitionId not found."); + } + + if (empty($definition['is_active'])) { + throw new WorkflowNotAllowedException("Process is not active and cannot be started."); + } + + $eligibility = $this->checkEligibility($personId, $processDefinitionId, $context); + if (!$eligibility['is_eligible']) { + throw new WorkflowEligibilityException("Person is not eligible to start this process.", $eligibility['reasons']); + } + + $definition_json = !empty($definition['definition_json']) ? json_decode($definition['definition_json'], true) : []; + + $processCode = ($definition_json && isset($definition_json['type']) && $definition_json['type'] === 'checklist') + ? (string) $processDefinitionId + : $definition['code']; + + if ($processCode) { + $instanceId = $this->startProcess($processCode, $personId, $userId); + if ($instanceId) { + $stmt->execute([$personId, $processDefinitionId]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + } + } + } + + return $instance ?: null; + } + + public function getInstanceByDefId(int $personId, int $processDefinitionId): ?array { + $stmt = $this->pdo->prepare("SELECT * FROM process_instances WHERE `person_id` = ? AND `process_definition_id` = ?"); + $stmt->execute([$personId, $processDefinitionId]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + return $instance ?: null; + } + + public function getEvents(int $instanceId): array { + $stmt_events = $this->pdo->prepare("SELECT pe.*, p.email as user_email, p.first_name, p.last_name FROM process_events pe JOIN people p ON pe.created_by = p.id WHERE pe.process_instance_id = ? ORDER BY pe.created_at DESC"); + $stmt_events->execute([$instanceId]); + return $stmt_events->fetchAll(PDO::FETCH_ASSOC); + } + + public function getAvailableTransitions(int $instanceId): array { + $state = $this->getProcessState($instanceId); + if (!$state) { + return []; + } + + $currentNodeId = $state['instance']['current_node_id']; + $definition = $state['definition']; + + $transitions = []; + if (isset($definition['transitions'])) { + foreach ($definition['transitions'] as $t) { + if ($t['from'] === $currentNodeId) { + $transitions[] = $t; + } + } + } + + return $transitions; + } + + public function getProcessDefinitionNodes(int $processDefinitionId): array { + $stmt = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); + $stmt->execute([$processDefinitionId]); + $json = $stmt->fetchColumn(); + + if (!$json) { + return []; + } + + $definition = !empty($json) ? json_decode($json, true) : []; + return $definition['nodes'] ?? []; + } + + public function checkEligibility(int $personId, int $processDefinitionId, array $context = []): array { + $stmt_def = $this->pdo->prepare("SELECT definition_json FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$processDefinitionId]); + $definition_json = $stmt_def->fetchColumn(); + + $definition = !empty($definition_json) ? json_decode($definition_json, true) : []; + + $reasons = []; + if (empty($definition) || empty($definition['eligibility_rules'])) { + return ['is_eligible' => true, 'reasons' => []]; + } + + foreach ($definition['eligibility_rules'] as $rule) { + try { + $params = $rule['params'] ?? $rule; + switch ($rule['type']) { + case 'checkProcessCompletedRule': + case 'process_completed': // Backward compatibility + $this->checkProcessCompletedRule($personId, $params); + break; + case 'checkProcessDataRule': + $this->checkProcessDataRule($personId, $params); + break; + case 'deny_manual_start': + $this->checkDenyManualStartRule($context); + break; + case 'person_property_equals': + $this->checkPersonPropertyEqualsRule($personId, $params); + break; + // Add other rule types here + } + } catch (WorkflowNotAllowedException $e) { + $reasons[] = $e->getMessage(); + } + } + + return ['is_eligible' => empty($reasons), 'reasons' => $reasons]; + } + + private function checkDenyManualStartRule(array $context): void { + if (!isset($context['source']) || $context['source'] !== 'chain') { + throw new WorkflowNotAllowedException("This process can only be started automatically by another process."); + } + } + + private function checkPersonPropertyEqualsRule(int $personId, array $params): void { + $stmt = $this->pdo->prepare("SELECT * FROM people WHERE id = ?"); + $stmt->execute([$personId]); + $person = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$person) { + throw new WorkflowNotAllowedException("Person not found."); + } + + $property = $params['property']; + $expectedValue = $params['value']; + + if (!isset($person[$property])) { + throw new WorkflowNotAllowedException("Property '{$property}' not found on person."); + } + + if ($person[$property] !== $expectedValue) { + throw new WorkflowNotAllowedException("Person's property '{$property}' is not '{$expectedValue}'."); + } + } + + private function checkProcessCompletedRule(int $personId, array $params): void { + $stmt = $this->pdo->prepare("\n SELECT pi.id\n FROM process_instances pi\n JOIN process_definitions pd ON pi.process_definition_id = pd.id\n WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ?\n ORDER BY pi.last_activity_at DESC\n LIMIT 1\n "); + $stmt->execute([$personId, $params['process_code'], $params['expected_status']]); + $instance = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$instance) { + throw new WorkflowNotAllowedException("Prerequisite process '{$params['process_code']}' not completed with status '{$params['expected_status']}'."); + } + } + + private function checkProcessDataRule(int $personId, array $params): void { + $stmt = $this->pdo->prepare(" + SELECT pi.data_json + FROM process_instances pi + JOIN process_definitions pd ON pi.process_definition_id = pd.id + WHERE pi.person_id = ? AND pd.code = ? AND pi.current_status = ? + ORDER BY pi.last_activity_at DESC + LIMIT 1 + "); + $stmt->execute([$personId, $params['process_code'], $params['expected_status']]); + $data_json = $stmt->fetchColumn(); + + if (!$data_json) { + throw new WorkflowNotAllowedException("Not eligible to start this process. Prerequisite process '{$params['process_code']}' not found with status '{$params['expected_status']}'."); + } + + $data = json_decode($data_json, true); + if (!is_array($data)) { + $data = []; + } + + foreach ($params['expected_data'] as $key => $expected_value) { + if (!isset($data[$key]) || $data[$key] !== $expected_value) { + throw new WorkflowNotAllowedException("Not eligible. Condition not met: '$key' is not '$expected_value'."); + } + } + } + + private function checkTransitionCondition(array $transition, array $instanceData): bool + { + if (!isset($transition['condition'])) { + // A transition without a condition is not automatic, but it is valid to pass through. + // The calling context (findAutomaticTransition) will decide if this is an error. + return true; + } + + $condition = $transition['condition']; + $data = isset($instanceData['data_json']) ? json_decode($instanceData['data_json'], true) : []; + + $field = $condition['field'] ?? null; + $expectedValue = $condition['value'] ?? null; + + if ($field === null || $expectedValue === null) { + // Malformed condition + return false; + } + + return isset($data[$field]) && $data[$field] === $expectedValue; + } + + private function executeStartProcessAction(int $personId, array $action, int $userId): void { + $stmt = $this->pdo->prepare("SELECT id FROM process_definitions WHERE code = ?"); + $stmt->execute([$action['process_code']]); + $processDefinitionId = $stmt->fetchColumn(); + + if ($processDefinitionId) { + $this->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId, ['source' => 'chain']); + } + } + + private function executeSetDataAction(int $instanceId, array &$instance, array $action, array $payload): void { + $dataJson = $instance['data_json']; + $data = $dataJson ? json_decode($dataJson, true) : []; + + if (isset($action['params']['keys']) && is_array($action['params']['keys'])) { + foreach ($action['params']['keys'] as $key) { + if (array_key_exists($key, $payload)) { + $data[$key] = $payload[$key]; + } + } + } + + $newDataJson = json_encode($data); + + // Update the database + $stmt_update = $this->pdo->prepare("UPDATE process_instances SET data_json = ? WHERE id = ?"); + $stmt_update->execute([$newDataJson, $instanceId]); + + // Also update the in-memory instance for the next step in the chain + $instance['data_json'] = $newDataJson; + } + + public function deleteInstance(int $instanceId): void { + $this->pdo->beginTransaction(); + try { + // Delete events + $stmt_events = $this->pdo->prepare("DELETE FROM process_events WHERE process_instance_id = ?"); + $stmt_events->execute([$instanceId]); + + // Delete instance + $stmt_instance = $this->pdo->prepare("DELETE FROM process_instances WHERE id = ?"); + $stmt_instance->execute([$instanceId]); + + $this->pdo->commit(); + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; + } + } + + public function getOrCreateMeeting(int $bniGroupId, string $meetingDatetime): int { + $meetingKey = $bniGroupId . '_' . $meetingDatetime; + $stmt = $this->pdo->prepare("SELECT id FROM meetings WHERE meeting_key = ?"); + $stmt->execute([$meetingKey]); + $meetingId = $stmt->fetchColumn(); + + if (!$meetingId) { + $stmt = $this->pdo->prepare("INSERT INTO meetings (bni_group_id, meeting_date, meeting_datetime, meeting_key) VALUES (?, DATE(?), ?, ?)"); + $stmt->execute([$bniGroupId, $meetingDatetime, $meetingDatetime, $meetingKey]); + $meetingId = $this->pdo->lastInsertId(); + } + + return (int)$meetingId; + } + + public function getMeetingAttendance(int $meetingId): array { + $stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ?"); + $stmt->execute([$meetingId]); + $attendance_raw = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $attendance = []; + foreach ($attendance_raw as $att) { + $attendance[$att['person_id']] = $att; + } + + return $attendance; + } + + public function updateMeetingAttendance(int $meetingId, int $personId, int $bniGroupId, string $status, int $userId, ?string $guestSurvey = null): void { + $sql = "INSERT INTO meeting_attendance (meeting_id, person_id, bni_group_id, attendance_status, guest_survey, updated_by) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE attendance_status = VALUES(attendance_status), guest_survey = VALUES(guest_survey), updated_by = VALUES(updated_by), bni_group_id = VALUES(bni_group_id)"; + + // Log query and params + $params = [$meetingId, $personId, $bniGroupId, $status, $guestSurvey, $userId]; + error_log("SQL: $sql"); + error_log("Params: " . json_encode($params)); + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + } + + public function getMeetingAttendanceByGroupAndDate(int $groupId, string $meetingDatetime): array { + $meetingId = $this->getOrCreateMeeting($groupId, $meetingDatetime); + return $this->getMeetingAttendance($meetingId); + } + + public function isMemberOfGroup(int $personId, int $bniGroupId): bool { + $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM people WHERE id = ? AND bni_group_id = ?"); + $stmt->execute([$personId, $bniGroupId]); + return (int)$stmt->fetchColumn() > 0; + } + + public function getMeetingDetails(int $personId, int $bniGroupId, string $meetingDatetime): array { + $meetingId = $this->getOrCreateMeeting($bniGroupId, $meetingDatetime); + + $stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ? AND person_id = ?"); + $stmt->execute([$meetingId, $personId]); + $attendance = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($attendance) { + return $attendance; + } + + // If no record, return default state + $isMember = $this->isMemberOfGroup($personId, $bniGroupId); + return [ + 'meeting_id' => $meetingId, + 'person_id' => $personId, + 'attendance_status' => $isMember ? 'present' : 'none', + 'guest_survey' => null, + ]; + } + + public function getPeopleDetails(array $personIds): array { + if (empty($personIds)) { + return []; + } + + $placeholders = implode(',', array_fill(0, count($personIds), '?')); + + $sql = "SELECT p.id, p.first_name, p.last_name, p.company_name, p.industry, bg.name as bni_group_name + FROM people p + LEFT JOIN bni_groups bg ON p.bni_group_id = bg.id + WHERE p.id IN ($placeholders)"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($personIds); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } +} \ No newline at end of file diff --git a/_add_bni_group.php b/_add_bni_group.php new file mode 100644 index 0000000..015fcaf --- /dev/null +++ b/_add_bni_group.php @@ -0,0 +1,32 @@ +prepare("INSERT INTO bni_groups (name, city, active, display_order) VALUES (:name, :city, :active, :display_order)"); + $stmt->bindParam(':name', $name); + $stmt->bindParam(':city', $city); + $stmt->bindParam(':active', $active, PDO::PARAM_INT); + $stmt->bindParam(':display_order', $display_order, PDO::PARAM_INT); + $stmt->execute(); + + $_SESSION['success_message'] = 'BNI Group added successfully!'; + } catch (PDOException $e) { + $_SESSION['error_message'] = 'Error adding BNI group: ' . $e->getMessage(); + } +} + +header('Location: bni_groups.php'); +exit; diff --git a/_add_calendar_event.php b/_add_calendar_event.php new file mode 100644 index 0000000..34d9b4b --- /dev/null +++ b/_add_calendar_event.php @@ -0,0 +1,104 @@ +beginTransaction(); + + // Insert the main event + $stmt = $pdo->prepare("INSERT INTO calendar_events (title, description, start_datetime, end_datetime, event_type_id, recurrence, recurrence_end_date) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$title, $description, $start_datetime, $end_datetime, $event_type_id, $recurrence, $recurrence_end_date]); + $parent_event_id = $pdo->lastInsertId(); + + // Handle group associations + if (isset($_POST['group_ids']) && is_array($_POST['group_ids'])) { + $stmt_groups = $pdo->prepare("INSERT INTO calendar_event_groups (calendar_event_id, bni_group_id) VALUES (?, ?)"); + foreach ($_POST['group_ids'] as $group_id) { + $stmt_groups->execute([$parent_event_id, $group_id]); + } + } else { + // The field is required, so this is a failure case. + throw new Exception("Group IDs are required."); + } + + if ($recurrence && !empty($recurrence_end_date)) { + $start_date = new DateTime($start_datetime); + $end_date = new DateTime($end_datetime); + $recurrence_end = new DateTime($recurrence_end_date); + $interval_spec = ''; + + switch ($recurrence) { + case 'daily': + $interval_spec = 'P1D'; + break; + case 'weekly': + $interval_spec = 'P1W'; + break; + case 'monthly': + $interval_spec = 'P1M'; + break; + } + + if ($interval_spec) { + $interval = new DateInterval($interval_spec); + $period_start = clone $start_date; + $period_start->add($interval); + + $period = new DatePeriod($period_start, $interval, $recurrence_end); + + $stmt_recur = $pdo->prepare("INSERT INTO calendar_events (title, description, start_datetime, end_datetime, event_type_id, parent_event_id) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt_recur_groups = $pdo->prepare("INSERT INTO calendar_event_groups (calendar_event_id, bni_group_id) VALUES (?, ?)"); + + foreach ($period as $date) { + $new_start_datetime = $date->format('Y-m-d H:i:s'); + $end_date_clone = clone $date; + $new_end_datetime = $end_date_clone->add($start_date->diff($end_date))->format('Y-m-d H:i:s'); + $stmt_recur->execute([$title, $description, $new_start_datetime, $new_end_datetime, $event_type_id, $parent_event_id]); + $new_event_id = $pdo->lastInsertId(); + foreach ($_POST['group_ids'] as $group_id) { + $stmt_recur_groups->execute([$new_event_id, $group_id]); + } + } + } + } + + $pdo->commit(); + header("Location: calendar.php"); + exit(); + + } catch (Exception $e) { + $pdo->rollBack(); + error_log($e->getMessage()); + header("Location: calendar.php?error=db_error"); + exit(); + } +} diff --git a/_add_event_type.php b/_add_event_type.php new file mode 100644 index 0000000..d7db0bc --- /dev/null +++ b/_add_event_type.php @@ -0,0 +1,18 @@ +prepare("INSERT INTO event_types (name, color, display_order) VALUES (?, ?, ?)"); + $stmt->execute([$name, $color, $display_order]); + + session_start(); + $_SESSION['success_message'] = 'Event type added successfully.'; + header('Location: event_types.php'); + exit; +} +?> \ No newline at end of file diff --git a/_add_function.php b/_add_function.php new file mode 100644 index 0000000..35f5265 --- /dev/null +++ b/_add_function.php @@ -0,0 +1,35 @@ +query("SELECT MAX(display_order) FROM functions"); + $max_order = $stmt->fetchColumn(); + $new_order = $max_order + 1; + + $stmt = $pdo->prepare("INSERT INTO functions (name, bni_group_id, display_order) VALUES (:name, :bni_group_id, :display_order)"); + $stmt->execute(['name' => $name, 'bni_group_id' => $bni_group_id, 'display_order' => $new_order]); + $_SESSION['success_message'] = "Function added successfully."; + } catch (PDOException $e) { + // Handle potential errors, e.g., duplicate name + $_SESSION['error_message'] = "Error adding function: " . $e->getMessage(); + } + } +} else { + $_SESSION['error_message'] = "Name and group are required."; +} + +header('Location: functions.php'); +exit(); diff --git a/_add_process_event.php b/_add_process_event.php new file mode 100644 index 0000000..5b643dc --- /dev/null +++ b/_add_process_event.php @@ -0,0 +1,28 @@ +addNote((int)$instanceId, $message, (int)$userId); + +if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode(['message' => 'Note added successfully.']); +} else { + header('Location: ' . $_SERVER['HTTP_REFERER']); +} +exit; diff --git a/_apply_transition.php b/_apply_transition.php new file mode 100644 index 0000000..d928959 --- /dev/null +++ b/_apply_transition.php @@ -0,0 +1,41 @@ +addNote($instanceId, $message, $userId); + $response = ['success' => true, 'message' => 'Notatka została dodana.']; +} else { + $result = $engine->applyTransition($instanceId, $transitionId, $payload, $userId); + $response = ['success' => true, 'message' => 'Akcja została wykonana pomyślnie.', 'data' => $result]; +} + +echo json_encode($response); \ No newline at end of file diff --git a/_bulk_add_event.php b/_bulk_add_event.php new file mode 100644 index 0000000..1008fac --- /dev/null +++ b/_bulk_add_event.php @@ -0,0 +1,50 @@ +prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)"); +$params = array_merge([$process_id], $person_ids); +$stmt->execute($params); +$instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); + +if (empty($instance_ids)) { + $_SESSION['flash_message'] = "No instances found for the selected people and process."; + header('Location: ' . $_SERVER['HTTP_REFERER']); + exit; +} + +$notes = []; +foreach ($instance_ids as $instance_id) { + $notes[] = [ + 'instance_id' => $instance_id, + 'message' => $message, + 'user_id' => $userId + ]; +} + +$workflowEngine = new WorkflowEngine(); +$results = $workflowEngine->bulkAddNotes($notes); + +$_SESSION['flash_message'] = "Bulk note addition completed."; +$_SESSION['bulk_results'] = $results; + +header('Location: ' . $_SERVER['HTTP_REFERER']); +exit; diff --git a/_bulk_init_instances.php b/_bulk_init_instances.php new file mode 100644 index 0000000..f3f8274 --- /dev/null +++ b/_bulk_init_instances.php @@ -0,0 +1,56 @@ + [], + 'failed' => [], +]; + +foreach ($personIds as $personId) { + try { + $instance = $engine->getOrCreateInstanceByDefId($personId, $process_id, $userId); + if ($instance) { + $results['success'][] = $personId; + } else { + $results['failed'][] = $personId; + } + } catch (Exception $e) { + $results['failed'][] = $personId; + // Optionally log the error + error_log("Failed to initialize process for person $personId: " . $e->getMessage()); + } +} + +$message = "Bulk initialization completed. Success: " . count($results['success']) . ", Failed: " . count($results['failed']); + +if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode([ + 'message' => $message, + 'results' => $results + ]); +} else { + $_SESSION['success_message'] = $message; + header('Location: index.php'); +} +exit(); \ No newline at end of file diff --git a/_bulk_print_attendance_list.php b/_bulk_print_attendance_list.php new file mode 100644 index 0000000..b9e1966 --- /dev/null +++ b/_bulk_print_attendance_list.php @@ -0,0 +1,112 @@ + ['message' => $error_message], 'correlation_id' => $correlation_id]); + exit; +} + +// Input validation +$person_ids = json_decode($_POST['person_ids'] ?? '[]', true); +error_log($correlation_id . ': Received ' . count($person_ids) . ' person_ids.'); + +if (empty($person_ids)) { + if (ob_get_length()) ob_end_clean(); + http_response_code(400); + header('Content-Type: application/json'); + $error_message = 'Nie wybrano żadnych osób.'; + error_log($correlation_id . ': ' . $error_message); + echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]); + exit; +} + +$workflowEngine = new WorkflowEngine(); +$peopleDetails = $workflowEngine->getPeopleDetails($person_ids); +error_log($correlation_id . ': Fetched ' . count($peopleDetails) . ' rows from database.'); + +class PDF extends tFPDF +{ + function __construct($orientation = 'P', $unit = 'mm', $size = 'A4') + { + parent::__construct($orientation, $unit, $size); + $this->AddFont('DejaVu', '', 'DejaVuSans.ttf', true); + $this->AddFont('DejaVu', 'B', 'DejaVuSans-Bold.ttf', true); + $this->SetFont('DejaVu', '', 14); + } + + function generateAttendanceList($people) + { + $this->AddPage(); + + // Title + $this->SetFont('DejaVu', 'B', 20); + $this->Cell(0, 10, 'Lista obecności', 0, 1, 'C'); + $this->Ln(2); + + // Subtitle - Date + $this->SetFont('DejaVu', '', 10); + $this->Cell(0, 10, date('Y-m-d H:i'), 0, 1, 'C'); + $this->Ln(10); + + // Table Header + $this->SetFont('DejaVu', 'B', 12); + $this->SetFillColor(240, 240, 240); + $this->Cell($this->GetPageWidth() * 0.7, 10, 'Imię i nazwisko', 1, 0, 'L', true); + $this->Cell($this->GetPageWidth() * 0.3 - $this->lMargin - $this->rMargin, 10, 'Podpis', 1, 1, 'L', true); + + // Table Body + $this->SetFont('DejaVu', '', 12); + foreach ($people as $person) { + $name = $person['first_name'] . ' ' . $person['last_name']; + $this->Cell($this->GetPageWidth() * 0.7, 15, $name, 1, 0, 'L'); + $this->Cell($this->GetPageWidth() * 0.3 - $this->lMargin - $this->rMargin, 15, '', 1, 1, 'L'); + } + } +} + +$pdf = new PDF(); +$pdf->generateAttendanceList($peopleDetails); + +$pdfData = $pdf->Output('S'); +error_log($correlation_id . ': PDF data generated. Length: ' . strlen($pdfData) . ' bytes.'); + +if (empty($pdfData)) { + if (ob_get_length()) ob_end_clean(); + http_response_code(500); + header('Content-Type: application/json'); + $error_message = 'Failed to generate PDF data.'; + error_log($correlation_id . ': ' . $error_message); + echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]); + exit; +} + +if (ob_get_length()) { + ob_end_clean(); +} + +header('Content-Type: application/pdf'); +header('Content-Disposition: attachment; filename="lista-obecnosci.pdf"'); +header('Content-Transfer-Encoding: binary'); +header('Cache-Control: private, max-age=0, must-revalidate'); +header('Pragma: public'); +header('Content-Length: ' . strlen($pdfData)); + +echo $pdfData; +exit; \ No newline at end of file diff --git a/_bulk_print_badges.php b/_bulk_print_badges.php new file mode 100644 index 0000000..49c20ac --- /dev/null +++ b/_bulk_print_badges.php @@ -0,0 +1,109 @@ + ['message' => $error_message], 'correlation_id' => $correlation_id]); + exit; +} + +// Input validation +$person_ids = json_decode($_POST['person_ids'] ?? '[]', true); +error_log($correlation_id . ': Received ' . count($person_ids) . ' person_ids.'); + +if (empty($person_ids)) { + if (ob_get_length()) ob_end_clean(); + http_response_code(400); + header('Content-Type: application/json'); + $error_message = 'Nie wybrano żadnych osób.'; + error_log($correlation_id . ': ' . $error_message); + echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]); + exit; +} + +$workflowEngine = new WorkflowEngine(); +$peopleDetails = $workflowEngine->getPeopleDetails($person_ids); +error_log($correlation_id . ': Fetched ' . count($peopleDetails) . ' rows from database.'); + +class PDF extends tFPDF +{ + function __construct($orientation='P', $unit='mm', $size='A4') + { + parent::__construct($orientation, $unit, $size); + $this->AddFont('DejaVu','','DejaVuSans.ttf',true); + $this->AddFont('DejaVu','B','DejaVuSans-Bold.ttf',true); + $this->SetFont('DejaVu', '', 14); + } + + function generateBadge($person) { + $this->AddPage(); + $this->Image('assets/pasted-20260112-081646-4e946aad.png', 0, 0, $this->GetPageWidth(), $this->GetPageHeight()); + $this->SetY(20); + $this->SetFont('DejaVu', 'B', 12); + $this->Cell(0, 6, $person['first_name'] . ' ' . $person['last_name'], 0, 1, 'C'); + $this->SetFont('DejaVu', 'B', 8); + $this->Cell(0, 3, $person['company_name'] ?? 'N/A', 0, 1, 'C'); + $this->SetFont('DejaVu', '', 6); + $this->Cell(0, 3, $person['industry'] ?? 'N/A', 0, 1, 'C'); + + $this->SetXY(2.5,13); + $this->SetFont('DejaVu', 'B', 6); + $this->Cell(22, 3, $person['bni_group_name'] ?? 'GOŚĆ', 0, 0, 'C'); + } +} + +$pdf = new PDF('L', 'mm', array(85, 55)); + +foreach ($peopleDetails as $person) { + // No need for converting the entire array, FPDF with iconv handles it. + $pdf->generateBadge($person); +} + +// 2. Generate PDF content as a string +$pdfData = $pdf->Output('S'); +error_log($correlation_id . ': PDF data generated. Length: ' . strlen($pdfData) . ' bytes.'); +error_log($correlation_id . ': Memory usage: ' . memory_get_usage()); + +// 3. Validate PDF data +if (empty($pdfData) || !is_string($pdfData)) { + if (ob_get_length()) ob_end_clean(); + http_response_code(500); + header('Content-Type: application/json'); + $error_message = 'Failed to generate PDF data.'; + error_log($correlation_id . ': ' . $error_message); + echo json_encode(['error' => ['message' => $error_message], 'correlation_id' => $correlation_id]); + exit; +} + +// 4. Clean any potential output that occurred before this point +if (ob_get_length()) { + ob_end_clean(); +} + +// 5. Send correct headers +header('Content-Type: application/pdf'); +header('Content-Disposition: attachment; filename="badges.pdf"'); +header('Content-Transfer-Encoding: binary'); +header('Cache-Control: private, max-age=0, must-revalidate'); +header('Pragma: public'); +header('Content-Length: ' . strlen($pdfData)); + +// 6. Output the raw PDF data and terminate +echo $pdfData; +exit; \ No newline at end of file diff --git a/_bulk_update_status.php b/_bulk_update_status.php new file mode 100644 index 0000000..265610c --- /dev/null +++ b/_bulk_update_status.php @@ -0,0 +1,52 @@ +prepare("SELECT id FROM process_instances WHERE process_definition_id = ? AND person_id IN ($placeholders)"); +$params = array_merge([$process_id], $person_ids); +$stmt->execute($params); +$instance_ids = $stmt->fetchAll(PDO::FETCH_COLUMN); + +if (empty($instance_ids)) { + $_SESSION['flash_message'] = "No instances found for the selected people and process."; + header('Location: ' . $_SERVER['HTTP_REFERER']); + exit; +} + +$statuses = []; +foreach ($instance_ids as $instance_id) { + $statuses[] = [ + 'instance_id' => $instance_id, + 'status' => $status, + 'reason' => $reason, + 'user_id' => $userId + ]; +} + +$workflowEngine = new WorkflowEngine(); +$results = $workflowEngine->bulkManualStatus($statuses); + +$_SESSION['flash_message'] = "Bulk status update completed."; +$_SESSION['bulk_results'] = $results; + +header('Location: ' . $_SERVER['HTTP_REFERER']); +exit; diff --git a/_create_person.php b/_create_person.php new file mode 100644 index 0000000..821ff8b --- /dev/null +++ b/_create_person.php @@ -0,0 +1,119 @@ + ['message' => 'Method not allowed.'], 'correlation_id' => uniqid()]); + exit; +} + +$first_name = $_POST['first_name'] ?? ''; +$last_name = $_POST['last_name'] ?? ''; +$email = $_POST['email'] ?? ''; +$password = $_POST['password'] ?? ''; +$company_name = $_POST['company_name'] ?? null; +$phone = $_POST['phone'] ?? null; +$role = $_POST['role'] ?? 'guest'; +$functions = isset($_POST['functions']) ? (array)$_POST['functions'] : []; +$bni_group_id = isset($_POST['bni_group_id']) && !empty($_POST['bni_group_id']) ? $_POST['bni_group_id'] : null; + +$nip = $_POST['nip'] ?? null; +$industry = $_POST['industry'] ?? null; +$company_size_revenue = $_POST['company_size_revenue'] ?? null; +$business_description = $_POST['business_description'] ?? null; + +if (empty($first_name) || empty($last_name) || empty($email) || empty($password)) { + http_response_code(422); + echo json_encode(['error' => ['message' => 'First name, last name, email, and password are required.'], 'correlation_id' => uniqid()]); + exit; +} + +if ($role !== 'member') { + $bni_group_id = null; +} + +$pdo = db(); +try { + $pdo->beginTransaction(); + + $sql = 'INSERT INTO people (first_name, last_name, email, password, company_name, phone, role, bni_group_id, nip, industry, company_size_revenue, business_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$first_name, $last_name, $email, password_hash($password, PASSWORD_DEFAULT), $company_name, $phone, $role, $bni_group_id, $nip, $industry, $company_size_revenue, $business_description]); + $personId = $pdo->lastInsertId(); + + $upload_dir = 'uploads/people/' . $personId . '/'; + if (!is_dir($upload_dir)) { + if (!mkdir($upload_dir, 0777, true) && !is_dir($upload_dir)) { + throw new RuntimeException(sprintf('Directory "%s" was not created', $upload_dir)); + } + } + + $file_fields = [ + 'company_logo' => 'company_logo_path', + 'person_photo' => 'person_photo_path', + 'gains_sheet' => 'gains_sheet_path', + 'top_wanted_contacts' => 'top_wanted_contacts_path', + 'top_owned_contacts' => 'top_owned_contacts_path' + ]; + $file_paths_to_update = []; + + foreach ($file_fields as $form_field_name => $db_column_name) { + if (isset($_FILES[$form_field_name]) && $_FILES[$form_field_name]['error'] == UPLOAD_ERR_OK) { + $tmp_name = $_FILES[$form_field_name]['tmp_name']; + $original_name = basename($_FILES[$form_field_name]['name']); + $file_ext = pathinfo($original_name, PATHINFO_EXTENSION); + $new_filename = uniqid($form_field_name . '_', true) . '.' . $file_ext; + $destination = $upload_dir . $new_filename; + + if (move_uploaded_file($tmp_name, $destination)) { + $file_paths_to_update[$db_column_name] = $destination; + } else { + throw new RuntimeException("Failed to move uploaded file for {$form_field_name}."); + } + } + } + + if (!empty($file_paths_to_update)) { + $sql_parts = []; + $params = []; + foreach ($file_paths_to_update as $column => $path) { + $sql_parts[] = "$column = ?"; + $params[] = $path; + } + $params[] = $personId; + $sql = "UPDATE people SET " . implode(', ', $sql_parts) . " WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + } + + if (!empty($functions)) { + $sql = "INSERT INTO user_functions (user_id, function_id) VALUES (?, ?)"; + $stmt = $pdo->prepare($sql); + foreach ($functions as $functionId) { + $stmt->execute([$personId, $functionId]); + } + } + + $pdo->commit(); + + echo json_encode(['success' => true, 'person_id' => $personId, 'message' => 'Person created successfully.']); + +} catch (PDOException $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + if ($e->errorInfo[1] == 1062) { + http_response_code(409); // Conflict + echo json_encode(['error' => ['message' => 'An account with this email address already exists.'], 'correlation_id' => uniqid()]); + } else { + throw $e; // Re-throw to be caught by the global error handler + } +} catch (Exception $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + throw $e; // Re-throw to be caught by the global error handler +} \ No newline at end of file diff --git a/_delete_bni_group.php b/_delete_bni_group.php new file mode 100644 index 0000000..e6a55e9 --- /dev/null +++ b/_delete_bni_group.php @@ -0,0 +1,20 @@ +prepare("DELETE FROM bni_groups WHERE id = :id"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + + $_SESSION['success_message'] = 'BNI Group deleted successfully!'; + } catch (PDOException $e) { + $_SESSION['error_message'] = 'Error deleting BNI group: ' . $e->getMessage(); + } +} + +header('Location: bni_groups.php'); +exit; diff --git a/_delete_calendar_event.php b/_delete_calendar_event.php new file mode 100644 index 0000000..8d96636 --- /dev/null +++ b/_delete_calendar_event.php @@ -0,0 +1,48 @@ +beginTransaction(); + + // Check if the event is a parent event + $stmt = $pdo->prepare("SELECT parent_event_id FROM calendar_events WHERE id = ?"); + $stmt->execute([$event_id]); + $event = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($event) { + if ($event['parent_event_id'] === null) { + // It's a parent event, delete it and all its children + $stmt_delete_children = $pdo->prepare("DELETE FROM calendar_events WHERE parent_event_id = ?"); + $stmt_delete_children->execute([$event_id]); + } + + // Delete the event itself + $stmt_delete = $pdo->prepare("DELETE FROM calendar_events WHERE id = ?"); + $stmt_delete->execute([$event_id]); + } + + $pdo->commit(); + header("Location: calendar.php"); + exit(); + + } catch (Exception $e) { + $pdo->rollBack(); + error_log($e->getMessage()); + header("Location: calendar.php?error=db_error"); + exit(); + } +} else { + header("Location: calendar.php"); + exit(); +} diff --git a/_delete_event_type.php b/_delete_event_type.php new file mode 100644 index 0000000..868a28c --- /dev/null +++ b/_delete_event_type.php @@ -0,0 +1,16 @@ +prepare("DELETE FROM event_types WHERE id = ?"); + $stmt->execute([$id]); + + session_start(); + $_SESSION['success_message'] = 'Event type deleted successfully.'; + header('Location: event_types.php'); + exit; +} +?> \ No newline at end of file diff --git a/_delete_function.php b/_delete_function.php new file mode 100644 index 0000000..bb5b1c1 --- /dev/null +++ b/_delete_function.php @@ -0,0 +1,21 @@ +prepare("DELETE FROM functions WHERE id = :id"); +$stmt->execute(['id' => $id]); + +// Optional: Also delete user_functions associated with this function +$stmt = $pdo->prepare("DELETE FROM user_functions WHERE function_id = :function_id"); +$stmt->execute(['function_id' => $id]); + +header('Location: functions.php'); +exit(); diff --git a/_delete_person.php b/_delete_person.php new file mode 100644 index 0000000..fd29201 --- /dev/null +++ b/_delete_person.php @@ -0,0 +1,51 @@ +prepare("DELETE FROM people WHERE id = ?"); + $stmt->execute([$id]); + + if ($stmt->rowCount() > 0) { + echo json_encode(['success' => true, 'message' => 'Osoba usunięta pomyślnie.']); + } else { + http_response_code(404); + echo json_encode(['success' => false, 'error' => 'Nie znaleziono osoby.']); + } + } catch (PDOException $e) { + http_response_code(500); + // Log the real error to a secure log file + error_log("Database error on person delete: " . $e->getMessage()); + // Send a generic error message to the client + echo json_encode(['success' => false, 'error' => 'Błąd serwera podczas usuwania osoby.']); + } + exit; + } + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Brak ID osoby.']); + exit; +} + +// Keep GET for backwards compatibility or simple cases, but it redirects. +if (isset($_GET['id'])) { + $id = $_GET['id']; + + $pdo = db(); + $stmt = $pdo->prepare("DELETE FROM people WHERE id = ?"); + $stmt->execute([$id]); + + $_SESSION['success_message'] = 'Osoba usunięta pomyślnie.'; + header('Location: persons.php'); + exit; +} + +http_response_code(405); +echo json_encode(['success' => false, 'error' => 'Nieprawidłowa metoda żądania.']); +?> \ No newline at end of file diff --git a/_footer.php b/_footer.php new file mode 100644 index 0000000..6ec3293 --- /dev/null +++ b/_footer.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_get_event_details.php b/_get_event_details.php new file mode 100644 index 0000000..c6c4671 --- /dev/null +++ b/_get_event_details.php @@ -0,0 +1,40 @@ +prepare(" + SELECT c.*, t.name as type_name, GROUP_CONCAT(g.id) as group_ids + FROM calendar_events c + LEFT JOIN event_types t ON c.event_type_id = t.id + LEFT JOIN calendar_event_groups ceg ON c.id = ceg.calendar_event_id + LEFT JOIN bni_groups g ON ceg.bni_group_id = g.id + WHERE c.id = ? + GROUP BY c.id + "); + $stmt->execute([$event_id]); + $event = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($event) { + if ($event['group_ids']) { + $event['group_ids'] = explode(',', $event['group_ids']); + } else { + $event['group_ids'] = []; + } + + header('Content-Type: application/json'); + echo json_encode($event); + } else { + header("HTTP/1.1 404 Not Found"); + } +} else { + header("HTTP/1.1 400 Bad Request"); +} diff --git a/_get_future_meetings.php b/_get_future_meetings.php new file mode 100644 index 0000000..114daca --- /dev/null +++ b/_get_future_meetings.php @@ -0,0 +1,41 @@ + 'Invalid BNI Group ID']); + exit; +} + +try { + $pdo = db(); + + $sql = " + SELECT ce.*, bg.name as group_name + FROM calendar_events ce + JOIN calendar_event_groups ceg ON ce.id = ceg.calendar_event_id + JOIN bni_groups bg ON ceg.bni_group_id = bg.id + WHERE ceg.bni_group_id = :bni_group_id + AND ce.start_datetime > NOW() + ORDER BY ce.start_datetime ASC + LIMIT :limit OFFSET :offset + "; + + $stmt = $pdo->prepare($sql); + $stmt->bindParam(':bni_group_id', $bni_group_id, PDO::PARAM_INT); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + + $events = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode($events); + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'Database error: ' . $e->getMessage()]); +} diff --git a/_get_instance_details.php b/_get_instance_details.php new file mode 100644 index 0000000..be19a97 --- /dev/null +++ b/_get_instance_details.php @@ -0,0 +1,266 @@ + 'Unauthorized']); + exit; +} + +$person_id = $_GET['person_id'] ?? null; +$process_definition_id = $_GET['process_id'] ?? null; + +if (!$person_id || !$process_definition_id) { + http_response_code(400); + echo json_encode(['error' => 'Missing person_id or process_id']); + exit; +} + +$userId = $_SESSION['user_id']; +$engine = new WorkflowEngine(); +$pdo = db(); + +// Fetch Person and Process Definition details first +$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?"); +$stmt_person->execute([$person_id]); +$person = $stmt_person->fetch(); + +$stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?"); +$stmt_process->execute([$process_definition_id]); +$process = $stmt_process->fetch(); + +if (!$person || !$process) { + http_response_code(404); + echo "

Could not find person or process.

"; + exit; +} + +// Try to find an existing instance +$instance = $engine->getInstanceByDefId($person_id, $process_definition_id); + +?> + + +
+ - +
+ + + getEvents($instanceId); + ?> + + + +
+
Zadania do wykonania
+
+ +
+ > + +
+ +
+
+ + getProcessDefinitionNodes($process_definition_id); + $availableTransitions = $engine->getAvailableTransitions($instanceId); + + $available_target_node_ids = array_map(function($t) { return $t['to']; }, $availableTransitions); + $available_transitions_map = []; + foreach ($availableTransitions as $t) { + $available_transitions_map[$t['id']] = $t; + } + + $visited_nodes = []; + foreach ($events as $event) { + if ($event['node_id']) { + $visited_nodes[$event['node_id']] = true; + } + } + ?> +
+
Kroki procesu
+ +
+
Available Actions
+ +
+ +
+ + + + + + + + +
+ +
+ + +

No actions available.

+ + + + + +
+
+ + +
+ +
+
Dodaj notatkę
+
+ +
+ +
+ +
+ +
+
Historia
+ +

Brak zdarzeń.

+ + + + +
+ + + checkEligibility($person_id, $process_definition_id); + ?> + +
+ +

Process Not Started

+

This process has not been started for this person.

+ + +

Not Eligible

+

This person is not eligible to start this process.

+ + +
+ \ No newline at end of file diff --git a/_get_meeting_attendance.php b/_get_meeting_attendance.php new file mode 100644 index 0000000..c56eb02 --- /dev/null +++ b/_get_meeting_attendance.php @@ -0,0 +1,35 @@ + false, 'message' => 'An error occurred.', 'attendance' => []]; + +if (!isset($_SESSION['user_id'])) { + $response['message'] = 'You must be logged in to perform this action.'; + echo json_encode($response); + exit; +} + +$groupId = $_GET['group_id'] ?? null; +$meetingDate = $_GET['meeting_date'] ?? null; + +if (!$groupId || !$meetingDate) { + $response['message'] = 'Missing required parameters.'; + echo json_encode($response); + exit; +} + +try { + $workflowEngine = new WorkflowEngine(); + $attendance = $workflowEngine->getMeetingAttendanceByGroupAndDate((int)$groupId, $meetingDate); + $response['success'] = true; + $response['attendance'] = $attendance; +} catch (Exception $e) { + error_log($e->getMessage()); + $response['message'] = 'Error fetching attendance: ' . $e->getMessage(); +} + +echo json_encode($response); diff --git a/_get_meeting_details.php b/_get_meeting_details.php new file mode 100644 index 0000000..abfeb9f --- /dev/null +++ b/_get_meeting_details.php @@ -0,0 +1,27 @@ + false, 'message' => 'Invalid request']; + +$personId = $_GET['person_id'] ?? null; +$bniGroupId = $_GET['bni_group_id'] ?? null; +$meetingDatetime = $_GET['meeting_datetime'] ?? null; +$userId = $_SESSION['user_id'] ?? 0; // Ensure you have a user ID in the session + +if ($personId && $bniGroupId && $meetingDatetime && $userId) { + try { + $workflowEngine = new WorkflowEngine(); + $details = $workflowEngine->getMeetingDetails((int)$personId, (int)$bniGroupId, $meetingDatetime); + $response = ['success' => true, 'details' => $details]; + } catch (Exception $e) { + $response['message'] = $e->getMessage(); + } +} else { + $response['message'] = 'Missing required parameters.'; +} + +echo json_encode($response); diff --git a/_get_person_details.php b/_get_person_details.php new file mode 100644 index 0000000..696f18d --- /dev/null +++ b/_get_person_details.php @@ -0,0 +1,66 @@ +prepare("SELECT * FROM people WHERE id = ?"); + $stmt->execute([$person_id]); + $person = $stmt->fetch(PDO::FETCH_ASSOC); + + // Fetch all functions + $stmt = $pdo->query(" + SELECT f.id, f.name, bg.name as group_name + FROM functions f + LEFT JOIN bni_groups bg ON f.bni_group_id = bg.id + ORDER BY bg.display_order, f.display_order + "); + $all_functions = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Fetch person's functions + $stmt = $pdo->prepare("SELECT function_id FROM user_functions WHERE user_id = ?"); + $stmt->execute([$person_id]); + $person_functions = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); + + // --- Fetch Follow-up Process Summary --- + $follow_up_summary = null; + $stmt_def = $pdo->prepare("SELECT id FROM process_definitions WHERE code = 'guest_handling' LIMIT 1"); + $stmt_def->execute(); + $follow_up_def_id = $stmt_def->fetchColumn(); + + if ($follow_up_def_id) { + $stmt_inst = $pdo->prepare("SELECT * FROM process_instances WHERE person_id = ? AND process_definition_id = ? ORDER BY id DESC LIMIT 1"); + $stmt_inst->execute([$person_id, $follow_up_def_id]); + $instance = $stmt_inst->fetch(PDO::FETCH_ASSOC); + + if ($instance) { + $data = $instance['data_json'] ? json_decode($instance['data_json'], true) : []; + $follow_up_summary = [ + 'last_call_outcome' => $data['outcome_status'] ?? null, + 'last_call_date' => $data['call_date'] ?? null, + 'next_contact_date' => $data['next_contact_date'] ?? null, + 'final_outcome' => $instance['current_status'], // e.g., completed, terminated + 'reason' => $instance['current_reason'] + ]; + } + } + + $response = [ + 'person' => $person, + 'all_functions' => $all_functions, + 'person_functions' => $person_functions, + 'follow_up_summary' => $follow_up_summary + ]; + + echo json_encode($response); + } catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + exit; +} +?> \ No newline at end of file diff --git a/_get_process_bulk_details.php b/_get_process_bulk_details.php new file mode 100644 index 0000000..580e1d3 --- /dev/null +++ b/_get_process_bulk_details.php @@ -0,0 +1,96 @@ + 'Process ID is required.']); + exit; +} + +$process_id = $_GET['process_id']; + +try { + $pdo = db(); + + // 1. Get process definition details + $stmt_def = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?"); + $stmt_def->execute([$process_id]); + $process_definition = $stmt_def->fetch(PDO::FETCH_ASSOC); + + if (!$process_definition) { + http_response_code(404); + echo json_encode(['error' => 'Process definition not found.']); + exit; + } + + if (!empty($process_definition['definition_json'])) { + $process_definition['definition_json'] = json_decode($process_definition['definition_json'], true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("Failed to decode process definition JSON. Error: " . json_last_error_msg()); + } + } else { + $process_definition['definition_json'] = []; + } + + // 2. Get all instances for this process + $stmt_instances = $pdo->prepare("SELECT * FROM process_instances WHERE process_definition_id = ?"); + $stmt_instances->execute([$process_id]); + $instances = $stmt_instances->fetchAll(PDO::FETCH_ASSOC); + + $instance_ids = array_map(function($i) { return $i['id']; }, $instances); + + // 3. Get all events for these instances + $events = []; + if (!empty($instance_ids)) { + $placeholders = implode(',', array_fill(0, count($instance_ids), '?')); + $stmt_events = $pdo->prepare("SELECT * FROM process_events WHERE process_instance_id IN ($placeholders) ORDER BY created_at, id"); + $stmt_events->execute($instance_ids); + $all_events = $stmt_events->fetchAll(PDO::FETCH_ASSOC); + // Group events by instance_id + foreach ($all_events as $event) { + $events[$event['process_instance_id']][] = $event; + } + } + + // 4. Get People details + $people_ids = array_unique(array_column($instances, 'person_id')); + $people = []; + if (!empty($people_ids)) { + $valid_people_ids = array_filter($people_ids, 'is_numeric'); + + if (!empty($valid_people_ids)) { + $placeholders = implode(',', array_fill(0, count($valid_people_ids), '?')); + $stmt_people = $pdo->prepare("SELECT id, first_name, last_name FROM people WHERE id IN ($placeholders)"); + $stmt_people->execute(array_values($valid_people_ids)); + $people_results = $stmt_people->fetchAll(PDO::FETCH_ASSOC); + foreach ($people_results as $person) { + $people[$person['id']] = $person; + $people[$person['id']]['name'] = trim($person['first_name'] . ' ' . $person['last_name']); + } + } + } + + // Assemble the response + // Ensure steps are available, even if the JSON is empty or malformed. + $steps = !empty($process_definition['definition_json']['steps']) ? $process_definition['definition_json']['steps'] : []; + + $response = [ + 'process' => $process_definition, + 'steps' => $steps, + 'instances' => $instances, + 'events' => $events, + 'people' => $people + ]; + + echo json_encode($response); + +} catch (PDOException $e) { + http_response_code(500); + echo json_encode(['error' => 'A database error occurred.', 'details' => $e->getMessage()]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => 'A general error occurred.', 'details' => $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/_header.php b/_header.php new file mode 100644 index 0000000..8deeb98 --- /dev/null +++ b/_header.php @@ -0,0 +1,52 @@ + + + + + + + <?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?> - Dashboard + + + + + + + + + + + + + + + + + + + + "> + + + + + + + + + + + diff --git a/_init_instances.php b/_init_instances.php new file mode 100644 index 0000000..cfe883f --- /dev/null +++ b/_init_instances.php @@ -0,0 +1,37 @@ +prepare("SELECT id FROM people WHERE active = 1"); +$stmt_people->execute(); +$people = $stmt_people->fetchAll(PDO::FETCH_COLUMN); + +// Get all active process definitions +$stmt_processes = $pdo->prepare("SELECT id FROM process_definitions WHERE is_active = 1"); +$stmt_processes->execute(); +$processes = $stmt_processes->fetchAll(PDO::FETCH_COLUMN); + +$insert_stmt = $pdo->prepare("INSERT IGNORE INTO process_instances (person_id, process_definition_id, current_status) VALUES (?, ?, 'none')"); + +$count = 0; +foreach ($people as $person_id) { + foreach ($processes as $process_id) { + $insert_stmt->execute([$person_id, $process_id]); + if ($insert_stmt->rowCount() > 0) { + $count++; + } + } +} + +$_SESSION['flash_message'] = "Initialized $count new process instances."; + +header("Location: process_dashboard.php"); // Redirect to the main dashboard +exit; diff --git a/_init_single_instance.php b/_init_single_instance.php new file mode 100644 index 0000000..8aa87f0 --- /dev/null +++ b/_init_single_instance.php @@ -0,0 +1,52 @@ + ['message' => 'Authentication required.']]); + exit; +} + +$userId = $_SESSION['user_id']; +$personId = filter_input(INPUT_POST, 'person_id', FILTER_VALIDATE_INT); +$processDefinitionId = filter_input(INPUT_POST, 'process_id', FILTER_VALIDATE_INT); +$deleteExisting = filter_input(INPUT_POST, 'delete_existing'); + +if (!$personId || !$processDefinitionId) { + // InvalidArgumentException will be caught by the handler and result in a 400 Bad Request + throw new InvalidArgumentException('Invalid or missing person_id or process_id.'); +} + +$engine = new WorkflowEngine(); + +if($deleteExisting === '1') { + $instance = $engine->getInstanceByDefId($personId, $processDefinitionId); + if ($instance) { + $engine->deleteInstance($instance['id']); + } +} + +// The getOrCreateInstanceByDefId method is now responsible for all checks: +// 1. Validating the process definition exists. +// 2. Checking if the process is active. +// 3. Checking if the person is eligible. +// 4. Creating the instance if it doesn't exist. +// It will throw specific exceptions (WorkflowNotFoundException, WorkflowNotAllowedException, WorkflowEligibilityException) which our ErrorHandler will turn into 404, 409, and 422 responses. +$instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); + +if ($instance) { + echo json_encode(['success' => true, 'message' => 'Process initialized successfully.', 'instance_id' => $instance['id']]); +} else { + // This case should not be reached if the engine works as expected, as failures should throw exceptions. + throw new Exception("Failed to initialize process for an unknown reason."); +} \ No newline at end of file diff --git a/_navbar.php b/_navbar.php new file mode 100644 index 0000000..49a0efe --- /dev/null +++ b/_navbar.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/_save_process_definition.php b/_save_process_definition.php new file mode 100644 index 0000000..094a75c --- /dev/null +++ b/_save_process_definition.php @@ -0,0 +1,97 @@ +prepare($sql); + $stmt->execute($params); + if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode(['message' => $message]); + } else { + $_SESSION['success_message'] = $message; + header('Location: process_definitions.php'); + exit(); + } + } +} catch (WorkflowRuleFailedException $e) { + header('Content-Type: application/json'); + echo json_encode(['error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/_sidebar.php b/_sidebar.php new file mode 100644 index 0000000..f0bd2e6 --- /dev/null +++ b/_sidebar.php @@ -0,0 +1,56 @@ + + \ No newline at end of file diff --git a/_update_bni_group.php b/_update_bni_group.php new file mode 100644 index 0000000..e66be93 --- /dev/null +++ b/_update_bni_group.php @@ -0,0 +1,34 @@ +prepare("UPDATE bni_groups SET name = :name, city = :city, active = :active, display_order = :display_order WHERE id = :id"); + $stmt->bindParam(':id', $id, PDO::PARAM_INT); + $stmt->bindParam(':name', $name); + $stmt->bindParam(':city', $city); + $stmt->bindParam(':active', $active, PDO::PARAM_INT); + $stmt->bindParam(':display_order', $display_order, PDO::PARAM_INT); + $stmt->execute(); + + $_SESSION['success_message'] = 'BNI Group updated successfully!'; + } catch (PDOException $e) { + $_SESSION['error_message'] = 'Error updating BNI group: ' . $e->getMessage(); + } +} + +header('Location: bni_groups.php'); +exit; diff --git a/_update_bni_group_order.php b/_update_bni_group_order.php new file mode 100644 index 0000000..854049b --- /dev/null +++ b/_update_bni_group_order.php @@ -0,0 +1,67 @@ +beginTransaction(); + + try { + foreach ($ordered_ids as $index => $id) { + $sql = "UPDATE bni_groups SET display_order = ? WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$index + 1, $id]); + } + $pdo->commit(); + header('Content-Type: application/json'); + echo json_encode(['success' => true, 'message' => 'Order updated successfully.']); + exit(); + + } catch (PDOException $e) { + $pdo->rollBack(); + header('Content-Type: application/json'); + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Error updating display order: ' . $e->getMessage()]); + exit(); + } + } +} + +// Fallback for old form submission, though it's being deprecated +if (isset($_POST['ids']) && isset($_POST['display_order'])) { + $ids = $_POST['ids']; + $display_orders = $_POST['display_order']; + + if (count($ids) !== count($display_orders)) { + $_SESSION['error_message'] = "Something went wrong. Please try again."; + header("Location: bni_groups.php"); + exit(); + } + + $pdo = db(); + $pdo->beginTransaction(); + + try { + for ($i = 0; $i < count($ids); $i++) { + $sql = "UPDATE bni_groups SET display_order = ? WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$display_orders[$i], $ids[$i]]); + } + $pdo->commit(); + $_SESSION['success_message'] = "Display order updated successfully."; + } catch (PDOException $e) { + $pdo->rollBack(); + $_SESSION['error_message'] = "Error updating display order: " . $e->getMessage(); + } + + header("Location: bni_groups.php"); + exit(); +} + +http_response_code(400); +header('Content-Type: application/json'); +echo json_encode(['success' => false, 'message' => 'Invalid request.']); diff --git a/_update_calendar_event.php b/_update_calendar_event.php new file mode 100644 index 0000000..68216f8 --- /dev/null +++ b/_update_calendar_event.php @@ -0,0 +1,81 @@ +beginTransaction(); + + $event_ids_to_update = []; + + if ($update_scope === 'all') { + // Find the parent event id + $stmt = $pdo->prepare("SELECT parent_event_id, recurrence FROM calendar_events WHERE id = ?"); + $stmt->execute([$event_id]); + $event = $stmt->fetch(); + + $parent_event_id = $event['parent_event_id'] ?? $event_id; + + // Get all event ids in the series + $stmt = $pdo->prepare("SELECT id FROM calendar_events WHERE id = ? OR parent_event_id = ?"); + $stmt->execute([$parent_event_id, $parent_event_id]); + $event_ids_to_update = $stmt->fetchAll(PDO::FETCH_COLUMN); + } else { + $event_ids_to_update[] = $event_id; + } + + // Prepare statements + $stmt_update_event = $pdo->prepare("UPDATE calendar_events SET title = ?, description = ?, event_type_id = ? WHERE id = ?"); + if($update_scope === 'one'){ + $stmt_update_event = $pdo->prepare("UPDATE calendar_events SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, event_type_id = ? WHERE id = ?"); + } + + $stmt_delete_groups = $pdo->prepare("DELETE FROM calendar_event_groups WHERE calendar_event_id = ?"); + $stmt_add_groups = $pdo->prepare("INSERT INTO calendar_event_groups (calendar_event_id, bni_group_id) VALUES (?, ?)"); + + foreach ($event_ids_to_update as $id) { + // Update event details + if($update_scope === 'one'){ + $stmt_update_event->execute([$title, $description, $start_datetime, $end_datetime, $event_type_id, $id]); + } else { + $stmt_update_event->execute([$title, $description, $event_type_id, $id]); + } + + // Update group associations + $stmt_delete_groups->execute([$id]); + foreach ($group_ids as $group_id) { + $stmt_add_groups->execute([$id, $group_id]); + } + } + + $pdo->commit(); + + header("Location: calendar.php"); + exit(); + } catch (Exception $e) { + $pdo->rollBack(); + error_log("Error updating event: " . $e->getMessage()); + throw $e; + } +} diff --git a/_update_event_type.php b/_update_event_type.php new file mode 100644 index 0000000..8d9cee2 --- /dev/null +++ b/_update_event_type.php @@ -0,0 +1,19 @@ +prepare("UPDATE event_types SET name = ?, color = ?, display_order = ? WHERE id = ?"); + $stmt->execute([$name, $color, $display_order, $id]); + + session_start(); + $_SESSION['success_message'] = 'Event type updated successfully.'; + header('Location: event_types.php'); + exit; +} +?> \ No newline at end of file diff --git a/_update_event_type_order.php b/_update_event_type_order.php new file mode 100644 index 0000000..0652ba3 --- /dev/null +++ b/_update_event_type_order.php @@ -0,0 +1,67 @@ +beginTransaction(); + + try { + foreach ($ordered_ids as $index => $id) { + $sql = "UPDATE event_types SET display_order = ? WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$index + 1, $id]); + } + $pdo->commit(); + header('Content-Type: application/json'); + echo json_encode(['success' => true, 'message' => 'Order updated successfully.']); + exit(); + + } catch (PDOException $e) { + $pdo->rollBack(); + header('Content-Type: application/json'); + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Error updating display order: ' . $e->getMessage()]); + exit(); + } + } +} + +// Fallback for old form submission, though it's being deprecated +if (isset($_POST['ids']) && isset($_POST['display_order'])) { + $ids = $_POST['ids']; + $display_orders = $_POST['display_order']; + + if (count($ids) !== count($display_orders)) { + $_SESSION['error_message'] = "Something went wrong. Please try again."; + header("Location: event_types.php"); + exit(); + } + + $pdo = db(); + $pdo->beginTransaction(); + + try { + for ($i = 0; $i < count($ids); $i++) { + $sql = "UPDATE event_types SET display_order = ? WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$display_orders[$i], $ids[$i]]); + } + $pdo->commit(); + $_SESSION['success_message'] = "Display order updated successfully."; + } catch (PDOException $e) { + $pdo->rollBack(); + $_SESSION['error_message'] = "Error updating display order: " . $e->getMessage(); + } + + header("Location: event_types.php"); + exit(); +} + +http_response_code(400); +header('Content-Type: application/json'); +echo json_encode(['success' => false, 'message' => 'Invalid request.']); diff --git a/_update_function.php b/_update_function.php new file mode 100644 index 0000000..4ecc980 --- /dev/null +++ b/_update_function.php @@ -0,0 +1,30 @@ +prepare("UPDATE functions SET name = :name, bni_group_id = :bni_group_id WHERE id = :id"); + $stmt->execute(['name' => $name, 'bni_group_id' => $bni_group_id, 'id' => $id]); + $_SESSION['success_message'] = "Function updated successfully."; + } catch (PDOException $e) { + $_SESSION['error_message'] = "Error updating function: " . $e->getMessage(); + } + } +} else { + $_SESSION['error_message'] = "Name and group are required."; +} + +header('Location: functions.php'); +exit(); diff --git a/_update_function_order.php b/_update_function_order.php new file mode 100644 index 0000000..f9a3434 --- /dev/null +++ b/_update_function_order.php @@ -0,0 +1,67 @@ +beginTransaction(); + + try { + foreach ($ordered_ids as $index => $id) { + $sql = "UPDATE functions SET display_order = ? WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$index + 1, $id]); + } + $pdo->commit(); + header('Content-Type: application/json'); + echo json_encode(['success' => true, 'message' => 'Order updated successfully.']); + exit(); + + } catch (PDOException $e) { + $pdo->rollBack(); + header('Content-Type: application/json'); + http_response_code(500); + echo json_encode(['success' => false, 'message' => 'Error updating display order: ' . $e->getMessage()]); + exit(); + } + } +} + +// Fallback for old form submission, though it's being deprecated +if (isset($_POST['ids']) && isset($_POST['display_order'])) { + $ids = $_POST['ids']; + $display_orders = $_POST['display_order']; + + if (count($ids) !== count($display_orders)) { + $_SESSION['error_message'] = "Something went wrong. Please try again."; + header("Location: functions.php"); + exit(); + } + + $pdo = db(); + $pdo->beginTransaction(); + + try { + for ($i = 0; $i < count($ids); $i++) { + $sql = "UPDATE functions SET display_order = ? WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$display_orders[$i], $ids[$i]]); + } + $pdo->commit(); + $_SESSION['success_message'] = "Display order updated successfully."; + } catch (PDOException $e) { + $pdo->rollBack(); + $_SESSION['error_message'] = "Error updating display order: " . $e->getMessage(); + } + + header("Location: functions.php"); + exit(); +} + +http_response_code(400); +header('Content-Type: application/json'); +echo json_encode(['success' => false, 'message' => 'Invalid request.']); diff --git a/_update_instance_status.php b/_update_instance_status.php new file mode 100644 index 0000000..7b78c15 --- /dev/null +++ b/_update_instance_status.php @@ -0,0 +1,29 @@ +applyManualStatus((int)$instanceId, $status, $reason, (int)$userId); + +if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { + header('Content-Type: application/json'); + echo json_encode(['message' => 'Status updated successfully.']); +} else { + header('Location: index.php'); +} +exit; diff --git a/_update_meeting_attendance.php b/_update_meeting_attendance.php new file mode 100644 index 0000000..7289999 --- /dev/null +++ b/_update_meeting_attendance.php @@ -0,0 +1,47 @@ +getOrCreateMeeting((int)$bniGroupId, $meetingDate); +$workflowEngine->updateMeetingAttendance($meetingId, (int)$personId, (int)$bniGroupId, $status, (int)$userId, $guestSurvey); + +$response = [ + 'success' => true, + 'message' => 'Attendance updated successfully.' +]; + +echo json_encode($response); + diff --git a/_update_person.php b/_update_person.php new file mode 100644 index 0000000..ec39c8a --- /dev/null +++ b/_update_person.php @@ -0,0 +1,124 @@ + ['message' => 'First name, last name, and email are required.'], 'correlation_id' => uniqid()]); + exit; + } + + // Only members can be in a group + if ($role !== 'member') { + $bni_group_id = null; + } + + try { + $pdo = db(); + $pdo->beginTransaction(); + + // Handle file uploads + $upload_dir = 'uploads/people/' . $personId . '/'; + if (!is_dir($upload_dir)) { + mkdir($upload_dir, 0777, true); + } + + $file_fields = [ + 'company_logo' => 'company_logo_path', + 'person_photo' => 'person_photo_path', + 'gains_sheet' => 'gains_sheet_path', + 'top_wanted_contacts' => 'top_wanted_contacts_path', + 'top_owned_contacts' => 'top_owned_contacts_path' + ]; + + $file_paths = []; + + foreach ($file_fields as $form_field_name => $db_column_name) { + if (isset($_FILES[$form_field_name]) && $_FILES[$form_field_name]['error'] == UPLOAD_ERR_OK) { + $tmp_name = $_FILES[$form_field_name]['tmp_name']; + $original_name = basename($_FILES[$form_field_name]['name']); + $file_ext = pathinfo($original_name, PATHINFO_EXTENSION); + $new_filename = uniqid($form_field_name . '_', true) . '.' . $file_ext; + $destination = $upload_dir . $new_filename; + + if (move_uploaded_file($tmp_name, $destination)) { + $file_paths[$db_column_name] = $destination; + } + } + } + + // Prepare SQL for updating person details + $sql_parts = [ + 'first_name = ?', 'last_name = ?', 'email = ?', 'company_name = ?', 'phone = ?', + 'role = ?', 'bni_group_id = ?', 'nip = ?', 'industry = ?', 'company_size_revenue = ?', + 'business_description = ?' + ]; + $params = [ + $first_name, $last_name, $email, $company_name, $phone, $role, $bni_group_id, + $nip, $industry, $company_size_revenue, $business_description + ]; + + if (!empty($password)) { + $sql_parts[] = 'password = ?'; + $params[] = password_hash($password, PASSWORD_DEFAULT); + } + + foreach ($file_paths as $column => $path) { + $sql_parts[] = "$column = ?"; + $params[] = $path; + } + + $sql = "UPDATE people SET " . implode(', ', $sql_parts) . " WHERE id = ?"; + $params[] = $personId; + + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + + // Update functions + $stmt = $pdo->prepare("DELETE FROM user_functions WHERE user_id = ?"); + $stmt->execute([$personId]); + + if (!empty($functions)) { + $sql = "INSERT INTO user_functions (user_id, function_id) VALUES (?, ?)"; + $stmt = $pdo->prepare($sql); + foreach ($functions as $functionId) { + $stmt->execute([$personId, $functionId]); + } + } + + $pdo->commit(); + $_SESSION['success_message'] = 'Osoba zaktualizowana pomyślnie.'; + + } catch (PDOException $e) { + $pdo->rollBack(); + error_log('Update failed: ' . $e->getMessage()); + $_SESSION['error_message'] = "Błąd podczas aktualizacji osoby: " . $e->getMessage(); + } catch (Exception $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + error_log('File upload or other error: ' . $e->getMessage()); + $_SESSION['error_message'] = "Błąd: " . $e->getMessage(); + } + + header('Location: index.php'); + exit(); +} \ No newline at end of file diff --git a/_update_training_checklist_status.php b/_update_training_checklist_status.php new file mode 100644 index 0000000..7191fd2 --- /dev/null +++ b/_update_training_checklist_status.php @@ -0,0 +1,36 @@ +updateChecklistStatus((int)$instanceId, $taskCode, (bool)$isChecked, (int)$userId); +echo json_encode($result); +exit; \ No newline at end of file diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php new file mode 100644 index 0000000..d428248 --- /dev/null +++ b/ai/LocalAIApi.php @@ -0,0 +1,493 @@ + [ +// ['role' => 'system', 'content' => 'You are a helpful assistant.'], +// ['role' => 'user', 'content' => 'Tell me a bedtime story.'], +// ], +// ]); +// if (!empty($response['success'])) { +// // response['data'] contains full payload, e.g.: +// // { +// // "id": "resp_xxx", +// // "status": "completed", +// // "output": [ +// // {"type": "reasoning", "summary": []}, +// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} +// // ] +// // } +// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...) +// } +// Poll settings override: +// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]); + +class LocalAIApi +{ + /** @var array|null */ + private static ?array $configCache = null; + + /** + * Signature compatible with the OpenAI Responses API. + * + * @param array $params Request body (model, input, text, reasoning, metadata, etc.). + * @param array $options Extra options (timeout, verify_tls, headers, path, project_uuid). + * @return array{ + * success:bool, + * status?:int, + * data?:mixed, + * error?:string, + * response?:mixed, + * message?:string + * } + */ + public static function createResponse(array $params, array $options = []): array + { + $cfg = self::config(); + $payload = $params; + + if (empty($payload['input']) || !is_array($payload['input'])) { + return [ + 'success' => false, + 'error' => 'input_missing', + 'message' => 'Parameter "input" is required and must be an array.', + ]; + } + + if (!isset($payload['model']) || $payload['model'] === '') { + $payload['model'] = $cfg['default_model']; + } + + $initial = self::request($options['path'] ?? null, $payload, $options); + if (empty($initial['success'])) { + return $initial; + } + + // Async flow: if backend returns ai_request_id, poll status until ready + $data = $initial['data'] ?? null; + if (is_array($data) && isset($data['ai_request_id'])) { + $aiRequestId = $data['ai_request_id']; + $pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds + $pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds + return self::awaitResponse($aiRequestId, [ + 'timeout' => $pollTimeout, + 'interval' => $pollInterval, + 'headers' => $options['headers'] ?? [], + 'timeout_per_call' => $options['timeout'] ?? null, + ]); + } + + return $initial; + } + + /** + * Snake_case alias for createResponse (matches the provided example). + * + * @param array $params + * @param array $options + * @return array + */ + public static function create_response(array $params, array $options = []): array + { + return self::createResponse($params, $options); + } + + /** + * Perform a raw request to the AI proxy. + * + * @param string $path Endpoint (may be an absolute URL). + * @param array $payload JSON payload. + * @param array $options Additional request options. + * @return array + */ + public static function request(?string $path = null, array $payload = [], array $options = []): array + { + $cfg = self::config(); + + $projectUuid = $cfg['project_uuid']; + if (empty($projectUuid)) { + return [ + 'success' => false, + 'error' => 'project_uuid_missing', + 'message' => 'PROJECT_UUID is not defined; aborting AI request.', + ]; + } + + $defaultPath = $cfg['responses_path'] ?? null; + $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); + if (empty($resolvedPath)) { + return [ + 'success' => false, + 'error' => 'project_id_missing', + 'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', + ]; + } + + $url = self::buildUrl($resolvedPath, $cfg['base_url']); + $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; + if ($timeout <= 0) { + $timeout = 30; + } + + $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; + $verifyTls = array_key_exists('verify_tls', $options) + ? (bool) $options['verify_tls'] + : $baseVerifyTls; + + $projectHeader = $cfg['project_header']; + + $headers = [ + 'Content-Type: application/json', + 'Accept: application/json', + ]; + $headers[] = $projectHeader . ': ' . $projectUuid; + if (!empty($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && $header !== '') { + $headers[] = $header; + } + } + } + + if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { + $payload['project_uuid'] = $projectUuid; + } + + $body = json_encode($payload, JSON_UNESCAPED_UNICODE); + if ($body === false) { + return [ + 'success' => false, + 'error' => 'json_encode_failed', + 'message' => 'Failed to encode request body to JSON.', + ]; + } + + return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls); + } + + /** + * Poll AI request status until ready or timeout. + * + * @param int|string $aiRequestId + * @param array $options + * @return array + */ + public static function awaitResponse($aiRequestId, array $options = []): array + { + $cfg = self::config(); + + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds + $interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds + if ($interval <= 0) { + $interval = 5; + } + $perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null; + + $deadline = time() + max($timeout, $interval); + $headers = $options['headers'] ?? []; + + while (true) { + $statusResp = self::fetchStatus($aiRequestId, [ + 'headers' => $headers, + 'timeout' => $perCallTimeout, + ]); + if (!empty($statusResp['success'])) { + $data = $statusResp['data'] ?? []; + if (is_array($data)) { + $statusValue = $data['status'] ?? null; + if ($statusValue === 'success') { + return [ + 'success' => true, + 'status' => 200, + 'data' => $data['response'] ?? $data, + ]; + } + if ($statusValue === 'failed') { + return [ + 'success' => false, + 'status' => 500, + 'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed', + 'data' => $data, + ]; + } + } + } else { + return $statusResp; + } + + if (time() >= $deadline) { + return [ + 'success' => false, + 'error' => 'timeout', + 'message' => 'Timed out waiting for AI response.', + ]; + } + sleep($interval); + } + } + + /** + * Fetch status for queued AI request. + * + * @param int|string $aiRequestId + * @param array $options + * @return array + */ + public static function fetchStatus($aiRequestId, array $options = []): array + { + $cfg = self::config(); + $projectUuid = $cfg['project_uuid']; + if (empty($projectUuid)) { + return [ + 'success' => false, + 'error' => 'project_uuid_missing', + 'message' => 'PROJECT_UUID is not defined; aborting status check.', + ]; + } + + $statusPath = self::resolveStatusPath($aiRequestId, $cfg); + $url = self::buildUrl($statusPath, $cfg['base_url']); + + $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; + if ($timeout <= 0) { + $timeout = 30; + } + + $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; + $verifyTls = array_key_exists('verify_tls', $options) + ? (bool) $options['verify_tls'] + : $baseVerifyTls; + + $projectHeader = $cfg['project_header']; + $headers = [ + 'Accept: application/json', + $projectHeader . ': ' . $projectUuid, + ]; + if (!empty($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && $header !== '') { + $headers[] = $header; + } + } + } + + return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls); + } + + /** + * Extract plain text from a Responses API payload. + * + * @param array $response Result of LocalAIApi::createResponse|request. + * @return string + */ + public static function extractText(array $response): string + { + $payload = $response['data'] ?? $response; + if (!is_array($payload)) { + return ''; + } + + if (!empty($payload['output']) && is_array($payload['output'])) { + $combined = ''; + foreach ($payload['output'] as $item) { + if (!isset($item['content']) || !is_array($item['content'])) { + continue; + } + foreach ($item['content'] as $block) { + if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { + $combined .= $block['text']; + } + } + } + if ($combined !== '') { + return $combined; + } + } + + if (!empty($payload['choices'][0]['message']['content'])) { + return (string) $payload['choices'][0]['message']['content']; + } + + return ''; + } + + /** + * Attempt to decode JSON emitted by the model (handles markdown fences). + * + * @param array $response + * @return array|null + */ + public static function decodeJsonFromResponse(array $response): ?array + { + $text = self::extractText($response); + if ($text === '') { + return null; + } + + $decoded = json_decode($text, true); + if (is_array($decoded)) { + return $decoded; + } + + $stripped = preg_replace('/^```json|```$/m', '', trim($text)); + if ($stripped !== null && $stripped !== $text) { + $decoded = json_decode($stripped, true); + if (is_array($decoded)) { + return $decoded; + } + } + + return null; + } + + /** + * Load configuration from ai/config.php. + * + * @return array + */ + private static function config(): array + { + if (self::$configCache === null) { + $configPath = __DIR__ . '/config.php'; + if (!file_exists($configPath)) { + throw new RuntimeException('AI config file not found: ai/config.php'); + } + $cfg = require $configPath; + if (!is_array($cfg)) { + throw new RuntimeException('Invalid AI config format: expected array'); + } + self::$configCache = $cfg; + } + + return self::$configCache; + } + + /** + * Build an absolute URL from base_url and a path. + */ + private static function buildUrl(string $path, string $baseUrl): string + { + $trimmed = trim($path); + if ($trimmed === '') { + return $baseUrl; + } + if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { + return $trimmed; + } + if ($trimmed[0] === '/') { + return $baseUrl . $trimmed; + } + return $baseUrl . '/' . $trimmed; + } + + /** + * Resolve status path based on configured responses_path and ai_request_id. + * + * @param int|string $aiRequestId + * @param array $cfg + * @return string + */ + private static function resolveStatusPath($aiRequestId, array $cfg): string + { + $basePath = $cfg['responses_path'] ?? ''; + $trimmed = rtrim($basePath, '/'); + if ($trimmed === '') { + return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status'; + } + if (substr($trimmed, -11) !== '/ai-request') { + $trimmed .= '/ai-request'; + } + return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status'; + } + + /** + * Shared CURL sender for GET/POST requests. + * + * @param string $url + * @param string $method + * @param string|null $body + * @param array $headers + * @param int $timeout + * @param bool $verifyTls + * @return array + */ + private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array + { + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'curl_missing', + 'message' => 'PHP cURL extension is missing. Install or enable it on the VM.', + ]; + } + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + + $upper = strtoupper($method); + if ($upper === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? ''); + } else { + curl_setopt($ch, CURLOPT_HTTPGET, true); + } + + $responseBody = curl_exec($ch); + if ($responseBody === false) { + $error = curl_error($ch) ?: 'Unknown cURL error'; + curl_close($ch); + return [ + 'success' => false, + 'error' => 'curl_error', + 'message' => $error, + ]; + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $decoded = null; + if ($responseBody !== '' && $responseBody !== null) { + $decoded = json_decode($responseBody, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $decoded = null; + } + } + + if ($status >= 200 && $status < 300) { + return [ + 'success' => true, + 'status' => $status, + 'data' => $decoded ?? $responseBody, + ]; + } + + $errorMessage = 'AI proxy request failed'; + if (is_array($decoded)) { + $errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage; + } elseif (is_string($responseBody) && $responseBody !== '') { + $errorMessage = $responseBody; + } + + return [ + 'success' => false, + 'status' => $status, + 'error' => $errorMessage, + 'response' => $decoded ?? $responseBody, + ]; + } +} + +// Legacy alias for backward compatibility with the previous class name. +if (!class_exists('OpenAIService')) { + class_alias(LocalAIApi::class, 'OpenAIService'); +} diff --git a/ai/config.php b/ai/config.php new file mode 100644 index 0000000..c890698 --- /dev/null +++ b/ai/config.php @@ -0,0 +1,52 @@ + $baseUrl, + 'responses_path' => $responsesPath, + 'project_id' => $projectId, + 'project_uuid' => $projectUuid, + 'project_header' => 'project-uuid', + 'default_model' => 'gpt-5-mini', + 'timeout' => 30, + 'verify_tls' => true, +]; diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..9fb6340 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,169 @@ +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #f8f9fa; +} + +#sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 100; + padding: 48px 0 0; + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); + width: 250px; + transition: width 0.3s ease-in-out; +} + +#main-content { + margin-left: 250px; + transition: margin-left 0.3s ease-in-out; + padding: 20px; +} + +body.sidebar-collapsed #sidebar { + width: 80px; +} + +body.sidebar-collapsed #main-content { + margin-left: 80px; +} + +body.sidebar-collapsed #sidebar .nav-link-text { + display: none; +} + +body.sidebar-collapsed #sidebar .nav-link i { + font-size: 1.5rem; + margin-right: 0; +} + +body.sidebar-collapsed #sidebar .nav-item { + text-align: center; +} + +.nav-link { + color: #333; + display: flex; + align-items: center; +} + +.nav-link i { + margin-right: 10px; + width: 24px; + text-align: center; +} + +.nav-link.active { + color: #0d6efd; + font-weight: 500; +} + +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + font-size: 1rem; + /* background-color: rgba(0, 0, 0, .25); */ + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); + line-height: 1.8; +} + +.navbar .form-control { + padding: .75rem 1rem; + border-width: 0; + border-radius: 0; +} + +/* Calendar styles */ +.calendar { + table-layout: fixed; +} +.calendar td { + height: 120px; + vertical-align: top; + border: 1px solid #ddd; + padding: 4px; +} +.calendar .day-number { + font-size: 0.8rem; + font-weight: bold; + color: #333; +} +.calendar .not-month { + background-color: #f8f9fa; +} +.events { + margin-top: 4px; +} +.event { + font-size: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.submenu-item .nav-link { + padding-left: 2.5rem; +} + +.modal-fullscreen-xl { + width: 95%; + max-width: 1400px; +} + +.status-dot { + height: 12px; + width: 12px; + border-radius: 50%; + display: inline-block; + margin-left: 4px; +} + +.person-cell { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; +} + +.person-name { + font-weight: bold; +} + +.person-details { + font-size: 0.75rem; + color: #6c757d; +} + +.person-details .person-group { + font-weight: bold; + color: #198754; +} + +.person-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; +} + +.status-dots { + display: flex; + justify-content: flex-end; + margin-bottom: 0.25rem; +} + +.navbar-logo { + height: 30px; + width: auto; + margin-right: 30px; +} + +#sidebar-toggler i { + font-size: 1.5rem; +} + +/* Make sidebar link text visible */ +.nav-link-text { + display: inline; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..a75f632 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,253 @@ + +document.addEventListener('DOMContentLoaded', function () { + const sidebarToggler = document.getElementById('sidebar-toggler'); + + if (sidebarToggler) { + sidebarToggler.addEventListener('click', function () { + const isCollapsed = document.body.classList.toggle('sidebar-collapsed'); + localStorage.setItem('sidebarCollapsed', isCollapsed); + sidebarToggler.setAttribute('aria-expanded', !isCollapsed); + }); + + // Set initial aria-expanded state + const isInitiallyCollapsed = localStorage.getItem('sidebarCollapsed') === 'true'; + sidebarToggler.setAttribute('aria-expanded', !isInitiallyCollapsed); + } + + // --- The rest of your original main.js --- // + + $(document).ready(function() { + // Handler for showing the edit person modal + $('#editPersonModal').on('show.bs.modal', function (event) { + var button = $(event.relatedTarget); // Button that triggered the modal + var personId = button.data('person-id'); // Extract info from data-* attributes + var modal = $(this); + + // Clear previous data + modal.find('form').trigger('reset'); + modal.find('#editPersonId').val(''); + modal.find('#editRoles').empty(); + modal.find('#followUpSummaryContainer').empty(); // Clear summary container + // Clear file paths + modal.find('#editCompanyLogoPath, #editPersonPhotoPath, #editGainsSheetPath, #editTopWantedPath, #editTopOwnedPath').text(''); + + if (personId) { + // AJAX request to get person details + $.ajax({ + url: '_get_person_details.php', + type: 'GET', + data: { id: personId }, + dataType: 'json', + success: function(response) { + if (response.error) { + alert('Error fetching person details: ' + response.error); + return; + } + + var person = response.person; + var all_functions = response.all_functions; + var person_functions = response.person_functions; + var followUpSummary = response.follow_up_summary; + + if (!person) { + alert('Could not find person data.'); + return; + } + + // Populate the Follow-up Summary + var summaryContainer = modal.find('#followUpSummaryContainer'); + if (followUpSummary) { + let summaryHtml = '
Follow-up Process Summary
'; + summaryHtml += '
'; + + if (followUpSummary.last_call_outcome) { + summaryHtml += `
Last Call Outcome
${followUpSummary.last_call_outcome.replace(/_/g, ' ')}
`; + } + if (followUpSummary.last_call_date) { + summaryHtml += `
Last Call Date
${new Date(followUpSummary.last_call_date).toLocaleString()}
`; + } + if (followUpSummary.next_contact_date) { + summaryHtml += `
Next Contact Date
${new Date(followUpSummary.next_contact_date).toLocaleString()}
`; + } + if (followUpSummary.final_outcome) { + summaryHtml += `
Final Status
${followUpSummary.final_outcome} (${followUpSummary.reason || 'N/A'})
`; + } + + summaryHtml += '
'; + summaryContainer.html(summaryHtml); + } else { + summaryContainer.html('

No Follow-up process data found for this person.

'); + } + + // Populate the form fields + modal.find('#editPersonId').val(person.id); + modal.find('#editFirstName').val(person.first_name); + modal.find('#editLastName').val(person.last_name); + modal.find('#editPhone').val(person.phone); + modal.find('#editEmail').val(person.email); + modal.find('#editRole').val(person.role); + modal.find('#editBniGroup').val(person.bni_group_id); + modal.find('#editCompanyName').val(person.company_name); + modal.find('#editNip').val(person.nip); + modal.find('#editIndustry').val(person.industry); + modal.find('#editCompanySize').val(person.company_size_revenue); + modal.find('#editBusinessDescription').val(person.business_description); + + // Populate file paths + if (person.company_logo_path) { + modal.find('#editCompanyLogoPath').text('Current file: ' + person.company_logo_path.split('/').pop()); + } + if (person.person_photo_path) { + modal.find('#editPersonPhotoPath').text('Current file: ' + person.person_photo_path.split('/').pop()); + } + if (person.gains_sheet_path) { + modal.find('#editGainsSheetPath').text('Current file: ' + person.gains_sheet_path.split('/').pop()); + } + if (person.top_wanted_contacts_path) { + modal.find('#editTopWantedPath').text('Current file: ' + person.top_wanted_contacts_path.split('/').pop()); + } + if (person.top_owned_contacts_path) { + modal.find('#editTopOwnedPath').text('Current file: ' + person.top_owned_contacts_path.split('/').pop()); + } + + // Populate functions/roles dropdown and select assigned ones + var rolesSelect = modal.find('#editRoles'); + rolesSelect.empty(); // Clear existing options + + if (all_functions && all_functions.length > 0) { + const groupedFunctions = all_functions.reduce((acc, func) => { + const groupName = func.group_name || 'General'; + if (!acc[groupName]) { + acc[groupName] = []; + } + acc[groupName].push(func); + return acc; + }, {}); + + for (const groupName in groupedFunctions) { + const optgroup = $('').attr('label', groupName); + groupedFunctions[groupName].forEach(function(func) { + var option = $('').val(func.id).text(func.name); + if (person_functions && person_functions.includes(String(func.id))) { + option.prop('selected', true); + } + optgroup.append(option); + }); + rolesSelect.append(optgroup); + } + } + + // Trigger change to show/hide conditional fields + modal.find('#editRole').trigger('change'); + + // Also set up the delete button + $('#deleteUserBtn').data('person-id', person.id); + $('#personNameToDelete').text(person.firstName + ' ' + person.lastName); + + }, + error: function(xhr, status, error) { + alert('An error occurred while fetching person data. Please try again.'); + console.error("AJAX Error:", status, error); + } + }); + } + }); + + // Show/hide group selection based on role for both Edit and Create modals + $(document).on('change', '#editRole, #createRole', function() { + const role = $(this).val(); + const isMember = role === 'member'; + + // Find the correct context (modal) for the elements + const modal = $(this).closest('.modal-content'); + + modal.find('.member-only-fields').toggle(isMember); + modal.find('#edit-group-selection-div, #create-group-selection-div').toggle(isMember); + }); + + // Handle Delete Person confirmation + $('#confirmDeleteBtn').on('click', function() { + var personId = $('#deleteUserBtn').data('person-id'); + if (personId) { + // Use a form submission to perform the delete + var form = $('
'); + form.attr("method", "post"); + form.attr("action", "_delete_person.php"); + + var field = $(''); + field.attr("type", "hidden"); + field.attr("name", "id"); + field.attr("value", personId); + form.append(field); + + $(document.body).append(form); + form.submit(); + } + }); + + // Set initial state for create form + $('#createPersonModal').on('show.bs.modal', function () { + $('#createRole').trigger('change'); + }); + + function handleFormSubmit(form, errorContainer, successCallback) { + event.preventDefault(); + const formData = new FormData(form); + const errorDiv = $(errorContainer).hide(); + + fetch(form.action, { + method: 'POST', + body: formData + }) + .then(response => { + // Clone the response so we can read it twice (once as JSON, once as text if needed) + const responseClone = response.clone(); + return response.json() + .then(data => ({ status: response.status, ok: response.ok, body: data })) + .catch(() => responseClone.text().then(text => ({ status: response.status, ok: response.ok, body: text, isText: true }))); + }) + .then(res => { + const { status, ok, body, isText } = res; + + if (!ok) { + if (isText) { + throw new Error(`Server Error: ${status}. Response: ${body}`); + } + throw new Error(body.error?.message || `An unknown server error occurred (Status: ${status})`); + } + + if (isText) { + console.error("Received non-JSON response:", body); + throw new Error("The server sent an invalid response that could not be parsed. See console for details."); + } + + if (body.success) { + if (successCallback) { + successCallback(body); + } else { + // Default success behavior: close modal and reload + $(form).closest('.modal').modal('hide'); + window.location.reload(); + } + } else { + throw new Error(body.error?.message || 'An operation error occurred.'); + } + }) + .catch(error => { + errorDiv.text(error.message).show(); + }); + } + + $('#createPersonForm').on('submit', function(event) { + handleFormSubmit(this, '#createPersonError'); + }); + + $('#editPersonForm').on('submit', function(event) { + handleFormSubmit(this, '#editPersonError', function(data) { + // close modal and reload page + $('#editPersonModal').modal('hide'); + window.location.reload(); + }); + }); + }); +}); diff --git a/assets/js/process_definitions.js b/assets/js/process_definitions.js new file mode 100644 index 0000000..56b0289 --- /dev/null +++ b/assets/js/process_definitions.js @@ -0,0 +1,157 @@ + +document.addEventListener('DOMContentLoaded', function () { + const createProcessModal = document.getElementById('createProcessModal'); + const modalTitle = createProcessModal.querySelector('.modal-title'); + const form = createProcessModal.querySelector('#createProcessForm'); + const processIdInput = createProcessModal.querySelector('#processId'); + const processNameInput = createProcessModal.querySelector('#processName'); + const definitionJsonTextarea = createProcessModal.querySelector('#definitionJson'); + const eligibilityRulesJsonTextarea = createProcessModal.querySelector('#eligibilityRulesJson'); + + let definition = {}; + + function render() { + const statusesList = document.getElementById('statuses-list'); + const transitionsList = document.getElementById('transitions-list'); + const initialStatusSelect = document.getElementById('initialStatus'); + const fromStatusSelect = document.getElementById('fromStatusSelect'); + const toStatusSelect = document.getElementById('toStatusSelect'); + + statusesList.innerHTML = ''; + transitionsList.innerHTML = ''; + initialStatusSelect.innerHTML = ''; + fromStatusSelect.innerHTML = ''; + toStatusSelect.innerHTML = ''; + + if (definition.nodes) { + for (const nodeId in definition.nodes) { + const node = definition.nodes[nodeId]; + const statusItem = document.createElement('div'); + statusItem.textContent = node.name; + statusesList.appendChild(statusItem); + + const option = document.createElement('option'); + option.value = node.id; + option.textContent = node.name; + initialStatusSelect.appendChild(option.cloneNode(true)); + fromStatusSelect.appendChild(option.cloneNode(true)); + toStatusSelect.appendChild(option.cloneNode(true)); + } + } + + if (definition.start_node_id) { + initialStatusSelect.value = definition.start_node_id; + } + + if (definition.transitions) { + definition.transitions.forEach(transition => { + const transitionItem = document.createElement('div'); + const fromNode = definition.nodes[transition.from] ? definition.nodes[transition.from].name : 'N/A'; + const toNode = definition.nodes[transition.to] ? definition.nodes[transition.to].name : 'N/A'; + let actions = ''; + if(transition.actions) { + actions = ' - Actions: ' + JSON.stringify(transition.actions); + } + transitionItem.textContent = `${transition.name}: ${fromNode} => ${toNode}${actions}`; + transitionsList.appendChild(transitionItem); + }); + } + + if (definition.eligibility_rules) { + eligibilityRulesJsonTextarea.value = JSON.stringify(definition.eligibility_rules, null, 2); + } + } + + document.getElementById('addStatusBtn').addEventListener('click', function () { + const newStatusInput = document.getElementById('newStatusInput'); + const newStatusName = newStatusInput.value.trim(); + if (newStatusName) { + const newNodeId = (Object.keys(definition.nodes || {}).length + 1).toString(); + if (!definition.nodes) { + definition.nodes = {}; + } + definition.nodes[newNodeId] = { id: newNodeId, name: newStatusName }; + newStatusInput.value = ''; + render(); + } + }); + + document.getElementById('addTransitionBtn').addEventListener('click', function () { + const fromStatus = document.getElementById('fromStatusSelect').value; + const toStatus = document.getElementById('toStatusSelect').value; + const transitionActionJson = document.getElementById('transitionActionJson').value; + + if (fromStatus && toStatus) { + if (!definition.transitions) { + definition.transitions = []; + } + const newTransition = { + name: `Transition ${definition.transitions.length + 1}`, + from: fromStatus, + to: toStatus, + }; + + if(transitionActionJson) { + try { + newTransition.actions = JSON.parse(transitionActionJson); + } catch(e) { + alert('Invalid JSON in transition actions'); + return; + } + } + + definition.transitions.push(newTransition); + document.getElementById('transitionActionJson').value = ''; + render(); + } + }); + + form.addEventListener('submit', function (event) { + definition.start_node_id = document.getElementById('initialStatus').value; + + try { + const eligibilityRules = eligibilityRulesJsonTextarea.value; + if(eligibilityRules) { + definition.eligibility_rules = JSON.parse(eligibilityRules); + } else { + delete definition.eligibility_rules; + } + } catch(e) { + alert('Invalid JSON in eligibility rules'); + event.preventDefault(); + return; + } + + definitionJsonTextarea.value = JSON.stringify(definition, null, 2); + }); + + createProcessModal.addEventListener('show.bs.modal', function (event) { + const button = event.relatedTarget; + const isEdit = button.classList.contains('edit-process-btn'); + + if (isEdit) { + const processId = button.dataset.processId; + const processName = button.dataset.processName; + const processDefinition = button.dataset.processDefinition; + + modalTitle.textContent = 'Edit Process'; + processIdInput.value = processId; + processNameInput.value = processName; + + try { + definition = JSON.parse(processDefinition || '{}'); + } catch(e) { + definition = {}; + } + + } else { + modalTitle.textContent = 'Create Process'; + processIdInput.value = ''; + processNameInput.value = ''; + definition = {}; + } + + eligibilityRulesJsonTextarea.value = ''; + render(); + }); +}); diff --git a/assets/pasted-20260111-143449-befa41d3.png b/assets/pasted-20260111-143449-befa41d3.png new file mode 100644 index 0000000..b8cac1b Binary files /dev/null and b/assets/pasted-20260111-143449-befa41d3.png differ diff --git a/assets/pasted-20260111-144117-aba8ec29.jpg b/assets/pasted-20260111-144117-aba8ec29.jpg new file mode 100644 index 0000000..a09c7b4 Binary files /dev/null and b/assets/pasted-20260111-144117-aba8ec29.jpg differ diff --git a/bni_groups.php b/bni_groups.php new file mode 100644 index 0000000..15477ee --- /dev/null +++ b/bni_groups.php @@ -0,0 +1,201 @@ +query("SELECT * FROM bni_groups ORDER BY display_order"); +$bni_groups = $stmt->fetchAll(PDO::FETCH_ASSOC); + +?> + +
+
+ +
+

BNI Groups

+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
NameCityActiveActions
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + diff --git a/calendar.php b/calendar.php new file mode 100644 index 0000000..2805b88 --- /dev/null +++ b/calendar.php @@ -0,0 +1,352 @@ +format('t'); +$dayOfWeek = $firstDayOfMonth->format('N'); // 1 (for Monday) through 7 (for Sunday) + +// Get events for the current month +$pdo = db(); +$stmt = $pdo->prepare("SELECT c.*, t.name as type_name, t.color as type_color, GROUP_CONCAT(g.name SEPARATOR ', ') as group_names FROM calendar_events c LEFT JOIN event_types t ON c.event_type_id = t.id LEFT JOIN calendar_event_groups ceg ON c.id = ceg.calendar_event_id LEFT JOIN bni_groups g ON ceg.bni_group_id = g.id WHERE MONTH(c.start_datetime) = ? AND YEAR(c.start_datetime) = ? GROUP BY c.id ORDER BY c.start_datetime ASC"); +$stmt->execute([$month, $year]); +$events = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$eventsByDay = []; +foreach ($events as $event) { + $day = (new DateTime($event['start_datetime']))->format('j'); + if (!isset($eventsByDay[$day])) { + $eventsByDay[$day] = []; + } + $eventsByDay[$day][] = $event; +} + +// Get event types for the modal + $stmt_types = $pdo->query("SELECT * FROM event_types ORDER BY display_order"); + $event_types = $stmt_types->fetchAll(PDO::FETCH_ASSOC); + + // Get BNI groups for the modal + $stmt_groups = $pdo->query("SELECT * FROM bni_groups ORDER BY display_order"); + $bni_groups = $stmt_groups->fetchAll(PDO::FETCH_ASSOC); +$prevMonth = $month == 1 ? 12 : $month - 1; +$prevYear = $month == 1 ? $year - 1 : $year; +$nextMonth = $month == 12 ? 1 : $month + 1; +$nextYear = $month == 12 ? $year + 1 : $year; + +?> + + + +
+
+ +
+ +

format('F Y'); ?>

+
+ < Previous + Next > + +
+ + + + + + + + + + + + + + + "; + } + + $currentDay = 1; + while ($currentDay <= $daysInMonth) { + if ($dayOfWeek > 7) { + $dayOfWeek = 1; + echo ""; + } + + echo ""; + + $currentDay++; + $dayOfWeek++; + } + + // Print remaining empty cells + while ($dayOfWeek <= 7) { + echo ""; + $dayOfWeek++; + } + ?> + + +
MondayTuesdayWednesdayThursdayFridaySaturdaySunday
"; + echo "$currentDay"; + if (isset($eventsByDay[$currentDay])) { + echo "
    "; + foreach ($eventsByDay[$currentDay] as $event) { + echo '
  • ' . htmlspecialchars($event['title']); + if (!empty($event['group_names'])) { + echo '
    (' . htmlspecialchars($event['group_names']) . ')'; + } + echo "
  • "; + } + echo "
"; + } + echo "
+
+
+
+ + + + + + + + + + + + diff --git a/cookie.txt b/cookie.txt new file mode 100644 index 0000000..abe37fb --- /dev/null +++ b/cookie.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +localhost FALSE / FALSE 0 PHPSESSID rfo6k0p8l4tpnmgek7dkpkopbl diff --git a/current_definition.json b/current_definition.json new file mode 100644 index 0000000..28672ef --- /dev/null +++ b/current_definition.json @@ -0,0 +1,130 @@ +{ + "start_node_id": "awaiting_call", + "eligibility_rules": [ + { + "type": "person_property_equals", + "params": { + "property": "role", + "value": "guest" + } + } + ], + "nodes": { + "awaiting_call": { + "ui_hints": { + "title": "Follow-up Call", + "status": "active", + "reason": "Awaiting follow-up call with the guest.", + "next_step": "Log the outcome of the call.", + "form_schema": [ + { "name": "call_date", "label": "Call Date", "type": "datetime-local", "default": "now", "required": true }, + { + "name": "outcome_status", + "label": "Call Outcome", + "type": "select", + "required": true, + "options": [ + { "value": "", "label": "-- Select Outcome --" }, + { "value": "no_answer", "label": "No Answer" }, + { "value": "wants_to_join", "label": "Wants to Join" }, + { "value": "declined", "label": "Declined" }, + { "value": "call_later", "label": "Call Later" } + ] + }, + { "name": "note", "label": "Notes", "type": "textarea" }, + { "name": "next_contact_date", "label": "Next Contact Date", "type": "datetime-local", "condition": { "field": "outcome_status", "value": "call_later" }, "required": true } + ] + } + }, + "outcome_router": { "ui_hints": { "status": "processing" } }, + "waiting_for_next_contact": { + "ui_hints": { + "title": "Waiting for Scheduled Call", + "status": "paused", + "reason": "Waiting until the scheduled date for the next call.", + "next_step": "Resume contact on or after the scheduled date." + } + }, + "decide_after_no_answer": { + "ui_hints": { + "title": "No Answer", + "status": "paused", + "reason": "The guest did not answer the call.", + "next_step": "Decide whether to try again or end the process." + } + }, + "end_positive": { "ui_hints": { "title": "Wants to Join", "status": "completed", "reason": "Guest wants to join. New member process started.", "next_step": "" } }, + "end_negative_declined": { "ui_hints": { "title": "Declined", "status": "terminated", "reason": "Guest declined to join.", "next_step": "" } }, + "end_negative_no_answer": { "ui_hints": { "title": "Process Ended", "status": "terminated", "reason": "Process ended after no answer.", "next_step": "" } }, + "end_negative_terminated": { "ui_hints": { "title": "Process Terminated", "status": "terminated", "reason": "Process manually terminated by user.", "next_step": "" } } + }, + "transitions": [ + { + "id": "submit_outcome", + "from": "awaiting_call", + "to": "outcome_router", + "name": "Submit Outcome", + "actions": [ + { "type": "set_data", "params": { "keys": ["call_date", "outcome_status", "note", "next_contact_date"] } } + ] + }, + { + "id": "route_wants_to_join", + "from": "outcome_router", + "to": "end_positive", + "name": "Route to Positive End", + "condition": { "field": "outcome_status", "value": "wants_to_join" }, + "actions": [ + { "type": "start_process", "process_code": "obsluga-przyjecia-nowego-czlonka" } + ] + }, + { + "id": "route_declined", + "from": "outcome_router", + "to": "end_negative_declined", + "name": "Route to Declined", + "condition": { "field": "outcome_status", "value": "declined" } + }, + { + "id": "route_no_answer", + "from": "outcome_router", + "to": "decide_after_no_answer", + "name": "Route to No Answer", + "condition": { "field": "outcome_status", "value": "no_answer" } + }, + { + "id": "route_call_later", + "from": "outcome_router", + "to": "waiting_for_next_contact", + "name": "Route to Call Later", + "condition": { "field": "outcome_status", "value": "call_later" } + }, + { "id": "continue_attempts", "from": "decide_after_no_answer", "to": "awaiting_call", "name": "Try Again" }, + { "id": "end_attempts", "from": "decide_after_no_answer", "to": "end_negative_no_answer", "name": "End Process" }, + { "id": "resume_contact", "from": "waiting_for_next_contact", "to": "awaiting_call", "name": "Resume / Attempt Call" }, + { + "id": "terminate_from_awaiting_call", + "from": "awaiting_call", + "to": "end_negative_terminated", + "name": "Zakończ proces", + "actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ], + "form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ] + }, + { + "id": "terminate_from_decide", + "from": "decide_after_no_answer", + "to": "end_negative_terminated", + "name": "Zakończ proces", + "actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ], + "form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ] + }, + { + "id": "terminate_from_waiting", + "from": "waiting_for_next_contact", + "to": "end_negative_terminated", + "name": "Zakończ proces", + "actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ], + "form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ] + } + ] +} \ No newline at end of file diff --git a/db/config.php b/db/config.php new file mode 100644 index 0000000..c69ecef --- /dev/null +++ b/db/config.php @@ -0,0 +1,17 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } + return $pdo; +} diff --git a/db/migrations/001_add_process_definition_json.php b/db/migrations/001_add_process_definition_json.php new file mode 100644 index 0000000..a2b7375 --- /dev/null +++ b/db/migrations/001_add_process_definition_json.php @@ -0,0 +1,8 @@ +exec($sql); + echo "Migration 001 applied: Added definition_json to process_definitions.\n"; +} + diff --git a/db/migrations/002_create_calendar_events_table.php b/db/migrations/002_create_calendar_events_table.php new file mode 100644 index 0000000..07cf954 --- /dev/null +++ b/db/migrations/002_create_calendar_events_table.php @@ -0,0 +1,17 @@ +exec($sql); + echo "Migration 002 applied: Created calendar_events table.\n"; +} + + diff --git a/db/migrations/003_create_event_types_table.php b/db/migrations/003_create_event_types_table.php new file mode 100644 index 0000000..163a56d --- /dev/null +++ b/db/migrations/003_create_event_types_table.php @@ -0,0 +1,26 @@ +exec($sql); + + // Add some default values + $sql_insert = "INSERT INTO event_types (name, color) VALUES + ('Meeting', '#007bff'), + ('Training', '#ffc107'), + ('Other', '#28a745');"; + $pdo->exec($sql_insert); + + + echo "Migration 003 completed successfully.\n"; +} catch (PDOException $e) { + die("Migration 003 failed: " . $e->getMessage() . "\n"); +} \ No newline at end of file diff --git a/db/migrations/004_add_event_type_id_to_calendar_events.php b/db/migrations/004_add_event_type_id_to_calendar_events.php new file mode 100644 index 0000000..d07992b --- /dev/null +++ b/db/migrations/004_add_event_type_id_to_calendar_events.php @@ -0,0 +1,34 @@ +exec($sql_add_column); + + // Set a default value for existing rows to avoid foreign key constraint errors + // Get the ID of the 'Other' event type + $stmt = $pdo->query("SELECT id FROM event_types WHERE name = 'Other' LIMIT 1"); + $other_type = $stmt->fetch(PDO::FETCH_ASSOC); + $default_type_id = $other_type ? $other_type['id'] : 1; // Fallback to 1 if 'Other' not found + + $sql_update_existing = "UPDATE calendar_events SET event_type_id = ? WHERE event_type_id IS NULL;"; + $stmt_update = $pdo->prepare($sql_update_existing); + $stmt_update->execute([$default_type_id]); + + // Now, add the foreign key constraint + $sql_add_fk = "ALTER TABLE calendar_events ADD CONSTRAINT fk_event_type FOREIGN KEY (event_type_id) REFERENCES event_types(id) ON DELETE SET NULL;"; + $pdo->exec($sql_add_fk); + + echo "Migration 004 completed successfully.\n"; +} catch (PDOException $e) { + // Check if the column already exists, which might happen on re-runs + if (strpos($e->getMessage(), 'Duplicate column name') !== false) { + echo "Migration 004 seems to be already applied (Column exists). Skipping.\n"; + } else { + die("Migration 004 failed: " . $e->getMessage() . "\n"); + } +} + diff --git a/db/migrations/005_create_bni_groups_table.php b/db/migrations/005_create_bni_groups_table.php new file mode 100644 index 0000000..048dec0 --- /dev/null +++ b/db/migrations/005_create_bni_groups_table.php @@ -0,0 +1,18 @@ +exec($sql); + echo "Table 'bni_groups' created successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error creating table 'bni_groups': " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/006_add_recurrence_to_calendar_events.php b/db/migrations/006_add_recurrence_to_calendar_events.php new file mode 100644 index 0000000..3f10b1c --- /dev/null +++ b/db/migrations/006_add_recurrence_to_calendar_events.php @@ -0,0 +1,20 @@ +exec($sql); + + echo "Migration 006 executed successfully: Added recurrence columns to calendar_events table.\n"; + +} catch (Exception $e) { + die("Error executing migration 006: " . $e->getMessage() . "\n"); +} \ No newline at end of file diff --git a/db/migrations/007_remove_type_from_calendar_events.php b/db/migrations/007_remove_type_from_calendar_events.php new file mode 100644 index 0000000..bf39dec --- /dev/null +++ b/db/migrations/007_remove_type_from_calendar_events.php @@ -0,0 +1,17 @@ +exec($sql); + echo "Migration 007 completed successfully: Dropped 'type' column from calendar_events.\n"; +} catch (PDOException $e) { + // Check if the column has already been dropped + if (strpos($e->getMessage(), 'column not found') !== false || strpos($e->getMessage(), 'Unknown column') !== false) { + echo "Migration 007 seems to be already applied (Column not found). Skipping.\n"; + } else { + die("Migration 007 failed: " . $e->getMessage() . "\n"); + } +} + diff --git a/db/migrations/008_create_roles_table.php b/db/migrations/008_create_roles_table.php new file mode 100644 index 0000000..8b0aa4f --- /dev/null +++ b/db/migrations/008_create_roles_table.php @@ -0,0 +1,16 @@ +exec($sql); + echo "Table 'roles' created successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error creating table 'roles': " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/009_create_user_roles_table.php b/db/migrations/009_create_user_roles_table.php new file mode 100644 index 0000000..23a269e --- /dev/null +++ b/db/migrations/009_create_user_roles_table.php @@ -0,0 +1,18 @@ +exec($sql); + echo "Table 'user_roles' created successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error creating table 'user_roles': " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/010_add_display_order_to_event_types.php b/db/migrations/010_add_display_order_to_event_types.php new file mode 100644 index 0000000..a1deab8 --- /dev/null +++ b/db/migrations/010_add_display_order_to_event_types.php @@ -0,0 +1,12 @@ +exec($sql); + echo "Migration 010_add_display_order_to_event_types executed successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error executing migration 010_add_display_order_to_event_types: " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/011_add_display_order_to_bni_groups.php b/db/migrations/011_add_display_order_to_bni_groups.php new file mode 100644 index 0000000..7fe3bde --- /dev/null +++ b/db/migrations/011_add_display_order_to_bni_groups.php @@ -0,0 +1,12 @@ +exec($sql); + echo "Migration 011_add_display_order_to_bni_groups executed successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error executing migration 011_add_display_order_to_bni_groups: " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/012_add_display_order_to_roles.php b/db/migrations/012_add_display_order_to_roles.php new file mode 100644 index 0000000..7493560 --- /dev/null +++ b/db/migrations/012_add_display_order_to_roles.php @@ -0,0 +1,12 @@ +exec($sql); + echo "Migration 012_add_display_order_to_roles executed successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error executing migration 012_add_display_order_to_roles: " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/013_rename_roles_to_functions.php b/db/migrations/013_rename_roles_to_functions.php new file mode 100644 index 0000000..55b2e41 --- /dev/null +++ b/db/migrations/013_rename_roles_to_functions.php @@ -0,0 +1,21 @@ +exec($sql); + echo "Table 'roles' renamed to 'functions' successfully." . PHP_EOL; + + $sql = "RENAME TABLE `user_roles` TO `user_functions`;"; + $pdo->exec($sql); + echo "Table 'user_roles' renamed to 'user_functions' successfully." . PHP_EOL; + + $sql = "ALTER TABLE `user_functions` CHANGE `role_id` `function_id` INT(11) UNSIGNED NOT NULL;"; + $pdo->exec($sql); + echo "Column 'role_id' renamed to 'function_id' in 'user_functions' table successfully." . PHP_EOL; + +} catch (PDOException $e) { + echo "Error renaming tables or columns: " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/014_add_role_to_people_table.php b/db/migrations/014_add_role_to_people_table.php new file mode 100644 index 0000000..1e5d543 --- /dev/null +++ b/db/migrations/014_add_role_to_people_table.php @@ -0,0 +1,21 @@ +query("SELECT `role` FROM `people` LIMIT 1"); + } catch (PDOException $e) { + // Column does not exist, so add it + $pdo->exec("ALTER TABLE `people` ADD COLUMN `role` VARCHAR(50) NOT NULL DEFAULT 'członek' AFTER `email`"); + echo "Migration 014: Added 'role' column to 'people' table.\n"; + return; + } + + echo "Migration 014: 'role' column already exists in 'people' table. No changes made.\n"; +} + +migrate_014_add_role_to_people_table(); + diff --git a/db/migrations/015_add_group_id_to_functions.php b/db/migrations/015_add_group_id_to_functions.php new file mode 100644 index 0000000..116ee52 --- /dev/null +++ b/db/migrations/015_add_group_id_to_functions.php @@ -0,0 +1,61 @@ +query("SHOW COLUMNS FROM `functions` LIKE 'bni_group_id'"); + $exists = $stmt->fetch(PDO::FETCH_ASSOC); + + // Check if the foreign key constraint already exists + $stmt = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'functions' AND CONSTRAINT_NAME = 'fk_functions_bni_group'"); + $fk_exists = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($fk_exists) { + echo "Migration 015 skipped: Foreign key 'fk_functions_bni_group' already exists." . PHP_EOL; + return; + } + + if (!$exists) { + // Add the bni_group_id column allowing NULLs temporarily + $pdo->exec("ALTER TABLE `functions` ADD COLUMN `bni_group_id` INT NULL AFTER `id`;"); + echo "Column 'bni_group_id' added." . PHP_EOL; + } + + // Find a default group to assign to existing functions + $stmt = $pdo->query("SELECT id FROM `bni_groups` ORDER BY id LIMIT 1"); + $default_group = $stmt->fetch(PDO::FETCH_ASSOC); + $default_group_id = $default_group ? $default_group['id'] : null; + + if ($default_group_id) { + // Update existing functions to use the default group where bni_group_id is not valid + $pdo->exec("UPDATE `functions` SET `bni_group_id` = {$default_group_id} WHERE `bni_group_id` IS NULL OR `bni_group_id` NOT IN (SELECT id FROM bni_groups)"); + echo "Existing functions updated with a valid group ID." . PHP_EOL; + + // Now that existing rows are updated, alter the column to be NOT NULL + $pdo->exec("ALTER TABLE `functions` MODIFY COLUMN `bni_group_id` INT NOT NULL;"); + echo "Column 'bni_group_id' modified to NOT NULL." . PHP_EOL; + + // Add the foreign key constraint + $pdo->exec("ALTER TABLE `functions` ADD CONSTRAINT `fk_functions_bni_group` FOREIGN KEY (`bni_group_id`) REFERENCES `bni_groups`(`id`) ON DELETE CASCADE;"); + echo "Migration 015 successfully applied." . PHP_EOL; + + } else { + // If there are no groups, we can't proceed if there are functions. + $stmt = $pdo->query("SELECT COUNT(*) FROM `functions`"); + $function_count = $stmt->fetchColumn(); + if ($function_count > 0) { + die("Migration 015 failed: Cannot create a required association to a BNI group because no BNI groups exist, but functions that need them do exist." . PHP_EOL); + } else { + // No functions exist, so we can just add the column and constraint + $pdo->exec("ALTER TABLE `functions` MODIFY COLUMN `bni_group_id` INT NOT NULL;"); + $pdo->exec("ALTER TABLE `functions` ADD CONSTRAINT `fk_functions_bni_group` FOREIGN KEY (`bni_group_id`) REFERENCES `bni_groups`(`id`) ON DELETE CASCADE;"); + echo "Migration 015 successfully applied (no existing functions to update)." . PHP_EOL; + } + } + + +} catch (PDOException $e) { + die("Migration 015 failed: " . $e->getMessage() . PHP_EOL); +} diff --git a/db/migrations/016_create_calendar_event_groups_table.php b/db/migrations/016_create_calendar_event_groups_table.php new file mode 100644 index 0000000..130ee8f --- /dev/null +++ b/db/migrations/016_create_calendar_event_groups_table.php @@ -0,0 +1,30 @@ +query("SHOW TABLES LIKE 'calendar_event_groups'"); + $table_exists = $stmt->rowCount() > 0; + + if (!$table_exists) { + $pdo->exec(" + CREATE TABLE `calendar_event_groups` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `calendar_event_id` INT NOT NULL, + `bni_group_id` INT NOT NULL, + FOREIGN KEY (`calendar_event_id`) REFERENCES `calendar_events`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`bni_group_id`) REFERENCES `bni_groups`(`id`) ON DELETE CASCADE, + UNIQUE KEY `unique_event_group` (`calendar_event_id`, `bni_group_id`) + ) + "); + echo "Table 'calendar_event_groups' created successfully.\n"; + } else { + echo "Table 'calendar_event_groups' already exists.\n"; + } + +} catch (PDOException $e) { + die("Migration failed: " . $e->getMessage() . "\n"); +} + diff --git a/db/migrations/017_add_group_id_to_people.php b/db/migrations/017_add_group_id_to_people.php new file mode 100644 index 0000000..7538268 --- /dev/null +++ b/db/migrations/017_add_group_id_to_people.php @@ -0,0 +1,19 @@ +query("SHOW COLUMNS FROM people LIKE 'bni_group_id'"); + if ($stmt->rowCount() == 0) { + $pdo->exec("ALTER TABLE people ADD COLUMN bni_group_id INT NULL AFTER `role`;"); + $pdo->exec("ALTER TABLE people ADD CONSTRAINT fk_people_bni_group FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE SET NULL;"); + } + + echo "Migration 017 completed successfully." . PHP_EOL; + +} catch (PDOException $e) { + die("Migration 017 failed: " . $e->getMessage()); +} +?> \ No newline at end of file diff --git a/db/migrations/018_add_details_to_people_table.php b/db/migrations/018_add_details_to_people_table.php new file mode 100644 index 0000000..1969f48 --- /dev/null +++ b/db/migrations/018_add_details_to_people_table.php @@ -0,0 +1,24 @@ +exec($sql); + echo "Migration 018 successfully applied: Added detail columns to people table.\n"; +} catch (PDOException $e) { + echo "Error applying migration 018: " . $e->getMessage() . "\n"; + exit(1); +} + diff --git a/db/migrations/019_add_extra_fields_to_people.php b/db/migrations/019_add_extra_fields_to_people.php new file mode 100644 index 0000000..83e2f36 --- /dev/null +++ b/db/migrations/019_add_extra_fields_to_people.php @@ -0,0 +1,40 @@ + 'VARCHAR(255) DEFAULT NULL', + 'industry' => 'VARCHAR(255) DEFAULT NULL', + 'company_size_revenue' => 'VARCHAR(255) DEFAULT NULL', + 'business_description' => 'TEXT DEFAULT NULL', + 'company_logo_path' => 'VARCHAR(255) DEFAULT NULL', + 'person_photo_path' => 'VARCHAR(255) DEFAULT NULL', + 'gains_sheet_path' => 'VARCHAR(255) DEFAULT NULL', + 'top_wanted_contacts_path' => 'VARCHAR(255) DEFAULT NULL', + 'top_owned_contacts_path' => 'VARCHAR(255) DEFAULT NULL' +]; + +$sql_check_columns = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'people'"; +$stmt_check = $pdo->query($sql_check_columns); +$existing_columns = $stmt_check->fetchAll(PDO::FETCH_COLUMN); + +$sql_alter_parts = []; +foreach ($columns as $column_name => $column_definition) { + if (!in_array($column_name, $existing_columns)) { + $sql_alter_parts[] = "ADD COLUMN `$column_name` $column_definition"; + } +} + +if (!empty($sql_alter_parts)) { + $sql = "ALTER TABLE people " . implode(', ', $sql_alter_parts); + try { + $pdo->exec($sql); + echo "Migration 019 completed successfully: Added new fields to people table.\n"; + } catch (PDOException $e) { + die("Migration 019 failed: " . $e->getMessage() . "\n"); + } +} else { + echo "Migration 019 skipped: All columns already exist.\n"; +} + diff --git a/db/migrations/021_rename_camelcase_columns.php b/db/migrations/021_rename_camelcase_columns.php new file mode 100644 index 0000000..ffa7035 --- /dev/null +++ b/db/migrations/021_rename_camelcase_columns.php @@ -0,0 +1,26 @@ +exec("ALTER TABLE `process_instances` CHANGE `lastActivityAt` `last_activity_at` TIMESTAMP NULL;"); + + echo "Columns in 'process_instances' table renamed.\n"; + + // Process events table + $pdo->exec("ALTER TABLE `process_events` CHANGE `processInstanceId` `process_instance_id` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_events` CHANGE `createdBy` `created_by` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_events` CHANGE `createdAt` `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP;"); + + echo "Columns in 'process_events' table renamed.\n"; + + echo "Migration 021 completed successfully.\n"; + +} catch (PDOException $e) { + die("Migration 021 failed: " . $e->getMessage()); +} + diff --git a/db/migrations/022_add_is_active_to_process_definitions.php b/db/migrations/022_add_is_active_to_process_definitions.php new file mode 100644 index 0000000..988103a --- /dev/null +++ b/db/migrations/022_add_is_active_to_process_definitions.php @@ -0,0 +1,22 @@ +exec($sql); + echo "Migration successful: is_active column added to process_definitions table.\n"; + } catch (PDOException $e) { + // Ignore if column already exists + if ($e->getCode() !== '42S21') { + die("Migration failed: " . $e->getMessage() . "\n"); + } + } +} + +// If called directly, run the migration +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) { + require_once __DIR__ . '/../../db/config.php'; + $pdo = db(); + migrate_022($pdo); +} \ No newline at end of file diff --git a/db/migrations/023_add_new_member_process.php b/db/migrations/023_add_new_member_process.php new file mode 100644 index 0000000..d94bbda --- /dev/null +++ b/db/migrations/023_add_new_member_process.php @@ -0,0 +1,22 @@ +prepare("INSERT INTO process_definitions (name, code, definition_json, is_active) VALUES (?, ?, ?, ?)"); + $stmt->execute([ + 'Obsługa przyjęcia nowego członka', + 'obsluga-przyjecia-nowego-czlonka', + $json_definition, + 1 + ]); +} + +// If called directly, run the migration +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) { + require_once __DIR__ . '/../../db/config.php'; + $pdo = db(); + migrate_023($pdo); + echo "Migration 023 executed successfully.\n"; +} \ No newline at end of file diff --git a/db/migrations/024_update_follow_up_process.php b/db/migrations/024_update_follow_up_process.php new file mode 100644 index 0000000..0b29c72 --- /dev/null +++ b/db/migrations/024_update_follow_up_process.php @@ -0,0 +1,154 @@ +prepare("UPDATE process_definitions SET definition_json = :json, start_node_id = 'awaiting_call' WHERE code = :code"); + $stmt->execute([ + ':json' => $json_definition, + ':code' => $process_code, + ]); + + echo "Migration 024 executed successfully: Updated 'guest_handling' process definition.\n"; +} + +// Direct execution guard +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) { + try { + migrate_024(); + } catch (Exception $e) { + echo "Migration 24 failed: " . $e->getMessage() . "\n"; + } +} diff --git a/db/migrations/025_update_follow_up_process_structured.php b/db/migrations/025_update_follow_up_process_structured.php new file mode 100644 index 0000000..6e5ca5b --- /dev/null +++ b/db/migrations/025_update_follow_up_process_structured.php @@ -0,0 +1,161 @@ +prepare("UPDATE process_definitions SET definition_json = :json, start_node_id = 'awaiting_call' WHERE code = :code"); + $stmt->execute([ + ':json' => $json_definition, + ':code' => $process_code, + ]); + + echo "Migration 025 executed successfully: Updated 'guest_handling' process definition with structured data and router node."; +} + +// Direct execution guard +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) +{ + try { + migrate_025(); + } catch (Exception $e) { + echo "Migration 25 failed: " . $e->getMessage() . "\n"; + } +} \ No newline at end of file diff --git a/db/migrations/026_simplify_follow_up_ux.php b/db/migrations/026_simplify_follow_up_ux.php new file mode 100644 index 0000000..8b314ad --- /dev/null +++ b/db/migrations/026_simplify_follow_up_ux.php @@ -0,0 +1,11 @@ +prepare("UPDATE process_definitions SET definition_json = :json WHERE id = 4"); +$stmt->execute(['json' => $new_json_definition]); + +echo "Process definition for follow_up (ID: 4) updated successfully."; diff --git a/db/migrations/027_deactivate_sales_pipeline.php b/db/migrations/027_deactivate_sales_pipeline.php new file mode 100644 index 0000000..f6c7133 --- /dev/null +++ b/db/migrations/027_deactivate_sales_pipeline.php @@ -0,0 +1,19 @@ +exec($sql); + echo "Migration successful: Deactivated 'Sales Pipeline' process.\n"; + } catch (PDOException $e) { + die("Migration failed: " . $e->getMessage() . "\n"); + } +} + +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) { + require_once __DIR__ . '/../../db/config.php'; + $pdo = db(); + migrate_027($pdo); +} + diff --git a/db/migrations/028_add_sort_order_to_process_definitions.php b/db/migrations/028_add_sort_order_to_process_definitions.php new file mode 100644 index 0000000..8ba2c38 --- /dev/null +++ b/db/migrations/028_add_sort_order_to_process_definitions.php @@ -0,0 +1,22 @@ +exec($sql); + echo "Migration successful: sort_order column added to process_definitions table.\n"; + } catch (PDOException $e) { + // Ignore if column already exists + if (strpos($e->getMessage(), 'Duplicate column name') === false) { + die("Migration failed: " . $e->getMessage() . "\n"); + } + } +} + +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) { + require_once __DIR__ . '/../../db/config.php'; + $pdo = db(); + migrate_028($pdo); +} + diff --git a/db/migrations/029_set_initial_process_order.php b/db/migrations/029_set_initial_process_order.php new file mode 100644 index 0000000..2bc40c6 --- /dev/null +++ b/db/migrations/029_set_initial_process_order.php @@ -0,0 +1,32 @@ + 10, + 'Follow-up' => 20, + 'Obsługa przyjęcia nowego członka' => 30, + 'Wprowadzenie nowego członka' => 40, + 'Szkolenia dla młodego członka' => 50, + 'Mentoring' => 60, + ]; + + try { + $stmt = $pdo->prepare("UPDATE process_definitions SET sort_order = :sort_order WHERE name = :name"); + + foreach ($process_order as $name => $order) { + $stmt->execute(['sort_order' => $order, 'name' => $name]); + } + + echo "Migration successful: Initial process order set.\n"; + } catch (PDOException $e) { + die("Migration failed: " . $e->getMessage() . "\n"); + } +} + +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) { + require_once __DIR__ . '/../../db/config.php'; + $pdo = db(); + migrate_029($pdo); +} + diff --git a/db/migrations/030_create_meetings_table.php b/db/migrations/030_create_meetings_table.php new file mode 100644 index 0000000..4c915aa --- /dev/null +++ b/db/migrations/030_create_meetings_table.php @@ -0,0 +1,23 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $sql = "CREATE TABLE IF NOT EXISTS meetings ( + id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + bni_group_id INT(11) UNSIGNED NOT NULL, + meeting_datetime DATETIME NOT NULL, + meeting_key VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY meeting_key (meeting_key), + FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE CASCADE + )"; + + $pdoconn->exec($sql); + echo "Table 'meetings' created successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error creating table: " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/031_create_meeting_attendance_table.php b/db/migrations/031_create_meeting_attendance_table.php new file mode 100644 index 0000000..97c3c11 --- /dev/null +++ b/db/migrations/031_create_meeting_attendance_table.php @@ -0,0 +1,27 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $sql = "CREATE TABLE IF NOT EXISTS meeting_attendance ( + id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + meeting_id INT(11) UNSIGNED NOT NULL, + person_id INT(11) UNSIGNED NOT NULL, + attendance_status ENUM('present', 'absent', 'substitute', 'none') NOT NULL DEFAULT 'none', + guest_survey ENUM('1', '2', '3'), + updated_by INT(11) UNSIGNED, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY meeting_person (meeting_id, person_id), + FOREIGN KEY (meeting_id) REFERENCES meetings(id) ON DELETE CASCADE, + FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE, + FOREIGN KEY (updated_by) REFERENCES people(id) ON DELETE SET NULL + )"; + + $pdoconn->exec($sql); + echo "Table 'meeting_attendance' created successfully." . PHP_EOL; +} catch (PDOException $e) { + echo "Error creating table: " . $e->getMessage() . PHP_EOL; + exit(1); +} diff --git a/db/migrations/032_add_meeting_key_to_meetings.php b/db/migrations/032_add_meeting_key_to_meetings.php new file mode 100644 index 0000000..9a331ff --- /dev/null +++ b/db/migrations/032_add_meeting_key_to_meetings.php @@ -0,0 +1,73 @@ +exec("ALTER TABLE meetings ADD COLUMN bni_group_id INT(11) UNSIGNED NULL;"); + echo "SUCCESS: Added nullable bni_group_id column.\n"; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'Duplicate column name') !== false) { + echo "INFO: bni_group_id column already exists.\n"; + } else { + throw $e; + } + } + + // Step 2: Add meeting_datetime column + try { + echo "Step 2: Adding meeting_datetime column...\n"; + $db->exec("ALTER TABLE meetings ADD COLUMN meeting_datetime DATETIME NULL;"); + echo "SUCCESS: Added nullable meeting_datetime column.\n"; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'Duplicate column name') !== false) { + echo "INFO: meeting_datetime column already exists.\n"; + } else { + throw $e; + } + } + + // Step 3: Add meeting_key column + try { + echo "Step 3: Adding meeting_key column...\n"; + $db->exec("ALTER TABLE meetings ADD COLUMN meeting_key VARCHAR(255) NULL;"); + echo "SUCCESS: Added nullable meeting_key column.\n"; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'Duplicate column name') !== false) { + echo "INFO: meeting_key column already exists.\n"; + } else { + throw $e; + } + } + + // Step 4: Populate meeting_key + echo "Step 4: Populating meeting_key...\n"; + $updateStmt = $db->prepare("UPDATE meetings SET meeting_key = CONCAT(bni_group_id, '_', meeting_datetime) WHERE (meeting_key IS NULL OR meeting_key = '') AND bni_group_id IS NOT NULL AND meeting_datetime IS NOT NULL"); + $updateStmt->execute(); + echo "SUCCESS: Populated meeting_key for " . $updateStmt->rowCount() . " rows.\n"; + + // Step 5: Add unique index on meeting_key + try { + echo "Step 5: Adding unique index on meeting_key...\n"; + $db->exec("ALTER TABLE meetings ADD UNIQUE (meeting_key);"); + echo "SUCCESS: Added unique index on meeting_key.\n"; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'Duplicate entry') !== false) { + echo "WARNING: Could not add UNIQUE index because duplicate meeting_key values exist.\n"; + } elseif (strpos($e->getMessage(), 'already exists') !== false) { + echo "INFO: Unique index on meeting_key already exists.\n"; + } else { + throw $e; + } + } + + echo "Migration 032 completed successfully.\n"; + +} catch (Exception $e) { + error_log("Migration 032 failed: " . $e->getMessage()); + die("FATAL: Migration 032 failed: " . $e->getMessage() . "\n"); +} \ No newline at end of file diff --git a/db/migrations/033_standardize_bni_group_id.php b/db/migrations/033_standardize_bni_group_id.php new file mode 100644 index 0000000..0120d0c --- /dev/null +++ b/db/migrations/033_standardize_bni_group_id.php @@ -0,0 +1,71 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + echo "Starting migration 033: Standardize bni_group_id in meeting_attendance. +"; + + // Ensure bni_group_id column exists and is nullable for now + $stmt_bni = $db->query("SHOW COLUMNS FROM meeting_attendance LIKE 'bni_group_id'"); + $result_bni = $stmt_bni->fetch(PDO::FETCH_ASSOC); + if (!$result_bni) { + $db->exec("ALTER TABLE meeting_attendance ADD COLUMN bni_group_id INT(11) NULL;"); + echo "SUCCESS: Added nullable 'bni_group_id' column. +"; + } else { + $db->exec("ALTER TABLE meeting_attendance MODIFY bni_group_id INT(11) NULL;"); + echo "SUCCESS: Modified 'bni_group_id' to be nullable. +"; + } + + // Check for orphaned bni_group_ids + $orphan_check = $db->query("SELECT ma.bni_group_id, COUNT(*) as count FROM meeting_attendance ma LEFT JOIN bni_groups bg ON ma.bni_group_id = bg.id WHERE bg.id IS NULL AND ma.bni_group_id IS NOT NULL GROUP BY ma.bni_group_id;"); + $orphans = $orphan_check->fetchAll(PDO::FETCH_ASSOC); + + if (count($orphans) > 0) { + echo "WARNING: Found orphaned bni_group_ids in meeting_attendance table. +"; + foreach ($orphans as $orphan) { + echo " - bni_group_id: " . $orphan['bni_group_id'] . " (" . $orphan['count'] . " rows) +"; + } + // For now, we will set them to NULL to allow FK creation + foreach ($orphans as $orphan) { + if ($orphan['bni_group_id'] !== 0) { // we don't want to update rows that have 0 as this is default + $update_stmt = $db->prepare("UPDATE meeting_attendance SET bni_group_id = NULL WHERE bni_group_id = :orphan_id;"); + $update_stmt->execute(['orphan_id' => $orphan['bni_group_id']]); + echo "Set " . $update_stmt->rowCount() . " rows with orphaned bni_group_id " . $orphan['bni_group_id'] . " to NULL. +"; + } + } + } else { + echo "INFO: No orphaned bni_group_ids found. +"; + } + + // Add foreign key if it doesn't exist + $fk_check = $db->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'meeting_attendance' AND COLUMN_NAME = 'bni_group_id' AND REFERENCED_TABLE_NAME = 'bni_groups';")->fetch(); + if (!$fk_check) { + echo "Adding foreign key on 'bni_group_id' to 'bni_groups.id'. +"; + $db->exec("ALTER TABLE meeting_attendance ADD CONSTRAINT fk_meeting_attendance_bni_group FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE CASCADE;"); + echo "SUCCESS: Foreign key added. +"; + } else { + echo "INFO: Foreign key on 'bni_group_id' already exists. +"; + } + + // Now, alter the column to be NOT NULL. This will fail if there are any NULLs left. + // I will not do this for now, as the user might want to check the orphaned data first. + // I will let the column be nullable for now. + + echo "Migration 033 completed successfully (bni_group_id is currently nullable). +"; + +} catch (Exception $e) { + error_log("Migration 033 failed: " . $e->getMessage()); + die("FATAL: Migration 033 failed: " . $e->getMessage() . "\n"); +} diff --git a/db/migrations/034_set_bni_group_id_not_null.php b/db/migrations/034_set_bni_group_id_not_null.php new file mode 100644 index 0000000..56168b2 --- /dev/null +++ b/db/migrations/034_set_bni_group_id_not_null.php @@ -0,0 +1,37 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + echo "Starting migration 034: Set bni_group_id in meeting_attendance to NOT NULL.\n"; + + // First, check if there are any NULL values in the bni_group_id column. + $null_check = $db->query("SELECT COUNT(*) FROM meeting_attendance WHERE bni_group_id IS NULL;")->fetchColumn(); + + if ($null_check > 0) { + echo "WARNING: Found $null_check rows with NULL bni_group_id. Attempting to populate them.\n"; + + // Try to populate from the corresponding meeting record + $update_sql = "UPDATE meeting_attendance ma JOIN meetings m ON ma.meeting_id = m.id SET ma.bni_group_id = m.bni_group_id WHERE ma.bni_group_id IS NULL AND m.bni_group_id IS NOT NULL;"; + $stmt = $db->exec($update_sql); + echo "Populated $stmt rows based on meeting ID.\n"; + + // Re-check for NULLs + $null_check_after = $db->query("SELECT COUNT(*) FROM meeting_attendance WHERE bni_group_id IS NULL;")->fetchColumn(); + if($null_check_after > 0) { + // If still NULLs, we have to fail. + throw new Exception("$null_check_after rows still have NULL bni_group_id after population attempt. Cannot proceed."); + } + } + + echo "No NULL values found in bni_group_id. Altering column to NOT NULL.\n"; + $db->exec("ALTER TABLE meeting_attendance MODIFY bni_group_id INT(11) NOT NULL;"); + echo "SUCCESS: Column bni_group_id in meeting_attendance is now NOT NULL.\n"; + + echo "Migration 034 completed successfully.\n"; + +} catch (Exception $e) { + error_log("Migration 034 failed: " . $e->getMessage()); + die("FATAL: Migration 034 failed: " . $e->getMessage() . "\n"); +} diff --git a/db/migrations/035_cleanup_meetings_table.php b/db/migrations/035_cleanup_meetings_table.php new file mode 100644 index 0000000..734dd13 --- /dev/null +++ b/db/migrations/035_cleanup_meetings_table.php @@ -0,0 +1,18 @@ +exec("UPDATE meetings SET bni_group_id = group_id WHERE bni_group_id IS NULL"); + +// Step 2: Modify bni_group_id to be NOT NULL +$db->exec("ALTER TABLE meetings MODIFY COLUMN bni_group_id INT(11) NOT NULL"); + +// Step 3: Find and drop the foreign key constraint on group_id +$db->exec("ALTER TABLE meetings DROP FOREIGN KEY `meetings_ibfk_1`"); + +// Step 4: Drop the old group_id column +$db->exec("ALTER TABLE meetings DROP COLUMN group_id"); + +echo "Migration 035 cleanup meetings table applied successfully."; diff --git a/db_setup.php b/db_setup.php new file mode 100644 index 0000000..1823e68 --- /dev/null +++ b/db_setup.php @@ -0,0 +1,134 @@ +exec("CREATE TABLE IF NOT EXISTS `people` ( + `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `first_name` VARCHAR(255) NOT NULL, + `last_name` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) NOT NULL UNIQUE, + `password` VARCHAR(255) NULL, + `company_name` VARCHAR(255) DEFAULT NULL, + `phone` VARCHAR(50) DEFAULT NULL, + `role` ENUM('admin', 'team_member', 'member', 'guest') NOT NULL DEFAULT 'guest', + `is_user` BOOLEAN NOT NULL DEFAULT FALSE, + `active` BOOLEAN NOT NULL DEFAULT TRUE, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + echo "People table created or already exists.\n"; + + // Seed default admin user + $stmt = $pdo->prepare("SELECT id FROM people WHERE email = ?"); + $stmt->execute(['admin@example.com']); + if ($stmt->fetchColumn() === false) { + $password = password_hash('password', PASSWORD_DEFAULT); + $insert_stmt = $pdo->prepare( + "INSERT INTO people (email, password, role, first_name, last_name, is_user, active) VALUES (?, ?, 'admin', 'Admin', 'User', TRUE, TRUE)" + ); + $insert_stmt->execute(['admin@example.com', $password]); + echo "Default admin user created. Email: admin@example.com, Password: password\n"; + } + + // 2. Process Definitions table + $pdo->exec("CREATE TABLE IF NOT EXISTS `process_definitions` ( + `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `code` VARCHAR(255) NOT NULL UNIQUE, + `name` VARCHAR(255) NOT NULL, + `description` TEXT, + `version` INT NOT NULL DEFAULT 1, + `is_active` BOOLEAN NOT NULL DEFAULT TRUE, + `start_node_id` VARCHAR(255), + `definition_json` TEXT, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + echo "Process definitions table created or already exists.\n"; + + // Seed process_definitions + $processes = [ + ['code' => 'mentoring', 'name' => 'Mentoring nowego czlonka', 'description' => 'Proces wdrozenia nowego czlonka do organizacji.'], + ['code' => 'meeting_preparation', 'name' => 'Przygotowanie spotkania grupy', 'description' => 'Proces przygotowania do spotkania grupy, w tym agenda, materialy, etc.'], + ['code' => 'guest_handling', 'name' => 'Obsluga goscia', 'description' => 'Proces obslugi gosci odwiedzajacych organizacje.'] + ]; + $stmt = $pdo->prepare("SELECT id FROM process_definitions WHERE code = ?"); + $insert_stmt = $pdo->prepare("INSERT INTO process_definitions (code, name, description) VALUES (?, ?, ?)"); + foreach ($processes as $process) { + $stmt->execute([$process['code']]); + if ($stmt->fetchColumn() === false) { + $insert_stmt->execute([$process['code'], $process['name'], $process['description']]); + echo "Seeded process: " . $process['name'] . "\n"; + } + } + + // 3. Process Instances table (updated FK) + $pdo->exec("CREATE TABLE IF NOT EXISTS `process_instances` ( + `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `person_id` INT(11) UNSIGNED NOT NULL, + `process_definition_id` INT(11) UNSIGNED NOT NULL, + `current_status` VARCHAR(255) NOT NULL DEFAULT 'none', + `current_node_id` VARCHAR(255), + `current_reason` TEXT, + `suggested_next_step` TEXT, + `data_json` TEXT, + `last_activity_at` TIMESTAMP NULL, + FOREIGN KEY (person_id) REFERENCES people(id) ON DELETE CASCADE, + FOREIGN KEY (process_definition_id) REFERENCES process_definitions(id) ON DELETE CASCADE, + UNIQUE KEY `person_process` (`person_id`, `process_definition_id`) + )"); + echo "Process instances table created or already exists.\n"; + + // 4. Process Events table (updated FK) + $pdo->exec("CREATE TABLE IF NOT EXISTS `process_events` ( + `id` INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `process_instance_id` INT(11) UNSIGNED NOT NULL, + `event_type` VARCHAR(50) NOT NULL, + `message` TEXT, + `node_id` VARCHAR(255), + `payload_json` TEXT, + `created_by` INT(11) UNSIGNED NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (process_instance_id) REFERENCES process_instances(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES people(id) ON DELETE CASCADE + )"); + echo "Process events table created or already exists.\n"; + + // MIGRATIONS + echo "Starting migrations...\n"; + + // Migration: Rename `processId` to `processDefinitionId` in `process_instances` + $stmt = $pdo->query("SHOW COLUMNS FROM `process_instances` LIKE 'processId'"); + if ($stmt->fetch()) { + $checkFk = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = 'process_instances' AND COLUMN_NAME = 'processId';")->fetch(); + if ($checkFk) { + $pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;"); + } + $pdo->exec("ALTER TABLE `process_instances` CHANGE `processId` `process_definition_id` INT(11) UNSIGNED NOT NULL;"); + $pdo->exec("ALTER TABLE `process_instances` ADD FOREIGN KEY (`process_definition_id`) REFERENCES `process_definitions`(`id`) ON DELETE CASCADE;"); + echo "Migrated process_instances: processId -> processDefinitionId.\n"; + } + + // Migration: Rename `contactId` to `personId` and update foreign key in `process_instances` + $stmt = $pdo->query("SHOW COLUMNS FROM `process_instances` LIKE 'contactId'"); + if ($stmt->fetch()) { + $checkFk = $pdo->query("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = 'process_instances' AND COLUMN_NAME = 'contactId';")->fetch(); + if ($checkFk) { + $pdo->exec("ALTER TABLE `process_instances` DROP FOREIGN KEY `{$checkFk['CONSTRAINT_NAME']}`;"); + } + $pdo->exec("ALTER TABLE `process_instances` CHANGE `contactId` `person_id` INT(11) UNSIGNED NOT NULL;"); + echo "Migrated process_instances: contactId -> personId.\n"; + } + + + // Drop old tables if they exist + $pdo->exec("DROP TABLE IF EXISTS `users`, `contacts`;"); + echo "Dropped old 'users' and 'contacts' tables.\n"; + + echo "\nDatabase setup/update completed successfully.\n"; + +} catch (PDOException $e) { + die("Database setup failed: " . $e->getMessage()); +} \ No newline at end of file diff --git a/event_types.php b/event_types.php new file mode 100644 index 0000000..d5bfe74 --- /dev/null +++ b/event_types.php @@ -0,0 +1,185 @@ +query("SELECT * FROM event_types ORDER BY display_order"); +$event_types = $stmt->fetchAll(PDO::FETCH_ASSOC); + +?> + +
+
+ +
+

Event Types

+ + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
NameColorActions
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..86c403f --- /dev/null +++ b/functions.php @@ -0,0 +1,195 @@ +query("SELECT f.*, bg.name as group_name FROM functions f LEFT JOIN bni_groups bg ON f.bni_group_id = bg.id ORDER BY f.display_order"); +$functions = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$group_stmt = $pdo->query("SELECT * FROM bni_groups WHERE active = 1 ORDER BY name"); +$bni_groups = $group_stmt->fetchAll(PDO::FETCH_ASSOC); + + +?> + +
+
+ +
+

Functions

+ + +
+ + +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
NameGroupActions
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + diff --git a/get_definition.php b/get_definition.php new file mode 100644 index 0000000..6ce665b --- /dev/null +++ b/get_definition.php @@ -0,0 +1,7 @@ +prepare("SELECT definition_json FROM process_definitions WHERE id = 4"); +$stmt->execute(); +$result = $stmt->fetch(PDO::FETCH_ASSOC); +echo $result['definition_json']; \ No newline at end of file diff --git a/get_process_definitions.php b/get_process_definitions.php new file mode 100644 index 0000000..1d9ba4f --- /dev/null +++ b/get_process_definitions.php @@ -0,0 +1,29 @@ +query("SELECT id, code, name, is_active FROM process_definitions ORDER BY id"); + $definitions = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Identify which processes appear on the dashboard + foreach ($definitions as &$def) { + if (!in_array($def['name'], ['Obsluga goscia', 'Przygotowanie spotkania grupy'])) { + $def['on_dashboard'] = 'Yes'; + } else { + $def['on_dashboard'] = 'No'; + } + } + + echo "--- Process Definitions --- +"; + echo str_pad("ID", 5) . str_pad("Code", 40) . str_pad("Name", 40) . str_pad("Active", 10) . str_pad("On Dashboard", 15) . "\n"; + echo str_repeat("-", 110) . "\n"; + foreach ($definitions as $def) { + echo str_pad($def['id'], 5) . str_pad($def['code'], 40) . str_pad($def['name'], 40) . str_pad($def['is_active'] ? 'Yes' : 'No', 10) . str_pad($def['on_dashboard'], 15) . "\n"; + } + +} catch (PDOException $e) { + die("DB ERROR: " . $e->getMessage()); +} + diff --git a/index.php b/index.php new file mode 100644 index 0000000..4e998ed --- /dev/null +++ b/index.php @@ -0,0 +1,1184 @@ +getDashboardMatrix($searchTerm, $groupId, $activeProcessId, $meetingFilterGroupId, $meetingFilterDatetime); +$people = $matrixData['people']; +$processes = $matrixData['definitions']; +$instances = $matrixData['instances']; +$spotkania_cols = $matrixData['spotkania_cols']; +$bni_groups = $matrixData['bni_groups']; +$all_functions = $matrixData['all_functions']; + + +$status_colors = [ + 'none' => 'secondary', + 'negative' => 'danger', + 'in_progress' => 'warning', + 'positive' => 'success', + 'present' => 'success', + 'absent' => 'danger', + 'substitute' => 'warning', +]; + + +?> + + + +
+
+ + +
+ + + + + + + + + + +
+

Dashboard

+
+ + +
+
+ + + +
+ +
+ All Groups + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PersonSpotkaniaInne procesy
' class=""> + +
+ +
+
+ +
+ +
+
+
+ +
+
+
+
+ + + + + , Grupa: + +
+
+
+
+ + + + + +
+ +
+
+ isMemberOfGroup($person['id'], $col['group_id']); + $status = $isMember ? 'present' : 'none'; + + // The meeting date will be determined by JS, we will add it to the data attribute + // The initial meeting date is the first one in the list. + $meeting_datetime = $col['meetings'][0] ?? ''; + + $color = $status_colors[$status] ?? 'secondary'; + ?> + + +   + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/instance_details.html b/instance_details.html new file mode 100644 index 0000000..a1341fc --- /dev/null +++ b/instance_details.html @@ -0,0 +1,109 @@ + + +
+ Staszek Ptaszek - Follow-up
+ + +
+
Kroki procesu
+
    +
  • +
    + Follow-up Call +
    +
  • +
  • +
    + +
    +
  • +
  • +
    + Call Scheduled +
    +
  • +
  • +
    + No Answer Logged +
    +
  • +
  • +
    + Wants to Join +
    +
  • +
  • +
    + Declined +
    +
  • +
  • +
    + Process Ended +
    +
  • +
  • +
    + Process Terminated +
    +
  • +
+
+
Available Actions
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
Dodaj notatkę
+
+ +
+ +
+ +
+ +
+
Historia
+
    +
  • + System +

    Process started.

    By Admin User on 11.01.2026, 07:00 +
  • +
+
+ diff --git a/lib/ErrorHandler.php b/lib/ErrorHandler.php new file mode 100644 index 0000000..cf58855 --- /dev/null +++ b/lib/ErrorHandler.php @@ -0,0 +1,49 @@ +getHttpCode()); + $response = [ + 'error' => [ + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + 'details' => $e->getDetails(), + ], + 'correlation_id' => $correlation_id, + ]; + } else { + http_response_code(500); + error_log("Correlation ID: $correlation_id, Uncaught Exception: " . $e->getMessage() . "\n" . $e->getTraceAsString()); + $response = [ + 'error' => [ + 'code' => 500, + 'message' => 'Internal Server Error', + ], + 'correlation_id' => $correlation_id, + ]; + } + + header('Content-Type: application/json'); + echo json_encode($response); + exit; +} + +function register_error_handler() { + $GLOBALS['correlation_id'] = generate_correlation_id(); + set_exception_handler(function(Throwable $e) { + handle_exception($e, $GLOBALS['correlation_id']); + }); + // You can also set a general error handler for non-exception errors + set_error_handler(function($severity, $message, $file, $line) { + if (!(error_reporting() & $severity)) { + return; + } + error_log("Correlation ID: {$GLOBALS['correlation_id']}, Error: [$severity] $message in $file on line $line"); + // Don't output anything to the user for non-fatal errors, just log them + }); +} diff --git a/lib/WorkflowExceptions.php b/lib/WorkflowExceptions.php new file mode 100644 index 0000000..f17534b --- /dev/null +++ b/lib/WorkflowExceptions.php @@ -0,0 +1,18 @@ +reasons = $reasons; + } + + public function getReasons() { + return $this->reasons; + } +} \ No newline at end of file diff --git a/lib/fpdf/font/DejaVuSans-Bold.php b/lib/fpdf/font/DejaVuSans-Bold.php new file mode 100644 index 0000000..2beb27e --- /dev/null +++ b/lib/fpdf/font/DejaVuSans-Bold.php @@ -0,0 +1,20 @@ + 928, + 'Descent' => -236, + 'CapHeight' => 928, + 'Flags' => 262148, + 'FontBBox' => '[-917 -415 1682 1167]', + 'ItalicAngle' => 0, + 'StemV' => 165, + 'MissingWidth' => 602, +); +$up = -100; +$ut = 50; +$cw = json_decode(file_get_contents(__DIR__ . '/dejavusansb_cw.json'), true); +$enc = 'cp1252'; +$diff = ''; +$file = 'dejavusansb.ttf.z'; +$originalsize = 294260; diff --git a/lib/fpdf/font/DejaVuSans.php b/lib/fpdf/font/DejaVuSans.php new file mode 100644 index 0000000..191b5fd --- /dev/null +++ b/lib/fpdf/font/DejaVuSans.php @@ -0,0 +1,20 @@ + 928, + 'Descent' => -236, + 'CapHeight' => 928, + 'Flags' => 4, + 'FontBBox' => '[-917 -415 1682 1167]', + 'ItalicAngle' => 0, + 'StemV' => 87, + 'MissingWidth' => 540, +); +$up = -100; +$ut = 50; +$cw = json_decode(file_get_contents(__DIR__ . '/dejavusans_cw.json'), true); +$enc = 'cp1252'; +$diff = '' ; +$file = 'dejavusans.ttf.z'; +$originalsize = 294260; diff --git a/lib/fpdf/font/dejavusans_cw.json b/lib/fpdf/font/dejavusans_cw.json new file mode 100644 index 0000000..662eaec --- /dev/null +++ b/lib/fpdf/font/dejavusans_cw.json @@ -0,0 +1 @@ +{" ":278,"!":278,"\"":355,"#":556,"$":556,"%":889,"&":667,"' ":222,"":333,"":333,"*":389,"+":584,",":278,".":278,"-":333,"/":278,"":556,"":556,"":556,"":556,"":556,"":556,"":556,"":556,"":556,"":556,":":278,";":278,"<":584,"=":584,">":584,"?":556,"@":1015,"A":667,"B":667,"C":722,"D":722,"E":667,"F":611,"G":778,"H":722,"I":278,"J":500,"K":667,"L":611,"M":833,"N":722,"O":778,"P":667,"Q":778,"R":722,"S":667,"T":611,"U":722,"V":667,"W":944,"X":667,"Y":611,"Z":611,"[":333,"\":278,"]":333,"^":469,"_":556,"`":333,"a":556,"b":556,"c":500,"d":556,"e":556,"f":278,"g":556,"h":556,"i":222,"j":222,"k":500,"l":222,"m":833,"n":556,"o":556,"p":556,"q":556,"r":333,"s":500,"t":278,"u":556,"v":500,"w":722,"x":500,"y":500,"z":500,"{":334,"|":260,"}":334,"~":584,"€":556,"":556,"‚":278,"ƒ":556,"„":556,"…":556,"†":556,"‡":556,"ˆ":556,"‰":556,"Š":667,"‹":500,"Œ":778,"":556,"Ž":611,"":556,"":556,"‘":556,"’":556,"“":556,"”":556,"•":556,"–":556,"—":556,"˜":556,"™":833,"š":500,"›":500,"œ":556,"":556,"ž":500,"Ÿ":500,"¡":278,"¢":556,"£":556,"¤":556,"¥":556,"¦":260,"§":556,"¨":333,"©":737,"ª":370,"«":556,"¬":584,"®":737,"¯":333,"°":400,"±":584,"²":338,"³":338,"´":333,"µ":556,"¶":667,"·":278,"¸":333,"¹":338,"º":370,"»":556,"¼":833,"½":833,"¾":833,"¿":556,"À":667,"Á":667,"Â":667,"Ã":667,"Ä":667,"Å":667,"Æ":944,"Ç":722,"È":667,"É":667,"Ê":667,"Ë":667,"Ì":278,"Í":278,"Î":278,"Ï":278,"Ð":722,"Ñ":722,"Ò":778,"Ó":778,"Ô":778,"Õ":778,"Ö":778,"×":584,"Ø":778,"Ù":722,"Ú":722,"Û":722,"Ü":722,"Ý":667,"Þ":667,"ß":500,"à":556,"á":556,"â":556,"ã":556,"ä":556,"å":556,"æ":778,"ç":500,"è":556,"é":556,"ê":556,"ë":556,"ì":222,"í":222,"î":222,"ï":222,"ð":556,"ñ":556,"ò":556,"ó":556,"ô":556,"õ":556,"ö":556,"÷":584,"ø":556,"ù":556,"ú":556,"û":556,"ü":556,"ý":500,"þ":556,"ÿ":500} \ No newline at end of file diff --git a/lib/fpdf/font/dejavusansb_cw.json b/lib/fpdf/font/dejavusansb_cw.json new file mode 100644 index 0000000..61a3351 --- /dev/null +++ b/lib/fpdf/font/dejavusansb_cw.json @@ -0,0 +1 @@ +{" ":278,"!":333,"\"":424,"#":611,"$":611,"%":944,"&":722,"' ":278,"":333,"":333,"*":444,"+":611,",":278,".":278,"-":333,"/":278,"":611,"":611,"":611,"":611,"":611,"":611,"":611,"":611,"":611,"":611,":":278,";":278,"<":611,"=":611,">":611,"?":611,"@":1015,"A":722,"B":722,"C":778,"D":778,"E":722,"F":667,"G":833,"H":778,"I":333,"J":556,"K":722,"L":667,"M":889,"N":778,"O":833,"P":722,"Q":833,"R":778,"S":722,"T":667,"U":778,"V":722,"W":1000,"X":722,"Y":667,"Z":667,"[":333,"\":278,"]":333,"^":508,"_":611,"`":333,"a":611,"b":611,"c":556,"d":611,"e":611,"f":333,"g":611,"h":611,"i":278,"j":278,"k":556,"l":278,"m":889,"n":611,"o":611,"p":611,"q":611,"r":389,"s":556,"t":333,"u":611,"v":556,"w":778,"x":556,"y":556,"z":556,"{":389,"|":303,"}":389,"~":611,"€":611,"":611,"‚":278,"ƒ":611,"„":611,"…":611,"†":611,"‡":611,"ˆ":611,"‰":611,"Š":722,"‹":556,"Œ":833,"":611,"Ž":667,"":611,"":611,"‘":611,"’":611,"“":611,"”":611,"•":611,"–":611,"—":611,"˜":611,"™":889,"š":556,"›":556,"œ":611,"":611,"ž":556,"Ÿ":556,"¡":333,"¢":611,"£":611,"¤":611,"¥":611,"¦":303,"§":611,"¨":389,"©":737,"ª":401,"«":611,"¬":611,"®":737,"¯":389,"°":444,"±":611,"²":382,"³":382,"´":389,"µ":611,"¶":722,"·":278,"¸":333,"¹":382,"º":401,"»":611,"¼":944,"½":944,"¾":944,"¿":611,"À":722,"Á":722,"Â":722,"Ã":722,"Ä":722,"Å":722,"Æ":1000,"Ç":778,"È":722,"É":722,"Ê":722,"Ë":722,"Ì":333,"Í":333,"Î":333,"Ï":333,"Ð":778,"Ñ":778,"Ò":833,"Ó":833,"Ô":833,"Õ":833,"Ö":833,"×":611,"Ø":833,"Ù":778,"Ú":778,"Û":778,"Ü":778,"Ý":722,"Þ":722,"ß":556,"à":611,"á":611,"â":611,"ã":611,"ä":611,"å":611,"æ":889,"ç":556,"è":611,"é":611,"ê":611,"ë":611,"ì":278,"í":278,"î":278,"ï":278,"ð":611,"ñ":611,"ò":611,"ó":611,"ô":611,"õ":611,"ö":611,"÷":611,"ø":611,"ù":611,"ú":611,"û":611,"ü":611,"ý":556,"þ":611,"ÿ":556} \ No newline at end of file diff --git a/lib/fpdf/fpdf.php b/lib/fpdf/fpdf.php new file mode 100644 index 0000000..e416c24 --- /dev/null +++ b/lib/fpdf/fpdf.php @@ -0,0 +1,1822 @@ +_dochecks(); + // Initialization of properties + $this->state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + // Font path + if(defined('FPDF_FONTPATH')) + { + $this->fontpath = FPDF_FONTPATH; + if(substr($this->fontpath, -1)!='/' && substr($this->fontpath, -1)!="\") + $this->fontpath .= '/'; + } + elseif(is_dir(dirname(__FILE__).'/font')) + $this->fontpath = dirname(__FILE__).'/font/'; + else + $this->fontpath = ''; + // Core fonts + $this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats'); + // Scale factor + if($unit=='pt') + $this->k = 1; + elseif($unit=='mm') + $this->k = 72/25.4; + elseif($unit=='cm') + $this->k = 72/2.54; + elseif($unit=='in') + $this->k = 72; + else + $this->Error('Incorrect unit: '.$unit); + // Page sizes + $this->StdPageSizes = array('a3'=>array(841.89,1190.55), 'a4'=>array(595.28,841.89), 'a5'=>array(420.94,595.28), + 'letter'=>array(612,792), 'legal'=>array(612,1008)); + $size = $this->_getpagesize($size); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + // Page orientation + $orientation = strtolower($orientation); + if($orientation=='p' || $orientation=='portrait') + { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } + elseif($orientation=='l' || $orientation=='landscape') + { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } + else + $this->Error('Incorrect orientation: '.$orientation); + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + // Page margins (1 cm) + $margin = 28.35/$this->k; + $this->SetMargins($margin, $margin); + // Interior cell margin (1 mm) + $this->cMargin = $margin/10; + // Line width (0.2 mm) + $this->LineWidth = .567/$this->k; + // Automatic page break + $this->SetAutoPageBreak(true, 2*$margin); + // Full width display mode + $this->SetDisplayMode('fullwidth'); + // Enable compression + $this->SetCompression(true); + // Set default PDF version number + $this->PDFVersion = '1.3'; +} + +function SetMargins($left, $top, $right=null) +{ + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if($right===null) + $right = $left; + $this->rMargin = $right; +} + +function SetLeftMargin($margin) +{ + // Set left margin + $this->lMargin = $margin; + if($this->page>0 && $this->x<$margin) + $this->x = $margin; +} + +function SetTopMargin($margin) +{ + // Set top margin + $this->tMargin = $margin; +} + +function SetRightMargin($margin) +{ + // Set right margin + $this->rMargin = $margin; +} + +function SetAutoPageBreak($auto, $margin=0) +{ + // Set auto page break mode and triggering margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h-$margin; +} + +function SetDisplayMode($zoom, $layout='continuous') +{ + // Set display mode in viewer + if($zoom=='fullpage' || $zoom=='fullwidth' || $zoom=='real' || $zoom=='default' || !is_string($zoom)) + $this->ZoomMode = $zoom; + else + $this->Error('Incorrect zoom display mode: '.$zoom); + if($layout=='single' || $layout=='continuous' || $layout=='two' || $layout=='default') + $this->LayoutMode = $layout; + else + $this->Error('Incorrect layout display mode: '.$layout); +} + +function SetCompression($compress) +{ + // Set page compression + if(function_exists('gzcompress')) + $this->compress = $compress; + else + $this->compress = false; +} + +function SetTitle($title, $isUTF8=false) +{ + // Title of document + $this->metadata['Title'] = $isUTF8 ? $this->_UTF8toUTF16($title) : $title; +} + +function SetAuthor($author, $isUTF8=false) +{ + // Author of document + $this->metadata['Author'] = $isUTF8 ? $this->_UTF8toUTF16($author) : $author; +} + +function SetSubject($subject, $isUTF8=false) +{ + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $this->_UTF8toUTF16($subject) : $subject; +} + +function SetKeywords($keywords, $isUTF8=false) +{ + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $this->_UTF8toUTF16($keywords) : $keywords; +} + +function SetCreator($creator, $isUTF8=false) +{ + // Creator of document + $this->metadata['Creator'] = $isUTF8 ? $this->_UTF8toUTF16($creator) : $creator; +} + +function AliasNbPages($alias='{nb}') +{ + // Define an alias for total number of pages + $this->AliasNbPages = $alias; +} + +function Error($msg) +{ + // Fatal error + throw new Exception('FPDF error: '.$msg); +} + +function Close() +{ + // Terminate document + if($this->state==3) + return; + if($this->page==0) + $this->AddPage(); + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); +} + +function AddPage($orientation='', $size='', $rotation=0) +{ + // Start a new page + if($this->state==3) + $this->Error('The document is closed'); + $family = $this->FontFamily; + $style = $this->FontStyle.($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if($this->page>0) + { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage($orientation, $size, $rotation); + // Set line cap style to square + $this->_out('2 J'); + // Set line width + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw*$this->k)); + // Set font + if($family) + $this->SetFont($family, $style, $fontsize); + // Set colors + $this->DrawColor = $dc; + if($dc!='0 G') + $this->_out($dc); + $this->FillColor = $fc; + if($fc!='0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if($this->LineWidth!=$lw) + { + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw*$this->k)); + } + // Restore font + if($family) + $this->SetFont($family, $style, $fontsize); + // Restore colors + if($this->DrawColor!=$dc) + { + $this->DrawColor = $dc; + $this->_out($dc); + } + if($this->FillColor!=$fc) + { + $this->FillColor = $fc; + $this->_out($fc); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; +} + +function Header() +{ + // To be implemented in your own inherited class +} + +function Footer() +{ + // To be implemented in your own inherited class +} + +function PageNo() +{ + // Get current page number + return $this->page; +} + +function SetDrawColor($r, $g=null, $b=null) +{ + // Set color for all stroking operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->DrawColor = sprintf('%.3F G', $r/255); + else + $this->DrawColor = sprintf('%.3F %.3F %.3F RG', $r/255, $g/255, $b/255); + if($this->page>0) + $this->_out($this->DrawColor); +} + +function SetFillColor($r, $g=null, $b=null) +{ + // Set color for all filling operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->FillColor = sprintf('%.3F g', $r/255); + else + $this->FillColor = sprintf('%.3F %.3F %.3F rg', $r/255, $g/255, $b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); + if($this->page>0) + $this->_out($this->FillColor); +} + +function SetTextColor($r, $g=null, $b=null) +{ + // Set color for text + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->TextColor = sprintf('%.3F g', $r/255); + else + $this->TextColor = sprintf('%.3F %.3F %.3F rg', $r/255, $g/255, $b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); +} + +function GetStringWidth($s) +{ + // Get width of a string in the current font + $s = (string)$s; + $cw = &$this->CurrentFont['cw']; + $w = 0; + $l = strlen($s); + for($i=0;$i<$l;$i++) + $w += $cw[$s[$i]]; + return $w*$this->FontSize/1000; +} + +function SetLineWidth($width) +{ + // Set line width + $this->LineWidth = $width; + if($this->page>0) + $this->_out(sprintf('%.2F w', $width*$this->k)); +} + +function Line($x1, $y1, $x2, $y2) +{ + // Draw a line + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1*$this->k, ($this->h-$y1)*$this->k, $x2*$this->k, ($this->h-$y2)*$this->k)); +} + +function Rect($x, $y, $w, $h, $style='') +{ + // Draw a rectangle + if($style=='F') + $op = 'f'; + elseif($style=='FD' || $style=='DF') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s', $x*$this->k, ($this->h-$y)*$this->k, $w*$this->k, -$h*$this->k, $op)); +} + +function AddFont($family, $style='', $file='', $dir='') +{ + // Add a TrueType, OpenType or Type1 font + if($file=='') + $file = str_replace(' ', '', $family).strtolower($style).'.php'; + $family = strtolower($family); + if($family=='arial') + $family = 'helvetica'; + $style = strtoupper($style); + if($style=='IB') + $style = 'BI'; + $fontkey = $family.$style; + if(isset($this->fonts[$fontkey])) + return; + $info = $this->_loadfont($file, $dir); + $info['i'] = count($this->fonts)+1; + if(!empty($info['file'])) + { + // Embedded font + if($info['type']=='TrueType') + $this->FontFiles[$info['file']] = array('length1'=>$info['originalsize']); + else + $this->FontFiles[$info['file']] = array('length1'=>$info['size1'], 'length2'=>$info['size2']); + } + $this->fonts[$fontkey] = $info; +} + +function SetFont($family, $style='', $size=0) +{ + // Select a font; size given in points + if($family=='') + $family = $this->FontFamily; + else + $family = strtolower($family); + if($family=='arial') + $family = 'helvetica'; + elseif($family=='symbol' || $family=='zapfdingbats') + $style = ''; + $style = strtoupper($style); + if(strpos($style, 'U')!==false) + { + $this->underline = true; + $style = str_replace('U', '', $style); + } + else + $this->underline = false; + if($style=='IB') + $style = 'BI'; + if($size==0) + $size = $this->FontSizePt; + // Test if font is already selected + if($this->FontFamily==$family && $this->FontStyle==$style && $this->FontSizePt==$size) + return; + // Test if font is already loaded + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + { + // Test if one of the core fonts + if(in_array($fontkey, $this->CoreFonts)) + { + if(!isset($this->fonts[$fontkey])) + $this->AddFont($family, $style); + } + else + $this->Error('Undefined font: '.$family.' '.$style); + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + $this->CurrentFont = &$this->fonts[$fontkey]; + if($this->page>0) + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); +} + +function SetFontSize($size) +{ + // Set font size in points + if($this->FontSizePt==$size) + return; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + if($this->page>0) + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); +} + +function AddLink() +{ + // Create a new internal link + $n = count($this->links)+1; + $this->links[$n] = array(0, 0); + return $n; +} + +function SetLink($link, $y=0, $page=-1) +{ + // Set destination of internal link + if($y==-1) + $y = $this->y; + if($page==-1) + $page = $this->page; + $this->links[$link] = array($page, $y); +} + +function Link($x, $y, $w, $h, $link) +{ + // Put a link on the page + $this->PageLinks[$this->page][] = array($x*$this->k, $this->hPt-$y*$this->k, $w*$this->k, $h*$this->k, $link); +} + +function Text($x, $y, $txt) +{ + // Output a string + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x*$this->k, ($this->h-$y)*$this->k, $this->_escape($txt)); + if($this->underline && $txt!='') + $s .= ' '.$this->_dounderline($x, $y, $txt); + if($this->ColorFlag) + $s = 'q '.$this->TextColor.' '.$s.' Q'; + $this->_out($s); +} + +function AcceptPageBreak() +{ + // Accept automatic page break or not + return $this->AutoPageBreak; +} + +function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='') +{ + // Output a cell + $k = $this->k; + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if($ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); + $this->x = $x; + if($ws>0) + { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw', $ws*$k)); + } + } + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $s = ''; + if($fill || $border==1) + { + if($fill) + $op = ($border==1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x*$k, ($this->h-$this->y)*$k, $w*$k, -$h*$k, $op); + } + if(is_string($border)) + { + $x = $this->x; + $y = $this->y; + if(strpos($border, 'L')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x*$k, ($this->h-$y)*$k, $x*$k, ($this->h-($y+$h))*$k); + if(strpos($border, 'T')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x*$k, ($this->h-$y)*$k, ($x+$w)*$k, ($this->h-$y)*$k); + if(strpos($border, 'R')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($x+$w)*$k, ($this->h-$y)*$k, ($x+$w)*$k, ($this->h-($y+$h))*$k); + if(strpos($border, 'B')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x*$k, ($this->h-($y+$h))*$k, ($x+$w)*$k, ($this->h-($y+$h))*$k); + } + if($txt!=='') + { + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if($align=='R') + $dx = $w-$this->cMargin-$this->GetStringWidth($txt); + elseif($align=='C') + $dx = ($w-$this->GetStringWidth($txt))/2; + else + $dx = $this->cMargin; + if($this->ColorFlag) + $s .= 'q '.$this->TextColor.' '; + $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', ($this->x+$dx)*$k, ($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k, $this->_escape($txt)); + if($this->underline) + $s .= ' '.$this->_dounderline($this->x+$dx, $this->y+.5*$h+.3*$this->FontSize, $txt); + if($this->ColorFlag) + $s .= ' Q'; + if($link) + $this->Link($this->x+$dx, $this->y+.5*$h-.5*$this->FontSize, $this->GetStringWidth($txt), $this->FontSize, $link); + } + if($s) + $this->_out($s); + $this->lasth = $h; + if($ln>0) + { + // Go to next line + $this->y += $h; + if($ln==1) + $this->x = $this->lMargin; + } + else + $this->x += $w; +} + +function MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false) +{ + // Output text with automatic or explicit line breaks + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = &$this->CurrentFont['cw']; + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + if($nb>0 && $s[$nb-1]=="\n") + $nb--; + $b = 0; + if($border) + { + if($border==1) + { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } + else + { + $b2 = ''; + if(strpos($border, 'L')!==false) + $b2 .= 'L'; + if(strpos($border, 'R')!==false) + $b2 .= 'R'; + $b = (strpos($border, 'T')!==false) ? $b2.'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + $c = $s[$i]; + if($c=="\n") + { + // Explicit line break + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w, $h, substr($s, $j, $i-$j), $b, 2, $align, $fill); + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + continue; + } + if($c==' ') + { + $sep = $i; + $ls = $l; + $ns++; + } + $l += $cw[$c]; + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($i==$j) + $i++; + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w, $h, substr($s, $j, $i-$j), $b, 2, $align, $fill); + } + else + { + if($align=='J') + { + $this->ws = ($ns>1) ? ($wmax-$ls)/1000*$this->FontSize/($ns-1) : 0; + $this->_out(sprintf('%.3F Tw', $this->ws*$this->k)); + } + $this->Cell($w, $h, substr($s, $j, $sep-$j), $b, 2, $align, $fill); + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + } + else + $i++; + } + // Last chunk + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + if($border && strpos($border, 'B')!==false) + $b .= 'B'; + $this->Cell($w, $h, substr($s, $j, $i-$j), $b, 2, $align, $fill); + $this->x = $this->lMargin; +} + +function Write($h, $txt, $link='') +{ + // Output text in flowing mode + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = &$this->CurrentFont['cw']; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + $c = $s[$i]; + if($c=="\n") + { + // Explicit line break + $this->Cell($w, $h, substr($s, $j, $i-$j), 0, 2, '', false, $link); + $i++; + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + } + $nl++; + continue; + } + if($c==' ') + $sep = $i; + $l += $cw[$c]; + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($this->x>$this->lMargin) + { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $i--; + $nl++; + continue; + } + if($i==$j) + $i++; + $this->Cell($w, $h, substr($s, $j, $i-$j), 0, 2, '', false, $link); + } + else + { + $this->Cell($this->GetStringWidth(substr($s, $j, $sep-$j))+$this->cMargin, $h, substr($s, $j, $sep-$j), 0, 0, '', false, $link); + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + } + $nl++; + } + else + $i++; + } + // Last chunk + if($i!=$j) + $this->Cell($this->GetStringWidth(substr($s, $j, $i-$j))+$this->cMargin, $h, substr($s, $j, $i-$j), 0, 0, '', false, $link); +} + +function Ln($h=null) +{ + // Line feed; default value is the height of the last cell + $this->x = $this->lMargin; + if($h===null) + $this->y += $this->lasth; + else + $this->y += $h; +} + +function Image($file, $x=null, $y=null, $w=0, $h=0, $type='', $link='') +{ + // Put an image on the page + if($file=='') + $this->Error('Image file name is empty'); + if(!isset($this->images[$file])) + { + // First use of this image, get info + if($type=='') + { + $pos = strrpos($file, '.'); + if(!$pos) + $this->Error('Image file has no extension and no type was specified: '.$file); + $type = substr($file, $pos+1); + } + $type = strtolower($type); + if($type=='jpeg') + $type = 'jpg'; + $mtd = '_parse'.$type; + if(!method_exists($this, $mtd)) + $this->Error('Unsupported image type: '.$type); + $info = $this->$mtd($file); + $info['i'] = count($this->images)+1; + $this->images[$file] = $info; + } + else + $info = $this->images[$file]; + + // Automatic width and height calculation if needed + if($w==0 && $h==0) + { + // Put image at 96 dpi + $w = -96; + $h = -96; + } + if($w<0) + $w = -$info['w']*72/$w/$this->k; + if($h<0) + $h = -$info['h']*72/$h/$this->k; + if($w==0) + $w = $h*$info['w']/$info['h']; + if($h==0) + $h = $w*$info['h']/$info['w']; + + // Flowing mode + if($y===null) + { + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x2 = $this->x; + $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); + $this->x = $x2; + } + $y = $this->y; + $this->y += $h; + } + + if($x===null) + $x = $this->x; + $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $w*$this->k, $h*$this->k, $x*$this->k, ($this->h-($y+$h))*$this->k, $info['i'])); + if($link) + $this->Link($x, $y, $w, $h, $link); +} + +function GetPageWidth() +{ + // Get current page width + return $this->w; +} + +function GetPageHeight() +{ + // Get current page height + return $this->h; +} + +function GetX() +{ + // Get x position + return $this->x; +} + +function SetX($x) +{ + // Set x position + if($x>=0) + $this->x = $x; + else + $this->x = $this->w+$x; +} + +function GetY() +{ + // Get y position + return $this->y; +} + +function SetY($y, $resetX=true) +{ + // Set y position and optionally reset x + if($y>=0) + $this->y = $y; + else + $this->y = $this->h+$y; + if($resetX) + $this->x = $this->lMargin; +} + +function SetXY($x, $y) +{ + // Set x and y positions + $this->SetY($y, false); + $this->SetX($x); +} + +function Output($dest='', $name='', $isUTF8=false) +{ + // Output PDF to some destination + $this->Close(); + if(strlen($name)==1 && strlen($dest)==1) + { + // Fix for IE + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if($dest=='') + $dest = 'I'; + if($name=='') + $name = 'doc.pdf'; + switch(strtoupper($dest)) + { + case 'I': + // Send to standard output + $this->_checkoutput(); + if(PHP_SAPI!='cli') + { + // We send to a browser + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="'.$name.'"'); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header('Content-Type: application/x-download'); + header('Content-Disposition: attachment; filename="'.$name.'"'); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + echo $this->buffer; + break; + case 'F': + // Save to local file + if(!file_put_contents($name, $this->buffer)) + $this->Error('Unable to create output file: '.$name); + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error('Incorrect output destination: '.$dest); + } + return ''; +} + +/******************************************************************************* +* * +* Protected methods * +* * +*******************************************************************************/ + +function _dochecks() +{ + // Check for locale-related bug + if(1/2==0) + $this->Error('Don\\'t alter the locale before including class file'); + // Check for mbstring overloading + if(ini_get('mbstring.func_overload') & 2) + $this->Error('mbstring overloading must be disabled'); +} + +function _checkoutput() +{ + if(PHP_SAPI!='cli') + { + if(headers_sent($file, $line)) + $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); + } + if(ob_get_length()) + { + // The output buffer is not empty + if(preg_match('/^(\xEF\xBB\xBF)?
StdPageSizes[$size])) + $this->Error('Unknown page size: '.$size); + $a = $this->StdPageSizes[$size]; + return array($a[0]/$this->k, $a[1]/$this->k); + } + else + { + if($size[0]>$size[1]) + return array($size[1], $size[0]); + else + return $size; + } +} + +function _beginpage($orientation, $size, $rotation) +{ + $this->page++; + $this->pages[$this->page] = ''; + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if($orientation=='') + $orientation = $this->DefOrientation; + else + $orientation = strtoupper($orientation[0]); + if($size=='') + $size = $this->DefPageSize; + else + $size = $this->_getpagesize($size); + if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1]) + { + // New size or orientation + if($orientation=='P') + { + $this->w = $size[0]; + $this->h = $size[1]; + } + else + { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + $this->PageBreakTrigger = $this->h-$this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if($orientation!=$this->DefOrientation || $size[0]!=$this->DefPageSize[0] || $size[1]!=$this->DefPageSize[1]) + $this->CurPageFormat = array('w'=>$this->wPt, 'h'=>$this->hPt); + else + $this->CurPageFormat = null; + if($rotation!=0) + { + if($rotation%90!=0) + $this->Error('Incorrect rotation value: '.$rotation); + if(!isset($this->CurPageFormat)) + $this->CurPageFormat = array('w'=>$this->wPt, 'h'=>$this->hPt); + $this->CurPageFormat['Rotate'] = $rotation; + } + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + if(isset($this->CurPageFormat)) + $this->PageInfo[$this->page]['format'] = $this->CurPageFormat; +} + +function _endpage() +{ + $this->state = 1; +} + +function _loadfont($font, $dir) +{ + // Load a font definition file from the font directory + if($dir=='') + $dir = $this->fontpath; + if(substr($dir, -1)!='/' && substr($dir, -1)!='\') + $dir .= '/'; + @include($dir.$font); + if(!isset($name)) + $this->Error('Could not include font definition file'); + if(!isset($cw)) + $this->Error('Missing character widths array'); + return get_defined_vars(); +} + +function _UTF8toUTF16($s) +{ + // Convert UTF-8 string to UTF-16BE + $res = ""; + $nb = strlen($s); + $i = 0; + while($i<$nb) + { + $c1 = ord($s[$i++]); + if($c1>=224) + { + // 3-byte character + $c2 = ord($s[$i++]); + $c3 = ord($s[$i++]); + $res .= chr((($c1 & 0x0F) << 4) + (($c2 & 0x3C) >> 2)); + $res .= chr((($c2 & 0x03) << 6) + ($c3 & 0x3F)); + } + elseif($c1>=192) + { + // 2-byte character + $c2 = ord($s[$i++]); + $res .= chr(($c1 & 0x1C) >> 2); + $res .= chr((($c1 & 0x03) << 6) + ($c2 & 0x3F)); + } + else + { + // Single-byte character + $res .= "\0".chr($c1); + } + } + return $res; +} + +function _escape($s) +{ + // Escape special characters in strings + $s = str_replace('\\', '\\\\', $s); // Escape backslashes first + $s = str_replace('(', '\(', $s); + $s = str_replace(')', '\)', $s); + $s = str_replace("\r", '\r', $s); // Escape carriage return + return $s; +} + +function _textstring($s) +{ + // Format a text string + if(!$this->_isascii($s)) + $s = $this->_UTF8toUTF16($s); + return '('.$this->_escape($s).')'; +} + +function _isascii($s) +{ + $nb = strlen($s); + for($i=0;$i<$nb;$i++) + { + if(ord($s[$i])>127) + return false; + } + return true; +} + +function _dounderline($x, $y, $txt) +{ + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth($txt)+$this->ws*substr_count($txt, ' '); + return sprintf('%.2F %.2F %.2F %.2F re f', $x*$this->k, ($this->h-($y-$up/1000*$this->FontSize))*$this->k, $w*$this->k, -$ut/1000*$this->FontSizePt); +} + +function _parsejpg($file) +{ + // Extract info from a JPEG file + $a = getimagesize($file); + if(!$a) + $this->Error('Missing or incorrect image file: '.$file); + if($a[2]!=2) + $this->Error('Not a JPEG file: '.$file); + if(!isset($a['channels']) || $a['channels']==3) + $colspace = 'DeviceRGB'; + elseif($a['channels']==4) + $colspace = 'DeviceCMYK'; + else + $colspace = 'DeviceGray'; + $bpc = isset($a['bits']) ? $a['bits'] : 8; + $data = file_get_contents($file); + return array('w'=>$a[0], 'h'=>$a[1], 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'DCTDecode', 'data'=>$data); +} + +function _parsepng($file) +{ + // Extract info from a PNG file + $f = fopen($file, 'rb'); + if(!$f) + $this->Error('Can\'t open image file: '.$file); + $info = $this->_parsepngstream($f, $file); + fclose($f); + return $info; +} + +function _parsepngstream($f, $file) +{ + // Check signature + if($this->_readstream($f, 8)!=chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10)) + $this->Error('Not a PNG file: '.$file); + + // Read header chunk + $this->_readstream($f, 4); + if($this->_readstream($f, 4)!='IHDR') + $this->Error('Incorrect PNG file: '.$file); + $w = $this->_readint($f); + $h = $this->_readint($f); + $bpc = ord($this->_readstream($f, 1)); + if($bpc>8) + $this->Error('16-bit depth not supported: '.$file); + $ct = ord($this->_readstream($f, 1)); + if($ct==0 || $ct==4) + $colspace = 'DeviceGray'; + elseif($ct==2 || $ct==6) + $colspace = 'DeviceRGB'; + elseif($ct==3) + $colspace = 'Indexed'; + else + $this->Error('Unknown color type: '.$file); + if(ord($this->_readstream($f, 1))!=0) + $this->Error('Unknown compression method: '.$file); + if(ord($this->_readstream($f, 1))!=0) + $this->Error('Unknown filter method: '.$file); + if(ord($this->_readstream($f, 1))!=0) + $this->Error('Interlacing not supported: '.$file); + $this->_readstream($f, 4); + $dp = '/Predictor 15 /Colors '.($colspace=='DeviceRGB' ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w; + + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do + { + $n = $this->_readint($f); + $type = $this->_readstream($f, 4); + if($type=='PLTE') + { + // Read palette + $pal = $this->_readstream($f, $n); + $this->_readstream($f, 4); + } + elseif($type=='tRNS') + { + // Read transparency info + $t = $this->_readstream($f, $n); + if($ct==0) + $trns = array(ord(substr($t, 1, 1))); + elseif($ct==2) + $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))); + else + { + $pos = strpos($t, chr(0)); + if($pos!==false) + $trns = array($pos); + } + $this->_readstream($f, 4); + } + elseif($type=='IDAT') + { + // Read image data block + $data .= $this->_readstream($f, $n); + $this->_readstream($f, 4); + } + elseif($type=='IEND') + break; + else + $this->_readstream($f, $n+4); + } + while($n); + + if($colspace=='Indexed' && empty($pal)) + $this->Error('Missing palette in '.$file); + if($ct==6 && $bpc==8) + $this->WithAlpha = true; + $info = array('w'=>$w, 'h'=>$h, 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'FlateDecode', 'dp'=>$dp, 'pal'=>$pal, 'trns'=>$trns); + if($this->compress) + { + $data = gzuncompress($data); + if($ct==6) + { + // Separate alpha channel + $color = ''; + $alpha = ''; + for($i=0;$i<$h;$i++) + { + $pos = (1+$w*4)*$i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + for($j=0;$j<$w;$j++) + { + $color .= substr($data, $pos+1+4*$j, 3); + $alpha .= $data[$pos+4+4*$j]; + } + } + $data = gzcompress($color); + $info['smask'] = gzcompress($alpha); + if($this->PDFVersion<'1.4') + $this->PDFVersion = '1.4'; + } + $info['data'] = $data; + } + else + { + $info['data'] = $data; + if($ct==6) + $this->Error('PNG compression required for alpha channel'); + } + return $info; +} + +function _readstream($f, $n) +{ + // Read n bytes from stream + $res = ''; + while($n>0 && !feof($f)) + { + $s = fread($f, $n); + if($s===false) + $this->Error('Error while reading stream'); + $n -= strlen($s); + $res .= $s; + } + if($n>0) + $this->Error('Unexpected end of stream'); + return $res; +} + +function _readint($f) +{ + // Read a 4-byte integer from stream + $a = unpack('Ni', $this->_readstream($f, 4)); + return $a['i']; +} + +function _parsegif($file) +{ + // Extract info from a GIF file (via PNG conversion) + if(!function_exists('imagepng')) + $this->Error('GD extension is required for GIF support'); + if(!function_exists('imagecreatefromgif')) + $this->Error('GD extension is required for GIF support'); + $im = imagecreatefromgif($file); + if(!$im) + $this->Error('Missing or incorrect image file: '.$file); + imageinterlace($im, 0); + ob_start(); + imagepng($im); + $data = ob_get_clean(); + imagedestroy($im); + $f = fopen('php://temp', 'rb+'); + if(!$f) + $this->Error('Unable to create temporary file'); + fwrite($f, $data); + rewind($f); + $info = $this->_parsepngstream($f, $file); + fclose($f); + return $info; +} + +function _out($s) +{ + // Add a line to the document + if($this->state==2) + $this->pages[$this->page] .= $s."\n"; + elseif($this->state==1) + $this->_put($s); + elseif($this->state==0) + $this->Error('No page has been added yet'); + elseif($this->state==3) + $this->Error('The document is closed'); +} + +function _put($s) +{ + $this->buffer .= $s."\n"; +} + +function _getoffset() +{ + return strlen($this->buffer); +} + +function _newobj($n=null) +{ + // Begin a new object + if($n===null) + $n = ++$this->n; + $this->offsets[$n] = $this->_getoffset(); + $this->_put($n.' 0 obj'); +} + +function _putstream($data) +{ + $this->_put('stream'); + $this->_put($data); + $this->_put('endstream'); +} + +function _putstreamobject($data) +{ + if($this->compress) + { + $entries = '/Filter /FlateDecode '; + $data = gzcompress($data); + } + else + $entries = ''; + $entries .= '/Length '.strlen($data); + $this->_newobj(); + $this->_put('<<'.$entries.'>>'); + $this->_putstream($data); + $this->_put('endobj'); +} + +function _putpage($n) +{ + $this->_newobj(); + $this->_put('<_put('/Parent 1 0 R'); + $this->_put('/Resources 2 0 R'); + $format = $this->PageInfo[$n]['size']; + if(isset($this->PageInfo[$n]['format'])) + { + $format = $this->PageInfo[$n]['format']; + if(isset($format['Rotate'])) + $format['MediaBox'] = array(0, 0, $this->PageInfo[$n]['size'][1], $this->PageInfo[$n]['size'][0]); + } + $this->_put('/MediaBox [0 0 '.sprintf('%.2F %.2F', $format['w'], $format['h']).']'); + if(isset($format['Rotate'])) + $this->_put('/Rotate '.$format['Rotate']); + $this->_put('/Contents '.($this->n+1).' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + // Page content + $p = $this->pages[$n]; + $this->_putstreamobject($p); +} + +function _putpages() +{ + $nb = $this->page; + if(!empty($this->AliasNbPages)) + { + // Replace number of pages + for($n=1;$n<=$nb;$n++) + $this->pages[$n] = str_replace($this->AliasNbPages, $nb, $this->pages[$n]); + } + // Pages root + $this->_newobj(1); + $this->_put('<_putpage($n); + $kids .= (2+2*$n).' 0 R '; + } + $this->_put($kids.']'); + $this->_put('/Count '.$nb); + if(isset($this->PageInfo[1]['format'])) + $format = $this->PageInfo[1]['format']; + else + $format = $this->PageInfo[1]['size']; + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $format['w'], $format['h'])); + $this->_put('>>'); + $this->_put('endobj'); +} + +function _putfonts() +{ + foreach($this->FontFiles as $file=>$info) + { + // Font file embedding + $this->_newobj(); + $this->FontFiles[$file]['n'] = $this->n; + $font = file_get_contents($this->fontpath.$file, true); + if(!$font) + $this->Error('Font file not found: '.$file); + $compressed = (substr($file, -2)=='.z'); + if(!$compressed && isset($info['length2'])) + $font = substr($font, 6, $info['length1']).substr($font, 6+$info['length1']+6, $info['length2']); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('/Length1 '.$info['length1']); + if(isset($info['length2'])) + $this->_put('/Length2 '.$info['length2']); + $this->_put('>>'); + $this->_putstream($font); + $this->_put('endobj'); + } + foreach($this->fonts as $k=>$font) + { + // Font objects + $this->fonts[$k]['n'] = $this->n+1; + $type = $font['type']; + $name = $font['name']; + if($type=='Core') + { + // Core font + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$name); + $this->_put('/Subtype /Type1'); + if($name!='Symbol' && $name!='ZapfDingbats') + $this->_put('/Encoding /WinAnsiEncoding'); + $this->_put('>>'); + $this->_put('endobj'); + } + elseif($type=='Type1' || $type=='TrueType') + { + // Additional Type1 or TrueType/OpenType font + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$name); + $this->_put('/Subtype /'.$type); + $this->_put('/FirstChar 32'); + $this->_put('/LastChar 255'); + $this->_put('/Widths '.($this->n+1).' 0 R'); + $this->_put('/FontDescriptor '.($this->n+2).' 0 R'); + if($font['enc']) + { + if(isset($font['diff'])) + $this->_put('/Encoding '.($this->n+4).' 0 R'); + else + $this->_put('/Encoding /WinAnsiEncoding'); + } + $this->_put('>>'); + $this->_put('endobj'); + // Widths + $this->_newobj(); + $cw = &$font['cw']; + $s = '['; + for($i=32;$i<=255;$i++) + $s .= $cw[chr($i)].' '; + $this->_put($s.']'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '<$v) + $s .= ' /'.$k2.' '.$v; + if(!empty($font['file'])) + $s .= ' /FontFile'.($type=='Type1' ? '' : '2').' '.$this->FontFiles[$font['file']]['n'].' 0 R'; + $this->_put($s.'>>'); + $this->_put('endobj'); + // Differences + if(isset($font['diff'])) + { + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + } + } + elseif($type=='CMap') + { + // CJK font + $this->_newobj(); + $this->_put('<_put('/Subtype /Type0'); + $this->_put('/BaseFont /'.$name); + $this->_put('/Encoding /'.$font['enc']); + $this->_put('/DescendantFonts ['.($this->n+1).' 0 R]'); + $this->_put('>>'); + $this->_put('endobj'); + $this->_newobj(); + $this->_put('<_put('/Subtype /CIDFontType0'); + $this->_put('/BaseFont /'.$name); + $this->_put('/CIDSystemInfo '.($this->n+1).' 0 R'); + $this->_put('/FontDescriptor '.($this->n+2).' 0 R'); + if(isset($font['dw'])) + $this->_put('/DW '.$font['dw']); + if(isset($font['w'])) + $this->_put('/W '.($this->n+5).' 0 R'); + if(isset($font['cid_info'])) + $this->_put('/CIDToGIDMap /'.$font['cid_info']); + $this->_put('>>'); + $this->_put('endobj'); + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '<$v) + $s .= ' /'.$k2.' '.$v; + if(!empty($font['file'])) + $s .= ' /FontFile2 '.$this->FontFiles[$font['file']]['n'].' 0 R'; + $this->_put($s.'>>'); + $this->_put('endobj'); + // Widths + if(isset($font['w'])) + { + $this->_newobj(); + $this->_put('['.$font['w'].']'); + $this->_put('endobj'); + } + } + else + $this->Error('Unsupported font type: '.$type); + } +} + +function _putimages() +{ + foreach($this->images as $file=>$info) + { + $this->_newobj(); + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width '.$info['w']); + $this->_put('/Height '.$info['h']); + if($info['cs']=='Indexed') + $this->_put('/ColorSpace [/Indexed /DeviceRGB '.(strlen($info['pal'])/3-1).' '.($this->n+1).' 0 R]'); + else + { + $this->_put('/ColorSpace /'.$info['cs']); + if($info['cs']=='DeviceCMYK') + $this->_put('/Decode [1 0 1 0 1 0 1 0]'); + } + $this->_put('/BitsPerComponent '.$info['bpc']); + if(isset($info['f'])) + $this->_put('/Filter /'.$info['f']); + if(isset($info['dp'])) + $this->_put('/DecodeParms <<'.$info['dp'].'>>'); + if(isset($info['trns']) && is_array($info['trns'])) + { + $trns = ''; + for($i=0;$i_put('/Mask ['.$trns.']'); + } + if(isset($info['smask'])) + $this->_put('/SMask '.($this->n+1).' 0 R'); + $this->_put('/Length '.strlen($info['data']).'>>'); + $this->_putstream($info['data']); + $this->_put('endobj'); + // Soft mask + if(isset($info['smask'])) + { + $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns '.$info['w']; + $smask = array('w'=>$info['w'], 'h'=>$info['h'], 'cs'=>'DeviceGray', 'bpc'=>8, 'f'=>'FlateDecode', 'dp'=>$dp, 'data'=>$info['smask']); + $this->_putimage($smask); + } + // Palette + if($info['cs']=='Indexed') + $this->_putstreamobject($info['pal']); + } +} + +function _putimage(&$info) +{ + $this->_newobj(); + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width '.$info['w']); + $this->_put('/Height '.$info['h']); + $this->_put('/ColorSpace /'.$info['cs']); + $this->_put('/BitsPerComponent '.$info['bpc']); + $this->_put('/Filter /'.$info['f']); + $this->_put('/DecodeParms <<'.$info['dp'].'>>'); + $this->_put('/Length '.strlen($info['data']).'>>'); + $this->_putstream($info['data']); + $this->_put('endobj'); +} + +function _putxobjectdict() +{ + foreach($this->images as $file=>$info) + $this->_put('/I'.$info['i'].' '.($info['n']).' 0 R'); +} + +function _putresourcedict() +{ + $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_put('/Font <<'); + foreach($this->fonts as $font) + $this->_put('/F'.$font['i'].' '.$font['n'].' 0 R'); + $this->_put('>>'); + $this->_put('/XObject <<'); + $this->_putxobjectdict(); + $this->_put('>>'); +} + +function _putresources() +{ + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj(2); + $this->_put('<<'); + $this->_putresourcedict(); + $this->_put('>>'); + $this->_put('endobj'); +} + +function _putinfo() +{ + if(empty($this->metadata)) + return; + $this->_newobj(); + $this->_put('<<'); + foreach($this->metadata as $key=>$value) + $this->_put('/'.$key.' '.$this->_textstring($value)); + $this->_put('>>'); + $this->_put('endobj'); +} + +function _putcatalog() +{ + $this->_newobj(); + $this->_put('<_put('/Pages 1 0 R'); + if($this->ZoomMode=='fullpage') + $this->_put('/OpenAction [3 0 R /Fit]'); + elseif($this->ZoomMode=='fullwidth') + $this->put('/OpenAction [3 0 R /FitH null]'); + elseif($this->ZoomMode=='real') + $this->_put('/OpenAction [3 0 R /XYZ null null 1]'); + elseif(!is_string($this->ZoomMode)) + $this->_put('/OpenAction [3 0 R /XYZ null null '.sprintf('%.2F', $this->ZoomMode/100).']'); + if($this->LayoutMode=='single') + $this->_put('/PageLayout /SinglePage'); + elseif($this->LayoutMode=='continuous') + $this->put('/PageLayout /OneColumn'); + elseif($this->LayoutMode=='two') + $this->_put('/PageLayout /TwoColumnLeft'); + $this->_put('>>'); + $this->_put('endobj'); +} + +function _putheader() +{ + $this->_put('%PDF-'.$this->PDFVersion); +} + +function _puttrailer() +{ + $this->_put('trailer'); + $this->_put('<<'); + $this->_put('/Size '.($this->n+1)); + $this->_put('/Root '.($this->n).' 0 R'); + if(!empty($this->metadata)) + $this->_put('/Info '.($this->n-1).' 0 R'); + $this->_put('>>'); +} + +function _enddoc() +{ + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + $this->_putinfo(); + $this->_putcatalog(); + $offset = $this->_getoffset(); + $this->_puttrailer(); + $this->_put('startxref'); + $this->_put($offset); + $this->_put('%%EOF'); + $this->state = 3; +} +} \ No newline at end of file diff --git a/lib/tfpdf/font/courier.php b/lib/tfpdf/font/courier.php new file mode 100644 index 0000000..02525f5 --- /dev/null +++ b/lib/tfpdf/font/courier.php @@ -0,0 +1,8 @@ + diff --git a/lib/tfpdf/font/courierb.php b/lib/tfpdf/font/courierb.php new file mode 100644 index 0000000..f472063 --- /dev/null +++ b/lib/tfpdf/font/courierb.php @@ -0,0 +1,8 @@ + diff --git a/lib/tfpdf/font/courierbi.php b/lib/tfpdf/font/courierbi.php new file mode 100644 index 0000000..6ce8501 --- /dev/null +++ b/lib/tfpdf/font/courierbi.php @@ -0,0 +1,8 @@ + diff --git a/lib/tfpdf/font/courieri.php b/lib/tfpdf/font/courieri.php new file mode 100644 index 0000000..320150a --- /dev/null +++ b/lib/tfpdf/font/courieri.php @@ -0,0 +1,8 @@ + diff --git a/lib/tfpdf/font/helvetica.php b/lib/tfpdf/font/helvetica.php new file mode 100644 index 0000000..891ea25 --- /dev/null +++ b/lib/tfpdf/font/helvetica.php @@ -0,0 +1,19 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +?> diff --git a/lib/tfpdf/font/helveticab.php b/lib/tfpdf/font/helveticab.php new file mode 100644 index 0000000..6c9b972 --- /dev/null +++ b/lib/tfpdf/font/helveticab.php @@ -0,0 +1,19 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +?> diff --git a/lib/tfpdf/font/helveticabi.php b/lib/tfpdf/font/helveticabi.php new file mode 100644 index 0000000..4720311 --- /dev/null +++ b/lib/tfpdf/font/helveticabi.php @@ -0,0 +1,19 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); +?> diff --git a/lib/tfpdf/font/helveticai.php b/lib/tfpdf/font/helveticai.php new file mode 100644 index 0000000..25fb794 --- /dev/null +++ b/lib/tfpdf/font/helveticai.php @@ -0,0 +1,19 @@ +278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +?> diff --git a/lib/tfpdf/font/symbol.php b/lib/tfpdf/font/symbol.php new file mode 100644 index 0000000..a89f295 --- /dev/null +++ b/lib/tfpdf/font/symbol.php @@ -0,0 +1,19 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549, + ','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722, + 'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768, + 'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576, + 'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0, + chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603, + chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768, + chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042, + chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329, + chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0); +?> diff --git a/lib/tfpdf/font/times.php b/lib/tfpdf/font/times.php new file mode 100644 index 0000000..57b9bce --- /dev/null +++ b/lib/tfpdf/font/times.php @@ -0,0 +1,19 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722, + 'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944, + 'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500); +?> diff --git a/lib/tfpdf/font/timesb.php b/lib/tfpdf/font/timesb.php new file mode 100644 index 0000000..6946b9e --- /dev/null +++ b/lib/tfpdf/font/timesb.php @@ -0,0 +1,19 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000, + 'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833, + 'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); +?> diff --git a/lib/tfpdf/font/timesbi.php b/lib/tfpdf/font/timesbi.php new file mode 100644 index 0000000..cd5e5f1 --- /dev/null +++ b/lib/tfpdf/font/timesbi.php @@ -0,0 +1,19 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667, + 'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889, + 'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444); +?> diff --git a/lib/tfpdf/font/timesi.php b/lib/tfpdf/font/timesi.php new file mode 100644 index 0000000..d6da2a9 --- /dev/null +++ b/lib/tfpdf/font/timesi.php @@ -0,0 +1,19 @@ +250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611, + 'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833, + 'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444); +?> diff --git a/lib/tfpdf/font/unifont/DejaVuSans-Bold.ttf b/lib/tfpdf/font/unifont/DejaVuSans-Bold.ttf new file mode 100644 index 0000000..6d65fa7 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSans-Bold.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSans-BoldOblique.ttf b/lib/tfpdf/font/unifont/DejaVuSans-BoldOblique.ttf new file mode 100644 index 0000000..753f2d8 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSans-BoldOblique.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSans-ExtraLight.ttf b/lib/tfpdf/font/unifont/DejaVuSans-ExtraLight.ttf new file mode 100644 index 0000000..b09f32d Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSans-ExtraLight.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSans-Oblique.ttf b/lib/tfpdf/font/unifont/DejaVuSans-Oblique.ttf new file mode 100644 index 0000000..999bac7 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSans-Oblique.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSans.ttf b/lib/tfpdf/font/unifont/DejaVuSans.ttf new file mode 100644 index 0000000..e5f7eec Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSans.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansCondensed-Bold.ttf b/lib/tfpdf/font/unifont/DejaVuSansCondensed-Bold.ttf new file mode 100644 index 0000000..22987c6 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansCondensed-Bold.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansCondensed-BoldOblique.ttf b/lib/tfpdf/font/unifont/DejaVuSansCondensed-BoldOblique.ttf new file mode 100644 index 0000000..f5fa0ca Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansCondensed-BoldOblique.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansCondensed-Oblique.ttf b/lib/tfpdf/font/unifont/DejaVuSansCondensed-Oblique.ttf new file mode 100644 index 0000000..7fde907 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansCondensed-Oblique.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansCondensed.ttf b/lib/tfpdf/font/unifont/DejaVuSansCondensed.ttf new file mode 100644 index 0000000..3259bc2 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansCondensed.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansMono-Bold.ttf b/lib/tfpdf/font/unifont/DejaVuSansMono-Bold.ttf new file mode 100644 index 0000000..8184ced Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansMono-Bold.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansMono-BoldOblique.ttf b/lib/tfpdf/font/unifont/DejaVuSansMono-BoldOblique.ttf new file mode 100644 index 0000000..754dca7 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansMono-BoldOblique.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansMono-Oblique.ttf b/lib/tfpdf/font/unifont/DejaVuSansMono-Oblique.ttf new file mode 100644 index 0000000..4c858d4 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansMono-Oblique.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSansMono.ttf b/lib/tfpdf/font/unifont/DejaVuSansMono.ttf new file mode 100644 index 0000000..f578602 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSansMono.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerif-Bold.ttf b/lib/tfpdf/font/unifont/DejaVuSerif-Bold.ttf new file mode 100644 index 0000000..3bb755f Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerif-Bold.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerif-BoldItalic.ttf b/lib/tfpdf/font/unifont/DejaVuSerif-BoldItalic.ttf new file mode 100644 index 0000000..a36dd4b Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerif-BoldItalic.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerif-Italic.ttf b/lib/tfpdf/font/unifont/DejaVuSerif-Italic.ttf new file mode 100644 index 0000000..805daf2 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerif-Italic.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerif.ttf b/lib/tfpdf/font/unifont/DejaVuSerif.ttf new file mode 100644 index 0000000..0b803d2 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerif.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerifCondensed-Bold.ttf b/lib/tfpdf/font/unifont/DejaVuSerifCondensed-Bold.ttf new file mode 100644 index 0000000..222bf13 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerifCondensed-Bold.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerifCondensed-BoldItalic.ttf b/lib/tfpdf/font/unifont/DejaVuSerifCondensed-BoldItalic.ttf new file mode 100644 index 0000000..e446636 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerifCondensed-BoldItalic.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerifCondensed-Italic.ttf b/lib/tfpdf/font/unifont/DejaVuSerifCondensed-Italic.ttf new file mode 100644 index 0000000..c529df3 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerifCondensed-Italic.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVuSerifCondensed.ttf b/lib/tfpdf/font/unifont/DejaVuSerifCondensed.ttf new file mode 100644 index 0000000..d3959b3 Binary files /dev/null and b/lib/tfpdf/font/unifont/DejaVuSerifCondensed.ttf differ diff --git a/lib/tfpdf/font/unifont/DejaVu_LICENSE.txt b/lib/tfpdf/font/unifont/DejaVu_LICENSE.txt new file mode 100644 index 0000000..254e2cc --- /dev/null +++ b/lib/tfpdf/font/unifont/DejaVu_LICENSE.txt @@ -0,0 +1,99 @@ +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ diff --git a/lib/tfpdf/font/unifont/ttfonts.php b/lib/tfpdf/font/unifont/ttfonts.php new file mode 100644 index 0000000..9ccb8fb --- /dev/null +++ b/lib/tfpdf/font/unifont/ttfonts.php @@ -0,0 +1,1090 @@ + * +* License: LGPL * +* Copyright (c) Ian Back, 2010 * +* This header must be retained in any redistribution or * +* modification of the file. * +* * +*******************************************************************************/ + +// Define the value used in the "head" table of a created TTF file +// 0x74727565 "true" for Mac +// 0x00010000 for Windows +// Either seems to work for a font embedded in a PDF file +// when read by Adobe Reader on a Windows PC(!) +define("_TTF_MAC_HEADER", false); + + +// TrueType Font Glyph operators +define("GF_WORDS",(1 << 0)); +define("GF_SCALE",(1 << 3)); +define("GF_MORE",(1 << 5)); +define("GF_XYSCALE",(1 << 6)); +define("GF_TWOBYTWO",(1 << 7)); + + + +class TTFontFile { + +public $maxUni; +public $maxUniChar; +public $sFamilyClass; +public $sFamilySubClass; +public $_pos; +public $numTables; +public $searchRange; +public $entrySelector; +public $rangeShift; +public $tables; +public $otables; +public $filename; +public $fh; +public $hmetrics; +public $glyphPos; +public $charToGlyph; +public $codeToGlyph; +public $glyphdata; +public $ascent; +public $descent; +public $TTCFonts; +public $version; +public $name; +public $familyName; +public $styleName; +public $fullName; +public $uniqueFontID; +public $unitsPerEm; +public $bbox; +public $capHeight; +public $stemV; +public $italicAngle; +public $flags; +public $underlinePosition; +public $underlineThickness; +public $charWidths; +public $defaultWidth; +public $maxStrLenRead; + + function __construct() { + $this->maxStrLenRead = 200000; // Maximum size of glyf table to read in as string (otherwise reads each glyph from file) + } + + + function getMetrics($file) { + $this->filename = $file; + $this->fh = fopen($file,'rb') or die('Can\'t open file ' . $file); + $this->_pos = 0; + $this->charWidths = ''; + $this->glyphPos = array(); + $this->charToGlyph = array(); + $this->tables = array(); + $this->otables = array(); + $this->ascent = 0; + $this->descent = 0; + $this->TTCFonts = array(); + $this->version = $version = $this->read_ulong(); + if ($version==0x4F54544F) + die("Postscript outlines are not supported"); + if ($version==0x74746366) + die("ERROR - TrueType Fonts Collections not supported"); + if (!in_array($version, array(0x00010000,0x74727565))) + die("Not a TrueType font: version=".$version); + $this->readTableDirectory(); + $this->extractInfo(); + fclose($this->fh); + } + + + function readTableDirectory() { + $this->numTables = $this->read_ushort(); + $this->searchRange = $this->read_ushort(); + $this->entrySelector = $this->read_ushort(); + $this->rangeShift = $this->read_ushort(); + $this->tables = array(); + for ($i=0;$i<$this->numTables;$i++) { + $record = array(); + $record['tag'] = $this->read_tag(); + $record['checksum'] = array($this->read_ushort(),$this->read_ushort()); + $record['offset'] = $this->read_ulong(); + $record['length'] = $this->read_ulong(); + $this->tables[$record['tag']] = $record; + } + } + + + function sub32($x, $y) { + $xlo = $x[1]; + $xhi = $x[0]; + $ylo = $y[1]; + $yhi = $y[0]; + if ($ylo > $xlo) { $xlo += 1 << 16; $yhi += 1; } + $reslo = $xlo-$ylo; + if ($yhi > $xhi) { $xhi += 1 << 16; } + $reshi = $xhi-$yhi; + $reshi = $reshi & 0xFFFF; + return array($reshi, $reslo); + } + + function calcChecksum($data) { + if (strlen($data) % 4) { $data .= str_repeat("\0",(4-(strlen($data) % 4))); } + $hi=0x0000; + $lo=0x0000; + for($i=0;$i> 16; + $lo = $lo & 0xFFFF; + $hi = $hi & 0xFFFF; + } + return array($hi, $lo); + } + + function get_table_pos($tag) { + $offset = $this->tables[$tag]['offset']; + $length = $this->tables[$tag]['length']; + return array($offset, $length); + } + + function seek($pos) { + $this->_pos = $pos; + fseek($this->fh,$this->_pos); + } + + function skip($delta) { + $this->_pos = $this->_pos + $delta; + fseek($this->fh,$this->_pos); + } + + function seek_table($tag, $offset_in_table = 0) { + $tpos = $this->get_table_pos($tag); + $this->_pos = $tpos[0] + $offset_in_table; + fseek($this->fh, $this->_pos); + return $this->_pos; + } + + function read_tag() { + $this->_pos += 4; + return fread($this->fh,4); + } + + function read_short() { + $this->_pos += 2; + $s = fread($this->fh,2); + $a = (ord($s[0])<<8) + ord($s[1]); + if ($a & (1 << 15) ) { $a = ($a - (1 << 16)) ; } + return $a; + } + + function unpack_short($s) { + $a = (ord($s[0])<<8) + ord($s[1]); + if ($a & (1 << 15) ) { + $a = ($a - (1 << 16)); + } + return $a; + } + + function read_ushort() { + $this->_pos += 2; + $s = fread($this->fh,2); + return (ord($s[0])<<8) + ord($s[1]); + } + + function read_ulong() { + $this->_pos += 4; + $s = fread($this->fh,4); + // if large uInt32 as an integer, PHP converts it to -ve + return (ord($s[0])*16777216) + (ord($s[1])<<16) + (ord($s[2])<<8) + ord($s[3]); // 16777216 = 1<<24 + } + + function get_ushort($pos) { + fseek($this->fh,$pos); + $s = fread($this->fh,2); + return (ord($s[0])<<8) + ord($s[1]); + } + + function get_ulong($pos) { + fseek($this->fh,$pos); + $s = fread($this->fh,4); + // iF large uInt32 as an integer, PHP converts it to -ve + return (ord($s[0])*16777216) + (ord($s[1])<<16) + (ord($s[2])<<8) + ord($s[3]); // 16777216 = 1<<24 + } + + function pack_short($val) { + if ($val<0) { + $val = abs($val); + $val = ~$val; + $val += 1; + } + return pack("n",$val); + } + + function splice($stream, $offset, $value) { + return substr($stream,0,$offset) . $value . substr($stream,$offset+strlen($value)); + } + + function _set_ushort($stream, $offset, $value) { + $up = pack("n", $value); + return $this->splice($stream, $offset, $up); + } + + function _set_short($stream, $offset, $val) { + if ($val<0) { + $val = abs($val); + $val = ~$val; + $val += 1; + } + $up = pack("n",$val); + return $this->splice($stream, $offset, $up); + } + + function get_chunk($pos, $length) { + fseek($this->fh,$pos); + if ($length <1) { return ''; } + return (fread($this->fh,$length)); + } + + function get_table($tag) { + list($pos, $length) = $this->get_table_pos($tag); + if ($length == 0) { die('Truetype font ('.$this->filename.'): error reading table: '.$tag); } + fseek($this->fh,$pos); + return (fread($this->fh,$length)); + } + + function add($tag, $data) { + if ($tag == 'head') { + $data = $this->splice($data, 8, "\0\0\0\0"); + } + $this->otables[$tag] = $data; + } + + + +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +///////////////////////////////////////////////////////////////////////////////////////// + + function extractInfo() { + /////////////////////////////////// + // name - Naming table + /////////////////////////////////// + $this->sFamilyClass = 0; + $this->sFamilySubClass = 0; + + $name_offset = $this->seek_table("name"); + $format = $this->read_ushort(); + if ($format != 0) + die("Unknown name table format ".$format); + $numRecords = $this->read_ushort(); + $string_data_offset = $name_offset + $this->read_ushort(); + $names = array(1=>'',2=>'',3=>'',4=>'',6=>''); + $K = array_keys($names); + $nameCount = count($names); + for ($i=0;$i<$numRecords; $i++) { + $platformId = $this->read_ushort(); + $encodingId = $this->read_ushort(); + $languageId = $this->read_ushort(); + $nameId = $this->read_ushort(); + $length = $this->read_ushort(); + $offset = $this->read_ushort(); + if (!in_array($nameId,$K)) continue; + $N = ''; + if ($platformId == 3 && $encodingId == 1 && $languageId == 0x409) { // Microsoft, Unicode, US English, PS Name + $opos = $this->_pos; + $this->seek($string_data_offset + $offset); + if ($length % 2 != 0) + die("PostScript name is UTF-16BE string of odd length"); + $length /= 2; + $N = ''; + while ($length > 0) { + $char = $this->read_ushort(); + $N .= (chr($char)); + $length -= 1; + } + $this->_pos = $opos; + $this->seek($opos); + } + else if ($platformId == 1 && $encodingId == 0 && $languageId == 0) { // Macintosh, Roman, English, PS Name + $opos = $this->_pos; + $N = $this->get_chunk($string_data_offset + $offset, $length); + $this->_pos = $opos; + $this->seek($opos); + } + if ($N && $names[$nameId]=='') { + $names[$nameId] = $N; + $nameCount -= 1; + if ($nameCount==0) break; + } + } + if ($names[6]) + $psName = $names[6]; + else if ($names[4]) + $psName = preg_replace('/ /','-',$names[4]); + else if ($names[1]) + $psName = preg_replace('/ /','-',$names[1]); + else + $psName = ''; + if (!$psName) + die("Could not find PostScript font name"); + $this->name = $psName; + if ($names[1]) { $this->familyName = $names[1]; } else { $this->familyName = $psName; } + if ($names[2]) { $this->styleName = $names[2]; } else { $this->styleName = 'Regular'; } + if ($names[4]) { $this->fullName = $names[4]; } else { $this->fullName = $psName; } + if ($names[3]) { $this->uniqueFontID = $names[3]; } else { $this->uniqueFontID = $psName; } + if ($names[6]) { $this->fullName = $names[6]; } + + /////////////////////////////////// + // head - Font header table + /////////////////////////////////// + $this->seek_table("head"); + $this->skip(18); + $this->unitsPerEm = $unitsPerEm = $this->read_ushort(); + $scale = 1000 / $unitsPerEm; + $this->skip(16); + $xMin = $this->read_short(); + $yMin = $this->read_short(); + $xMax = $this->read_short(); + $yMax = $this->read_short(); + $this->bbox = array(($xMin*$scale), ($yMin*$scale), ($xMax*$scale), ($yMax*$scale)); + $this->skip(3*2); + $indexToLocFormat = $this->read_ushort(); + $glyphDataFormat = $this->read_ushort(); + if ($glyphDataFormat != 0) + die('Unknown glyph data format '.$glyphDataFormat); + + /////////////////////////////////// + // hhea metrics table + /////////////////////////////////// + // ttf2t1 seems to use this value rather than the one in OS/2 - so put in for compatibility + if (isset($this->tables["hhea"])) { + $this->seek_table("hhea"); + $this->skip(4); + $hheaAscender = $this->read_short(); + $hheaDescender = $this->read_short(); + $this->ascent = ($hheaAscender *$scale); + $this->descent = ($hheaDescender *$scale); + } + + /////////////////////////////////// + // OS/2 - OS/2 and Windows metrics table + /////////////////////////////////// + if (isset($this->tables["OS/2"])) { + $this->seek_table("OS/2"); + $version = $this->read_ushort(); + $this->skip(2); + $usWeightClass = $this->read_ushort(); + $this->skip(2); + $fsType = $this->read_ushort(); + if ($fsType == 0x0002 || ($fsType & 0x0300) != 0) { + die('ERROR - Font file '.$this->filename.' cannot be embedded due to copyright restrictions.'); + $this->restrictedUse = true; + } + $this->skip(20); + $sF = $this->read_short(); + $this->sFamilyClass = ($sF >> 8); + $this->sFamilySubClass = ($sF & 0xFF); + $this->_pos += 10; //PANOSE = 10 byte length + $panose = fread($this->fh,10); + $this->skip(26); + $sTypoAscender = $this->read_short(); + $sTypoDescender = $this->read_short(); + if (!$this->ascent) $this->ascent = ($sTypoAscender*$scale); + if (!$this->descent) $this->descent = ($sTypoDescender*$scale); + if ($version > 1) { + $this->skip(16); + $sCapHeight = $this->read_short(); + $this->capHeight = ($sCapHeight*$scale); + } + else { + $this->capHeight = $this->ascent; + } + } + else { + $usWeightClass = 500; + if (!$this->ascent) $this->ascent = ($yMax*$scale); + if (!$this->descent) $this->descent = ($yMin*$scale); + $this->capHeight = $this->ascent; + } + $this->stemV = 50 + intval(pow(($usWeightClass / 65.0),2)); + + /////////////////////////////////// + // post - PostScript table + /////////////////////////////////// + $this->seek_table("post"); + $this->skip(4); + $this->italicAngle = $this->read_short() + $this->read_ushort() / 65536.0; + $this->underlinePosition = $this->read_short() * $scale; + $this->underlineThickness = $this->read_short() * $scale; + $isFixedPitch = $this->read_ulong(); + + $this->flags = 4; + + if ($this->italicAngle!= 0) + $this->flags = $this->flags | 64; + if ($usWeightClass >= 600) + $this->flags = $this->flags | 262144; + if ($isFixedPitch) + $this->flags = $this->flags | 1; + + /////////////////////////////////// + // hhea - Horizontal header table + /////////////////////////////////// + $this->seek_table("hhea"); + $this->skip(32); + $metricDataFormat = $this->read_ushort(); + if ($metricDataFormat != 0) + die('Unknown horizontal metric data format '.$metricDataFormat); + $numberOfHMetrics = $this->read_ushort(); + if ($numberOfHMetrics == 0) + die('Number of horizontal metrics is 0'); + + /////////////////////////////////// + // maxp - Maximum profile table + /////////////////////////////////// + $this->seek_table("maxp"); + $this->skip(4); + $numGlyphs = $this->read_ushort(); + + + /////////////////////////////////// + // cmap - Character to glyph index mapping table + /////////////////////////////////// + $cmap_offset = $this->seek_table("cmap"); + $this->skip(2); + $cmapTableCount = $this->read_ushort(); + $unicode_cmap_offset = 0; + for ($i=0;$i<$cmapTableCount;$i++) { + $platformID = $this->read_ushort(); + $encodingID = $this->read_ushort(); + $offset = $this->read_ulong(); + $save_pos = $this->_pos; + if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode + $format = $this->get_ushort($cmap_offset + $offset); + if ($format == 4) { + if (!$unicode_cmap_offset) $unicode_cmap_offset = $cmap_offset + $offset; + break; + } + } + $this->seek($save_pos ); + } + if (!$unicode_cmap_offset) + die('Font ('.$this->filename .') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)'); + + + $glyphToChar = array(); + $charToGlyph = array(); + $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph ); + + /////////////////////////////////// + // hmtx - Horizontal metrics table + /////////////////////////////////// + $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale); + + } + + +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + + + function makeSubset($file, &$subset) { + $this->filename = $file; + $this->fh = fopen($file ,'rb') or die('Can\'t open file ' . $file); + $this->_pos = 0; + $this->charWidths = ''; + $this->glyphPos = array(); + $this->charToGlyph = array(); + $this->tables = array(); + $this->otables = array(); + $this->ascent = 0; + $this->descent = 0; + $this->skip(4); + $this->maxUni = 0; + $this->readTableDirectory(); + + + /////////////////////////////////// + // head - Font header table + /////////////////////////////////// + $this->seek_table("head"); + $this->skip(50); + $indexToLocFormat = $this->read_ushort(); + $glyphDataFormat = $this->read_ushort(); + + /////////////////////////////////// + // hhea - Horizontal header table + /////////////////////////////////// + $this->seek_table("hhea"); + $this->skip(32); + $metricDataFormat = $this->read_ushort(); + $orignHmetrics = $numberOfHMetrics = $this->read_ushort(); + + /////////////////////////////////// + // maxp - Maximum profile table + /////////////////////////////////// + $this->seek_table("maxp"); + $this->skip(4); + $numGlyphs = $this->read_ushort(); + + + /////////////////////////////////// + // cmap - Character to glyph index mapping table + /////////////////////////////////// + $cmap_offset = $this->seek_table("cmap"); + $this->skip(2); + $cmapTableCount = $this->read_ushort(); + $unicode_cmap_offset = 0; + for ($i=0;$i<$cmapTableCount;$i++) { + $platformID = $this->read_ushort(); + $encodingID = $this->read_ushort(); + $offset = $this->read_ulong(); + $save_pos = $this->_pos; + if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode + $format = $this->get_ushort($cmap_offset + $offset); + if ($format == 4) { + $unicode_cmap_offset = $cmap_offset + $offset; + break; + } + } + $this->seek($save_pos ); + } + + if (!$unicode_cmap_offset) + die('Font ('.$this->filename .') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)'); + + + $glyphToChar = array(); + $charToGlyph = array(); + $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph ); + + $this->charToGlyph = $charToGlyph; + + /////////////////////////////////// + // hmtx - Horizontal metrics table + /////////////////////////////////// + $scale = 1; // not used + $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale); + + /////////////////////////////////// + // loca - Index to location + /////////////////////////////////// + $this->getLOCA($indexToLocFormat, $numGlyphs); + + $subsetglyphs = array(0=>0); + $subsetCharToGlyph = array(); + foreach($subset AS $code) { + if (isset($this->charToGlyph[$code])) { + $subsetglyphs[$this->charToGlyph[$code]] = $code; // Old Glyph ID => Unicode + $subsetCharToGlyph[$code] = $this->charToGlyph[$code]; // Unicode to old GlyphID + + } + $this->maxUni = max($this->maxUni, $code); + } + + list($start,$dummy) = $this->get_table_pos('glyf'); + + $glyphSet = array(); + ksort($subsetglyphs); + $n = 0; + $fsLastCharIndex = 0; // maximum Unicode index (character code) in this font, according to the cmap subtable for platform ID 3 and platform- specific encoding ID 0 or 1. + foreach($subsetglyphs AS $originalGlyphIdx => $uni) { + $fsLastCharIndex = max($fsLastCharIndex , $uni); + $glyphSet[$originalGlyphIdx] = $n; // old glyphID to new glyphID + $n++; + } + + ksort($subsetCharToGlyph); + foreach($subsetCharToGlyph AS $uni => $originalGlyphIdx) { + $codeToGlyph[$uni] = $glyphSet[$originalGlyphIdx] ; + } + $this->codeToGlyph = $codeToGlyph; + + ksort($subsetglyphs); + foreach($subsetglyphs AS $originalGlyphIdx => $uni) { + $this->getGlyphs($originalGlyphIdx, $start, $glyphSet, $subsetglyphs); + } + + $numGlyphs = $numberOfHMetrics = count($subsetglyphs ); + + //tables copied from the original + $tags = array ('name'); + foreach($tags AS $tag) { $this->add($tag, $this->get_table($tag)); } + $tags = array ('cvt ', 'fpgm', 'prep', 'gasp'); + foreach($tags AS $tag) { + if (isset($this->tables[$tag])) { $this->add($tag, $this->get_table($tag)); } + } + + // post - PostScript + $opost = $this->get_table('post'); + $post = "\x00\x03\x00\x00" . substr($opost,4,12) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + $this->add('post', $post); + + // Sort CID2GID map into segments of contiguous codes + ksort($codeToGlyph); + unset($codeToGlyph[0]); + //unset($codeToGlyph[65535]); + $rangeid = 0; + $range = array(); + $prevcid = -2; + $prevglidx = -1; + // for each character + foreach ($codeToGlyph as $cid => $glidx) { + if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) { + $range[$rangeid][] = $glidx; + } else { + // new range + $rangeid = $cid; + $range[$rangeid] = array(); + $range[$rangeid][] = $glidx; + } + $prevcid = $cid; + $prevglidx = $glidx; + } + + // cmap - Character to glyph mapping - Format 4 (MS / ) + $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF + $searchRange = 1; + $entrySelector = 0; + while ($searchRange * 2 <= $segCount ) { + $searchRange = $searchRange * 2; + $entrySelector = $entrySelector + 1; + } + $searchRange = $searchRange * 2; + $rangeShift = $segCount * 2 - $searchRange; + $length = 16 + (8*$segCount ) + ($numGlyphs+1); + $cmap = array(0, 1, // Index : version, number of encoding subtables + 3, 1, // Encoding Subtable : platform (MS=3), encoding (Unicode) + 0, 12, // Encoding Subtable : offset (hi,lo) + 4, $length, 0, // Format 4 Mapping subtable: format, length, language + $segCount*2, + $searchRange, + $entrySelector, + $rangeShift); + + // endCode(s) + foreach($range AS $start=>$subrange) { + $endCode = $start + (count($subrange)-1); + $cmap[] = $endCode; // endCode(s) + } + $cmap[] = 0xFFFF; // endCode of last Segment + $cmap[] = 0; // reservedPad + + // startCode(s) + foreach($range AS $start=>$subrange) { + $cmap[] = $start; // startCode(s) + } + $cmap[] = 0xFFFF; // startCode of last Segment + // idDelta(s) + foreach($range AS $start=>$subrange) { + $idDelta = -($start-$subrange[0]); + $n += count($subrange); + $cmap[] = $idDelta; // idDelta(s) + } + $cmap[] = 1; // idDelta of last Segment + // idRangeOffset(s) + foreach($range AS $subrange) { + $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 + + } + $cmap[] = 0; // idRangeOffset of last Segment + foreach($range AS $subrange) { + foreach($subrange AS $glidx) { + $cmap[] = $glidx; + } + } + $cmap[] = 0; // Mapping for last character + $cmapstr = ''; + foreach($cmap AS $cm) { $cmapstr .= pack("n",$cm); } + $this->add('cmap', $cmapstr); + + + // glyf - Glyph data + list($glyfOffset,$glyfLength) = $this->get_table_pos('glyf'); + if ($glyfLength < $this->maxStrLenRead) { + $glyphData = $this->get_table('glyf'); + } + + $offsets = array(); + $glyf = ''; + $pos = 0; + + $hmtxstr = ''; + $xMinT = 0; + $yMinT = 0; + $xMaxT = 0; + $yMaxT = 0; + $advanceWidthMax = 0; + $minLeftSideBearing = 0; + $minRightSideBearing = 0; + $xMaxExtent = 0; + $maxPoints = 0; // points in non-compound glyph + $maxContours = 0; // contours in non-compound glyph + $maxComponentPoints = 0; // points in compound glyph + $maxComponentContours = 0; // contours in compound glyph + $maxComponentElements = 0; // number of glyphs referenced at top level + $maxComponentDepth = 0; // levels of recursion, set to 0 if font has only simple glyphs + $this->glyphdata = array(); + + foreach($subsetglyphs AS $originalGlyphIdx => $uni) { + // hmtx - Horizontal Metrics + $hm = $this->getHMetric($orignHmetrics, $originalGlyphIdx); + $hmtxstr .= $hm; + + $offsets[] = $pos; + $glyphPos = $this->glyphPos[$originalGlyphIdx]; + $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos; + if ($glyfLength < $this->maxStrLenRead) { + $data = substr($glyphData,$glyphPos,$glyphLen); + } + else { + if ($glyphLen > 0) $data = $this->get_chunk($glyfOffset+$glyphPos,$glyphLen); + else $data = ''; + } + + if ($glyphLen > 0) { + $up = unpack("n", substr($data,0,2)); + } + + if ($glyphLen > 2 && ($up[1] & (1 << 15)) ) { // If number of contours <= -1 i.e. composite glyph + $pos_in_glyph = 10; + $flags = GF_MORE; + $nComponentElements = 0; + while ($flags & GF_MORE) { + $nComponentElements += 1; // number of glyphs referenced at top level + $up = unpack("n", substr($data,$pos_in_glyph,2)); + $flags = $up[1]; + $up = unpack("n", substr($data,$pos_in_glyph+2,2)); + $glyphIdx = $up[1]; + $this->glyphdata[$originalGlyphIdx]['compGlyphs'][] = $glyphIdx; + $data = $this->_set_ushort($data, $pos_in_glyph + 2, $glyphSet[$glyphIdx]); + $pos_in_glyph += 4; + if ($flags & GF_WORDS) { $pos_in_glyph += 4; } + else { $pos_in_glyph += 2; } + if ($flags & GF_SCALE) { $pos_in_glyph += 2; } + else if ($flags & GF_XYSCALE) { $pos_in_glyph += 4; } + else if ($flags & GF_TWOBYTWO) { $pos_in_glyph += 8; } + } + $maxComponentElements = max($maxComponentElements, $nComponentElements); + } + + $glyf .= $data; + $pos += $glyphLen; + if ($pos % 4 != 0) { + $padding = 4 - ($pos % 4); + $glyf .= str_repeat("\0",$padding); + $pos += $padding; + } + } + + $offsets[] = $pos; + $this->add('glyf', $glyf); + + // hmtx - Horizontal Metrics + $this->add('hmtx', $hmtxstr); + + // loca - Index to location + $locastr = ''; + if ((($pos + 1) >> 1) > 0xFFFF) { + $indexToLocFormat = 1; // long format + foreach($offsets AS $offset) { $locastr .= pack("N",$offset); } + } + else { + $indexToLocFormat = 0; // short format + foreach($offsets AS $offset) { $locastr .= pack("n",($offset/2)); } + } + $this->add('loca', $locastr); + + // head - Font header + $head = $this->get_table('head'); + $head = $this->_set_ushort($head, 50, $indexToLocFormat); + $this->add('head', $head); + + + // hhea - Horizontal Header + $hhea = $this->get_table('hhea'); + $hhea = $this->_set_ushort($hhea, 34, $numberOfHMetrics); + $this->add('hhea', $hhea); + + // maxp - Maximum Profile + $maxp = $this->get_table('maxp'); + $maxp = $this->_set_ushort($maxp, 4, $numGlyphs); + $this->add('maxp', $maxp); + + + // OS/2 - OS/2 + $os2 = $this->get_table('OS/2'); + $this->add('OS/2', $os2 ); + + fclose($this->fh); + + // Put the TTF file together + $stm = ''; + $this->endTTFile($stm); + return $stm ; + } + + ////////////////////////////////////////////////////////////////////////////////// + // Recursively get composite glyph data + function getGlyphData($originalGlyphIdx, &$maxdepth, &$depth, &$points, &$contours) { + $depth++; + $maxdepth = max($maxdepth, $depth); + if (count($this->glyphdata[$originalGlyphIdx]['compGlyphs'])) { + foreach($this->glyphdata[$originalGlyphIdx]['compGlyphs'] AS $glyphIdx) { + $this->getGlyphData($glyphIdx, $maxdepth, $depth, $points, $contours); + } + } + else if (($this->glyphdata[$originalGlyphIdx]['nContours'] > 0) && $depth > 0) { // simple + $contours += $this->glyphdata[$originalGlyphIdx]['nContours']; + $points += $this->glyphdata[$originalGlyphIdx]['nPoints']; + } + $depth--; + } + + + ////////////////////////////////////////////////////////////////////////////////// + // Recursively get composite glyphs + function getGlyphs($originalGlyphIdx, &$start, &$glyphSet, &$subsetglyphs) { + $glyphPos = $this->glyphPos[$originalGlyphIdx]; + $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos; + if (!$glyphLen) { + return; + } + $this->seek($start + $glyphPos); + $numberOfContours = $this->read_short(); + if ($numberOfContours < 0) { + $this->skip(8); + $flags = GF_MORE; + while ($flags & GF_MORE) { + $flags = $this->read_ushort(); + $glyphIdx = $this->read_ushort(); + if (!isset($glyphSet[$glyphIdx])) { + $glyphSet[$glyphIdx] = count($subsetglyphs); // old glyphID to new glyphID + $subsetglyphs[$glyphIdx] = true; + } + $savepos = ftell($this->fh); + $this->getGlyphs($glyphIdx, $start, $glyphSet, $subsetglyphs); + $this->seek($savepos); + if ($flags & GF_WORDS) + $this->skip(4); + else + $this->skip(2); + if ($flags & GF_SCALE) + $this->skip(2); + else if ($flags & GF_XYSCALE) + $this->skip(4); + else if ($flags & GF_TWOBYTWO) + $this->skip(8); + } + } + } + + ////////////////////////////////////////////////////////////////////////////////// + + function getHMTX($numberOfHMetrics, $numGlyphs, &$glyphToChar, $scale) { + $start = $this->seek_table("hmtx"); + $aw = 0; + $this->charWidths = str_pad('', 256*256*2, "\x00"); + $nCharWidths = 0; + if (($numberOfHMetrics*4) < $this->maxStrLenRead) { + $data = $this->get_chunk($start,($numberOfHMetrics*4)); + $arr = unpack("n*", $data); + } + else { $this->seek($start); } + for( $glyph=0; $glyph<$numberOfHMetrics; $glyph++) { + + if (($numberOfHMetrics*4) < $this->maxStrLenRead) { + $aw = $arr[($glyph*2)+1]; + } + else { + $aw = $this->read_ushort(); + $lsb = $this->read_ushort(); + } + if (isset($glyphToChar[$glyph]) || $glyph == 0) { + + if ($aw >= (1 << 15) ) { $aw = 0; } // 1.03 Some (arabic) fonts have -ve values for width + // although should be unsigned value - comes out as e.g. 65108 (intended -50) + if ($glyph == 0) { + $this->defaultWidth = $scale*$aw; + continue; + } + foreach($glyphToChar[$glyph] AS $char) { + if ($char != 0 && $char != 65535) { + $w = intval(round($scale*$aw)); + if ($w == 0) { $w = 65535; } + if ($char < 196608) { + $this->charWidths[$char*2] = chr($w >> 8); + $this->charWidths[$char*2 + 1] = chr($w & 0xFF); + $nCharWidths++; + } + } + } + } + } + $data = $this->get_chunk(($start+$numberOfHMetrics*4),($numGlyphs*2)); + $arr = unpack("n*", $data); + $diff = $numGlyphs-$numberOfHMetrics; + for( $pos=0; $pos<$diff; $pos++) { + $glyph = $pos + $numberOfHMetrics; + if (isset($glyphToChar[$glyph])) { + foreach($glyphToChar[$glyph] AS $char) { + if ($char != 0 && $char != 65535) { + $w = intval(round($scale*$aw)); + if ($w == 0) { $w = 65535; } + if ($char < 196608) { + $this->charWidths[$char*2] = chr($w >> 8); + $this->charWidths[$char*2 + 1] = chr($w & 0xFF); + $nCharWidths++; + } + } + } + } + } + // NB 65535 is a set width of 0 + // First bytes define number of chars in font + $this->charWidths[0] = chr($nCharWidths >> 8); + $this->charWidths[1] = chr($nCharWidths & 0xFF); + } + + function getHMetric($numberOfHMetrics, $gid) { + $start = $this->seek_table("hmtx"); + if ($gid < $numberOfHMetrics) { + $this->seek($start+($gid*4)); + $hm = fread($this->fh,4); + } + else { + $this->seek($start+(($numberOfHMetrics-1)*4)); + $hm = fread($this->fh,2); + $this->seek($start+($numberOfHMetrics*2)+($gid*2)); + $hm .= fread($this->fh,2); + } + return $hm; + } + + function getLOCA($indexToLocFormat, $numGlyphs) { + $start = $this->seek_table('loca'); + $this->glyphPos = array(); + if ($indexToLocFormat == 0) { + $data = $this->get_chunk($start,($numGlyphs*2)+2); + $arr = unpack("n*", $data); + for ($n=0; $n<=$numGlyphs; $n++) { + $this->glyphPos[] = ($arr[$n+1] * 2); + } + } + else if ($indexToLocFormat == 1) { + $data = $this->get_chunk($start,($numGlyphs*4)+4); + $arr = unpack("N*", $data); + for ($n=0; $n<=$numGlyphs; $n++) { + $this->glyphPos[] = ($arr[$n+1]); + } + } + else + die('Unknown location table format '.$indexToLocFormat); + } + + + // CMAP Format 4 + function getCMAP4($unicode_cmap_offset, &$glyphToChar, &$charToGlyph ) { + $this->maxUniChar = 0; + $this->seek($unicode_cmap_offset + 2); + $length = $this->read_ushort(); + $limit = $unicode_cmap_offset + $length; + $this->skip(2); + + $segCount = $this->read_ushort() / 2; + $this->skip(6); + $endCount = array(); + for($i=0; $i<$segCount; $i++) { $endCount[] = $this->read_ushort(); } + $this->skip(2); + $startCount = array(); + for($i=0; $i<$segCount; $i++) { $startCount[] = $this->read_ushort(); } + $idDelta = array(); + for($i=0; $i<$segCount; $i++) { $idDelta[] = $this->read_short(); } // ???? was unsigned short + $idRangeOffset_start = $this->_pos; + $idRangeOffset = array(); + for($i=0; $i<$segCount; $i++) { $idRangeOffset[] = $this->read_ushort(); } + + for ($n=0;$n<$segCount;$n++) { + $endpoint = ($endCount[$n] + 1); + for ($unichar=$startCount[$n];$unichar<$endpoint;$unichar++) { + if ($idRangeOffset[$n] == 0) + $glyph = ($unichar + $idDelta[$n]) & 0xFFFF; + else { + $offset = ($unichar - $startCount[$n]) * 2 + $idRangeOffset[$n]; + $offset = $idRangeOffset_start + 2 * $n + $offset; + if ($offset >= $limit) + $glyph = 0; + else { + $glyph = $this->get_ushort($offset); + if ($glyph != 0) + $glyph = ($glyph + $idDelta[$n]) & 0xFFFF; + } + } + $charToGlyph[$unichar] = $glyph; + if ($unichar < 196608) { $this->maxUniChar = max($unichar,$this->maxUniChar); } + $glyphToChar[$glyph][] = $unichar; + } + } + } + + + // Put the TTF file together + function endTTFile(&$stm) { + $stm = ''; + $numTables = count($this->otables); + $searchRange = 1; + $entrySelector = 0; + while ($searchRange * 2 <= $numTables) { + $searchRange = $searchRange * 2; + $entrySelector = $entrySelector + 1; + } + $searchRange = $searchRange * 16; + $rangeShift = $numTables * 16 - $searchRange; + + // Header + if (_TTF_MAC_HEADER) { + $stm .= (pack("Nnnnn", 0x74727565, $numTables, $searchRange, $entrySelector, $rangeShift)); // Mac + } + else { + $stm .= (pack("Nnnnn", 0x00010000 , $numTables, $searchRange, $entrySelector, $rangeShift)); // Windows + } + + // Table directory + $tables = $this->otables; + + ksort ($tables); + $offset = 12 + $numTables * 16; + foreach ($tables AS $tag=>$data) { + if ($tag == 'head') { $head_start = $offset; } + $stm .= $tag; + $checksum = $this->calcChecksum($data); + $stm .= pack("nn", $checksum[0],$checksum[1]); + $stm .= pack("NN", $offset, strlen($data)); + $paddedLength = (strlen($data)+3)&~3; + $offset = $offset + $paddedLength; + } + + // Table data + foreach ($tables AS $tag=>$data) { + $data .= "\0\0\0"; + $stm .= substr($data,0,(strlen($data)&~3)); + } + + $checksum = $this->calcChecksum($stm); + $checksum = $this->sub32(array(0xB1B0,0xAFBA), $checksum); + $chk = pack("nn", $checksum[0],$checksum[1]); + $stm = $this->splice($stm,($head_start + 8),$chk); + return $stm ; + } + +} + +?> diff --git a/lib/tfpdf/font/zapfdingbats.php b/lib/tfpdf/font/zapfdingbats.php new file mode 100644 index 0000000..8f7d490 --- /dev/null +++ b/lib/tfpdf/font/zapfdingbats.php @@ -0,0 +1,19 @@ +0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0, + chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939, + ','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692, + 'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776, + 'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873, + 'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317, + chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788, + chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788, + chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918, + chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874, + chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0); +?> diff --git a/lib/tfpdf/tfpdf.php b/lib/tfpdf/tfpdf.php new file mode 100644 index 0000000..5c03f1c --- /dev/null +++ b/lib/tfpdf/tfpdf.php @@ -0,0 +1,2371 @@ + * +* Tycho Veltmeijer (versions 1.30+) * +* License: LGPL * +*******************************************************************************/ + +class tFPDF +{ +const VERSION = '1.33'; +protected $unifontSubset; +protected $page; // current page number +protected $n; // current object number +protected $offsets; // array of object offsets +protected $buffer; // buffer holding in-memory PDF +protected $pages; // array containing pages +protected $state; // current document state +protected $compress; // compression flag +protected $k; // scale factor (number of points in user unit) +protected $DefOrientation; // default orientation +protected $CurOrientation; // current orientation +protected $StdPageSizes; // standard page sizes +protected $DefPageSize; // default page size +protected $CurPageSize; // current page size +protected $CurRotation; // current page rotation +protected $PageInfo; // page-related data +protected $wPt, $hPt; // dimensions of current page in points +protected $w, $h; // dimensions of current page in user unit +protected $lMargin; // left margin +protected $tMargin; // top margin +protected $rMargin; // right margin +protected $bMargin; // page break margin +protected $cMargin; // cell margin +protected $x, $y; // current position in user unit +protected $lasth; // height of last printed cell +protected $LineWidth; // line width in user unit +protected $fontpath; // path containing fonts +protected $CoreFonts; // array of core font names +protected $fonts; // array of used fonts +protected $FontFiles; // array of font files +protected $encodings; // array of encodings +protected $cmaps; // array of ToUnicode CMaps +protected $FontFamily; // current font family +protected $FontStyle; // current font style +protected $underline; // underlining flag +protected $CurrentFont; // current font info +protected $FontSizePt; // current font size in points +protected $FontSize; // current font size in user unit +protected $DrawColor; // commands for drawing color +protected $FillColor; // commands for filling color +protected $TextColor; // commands for text color +protected $ColorFlag; // indicates whether fill and text colors are different +protected $WithAlpha; // indicates whether alpha channel is used +protected $ws; // word spacing +protected $images; // array of used images +protected $PageLinks; // array of links in pages +protected $links; // array of internal links +protected $AutoPageBreak; // automatic page breaking +protected $PageBreakTrigger; // threshold used to trigger page breaks +protected $InHeader; // flag set when processing header +protected $InFooter; // flag set when processing footer +protected $AliasNbPages; // alias for total number of pages +protected $ZoomMode; // zoom display mode +protected $LayoutMode; // layout display mode +protected $metadata; // document properties +protected $CreationDate; // document creation date +protected $PDFVersion; // PDF version number + +/******************************************************************************* +* Public methods * +*******************************************************************************/ + +function __construct($orientation='P', $unit='mm', $size='A4') +{ + // Some checks + $this->_dochecks(); + // Initialization of properties + $this->state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + // Font path + if(defined('FPDF_FONTPATH')) + { + $this->fontpath = FPDF_FONTPATH; + if(substr($this->fontpath,-1)!='/' && substr($this->fontpath,-1)!='\\') + $this->fontpath .= '/'; + } + elseif(is_dir(dirname(__FILE__).'/font')) + $this->fontpath = dirname(__FILE__).'/font/'; + else + $this->fontpath = ''; + // Core fonts + $this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats'); + // Scale factor + if($unit=='pt') + $this->k = 1; + elseif($unit=='mm') + $this->k = 72/25.4; + elseif($unit=='cm') + $this->k = 72/2.54; + elseif($unit=='in') + $this->k = 72; + else + $this->Error('Incorrect unit: '.$unit); + // Page sizes + $this->StdPageSizes = array('a3'=>array(841.89,1190.55), 'a4'=>array(595.28,841.89), 'a5'=>array(420.94,595.28), + 'letter'=>array(612,792), 'legal'=>array(612,1008)); + $size = $this->_getpagesize($size); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + // Page orientation + $orientation = strtolower($orientation); + if($orientation=='p' || $orientation=='portrait') + { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } + elseif($orientation=='l' || $orientation=='landscape') + { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } + else + $this->Error('Incorrect orientation: '.$orientation); + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + // Page rotation + $this->CurRotation = 0; + // Page margins (1 cm) + $margin = 28.35/$this->k; + $this->SetMargins($margin,$margin); + // Interior cell margin (1 mm) + $this->cMargin = $margin/10; + // Line width (0.2 mm) + $this->LineWidth = .567/$this->k; + // Automatic page break + $this->SetAutoPageBreak(true,2*$margin); + // Default display mode + $this->SetDisplayMode('default'); + // Enable compression + $this->SetCompression(true); + // Metadata + $this->metadata = array('Producer'=>'tFPDF '.self::VERSION); + // Set default PDF version number + $this->PDFVersion = '1.3'; +} + +function SetMargins($left, $top, $right=null) +{ + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if($right===null) + $right = $left; + $this->rMargin = $right; +} + +function SetLeftMargin($margin) +{ + // Set left margin + $this->lMargin = $margin; + if($this->page>0 && $this->x<$margin) + $this->x = $margin; +} + +function SetTopMargin($margin) +{ + // Set top margin + $this->tMargin = $margin; +} + +function SetRightMargin($margin) +{ + // Set right margin + $this->rMargin = $margin; +} + +function SetAutoPageBreak($auto, $margin=0) +{ + // Set auto page break mode and triggering margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h-$margin; +} + +function SetDisplayMode($zoom, $layout='default') +{ + // Set display mode in viewer + if($zoom=='fullpage' || $zoom=='fullwidth' || $zoom=='real' || $zoom=='default' || !is_string($zoom)) + $this->ZoomMode = $zoom; + else + $this->Error('Incorrect zoom display mode: '.$zoom); + if($layout=='single' || $layout=='continuous' || $layout=='two' || $layout=='default') + $this->LayoutMode = $layout; + else + $this->Error('Incorrect layout display mode: '.$layout); +} + +function SetCompression($compress) +{ + // Set page compression + if(function_exists('gzcompress')) + $this->compress = $compress; + else + $this->compress = false; +} + +function SetTitle($title, $isUTF8=false) +{ + // Title of document + $this->metadata['Title'] = $isUTF8 ? $title : $this->_UTF8encode($title); +} + +function SetAuthor($author, $isUTF8=false) +{ + // Author of document + $this->metadata['Author'] = $isUTF8 ? $author : $this->_UTF8encode($author); +} + +function SetSubject($subject, $isUTF8=false) +{ + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $subject : $this->_UTF8encode($subject); +} + +function SetKeywords($keywords, $isUTF8=false) +{ + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $keywords : $this->_UTF8encode($keywords); +} + +function SetCreator($creator, $isUTF8=false) +{ + // Creator of document + $this->metadata['Creator'] = $isUTF8 ? $creator : $this->_UTF8encode($creator); +} + +function AliasNbPages($alias='{nb}') +{ + // Define an alias for total number of pages + $this->AliasNbPages = $alias; +} + +function Error($msg) +{ + // Fatal error + throw new Exception('tFPDF error: '.$msg); +} + +function Close() +{ + // Terminate document + if($this->state==3) + return; + if($this->page==0) + $this->AddPage(); + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); +} + +function AddPage($orientation='', $size='', $rotation=0) +{ + // Start a new page + if($this->state==3) + $this->Error('The document is closed'); + $family = $this->FontFamily; + $style = $this->FontStyle.($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if($this->page>0) + { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage($orientation,$size,$rotation); + // Set line cap style to square + $this->_out('2 J'); + // Set line width + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w',$lw*$this->k)); + // Set font + if($family) + $this->SetFont($family,$style,$fontsize); + // Set colors + $this->DrawColor = $dc; + if($dc!='0 G') + $this->_out($dc); + $this->FillColor = $fc; + if($fc!='0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if($this->LineWidth!=$lw) + { + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w',$lw*$this->k)); + } + // Restore font + if($family) + $this->SetFont($family,$style,$fontsize); + // Restore colors + if($this->DrawColor!=$dc) + { + $this->DrawColor = $dc; + $this->_out($dc); + } + if($this->FillColor!=$fc) + { + $this->FillColor = $fc; + $this->_out($fc); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; +} + +function Header() +{ + // To be implemented in your own inherited class +} + +function Footer() +{ + // To be implemented in your own inherited class +} + +function PageNo() +{ + // Get current page number + return $this->page; +} + +function SetDrawColor($r, $g=null, $b=null) +{ + // Set color for all stroking operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->DrawColor = sprintf('%.3F G',$r/255); + else + $this->DrawColor = sprintf('%.3F %.3F %.3F RG',$r/255,$g/255,$b/255); + if($this->page>0) + $this->_out($this->DrawColor); +} + +function SetFillColor($r, $g=null, $b=null) +{ + // Set color for all filling operations + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->FillColor = sprintf('%.3F g',$r/255); + else + $this->FillColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); + if($this->page>0) + $this->_out($this->FillColor); +} + +function SetTextColor($r, $g=null, $b=null) +{ + // Set color for text + if(($r==0 && $g==0 && $b==0) || $g===null) + $this->TextColor = sprintf('%.3F g',$r/255); + else + $this->TextColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255); + $this->ColorFlag = ($this->FillColor!=$this->TextColor); +} + +function GetStringWidth($s) +{ + // Get width of a string in the current font + $s = (string)$s; + $cw = $this->CurrentFont['cw']; + $w=0; + if ($this->unifontSubset) { + $unicode = $this->UTF8StringToArray($s); + foreach($unicode as $char) { + if (isset($cw[2*$char])) { $w += (ord($cw[2*$char])<<8) + ord($cw[2*$char+1]); } + else if($char>0 && $char<128 && isset($cw[chr($char)])) { $w += $cw[chr($char)]; } + else if(isset($this->CurrentFont['desc']['MissingWidth'])) { $w += $this->CurrentFont['desc']['MissingWidth']; } + else if(isset($this->CurrentFont['MissingWidth'])) { $w += $this->CurrentFont['MissingWidth']; } + else { $w += 500; } + } + } + else { + $l = strlen($s); + for($i=0;$i<$l;$i++) + $w += $cw[$s[$i]]; + } + return $w*$this->FontSize/1000; +} + +function SetLineWidth($width) +{ + // Set line width + $this->LineWidth = $width; + if($this->page>0) + $this->_out(sprintf('%.2F w',$width*$this->k)); +} + +function Line($x1, $y1, $x2, $y2) +{ + // Draw a line + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S',$x1*$this->k,($this->h-$y1)*$this->k,$x2*$this->k,($this->h-$y2)*$this->k)); +} + +function Rect($x, $y, $w, $h, $style='') +{ + // Draw a rectangle + if($style=='F') + $op = 'f'; + elseif($style=='FD' || $style=='DF') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s',$x*$this->k,($this->h-$y)*$this->k,$w*$this->k,-$h*$this->k,$op)); +} + +function AddFont($family, $style='', $file='', $uni=false) +{ + // Add a TrueType, OpenType or Type1 font + $family = strtolower($family); + $style = strtoupper($style); + if($style=='IB') + $style = 'BI'; + if($file=='') { + if ($uni) { + $file = str_replace(' ','',$family).strtolower($style).'.ttf'; + } + else { + $file = str_replace(' ','',$family).strtolower($style).'.php'; + } + } + $fontkey = $family.$style; + if(isset($this->fonts[$fontkey])) + return; + if ($uni) { + if (defined("_SYSTEM_TTFONTS") && file_exists(_SYSTEM_TTFONTS.$file )) { $ttffilename = _SYSTEM_TTFONTS.$file ; } + else { $ttffilename = $this->fontpath.'unifont/'.$file ; } + $unifilename = $this->fontpath.'unifont/'.strtolower(substr($file ,0,(strpos($file ,'.')))); + $name = ''; + $originalsize = 0; + $ttfstat = stat($ttffilename); + if (file_exists($unifilename.'.mtx.php')) { + include($unifilename.'.mtx.php'); + } + if (!isset($type) || !isset($name) || $originalsize != $ttfstat['size']) { + $ttffile = $ttffilename; +// require_once($this->fontpath.'unifont/ttfonts.php'); + $ttf = new TTFontFile(); + $ttf->getMetrics($ttffile); + $cw = $ttf->charWidths; + $name = preg_replace('/[ ()]/','',$ttf->fullName); + + $desc= array('Ascent'=>round($ttf->ascent), + 'Descent'=>round($ttf->descent), + 'CapHeight'=>round($ttf->capHeight), + 'Flags'=>$ttf->flags, + 'FontBBox'=>'['.round($ttf->bbox[0])." ".round($ttf->bbox[1])." ".round($ttf->bbox[2])." ".round($ttf->bbox[3]).']', + 'ItalicAngle'=>$ttf->italicAngle, + 'StemV'=>round($ttf->stemV), + 'MissingWidth'=>round($ttf->defaultWidth)); + $up = round($ttf->underlinePosition); + $ut = round($ttf->underlineThickness); + $originalsize = $ttfstat['size']+0; + $type = 'TTF'; + // Generate metrics .php file + $s='"; + if (is_writable(dirname($this->fontpath.'unifont/'.'x'))) { + $fh = fopen($unifilename.'.mtx.php',"w"); + fwrite($fh,$s,strlen($s)); + fclose($fh); + $fh = fopen($unifilename.'.cw.dat',"wb"); + fwrite($fh,$cw,strlen($cw)); + fclose($fh); + @unlink($unifilename.'.cw127.php'); + } + unset($ttf); + } + else { + $cw = @file_get_contents($unifilename.'.cw.dat'); + } + $i = count($this->fonts)+1; + if(!empty($this->AliasNbPages)) + $sbarr = range(0,57); + else + $sbarr = range(0,32); + $this->fonts[$fontkey] = array('i'=>$i, 'type'=>$type, 'name'=>$name, 'desc'=>$desc, 'up'=>$up, 'ut'=>$ut, 'cw'=>$cw, 'ttffile'=>$ttffile, 'fontkey'=>$fontkey, 'subset'=>$sbarr, 'unifilename'=>$unifilename); + + $this->FontFiles[$fontkey]=array('length1'=>$originalsize, 'type'=>"TTF", 'ttffile'=>$ttffile); + $this->FontFiles[$file]=array('type'=>"TTF"); + unset($cw); + } + else { + $info = $this->_loadfont($file); + $info['i'] = count($this->fonts)+1; + if(!empty($info['file'])) + { + // Embedded font + if($info['type']=='TrueType') + $this->FontFiles[$info['file']] = array('length1'=>$info['originalsize']); + else + $this->FontFiles[$info['file']] = array('length1'=>$info['size1'], 'length2'=>$info['size2']); + } + $this->fonts[$fontkey] = $info; + } +} + +function SetFont($family, $style='', $size=0) +{ + // Select a font; size given in points + if($family=='') + $family = $this->FontFamily; + else + $family = strtolower($family); + $style = strtoupper($style); + if(strpos($style,'U')!==false) + { + $this->underline = true; + $style = str_replace('U','',$style); + } + else + $this->underline = false; + if($style=='IB') + $style = 'BI'; + if($size==0) + $size = $this->FontSizePt; + // Test if font is already selected + if($this->FontFamily==$family && $this->FontStyle==$style && $this->FontSizePt==$size) + return; + + // Test if font is already loaded + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + { + // Test if one of the core fonts + if($family=='arial') + $family = 'helvetica'; + if(in_array($family,$this->CoreFonts)) + { + if($family=='symbol' || $family=='zapfdingbats') + $style = ''; + $fontkey = $family.$style; + if(!isset($this->fonts[$fontkey])) + $this->AddFont($family,$style); + } + else + $this->Error('Undefined font: '.$family.' '.$style); + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + $this->CurrentFont = &$this->fonts[$fontkey]; + if ($this->fonts[$fontkey]['type']=='TTF') { $this->unifontSubset = true; } + else { $this->unifontSubset = false; } + if($this->page>0) + $this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt)); +} + +function SetFontSize($size) +{ + // Set font size in points + if($this->FontSizePt==$size) + return; + $this->FontSizePt = $size; + $this->FontSize = $size/$this->k; + if($this->page>0) + $this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt)); +} + +function AddLink() +{ + // Create a new internal link + $n = count($this->links)+1; + $this->links[$n] = array(0, 0); + return $n; +} + +function SetLink($link, $y=0, $page=-1) +{ + // Set destination of internal link + if($y==-1) + $y = $this->y; + if($page==-1) + $page = $this->page; + $this->links[$link] = array($page, $y); +} + +function Link($x, $y, $w, $h, $link) +{ + // Put a link on the page + $this->PageLinks[$this->page][] = array($x*$this->k, $this->hPt-$y*$this->k, $w*$this->k, $h*$this->k, $link); +} + +function Text($x, $y, $txt) +{ + // Output a string + $txt = (string)$txt; + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if ($this->unifontSubset) + { + $txt2 = '('.$this->_escape($this->UTF8ToUTF16BE($txt, false)).')'; + foreach($this->UTF8StringToArray($txt) as $uni) + $this->CurrentFont['subset'][$uni] = $uni; + } + else + $txt2 = '('.$this->_escape($txt).')'; + $s = sprintf('BT %.2F %.2F Td %s Tj ET',$x*$this->k,($this->h-$y)*$this->k,$txt2); + if($this->underline && $txt!='') + $s .= ' '.$this->_dounderline($x,$y,$txt); + if($this->ColorFlag) + $s = 'q '.$this->TextColor.' '.$s.' Q'; + $this->_out($s); +} + +function AcceptPageBreak() +{ + // Accept automatic page break or not + return $this->AutoPageBreak; +} + +function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='') +{ + // Output a cell + $txt = (string)$txt; + $k = $this->k; + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if($ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation); + $this->x = $x; + if($ws>0) + { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw',$ws*$k)); + } + } + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $s = ''; + if($fill || $border==1) + { + if($fill) + $op = ($border==1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ',$this->x*$k,($this->h-$this->y)*$k,$w*$k,-$h*$k,$op); + } + if(is_string($border)) + { + $x = $this->x; + $y = $this->y; + if(strpos($border,'L')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,$x*$k,($this->h-($y+$h))*$k); + if(strpos($border,'T')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-$y)*$k); + if(strpos($border,'R')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',($x+$w)*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-($y+$h))*$k); + if(strpos($border,'B')!==false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-($y+$h))*$k,($x+$w)*$k,($this->h-($y+$h))*$k); + } + if($txt!=='') + { + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if($align=='R') + $dx = $w-$this->cMargin-$this->GetStringWidth($txt); + elseif($align=='C') + $dx = ($w-$this->GetStringWidth($txt))/2; + else + $dx = $this->cMargin; + if($this->ColorFlag) + $s .= 'q '.$this->TextColor.' '; + // If multibyte, Tw has no effect - do word spacing using an adjustment before each space + if ($this->ws && $this->unifontSubset) { + foreach($this->UTF8StringToArray($txt) as $uni) + $this->CurrentFont['subset'][$uni] = $uni; + $space = $this->_escape($this->UTF8ToUTF16BE(' ', false)); + $s .= sprintf('BT 0 Tw %.2F %.2F Td [',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k); + $t = explode(' ',$txt); + $numt = count($t); + for($i=0;$i<$numt;$i++) { + $tx = $t[$i]; + $tx = '('.$this->_escape($this->UTF8ToUTF16BE($tx, false)).')'; + $s .= sprintf('%s ',$tx); + if (($i+1)<$numt) { + $adj = -($this->ws*$this->k)*1000/$this->FontSizePt; + $s .= sprintf('%d(%s) ',$adj,$space); + } + } + $s .= '] TJ'; + $s .= ' ET'; + } + else { + if ($this->unifontSubset) + { + $txt2 = '('.$this->_escape($this->UTF8ToUTF16BE($txt, false)).')'; + foreach($this->UTF8StringToArray($txt) as $uni) + $this->CurrentFont['subset'][$uni] = $uni; + } + else + $txt2='('.$this->_escape($txt).')'; + $s .= sprintf('BT %.2F %.2F Td %s Tj ET',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k,$txt2); + } + if($this->underline) + $s .= ' '.$this->_dounderline($this->x+$dx,$this->y+.5*$h+.3*$this->FontSize,$txt); + if($this->ColorFlag) + $s .= ' Q'; + if($link) + $this->Link($this->x+$dx,$this->y+.5*$h-.5*$this->FontSize,$this->GetStringWidth($txt),$this->FontSize,$link); + } + if($s) + $this->_out($s); + $this->lasth = $h; + if($ln>0) + { + // Go to next line + $this->y += $h; + if($ln==1) + $this->x = $this->lMargin; + } + else + $this->x += $w; +} + +function MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false) +{ + // Output text with automatic or explicit line breaks + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + if($w==0) + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin); + //$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize; + $s = str_replace("\r",'',(string)$txt); + if ($this->unifontSubset) { + $nb=mb_strlen($s, 'utf-8'); + while($nb>0 && mb_substr($s,$nb-1,1,'utf-8')=="\n") $nb--; + } + else { + $nb = strlen($s); + if($nb>0 && $s[$nb-1]=="\n") + $nb--; + } + $b = 0; + if($border) + { + if($border==1) + { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } + else + { + $b2 = ''; + if(strpos($border,'L')!==false) + $b2 .= 'L'; + if(strpos($border,'R')!==false) + $b2 .= 'R'; + $b = (strpos($border,'T')!==false) ? $b2.'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + if ($this->unifontSubset) { + $c = mb_substr($s,$i,1,'UTF-8'); + } + else { + $c=$s[$i]; + } + if($c=="\n") + { + // Explicit line break + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),$b,2,$align,$fill); + } + else { + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + } + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + continue; + } + if($c==' ') + { + $sep = $i; + $ls = $l; + $ns++; + } + + if ($this->unifontSubset) { $l += $this->GetStringWidth($c); } + else { $l += $cw[$c]*$this->FontSize/1000; } + + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($i==$j) + $i++; + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),$b,2,$align,$fill); + } + else { + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + } + } + else + { + if($align=='J') + { + $this->ws = ($ns>1) ? ($wmax-$ls)/($ns-1) : 0; + $this->_out(sprintf('%.3F Tw',$this->ws*$this->k)); + } + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$sep-$j,'UTF-8'),$b,2,$align,$fill); + } + else { + $this->Cell($w,$h,substr($s,$j,$sep-$j),$b,2,$align,$fill); + } + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if($border && $nl==2) + $b = $b2; + } + else + $i++; + } + // Last chunk + if($this->ws>0) + { + $this->ws = 0; + $this->_out('0 Tw'); + } + if($border && strpos($border,'B')!==false) + $b .= 'B'; + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),$b,2,$align,$fill); + } + else { + $this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill); + } + $this->x = $this->lMargin; +} + +function Write($h, $txt, $link='') +{ + // Output text in flowing mode + if(!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin); + $s = str_replace("\r",'',(string)$txt); + if ($this->unifontSubset) { + $nb = mb_strlen($s, 'UTF-8'); + if($nb==1 && $s==" ") { + $this->x += $this->GetStringWidth($s); + return; + } + } + else { + $nb = strlen($s); + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while($i<$nb) + { + // Get next character + if ($this->unifontSubset) { + $c = mb_substr($s,$i,1,'UTF-8'); + } + else { + $c = $s[$i]; + } + if($c=="\n") + { + // Explicit line break + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),0,2,'',false,$link); + } + else { + $this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link); + } + $i++; + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin); + } + $nl++; + continue; + } + if($c==' ') + $sep = $i; + + if ($this->unifontSubset) { $l += $this->GetStringWidth($c); } + else { $l += $cw[$c]*$this->FontSize/1000; } + + if($l>$wmax) + { + // Automatic line break + if($sep==-1) + { + if($this->x>$this->lMargin) + { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin); + $i++; + $nl++; + continue; + } + if($i==$j) + $i++; + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$i-$j,'UTF-8'),0,2,'',false,$link); + } + else { + $this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link); + } + } + else + { + if ($this->unifontSubset) { + $this->Cell($w,$h,mb_substr($s,$j,$sep-$j,'UTF-8'),0,2,'',false,$link); + } + else { + $this->Cell($w,$h,substr($s,$j,$sep-$j),0,2,'',false,$link); + } + $i = $sep+1; + } + $sep = -1; + $j = $i; + $l = 0; + if($nl==1) + { + $this->x = $this->lMargin; + $w = $this->w-$this->rMargin-$this->x; + $wmax = ($w-2*$this->cMargin); + } + $nl++; + } + else + $i++; + } + // Last chunk + if($i!=$j) { + if ($this->unifontSubset) { + $this->Cell($l,$h,mb_substr($s,$j,$i-$j,'UTF-8'),0,0,'',false,$link); + } + else { + $this->Cell($l,$h,substr($s,$j),0,0,'',false,$link); + } + } +} + +function Ln($h=null) +{ + // Line feed; default value is the last cell height + $this->x = $this->lMargin; + if($h===null) + $this->y += $this->lasth; + else + $this->y += $h; +} + +function Image($file, $x=null, $y=null, $w=0, $h=0, $type='', $link='') +{ + // Put an image on the page + if($file=='') + $this->Error('Image file name is empty'); + if(!isset($this->images[$file])) + { + // First use of this image, get info + if($type=='') + { + $pos = strrpos($file,'.'); + if(!$pos) + $this->Error('Image file has no extension and no type was specified: '.$file); + $type = substr($file,$pos+1); + } + $type = strtolower($type); + if($type=='jpeg') + $type = 'jpg'; + $mtd = '_parse'.$type; + if(!method_exists($this,$mtd)) + $this->Error('Unsupported image type: '.$type); + $info = $this->$mtd($file); + $info['i'] = count($this->images)+1; + $this->images[$file] = $info; + } + else + $info = $this->images[$file]; + + // Automatic width and height calculation if needed + if($w==0 && $h==0) + { + // Put image at 96 dpi + $w = -96; + $h = -96; + } + if($w<0) + $w = -$info['w']*72/$w/$this->k; + if($h<0) + $h = -$info['h']*72/$h/$this->k; + if($w==0) + $w = $h*$info['w']/$info['h']; + if($h==0) + $h = $w*$info['h']/$info['w']; + + // Flowing mode + if($y===null) + { + if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) + { + // Automatic page break + $x2 = $this->x; + $this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation); + $this->x = $x2; + } + $y = $this->y; + $this->y += $h; + } + + if($x===null) + $x = $this->x; + $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q',$w*$this->k,$h*$this->k,$x*$this->k,($this->h-($y+$h))*$this->k,$info['i'])); + if($link) + $this->Link($x,$y,$w,$h,$link); +} + +function GetPageWidth() +{ + // Get current page width + return $this->w; +} + +function GetPageHeight() +{ + // Get current page height + return $this->h; +} + +function GetX() +{ + // Get x position + return $this->x; +} + +function SetX($x) +{ + // Set x position + if($x>=0) + $this->x = $x; + else + $this->x = $this->w+$x; +} + +function GetY() +{ + // Get y position + return $this->y; +} + +function SetY($y, $resetX=true) +{ + // Set y position and optionally reset x + if($y>=0) + $this->y = $y; + else + $this->y = $this->h+$y; + if($resetX) + $this->x = $this->lMargin; +} + +function SetXY($x, $y) +{ + // Set x and y positions + $this->SetX($x); + $this->SetY($y,false); +} + +function Output($dest='', $name='', $isUTF8=false) +{ + // Output PDF to some destination + $this->Close(); + if(strlen($name)==1 && strlen($dest)!=1) + { + // Fix parameter order + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if($dest=='') + $dest = 'I'; + if($name=='') + $name = 'doc.pdf'; + switch(strtoupper($dest)) + { + case 'I': + // Send to standard output + $this->_checkoutput(); + if(PHP_SAPI!='cli') + { + // We send to a browser + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; '.$this->_httpencode('filename',$name,$isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; '.$this->_httpencode('filename',$name,$isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + echo $this->buffer; + break; + case 'F': + // Save to local file + if(!file_put_contents($name,$this->buffer)) + $this->Error('Unable to create output file: '.$name); + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error('Incorrect output destination: '.$dest); + } + return ''; +} + +/******************************************************************************* +* Protected methods * +*******************************************************************************/ + +protected function _dochecks() +{ + // Check availability of mbstring + if(!function_exists('mb_strlen')) + $this->Error('mbstring extension is not available'); +} + +protected function _checkoutput() +{ + if(PHP_SAPI!='cli') + { + if(headers_sent($file,$line)) + $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); + } + if(ob_get_length()) + { + // The output buffer is not empty + if(preg_match('/^(\xEF\xBB\xBF)?\s*$/',ob_get_contents())) + { + // It contains only a UTF-8 BOM and/or whitespace, let's clean it + ob_clean(); + } + else + $this->Error("Some data has already been output, can't send PDF file"); + } +} + +protected function _getpagesize($size) +{ + if(is_string($size)) + { + $size = strtolower($size); + if(!isset($this->StdPageSizes[$size])) + $this->Error('Unknown page size: '.$size); + $a = $this->StdPageSizes[$size]; + return array($a[0]/$this->k, $a[1]/$this->k); + } + else + { + if($size[0]>$size[1]) + return array($size[1], $size[0]); + else + return $size; + } +} + +protected function _beginpage($orientation, $size, $rotation) +{ + $this->page++; + $this->pages[$this->page] = ''; + $this->PageLinks[$this->page] = array(); + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if($orientation=='') + $orientation = $this->DefOrientation; + else + $orientation = strtoupper($orientation[0]); + if($size=='') + $size = $this->DefPageSize; + else + $size = $this->_getpagesize($size); + if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1]) + { + // New size or orientation + if($orientation=='P') + { + $this->w = $size[0]; + $this->h = $size[1]; + } + else + { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w*$this->k; + $this->hPt = $this->h*$this->k; + $this->PageBreakTrigger = $this->h-$this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if($orientation!=$this->DefOrientation || $size[0]!=$this->DefPageSize[0] || $size[1]!=$this->DefPageSize[1]) + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + if($rotation!=0) + { + if($rotation%90!=0) + $this->Error('Incorrect rotation value: '.$rotation); + $this->PageInfo[$this->page]['rotation'] = $rotation; + } + $this->CurRotation = $rotation; +} + +protected function _endpage() +{ + $this->state = 1; +} + +protected function _loadfont($font) +{ + // Load a font definition file from the font directory + if(strpos($font,'/')!==false || strpos($font,"\\")!==false) + $this->Error('Incorrect font definition file name: '.$font); + include($this->fontpath.$font); + if(!isset($name)) + $this->Error('Could not include font definition file'); + if(isset($enc)) + $enc = strtolower($enc); + if(!isset($subsetted)) + $subsetted = false; + return get_defined_vars(); +} + +protected function _isascii($s) +{ + // Test if string is ASCII + $nb = strlen($s); + for($i=0;$i<$nb;$i++) + { + if(ord($s[$i])>127) + return false; + } + return true; +} + +protected function _httpencode($param, $value, $isUTF8) +{ + // Encode HTTP header field parameter + if($this->_isascii($value)) + return $param.'="'.$value.'"'; + if(!$isUTF8) + $value = $this->_UTF8encode($value); + return $param."*=UTF-8''".rawurlencode($value); +} + +protected function _UTF8encode($s) +{ + // Convert ISO-8859-1 to UTF-8 + return mb_convert_encoding($s,'UTF-8','ISO-8859-1'); +} + +protected function _UTF8toUTF16($s) +{ + // Convert UTF-8 to UTF-16BE with BOM + return "\xFE\xFF".mb_convert_encoding($s,'UTF-16BE','UTF-8'); +} + +protected function _escape($s) +{ + // Escape special characters + if(strpos($s,'(')!==false || strpos($s,')')!==false || strpos($s,'\\')!==false || strpos($s,"\r")!==false) + return str_replace(array('\\','(',')',"\r"), array('\\\\','\\(','\\)','\\r'), $s); + else + return $s; +} + +protected function _textstring($s) +{ + // Format a text string + if(!$this->_isascii($s)) + $s = $this->_UTF8toUTF16($s); + return '('.$this->_escape($s).')'; +} + +protected function _dounderline($x, $y, $txt) +{ + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth($txt)+$this->ws*substr_count($txt,' '); + return sprintf('%.2F %.2F %.2F %.2F re f',$x*$this->k,($this->h-($y-$up/1000*$this->FontSize))*$this->k,$w*$this->k,-$ut/1000*$this->FontSizePt); +} + +protected function _parsejpg($file) +{ + // Extract info from a JPEG file + $a = getimagesize($file); + if(!$a) + $this->Error('Missing or incorrect image file: '.$file); + if($a[2]!=2) + $this->Error('Not a JPEG file: '.$file); + if(!isset($a['channels']) || $a['channels']==3) + $colspace = 'DeviceRGB'; + elseif($a['channels']==4) + $colspace = 'DeviceCMYK'; + else + $colspace = 'DeviceGray'; + $bpc = isset($a['bits']) ? $a['bits'] : 8; + $data = file_get_contents($file); + return array('w'=>$a[0], 'h'=>$a[1], 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'DCTDecode', 'data'=>$data); +} + +protected function _parsepng($file) +{ + // Extract info from a PNG file + $f = fopen($file,'rb'); + if(!$f) + $this->Error('Can\'t open image file: '.$file); + $info = $this->_parsepngstream($f,$file); + fclose($f); + return $info; +} + +protected function _parsepngstream($f, $file) +{ + // Check signature + if($this->_readstream($f,8)!=chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10)) + $this->Error('Not a PNG file: '.$file); + + // Read header chunk + $this->_readstream($f,4); + if($this->_readstream($f,4)!='IHDR') + $this->Error('Incorrect PNG file: '.$file); + $w = $this->_readint($f); + $h = $this->_readint($f); + $bpc = ord($this->_readstream($f,1)); + if($bpc>8) + $this->Error('16-bit depth not supported: '.$file); + $ct = ord($this->_readstream($f,1)); + if($ct==0 || $ct==4) + $colspace = 'DeviceGray'; + elseif($ct==2 || $ct==6) + $colspace = 'DeviceRGB'; + elseif($ct==3) + $colspace = 'Indexed'; + else + $this->Error('Unknown color type: '.$file); + if(ord($this->_readstream($f,1))!=0) + $this->Error('Unknown compression method: '.$file); + if(ord($this->_readstream($f,1))!=0) + $this->Error('Unknown filter method: '.$file); + if(ord($this->_readstream($f,1))!=0) + $this->Error('Interlacing not supported: '.$file); + $this->_readstream($f,4); + $dp = '/Predictor 15 /Colors '.($colspace=='DeviceRGB' ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w; + + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do + { + $n = $this->_readint($f); + $type = $this->_readstream($f,4); + if($type=='PLTE') + { + // Read palette + $pal = $this->_readstream($f,$n); + $this->_readstream($f,4); + } + elseif($type=='tRNS') + { + // Read transparency info + $t = $this->_readstream($f,$n); + if($ct==0) + $trns = array(ord(substr($t,1,1))); + elseif($ct==2) + $trns = array(ord(substr($t,1,1)), ord(substr($t,3,1)), ord(substr($t,5,1))); + else + { + $pos = strpos($t,chr(0)); + if($pos!==false) + $trns = array($pos); + } + $this->_readstream($f,4); + } + elseif($type=='IDAT') + { + // Read image data block + $data .= $this->_readstream($f,$n); + $this->_readstream($f,4); + } + elseif($type=='IEND') + break; + else + $this->_readstream($f,$n+4); + } + while($n); + + if($colspace=='Indexed' && empty($pal)) + $this->Error('Missing palette in '.$file); + $info = array('w'=>$w, 'h'=>$h, 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'FlateDecode', 'dp'=>$dp, 'pal'=>$pal, 'trns'=>$trns); + if($ct>=4) + { + // Extract alpha channel + if(!function_exists('gzuncompress')) + $this->Error('Zlib not available, can\'t handle alpha channel: '.$file); + $data = gzuncompress($data); + $color = ''; + $alpha = ''; + if($ct==4) + { + // Gray image + $len = 2*$w; + for($i=0;$i<$h;$i++) + { + $pos = (1+$len)*$i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data,$pos+1,$len); + $color .= preg_replace('/(.)./s','$1',$line); + $alpha .= preg_replace('/.(.)/s','$1',$line); + } + } + else + { + // RGB image + $len = 4*$w; + for($i=0;$i<$h;$i++) + { + $pos = (1+$len)*$i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data,$pos+1,$len); + $color .= preg_replace('/(.{3})./s','$1',$line); + $alpha .= preg_replace('/.{3}(.)/s','$1',$line); + } + } + unset($data); + $data = gzcompress($color); + $info['smask'] = gzcompress($alpha); + $this->WithAlpha = true; + if($this->PDFVersion<'1.4') + $this->PDFVersion = '1.4'; + } + $info['data'] = $data; + return $info; +} + +protected function _readstream($f, $n) +{ + // Read n bytes from stream + $res = ''; + while($n>0 && !feof($f)) + { + $s = fread($f,$n); + if($s===false) + $this->Error('Error while reading stream'); + $n -= strlen($s); + $res .= $s; + } + if($n>0) + $this->Error('Unexpected end of stream'); + return $res; +} + +protected function _readint($f) +{ + // Read a 4-byte integer from stream + $a = unpack('Ni',$this->_readstream($f,4)); + return $a['i']; +} + +protected function _parsegif($file) +{ + // Extract info from a GIF file (via PNG conversion) + if(!function_exists('imagepng')) + $this->Error('GD extension is required for GIF support'); + if(!function_exists('imagecreatefromgif')) + $this->Error('GD has no GIF read support'); + $im = imagecreatefromgif($file); + if(!$im) + $this->Error('Missing or incorrect image file: '.$file); + imageinterlace($im,0); + ob_start(); + imagepng($im); + $data = ob_get_clean(); + imagedestroy($im); + $f = fopen('php://temp','rb+'); + if(!$f) + $this->Error('Unable to create memory stream'); + fwrite($f,$data); + rewind($f); + $info = $this->_parsepngstream($f,$file); + fclose($f); + return $info; +} + +protected function _out($s) +{ + // Add a line to the document + if($this->state==2) + $this->pages[$this->page] .= $s."\n"; + elseif($this->state==1) + $this->_put($s); + elseif($this->state==0) + $this->Error('No page has been added yet'); + elseif($this->state==3) + $this->Error('The document is closed'); +} + +protected function _put($s) +{ + $this->buffer .= $s."\n"; +} + +protected function _getoffset() +{ + return strlen($this->buffer); +} + +protected function _newobj($n=null) +{ + // Begin a new object + if($n===null) + $n = ++$this->n; + $this->offsets[$n] = $this->_getoffset(); + $this->_put($n.' 0 obj'); +} + +protected function _putstream($data) +{ + $this->_put('stream'); + $this->_put($data); + $this->_put('endstream'); +} + +protected function _putstreamobject($data) +{ + if($this->compress) + { + $entries = '/Filter /FlateDecode '; + $data = gzcompress($data); + } + else + $entries = ''; + $entries .= '/Length '.strlen($data); + $this->_newobj(); + $this->_put('<<'.$entries.'>>'); + $this->_putstream($data); + $this->_put('endobj'); +} + +protected function _putlinks($n) +{ + foreach($this->PageLinks[$n] as $pl) + { + $this->_newobj(); + $rect = sprintf('%.2F %.2F %.2F %.2F',$pl[0],$pl[1],$pl[0]+$pl[2],$pl[1]-$pl[3]); + $s = '<_textstring($pl[4]).'>>>>'; + else + { + $l = $this->links[$pl[4]]; + if(isset($this->PageInfo[$l[0]]['size'])) + $h = $this->PageInfo[$l[0]]['size'][1]; + else + $h = ($this->DefOrientation=='P') ? $this->DefPageSize[1]*$this->k : $this->DefPageSize[0]*$this->k; + $s .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>',$this->PageInfo[$l[0]]['n'],$h-$l[1]*$this->k); + } + $this->_put($s); + $this->_put('endobj'); + } +} + +protected function _putpage($n) +{ + $this->_newobj(); + $this->_put('<_put('/Parent 1 0 R'); + if(isset($this->PageInfo[$n]['size'])) + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$this->PageInfo[$n]['size'][0],$this->PageInfo[$n]['size'][1])); + if(isset($this->PageInfo[$n]['rotation'])) + $this->_put('/Rotate '.$this->PageInfo[$n]['rotation']); + $this->_put('/Resources 2 0 R'); + if(!empty($this->PageLinks[$n])) + { + $s = '/Annots ['; + foreach($this->PageLinks[$n] as $pl) + $s .= $pl[5].' 0 R '; + $s .= ']'; + $this->_put($s); + } + if($this->WithAlpha) + $this->_put('/Group <>'); + $this->_put('/Contents '.($this->n+1).' 0 R>>'); + $this->_put('endobj'); + // Page content + if(!empty($this->AliasNbPages)) { + $alias = $this->UTF8ToUTF16BE($this->AliasNbPages, false); + $r = $this->UTF8ToUTF16BE($this->page, false); + $this->pages[$n] = str_replace($alias,$r,$this->pages[$n]); + // Now repeat for no pages in non-subset fonts + $this->pages[$n] = str_replace($this->AliasNbPages,$this->page,$this->pages[$n]); + } + $this->_putstreamobject($this->pages[$n]); + // Link annotations + $this->_putlinks($n); +} + +protected function _putpages() +{ + $nb = $this->page; + $n = $this->n; + for($i=1;$i<=$nb;$i++) + { + $this->PageInfo[$i]['n'] = ++$n; + $n++; + foreach($this->PageLinks[$i] as &$pl) + $pl[5] = ++$n; + unset($pl); + } + for($i=1;$i<=$nb;$i++) + $this->_putpage($i); + // Pages root + $this->_newobj(1); + $this->_put('<PageInfo[$i]['n'].' 0 R '; + $kids .= ']'; + $this->_put($kids); + $this->_put('/Count '.$nb); + if($this->DefOrientation=='P') + { + $w = $this->DefPageSize[0]; + $h = $this->DefPageSize[1]; + } + else + { + $w = $this->DefPageSize[1]; + $h = $this->DefPageSize[0]; + } + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$w*$this->k,$h*$this->k)); + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putfonts() +{ + foreach($this->FontFiles as $file=>$info) + { + if (!isset($info['type']) || $info['type']!='TTF') { + // Font file embedding + $this->_newobj(); + $this->FontFiles[$file]['n'] = $this->n; + $font = file_get_contents($this->fontpath.$file,true); + if(!$font) + $this->Error('Font file not found: '.$file); + $compressed = (substr($file,-2)=='.z'); + if(!$compressed && isset($info['length2'])) + $font = substr($font,6,$info['length1']).substr($font,6+$info['length1']+6,$info['length2']); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('/Length1 '.$info['length1']); + if(isset($info['length2'])) + $this->_put('/Length2 '.$info['length2'].' /Length3 0'); + $this->_put('>>'); + $this->_putstream($font); + $this->_put('endobj'); + } + } + foreach($this->fonts as $k=>$font) + { + // Encoding + if(isset($font['diff'])) + { + if(!isset($this->encodings[$font['enc']])) + { + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + $this->encodings[$font['enc']] = $this->n; + } + } + // ToUnicode CMap + if(isset($font['uv'])) + { + if(isset($font['enc'])) + $cmapkey = $font['enc']; + else + $cmapkey = $font['name']; + if(!isset($this->cmaps[$cmapkey])) + { + $cmap = $this->_tounicodecmap($font['uv']); + $this->_putstreamobject($cmap); + $this->cmaps[$cmapkey] = $this->n; + } + } + // Font object + $type = $font['type']; + $name = $font['name']; + if($type=='Core') + { + // Core font + $this->fonts[$k]['n'] = $this->n+1; + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$name); + $this->_put('/Subtype /Type1'); + if($name!='Symbol' && $name!='ZapfDingbats') + $this->_put('/Encoding /WinAnsiEncoding'); + if(isset($font['uv'])) + $this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + } + elseif($type=='Type1' || $type=='TrueType') + { + // Additional Type1 or TrueType/OpenType font + if(isset($font['subsetted']) && $font['subsetted']) + $name = 'AAAAAA+'.$name; + $this->fonts[$k]['n'] = $this->n+1; + $this->_newobj(); + $this->_put('<_put('/BaseFont /'.$name); + $this->_put('/Subtype /'.$type); + $this->_put('/FirstChar 32 /LastChar 255'); + $this->_put('/Widths '.($this->n+1).' 0 R'); + $this->_put('/FontDescriptor '.($this->n+2).' 0 R'); + + if($font['enc']) + { + if(isset($font['diff'])) + $this->_put('/Encoding '.$this->encodings[$font['enc']].' 0 R'); + else + $this->_put('/Encoding /WinAnsiEncoding'); + } + + if(isset($font['uv'])) + $this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + // Widths + $this->_newobj(); + $cw = $font['cw']; + $s = '['; + for($i=32;$i<=255;$i++) + $s .= $cw[chr($i)].' '; + $this->_put($s.']'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '<$v) + $s .= ' /'.$k.' '.$v; + + if(!empty($font['file'])) + $s .= ' /FontFile'.($type=='Type1' ? '' : '2').' '.$this->FontFiles[$font['file']]['n'].' 0 R'; + $this->_put($s.'>>'); + $this->_put('endobj'); + } + // TrueType embedded SUBSETS or FULL + else if ($type=='TTF') { + $this->fonts[$k]['n']=$this->n+1; +// require_once($this->fontpath.'unifont/ttfonts.php'); + $ttf = new TTFontFile(); + $fontname = 'MPDFAA'.'+'.$font['name']; + $subset = $font['subset']; + unset($subset[0]); + $ttfontstream = $ttf->makeSubset($font['ttffile'], $subset); + $ttfontsize = strlen($ttfontstream); + $fontstream = gzcompress($ttfontstream); + $codeToGlyph = $ttf->codeToGlyph; + unset($codeToGlyph[0]); + + // Type0 Font + // A composite font - a font composed of other fonts, organized hierarchically + $this->_newobj(); + $this->_put('<_put('/Subtype /Type0'); + $this->_put('/BaseFont /'.$fontname.''); + $this->_put('/Encoding /Identity-H'); + $this->_put('/DescendantFonts ['.($this->n + 1).' 0 R]'); + $this->_put('/ToUnicode '.($this->n + 2).' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + + // CIDFontType2 + // A CIDFont whose glyph descriptions are based on TrueType font technology + $this->_newobj(); + $this->_put('<_put('/Subtype /CIDFontType2'); + $this->_put('/BaseFont /'.$fontname.''); + $this->_put('/CIDSystemInfo '.($this->n + 2).' 0 R'); + $this->_put('/FontDescriptor '.($this->n + 3).' 0 R'); + if (isset($font['desc']['MissingWidth'])){ + $this->_out('/DW '.$font['desc']['MissingWidth'].''); + } + + $this->_putTTfontwidths($font, $ttf->maxUni); + + $this->_put('/CIDToGIDMap '.($this->n + 4).' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + + // ToUnicode + $this->_newobj(); + $toUni = "/CIDInit /ProcSet findresource begin\n"; + $toUni .= "12 dict begin\n"; + $toUni .= "begincmap\n"; + $toUni .= "/CIDSystemInfo\n"; + $toUni .= "<_put('<>'); + $this->_putstream($toUni); + $this->_put('endobj'); + + // CIDSystemInfo dictionary + $this->_newobj(); + $this->_put('<_put('/Ordering (UCS)'); + $this->_put('/Supplement 0'); + $this->_put('>>'); + $this->_put('endobj'); + + // Font descriptor + $this->_newobj(); + $this->_put('<_put('/FontName /'.$fontname); + foreach($font['desc'] as $kd=>$v) { + if ($kd == 'Flags') { $v = $v | 4; $v = $v & ~32; } // SYMBOLIC font flag + $this->_out(' /'.$kd.' '.$v); + } + $this->_put('/FontFile2 '.($this->n + 2).' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + + // Embed CIDToGIDMap + // A specification of the mapping from CIDs to glyph indices + $cidtogidmap = ''; + $cidtogidmap = str_pad('', 256*256*2, "\x00"); + foreach($codeToGlyph as $cc=>$glyph) { + $cidtogidmap[$cc*2] = chr($glyph >> 8); + $cidtogidmap[$cc*2 + 1] = chr($glyph & 0xFF); + } + $cidtogidmap = gzcompress($cidtogidmap); + $this->_newobj(); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('>>'); + $this->_putstream($cidtogidmap); + $this->_put('endobj'); + + //Font file + $this->_newobj(); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('/Length1 '.$ttfontsize); + $this->_put('>>'); + $this->_putstream($fontstream); + $this->_put('endobj'); + unset($ttf); + } + else + { + // Allow for additional types + $this->fonts[$k]['n'] = $this->n+1; + $mtd = '_put'.strtolower($type); + if(!method_exists($this,$mtd)) + $this->Error('Unsupported font type: '.$type); + $this->$mtd($font); + } + } +} + +protected function _putTTfontwidths($font, $maxUni) { + if (file_exists($font['unifilename'].'.cw127.php')) { + include($font['unifilename'].'.cw127.php') ; + $startcid = 128; + } + else { + $rangeid = 0; + $range = array(); + $prevcid = -2; + $prevwidth = -1; + $interval = false; + $startcid = 1; + } + $cwlen = $maxUni + 1; + + // for each character + for ($cid=$startcid; $cid<$cwlen; $cid++) { + if ($cid==128 && (!file_exists($font['unifilename'].'.cw127.php'))) { + if (is_writable(dirname($this->fontpath.'unifont/x'))) { + $fh = fopen($font['unifilename'].'.cw127.php',"wb"); + $cw127='"; + fwrite($fh,$cw127,strlen($cw127)); + fclose($fh); + } + } + if ((!isset($font['cw'][$cid*2]) || !isset($font['cw'][$cid*2+1])) || + ($font['cw'][$cid*2] == "\00" && $font['cw'][$cid*2+1] == "\00")) { continue; } + + $width = (ord($font['cw'][$cid*2]) << 8) + ord($font['cw'][$cid*2+1]); + if ($width == 65535) { $width = 0; } + if ($cid > 255 && (!isset($font['subset'][$cid]) || !$font['subset'][$cid])) { continue; } + if (!isset($font['dw']) || (isset($font['dw']) && $width != $font['dw'])) { + if ($cid == ($prevcid + 1)) { + if ($width == $prevwidth) { + if ($width == $range[$rangeid][0]) { + $range[$rangeid][] = $width; + } + else { + array_pop($range[$rangeid]); + // new range + $rangeid = $prevcid; + $range[$rangeid] = array(); + $range[$rangeid][] = $prevwidth; + $range[$rangeid][] = $width; + } + $interval = true; + $range[$rangeid]['interval'] = true; + } else { + if ($interval) { + // new range + $rangeid = $cid; + $range[$rangeid] = array(); + $range[$rangeid][] = $width; + } + else { $range[$rangeid][] = $width; } + $interval = false; + } + } else { + $rangeid = $cid; + $range[$rangeid] = array(); + $range[$rangeid][] = $width; + $interval = false; + } + $prevcid = $cid; + $prevwidth = $width; + } + } + $prevk = -1; + $nextk = -1; + $prevint = false; + foreach ($range as $k => $ws) { + $cws = count($ws); + if (($k == $nextk) AND (!$prevint) AND ((!isset($ws['interval'])) OR ($cws < 4))) { + if (isset($range[$k]['interval'])) { unset($range[$k]['interval']); } + $range[$prevk] = array_merge($range[$prevk], $range[$k]); + unset($range[$k]); + } + else { $prevk = $k; } + $nextk = $k + $cws; + if (isset($ws['interval'])) { + if ($cws > 3) { $prevint = true; } + else { $prevint = false; } + unset($range[$k]['interval']); + --$nextk; + } + else { $prevint = false; } + } + $w = ''; + foreach ($range as $k => $ws) { + if (count(array_count_values($ws)) == 1) { $w .= ' '.$k.' '.($k + count($ws) - 1).' '.$ws[0]; } + else { $w .= ' '.$k.' [ '.implode(' ', $ws).' ]' . "\n"; } + } + $this->_out('/W ['.$w.' ]'); +} + +protected function _tounicodecmap($uv) +{ + $ranges = ''; + $nbr = 0; + $chars = ''; + $nbc = 0; + foreach($uv as $c=>$v) + { + if(is_array($v)) + { + $ranges .= sprintf("<%02X> <%02X> <%04X>\n",$c,$c+$v[1]-1,$v[0]); + $nbr++; + } + else + { + $chars .= sprintf("<%02X> <%04X>\n",$c,$v); + $nbc++; + } + } + $s = "/CIDInit /ProcSet findresource begin\n"; + $s .= "12 dict begin\n"; + $s .= "begincmap\n"; + $s .= "/CIDSystemInfo\n"; + $s .= "<0) + { + $s .= "$nbr beginbfrange\n"; + $s .= $ranges; + $s .= "endbfrange\n"; + } + if($nbc>0) + { + $s .= "$nbc beginbfchar\n"; + $s .= $chars; + $s .= "endbfchar\n"; + } + $s .= "endcmap\n"; + $s .= "CMapName currentdict /CMap defineresource pop\n"; + $s .= "end\n"; + $s .= "end"; + return $s; +} + +protected function _putimages() +{ + foreach(array_keys($this->images) as $file) + { + $this->_putimage($this->images[$file]); + unset($this->images[$file]['data']); + unset($this->images[$file]['smask']); + } +} + +protected function _putimage(&$info) +{ + $this->_newobj(); + $info['n'] = $this->n; + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width '.$info['w']); + $this->_put('/Height '.$info['h']); + if($info['cs']=='Indexed') + $this->_put('/ColorSpace [/Indexed /DeviceRGB '.(strlen($info['pal'])/3-1).' '.($this->n+1).' 0 R]'); + else + { + $this->_put('/ColorSpace /'.$info['cs']); + if($info['cs']=='DeviceCMYK') + $this->_put('/Decode [1 0 1 0 1 0 1 0]'); + } + $this->_put('/BitsPerComponent '.$info['bpc']); + if(isset($info['f'])) + $this->_put('/Filter /'.$info['f']); + if(isset($info['dp'])) + $this->_put('/DecodeParms <<'.$info['dp'].'>>'); + if(isset($info['trns']) && is_array($info['trns'])) + { + $trns = ''; + for($i=0;$i_put('/Mask ['.$trns.']'); + } + if(isset($info['smask'])) + $this->_put('/SMask '.($this->n+1).' 0 R'); + $this->_put('/Length '.strlen($info['data']).'>>'); + $this->_putstream($info['data']); + $this->_put('endobj'); + // Soft mask + if(isset($info['smask'])) + { + $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns '.$info['w']; + $smask = array('w'=>$info['w'], 'h'=>$info['h'], 'cs'=>'DeviceGray', 'bpc'=>8, 'f'=>$info['f'], 'dp'=>$dp, 'data'=>$info['smask']); + $this->_putimage($smask); + } + // Palette + if($info['cs']=='Indexed') + $this->_putstreamobject($info['pal']); +} + +protected function _putxobjectdict() +{ + foreach($this->images as $image) + $this->_put('/I'.$image['i'].' '.$image['n'].' 0 R'); +} + +protected function _putresourcedict() +{ + $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_put('/Font <<'); + foreach($this->fonts as $font) + $this->_put('/F'.$font['i'].' '.$font['n'].' 0 R'); + $this->_put('>>'); + $this->_put('/XObject <<'); + $this->_putxobjectdict(); + $this->_put('>>'); +} + +protected function _putresources() +{ + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj(2); + $this->_put('<<'); + $this->_putresourcedict(); + $this->_put('>>'); + $this->_put('endobj'); +} + +protected function _putinfo() +{ + $date = @date('YmdHisO',$this->CreationDate); + $this->metadata['CreationDate'] = 'D:'.substr($date,0,-2)."'".substr($date,-2)."'"; + foreach($this->metadata as $key=>$value) + $this->_put('/'.$key.' '.$this->_textstring($value)); +} + +protected function _putcatalog() +{ + $n = $this->PageInfo[1]['n']; + $this->_put('/Type /Catalog'); + $this->_put('/Pages 1 0 R'); + if($this->ZoomMode=='fullpage') + $this->_put('/OpenAction ['.$n.' 0 R /Fit]'); + elseif($this->ZoomMode=='fullwidth') + $this->_put('/OpenAction ['.$n.' 0 R /FitH null]'); + elseif($this->ZoomMode=='real') + $this->_put('/OpenAction ['.$n.' 0 R /XYZ null null 1]'); + elseif(!is_string($this->ZoomMode)) + $this->_put('/OpenAction ['.$n.' 0 R /XYZ null null '.sprintf('%.2F',$this->ZoomMode/100).']'); + if($this->LayoutMode=='single') + $this->_put('/PageLayout /SinglePage'); + elseif($this->LayoutMode=='continuous') + $this->_put('/PageLayout /OneColumn'); + elseif($this->LayoutMode=='two') + $this->_put('/PageLayout /TwoColumnLeft'); +} + +protected function _putheader() +{ + $this->_put('%PDF-'.$this->PDFVersion); +} + +protected function _puttrailer() +{ + $this->_put('/Size '.($this->n+1)); + $this->_put('/Root '.$this->n.' 0 R'); + $this->_put('/Info '.($this->n-1).' 0 R'); +} + +protected function _enddoc() +{ + $this->CreationDate = time(); + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + // Info + $this->_newobj(); + $this->_put('<<'); + $this->_putinfo(); + $this->_put('>>'); + $this->_put('endobj'); + // Catalog + $this->_newobj(); + $this->_put('<<'); + $this->_putcatalog(); + $this->_put('>>'); + $this->_put('endobj'); + // Cross-ref + $offset = $this->_getoffset(); + $this->_put('xref'); + $this->_put('0 '.($this->n+1)); + $this->_put('0000000000 65535 f '); + for($i=1;$i<=$this->n;$i++) + $this->_put(sprintf('%010d 00000 n ',$this->offsets[$i])); + // Trailer + $this->_put('trailer'); + $this->_put('<<'); + $this->_puttrailer(); + $this->_put('>>'); + $this->_put('startxref'); + $this->_put($offset); + $this->_put('%%EOF'); + $this->state = 3; +} + +// ********* NEW FUNCTIONS ********* +// Converts UTF-8 strings to UTF16-BE. +protected function UTF8ToUTF16BE($str, $setbom=true) { + $outstr = ""; + if ($setbom) { + $outstr .= "\xFE\xFF"; // Byte Order Mark (BOM) + } + $outstr .= mb_convert_encoding($str, 'UTF-16BE', 'UTF-8'); + return $outstr; +} + +// Converts UTF-8 strings to codepoints array +protected function UTF8StringToArray($str) { + $out = array(); + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $uni = -1; + $h = ord($str[$i]); + if ( $h <= 0x7F ) + $uni = $h; + elseif ( $h >= 0xC2 ) { + if ( ($h <= 0xDF) && ($i < $len -1) ) + $uni = ($h & 0x1F) << 6 | (ord($str[++$i]) & 0x3F); + elseif ( ($h <= 0xEF) && ($i < $len -2) ) + $uni = ($h & 0x0F) << 12 | (ord($str[++$i]) & 0x3F) << 6 + | (ord($str[++$i]) & 0x3F); + elseif ( ($h <= 0xF4) && ($i < $len -3) ) + $uni = ($h & 0x0F) << 18 | (ord($str[++$i]) & 0x3F) << 12 + | (ord($str[++$i]) & 0x3F) << 6 + | (ord($str[++$i]) & 0x3F); + } + if ($uni >= 0) { + $out[] = $uni; + } + } + return $out; +} + +} +?> diff --git a/login.php b/login.php new file mode 100644 index 0000000..bfe9000 --- /dev/null +++ b/login.php @@ -0,0 +1,80 @@ +prepare("SELECT * FROM people WHERE email = ? AND is_user = TRUE AND active = TRUE"); + $stmt->execute([$email]); + $person = $stmt->fetch(); + + if ($person && password_verify($password, $person['password'])) { + $_SESSION['user_id'] = $person['id']; + $_SESSION['user_email'] = $person['email']; + $_SESSION['user_role'] = $person['role']; + $_SESSION['user_name'] = $person['first_name'] . ' ' . $person['last_name']; + header('Location: index.php'); + exit; + } else { + $error = 'Nieprawidłowe dane logowania. Spróbuj ponownie.'; + } + } catch (PDOException $e) { + $error = 'Błąd bazy danych: ' . $e->getMessage(); + } + } +} +?> + + + + + + Logowanie + + + + + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..95db42c --- /dev/null +++ b/logout.php @@ -0,0 +1,6 @@ + false, 'error' => 'PHPMailer not available' ]; + } + + $mail = new PHPMailer\PHPMailer\PHPMailer(true); + try { + $mail->isSMTP(); + $mail->Host = $cfg['smtp_host'] ?? ''; + $mail->Port = (int)($cfg['smtp_port'] ?? 587); + $secure = $cfg['smtp_secure'] ?? 'tls'; + if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; + elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; + else $mail->SMTPSecure = false; + $mail->SMTPAuth = true; + $mail->Username = $cfg['smtp_user'] ?? ''; + $mail->Password = $cfg['smtp_pass'] ?? ''; + + $fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost'); + $fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App'); + $mail->setFrom($fromEmail, $fromName); + if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) { + $mail->addReplyTo($opts['reply_to']); + } elseif (!empty($cfg['reply_to'])) { + $mail->addReplyTo($cfg['reply_to']); + } + + // Recipients + $toList = []; + if ($to) { + if (is_string($to)) $toList = array_map('trim', explode(',', $to)); + elseif (is_array($to)) $toList = $to; + } elseif (!empty(getenv('MAIL_TO'))) { + $toList = array_map('trim', explode(',', getenv('MAIL_TO'))); + } + $added = 0; + foreach ($toList as $addr) { + if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; } + } + if ($added === 0) { + return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ]; + } + + foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); } + foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); } + + // Optional DKIM + if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) { + $mail->DKIM_domain = $cfg['dkim_domain']; + $mail->DKIM_selector = $cfg['dkim_selector']; + $mail->DKIM_private = $cfg['dkim_private_key_path']; + } + + $mail->isHTML(true); + $mail->Subject = $subject; + $mail->Body = $htmlBody; + $mail->AltBody = $textBody ?? strip_tags($htmlBody); + $ok = $mail->send(); + return [ 'success' => $ok ]; + } catch (\Throwable $e) { + return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ]; + } + } + private static function loadConfig(): array + { + $configPath = __DIR__ . '/config.php'; + if (!file_exists($configPath)) { + throw new \RuntimeException('Mail config not found. Copy mail/config.sample.php to mail/config.php and fill in credentials.'); + } + $cfg = require $configPath; + if (!is_array($cfg)) { + throw new \RuntimeException('Invalid mail config format: expected array'); + } + return $cfg; + } + + // Send a contact message + // $to can be: a single email string, a comma-separated list, an array of emails, or null (fallback to MAIL_TO/MAIL_FROM) + public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form') + { + $cfg = self::loadConfig(); + + // Try Composer autoload if available (for PHPMailer) + $autoload = __DIR__ . '/../vendor/autoload.php'; + if (file_exists($autoload)) { + require_once $autoload; + } + // Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer) + if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { + // Debian/Ubuntu package layout (libphp-phpmailer) + @require_once 'libphp-phpmailer/autoload.php'; + if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { + @require_once 'libphp-phpmailer/src/Exception.php'; + @require_once 'libphp-phpmailer/src/SMTP.php'; + @require_once 'libphp-phpmailer/src/PHPMailer.php'; + } + // Alternative layout (older PHPMailer package names) + if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { + @require_once 'PHPMailer/src/Exception.php'; + @require_once 'PHPMailer/src/SMTP.php'; + @require_once 'PHPMailer/src/PHPMailer.php'; + } + if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { + @require_once 'PHPMailer/Exception.php'; + @require_once 'PHPMailer/SMTP.php'; + @require_once 'PHPMailer/PHPMailer.php'; + } + } + + $transport = $cfg['transport'] ?? 'smtp'; + if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { + return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject); + } + + // Fallback: attempt native mail() — works only if MTA is configured on the VM + return self::sendViaNativeMail($cfg, $name, $email, $message, $to, $subject); + } + + private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject) + { + $mail = new PHPMailer\PHPMailer\PHPMailer(true); + try { + $mail->isSMTP(); + $mail->Host = $cfg['smtp_host'] ?? ''; + $mail->Port = (int)($cfg['smtp_port'] ?? 587); + $secure = $cfg['smtp_secure'] ?? 'tls'; + if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; + elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; + else $mail->SMTPSecure = false; + $mail->SMTPAuth = true; + $mail->Username = $cfg['smtp_user'] ?? ''; + $mail->Password = $cfg['smtp_pass'] ?? ''; + + $fromEmail = $cfg['from_email'] ?? 'no-reply@localhost'; + $fromName = $cfg['from_name'] ?? 'App'; + $mail->setFrom($fromEmail, $fromName); + + // Use Reply-To for the user's email to avoid spoofing From + if (filter_var($email, FILTER_VALIDATE_EMAIL)) { + $mail->addReplyTo($email, $name ?: $email); + } + if (!empty($cfg['reply_to'])) { + $mail->addReplyTo($cfg['reply_to']); + } + + // Destination: prefer dynamic recipients ($to), fallback to MAIL_TO; no silent FROM fallback + $toList = []; + if ($to) { + if (is_string($to)) { + // allow comma-separated list + $toList = array_map('trim', explode(',', $to)); + } elseif (is_array($to)) { + $toList = $to; + } + } elseif (!empty(getenv('MAIL_TO'))) { + $toList = array_map('trim', explode(',', getenv('MAIL_TO'))); + } + $added = 0; + foreach ($toList as $addr) { + if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { + $mail->addAddress($addr); + $added++; + } + } + if ($added === 0) { + return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ]; + } + + // DKIM (optional) + if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) { + $mail->DKIM_domain = $cfg['dkim_domain']; + $mail->DKIM_selector = $cfg['dkim_selector']; + $mail->DKIM_private = $cfg['dkim_private_key_path']; + } + + $mail->isHTML(true); + $mail->Subject = $subject; + $safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); + $mail->Body = "

Name: {$safeName}

Email: {$safeEmail}


{$safeBody}"; + $mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}"; + + $ok = $mail->send(); + return [ 'success' => $ok ]; + } catch (\Throwable $e) { + return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ]; + } + } + + private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject) + { + $opts = ['reply_to' => $email]; + $html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); + return self::sendMail($to, $subject, $html, $body, $opts); + } +} diff --git a/mail/config.php b/mail/config.php new file mode 100644 index 0000000..626cca1 --- /dev/null +++ b/mail/config.php @@ -0,0 +1,76 @@ + config array for MailService. + +function env_val(string $key, $default = null) { + $v = getenv($key); + return ($v === false || $v === null || $v === '') ? $default : $v; +} + +// Fallback: if critical vars are missing from process env, try to parse executor/.env +// This helps in web/Apache contexts where .env is not exported. +// Supports simple KEY=VALUE lines; ignores quotes and comments. +function load_dotenv_if_needed(array $keys): void { + $missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); + if (empty($missing)) return; + static $loaded = false; + if ($loaded) return; + $envPath = realpath(__DIR__ . '/../../.env'); // executor/.env + if ($envPath && is_readable($envPath)) { + $lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + foreach ($lines as $line) { + if ($line[0] === '#' || trim($line) === '') continue; + if (!str_contains($line, '=')) continue; + [$k, $v] = array_map('trim', explode('=', $line, 2)); + // Strip potential surrounding quotes + $v = trim($v, "\"' "); + // Do not override existing env + if ($k !== '' && (getenv($k) === false || getenv($k) === '')) { + putenv("{$k}={$v}"); + } + } + $loaded = true; + } +} + +load_dotenv_if_needed([ + 'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS', + 'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO', + 'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH' +]); + +$transport = env_val('MAIL_TRANSPORT', 'smtp'); +$smtp_host = env_val('SMTP_HOST'); +$smtp_port = (int) env_val('SMTP_PORT', 587); +$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null +$smtp_user = env_val('SMTP_USER'); +$smtp_pass = env_val('SMTP_PASS'); + +$from_email = env_val('MAIL_FROM', 'no-reply@localhost'); +$from_name = env_val('MAIL_FROM_NAME', 'App'); +$reply_to = env_val('MAIL_REPLY_TO'); + +$dkim_domain = env_val('DKIM_DOMAIN'); +$dkim_selector = env_val('DKIM_SELECTOR'); +$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH'); + +return [ + 'transport' => $transport, + + // SMTP + 'smtp_host' => $smtp_host, + 'smtp_port' => $smtp_port, + 'smtp_secure' => $smtp_secure, + 'smtp_user' => $smtp_user, + 'smtp_pass' => $smtp_pass, + + // From / Reply-To + 'from_email' => $from_email, + 'from_name' => $from_name, + 'reply_to' => $reply_to, + + // DKIM (optional) + 'dkim_domain' => $dkim_domain, + 'dkim_selector' => $dkim_selector, + 'dkim_private_key_path' => $dkim_private_key_path, +]; diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..5afe74d --- /dev/null +++ b/migrate.php @@ -0,0 +1,62 @@ +exec("CREATE TABLE IF NOT EXISTS migrations (id INT AUTO_INCREMENT PRIMARY KEY, migration VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); + + // Get all run migrations + $stmt = $pdo->query("SELECT migration FROM migrations"); + $run_migrations = $stmt->fetchAll(PDO::FETCH_COLUMN); + + // Get all migration files + $migration_files = glob('db/migrations/*.php'); + + foreach ($migration_files as $file) { + $migration_name = basename($file, '.php'); + + if (!in_array($migration_name, $run_migrations)) { + echo "Running migration: $migration_name...\n"; + require_once $file; + + try { + // Migration functions are named like migrate_001, migrate_002 etc. + $function_name = 'migrate_' . preg_replace('/[^0-9]/', '', $migration_name); + if(function_exists($function_name)){ + $function_name($pdo); + } else { + // Fallback for older naming convention + $function_name = str_replace('.php', '', basename($file)); + if(function_exists($function_name)) { + $function_name($pdo); + } + } + + // Record migration + $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); + $stmt->execute([$migration_name]); + echo "Migration $migration_name has been applied.\n"; + } catch (PDOException $e) { + echo "Error running migration $migration_name: " . $e->getMessage() . "\n"; + } + } else { + echo "Migration $migration_name already applied.\n"; + } + } + + echo "All migrations have been run.\n"; + + } catch (PDOException $e) { + die("Migration failed: " . $e->getMessage()); + } +} + +// If the script is run directly from the command line +if (php_sapi_name() === 'cli') { + run_migrations(); +} + + diff --git a/new_definition.json b/new_definition.json new file mode 100644 index 0000000..493223e --- /dev/null +++ b/new_definition.json @@ -0,0 +1,131 @@ + +{ + "start_node_id": "awaiting_call", + "eligibility_rules": [ + { + "type": "person_property_equals", + "params": { + "property": "role", + "value": "guest" + } + } + ], + "nodes": { + "awaiting_call": { + "ui_hints": { + "title": "Follow-up Call", + "status": "active", + "reason": "Ready to log a new call attempt with the guest.", + "next_step": "Fill out the form below to log the details of your call.", + "form_schema": [ + { "name": "call_date", "label": "Call Date", "type": "datetime-local", "default": "now", "required": true }, + { + "name": "outcome_status", + "label": "Call Outcome", + "type": "select", + "required": true, + "options": [ + { "value": "", "label": "-- Select Outcome --" }, + { "value": "no_answer", "label": "No Answer" }, + { "value": "wants_to_join", "label": "Wants to Join" }, + { "value": "declined", "label": "Declined" }, + { "value": "call_later", "label": "Call Later" } + ] + }, + { "name": "note", "label": "Notes", "type": "textarea" }, + { "name": "next_contact_date", "label": "Next Contact Date", "type": "datetime-local", "condition": { "field": "outcome_status", "value": "call_later" }, "required": true } + ] + } + }, + "outcome_router": { "ui_hints": { "status": "processing" } }, + "waiting_for_next_contact": { + "ui_hints": { + "title": "Call Scheduled", + "status": "paused", + "reason": "A follow-up call is scheduled for a future date.", + "next_step": "Click 'Log Next Call Attempt' to log the call now or on the scheduled date." + } + }, + "decide_after_no_answer": { + "ui_hints": { + "title": "No Answer Logged", + "status": "paused", + "reason": "The guest did not answer the last call.", + "next_step": "Decide whether to try again or end the follow-up process." + } + }, + "end_positive": { "ui_hints": { "title": "Wants to Join", "status": "completed", "reason": "Guest wants to join. New member process started.", "next_step": "" } }, + "end_negative_declined": { "ui_hints": { "title": "Declined", "status": "terminated", "reason": "Guest declined to join.", "next_step": "" } }, + "end_negative_no_answer": { "ui_hints": { "title": "Process Ended", "status": "terminated", "reason": "Process ended after no answer.", "next_step": "" } }, + "end_negative_terminated": { "ui_hints": { "title": "Process Terminated", "status": "terminated", "reason": "Process manually terminated by user.", "next_step": "" } } + }, + "transitions": [ + { + "id": "submit_outcome", + "from": "awaiting_call", + "to": "outcome_router", + "name": "Log Call Attempt", + "actions": [ + { "type": "set_data", "params": { "keys": ["call_date", "outcome_status", "note", "next_contact_date"] } } + ] + }, + { + "id": "route_wants_to_join", + "from": "outcome_router", + "to": "end_positive", + "name": "Route to Positive End", + "condition": { "field": "outcome_status", "value": "wants_to_join" }, + "actions": [ + { "type": "start_process", "process_code": "wprowadzenie-nowego-cz-onka" } + ] + }, + { + "id": "route_declined", + "from": "outcome_router", + "to": "end_negative_declined", + "name": "Route to Declined", + "condition": { "field": "outcome_status", "value": "declined" } + }, + { + "id": "route_no_answer", + "from": "outcome_router", + "to": "decide_after_no_answer", + "name": "Route to No Answer", + "condition": { "field": "outcome_status", "value": "no_answer" } + }, + { + "id": "route_call_later", + "from": "outcome_router", + "to": "waiting_for_next_contact", + "name": "Route to Call Later", + "condition": { "field": "outcome_status", "value": "call_later" } + }, + { "id": "continue_attempts", "from": "decide_after_no_answer", "to": "awaiting_call", "name": "Try Again" }, + { "id": "end_attempts", "from": "decide_after_no_answer", "to": "end_negative_no_answer", "name": "End Process" }, + { "id": "log_next_attempt", "from": "waiting_for_next_contact", "to": "awaiting_call", "name": "Log Next Call Attempt" }, + { + "id": "terminate_from_awaiting_call", + "from": "awaiting_call", + "to": "end_negative_terminated", + "name": "Zakończ proces", + "actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ], + "form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ] + }, + { + "id": "terminate_from_decide", + "from": "decide_after_no_answer", + "to": "end_negative_terminated", + "name": "Zakończ proces", + "actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ], + "form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ] + }, + { + "id": "terminate_from_waiting", + "from": "waiting_for_next_contact", + "to": "end_negative_terminated", + "name": "Zakończ proces", + "actions": [ { "type": "set_data", "params": { "keys": ["termination_note"] } } ], + "form_schema": [ { "name": "termination_note", "label": "Reason for Termination (optional)", "type": "textarea" } ] + } + ] +} diff --git a/process_definitions.php b/process_definitions.php new file mode 100644 index 0000000..04b054d --- /dev/null +++ b/process_definitions.php @@ -0,0 +1,133 @@ +query("SELECT * FROM process_definitions WHERE is_active = 1 ORDER BY sort_order, name"); +$processes = $stmt->fetchAll(PDO::FETCH_ASSOC); + +?> + + + +
+
+ + +
+
+

Process Definitions

+
+ +
+
+ +
+ + + + + + + + + + + + + + + +
NameActions
+ + +
+
+
+
+
+ + + + + + + diff --git a/test.php b/test.php new file mode 100644 index 0000000..61ace19 --- /dev/null +++ b/test.php @@ -0,0 +1,2 @@ +"; + +if (!isset($_SESSION['user_id'])) { + echo "user_id not set.
"; + // header('Location: login.php'); + // exit; +} + +echo "end of test"; +?> diff --git a/test_php.php b/test_php.php new file mode 100644 index 0000000..3072366 --- /dev/null +++ b/test_php.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test_workflow.php b/test_workflow.php new file mode 100644 index 0000000..f59ae67 --- /dev/null +++ b/test_workflow.php @@ -0,0 +1,54 @@ +"; + +// Establish a direct database connection for setup +$pdo = db(); + +// 1. Get the first person from the database +$stmt = $pdo->query("SELECT id FROM people LIMIT 1"); +$person = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$person) { + die("Error: No people found in the database. Please run `db_setup.php` or ensure the database is seeded correctly.
"); +} +$personId = $person['id']; +echo "Prerequisite check: Using person with ID $personId.
"; + +// 2. Get the first process definition from the database +$stmt = $pdo->query("SELECT id FROM process_definitions LIMIT 1"); +$process = $stmt->fetch(PDO::FETCH_ASSOC); +if (!$process) { + die("Error: No process definitions found in the database. Please run `db_setup.php` or ensure the database is seeded correctly.
"); +} +$processDefinitionId = $process['id']; +echo "Prerequisite check: Using process definition with ID $processDefinitionId.
"; + + +$engine = new WorkflowEngine(); +$userId = $personId; + +echo "Attempting to get or create instance with personId: $personId and processDefinitionId: $processDefinitionId
"; + +try { + $instance = $engine->getOrCreateInstanceByDefId($personId, $processDefinitionId, $userId); + + if ($instance) { + echo "Success! Successfully got or created instance:
"; + echo "
";
+        print_r($instance);
+        echo "
"; + } else { + echo "Error: Failed to get or create instance, but no exception was thrown.
"; + echo "This might happen if the process definition exists, but `startProcess` fails internally.
"; + echo "Check PHP error logs for more details.
"; + } +} catch (Exception $e) { + echo "An exception occurred: " . $e->getMessage() . "
"; + echo "Stack trace:
" . $e->getTraceAsString() . "
"; +} \ No newline at end of file diff --git a/update_follow_up_process.php b/update_follow_up_process.php new file mode 100644 index 0000000..6c4d54f --- /dev/null +++ b/update_follow_up_process.php @@ -0,0 +1,165 @@ +prepare("UPDATE process_definitions SET definition_json = :json, start_node_id = 'awaiting_call' WHERE id = 4"); + $stmt->execute([ + ':json' => $json_definition, + ]); + + echo "Updated process_definition_id=4 with the new follow-up workflow.\n"; + + // Deactivate process_definition_id=3 + $stmt = $pdo->prepare("UPDATE process_definitions SET is_active = 0 WHERE id = 3"); + $stmt->execute(); + + echo "Deactivated process_definition_id=3 ('guest_handling').\n"; +} + +// Direct execution guard +if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) +{ + try { + update_process_definition(); + } catch (Exception $e) { + echo "Failed to update process definitions: " . $e->getMessage() . "\n"; + } +} diff --git a/uploads/people/3/gains_sheet_69620e21328c53.29158052.pdf b/uploads/people/3/gains_sheet_69620e21328c53.29158052.pdf new file mode 100644 index 0000000..9e81951 Binary files /dev/null and b/uploads/people/3/gains_sheet_69620e21328c53.29158052.pdf differ diff --git a/verify_fix.sh b/verify_fix.sh new file mode 100644 index 0000000..6150c72 --- /dev/null +++ b/verify_fix.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Login and save cookie +curl -s -L -c cookie.txt -X POST -F 'email=admin@example.com' -F 'password=password' http://localhost/login.php > /dev/null + +# Initialize the process +# First, delete any existing instance for this person and process to ensure a clean slate +curl -s -L -b cookie.txt -X POST -F 'person_id=9' -F 'process_id=4' -F 'delete_existing=1' http://localhost/_init_single_instance.php > /dev/null + +# Get instance details +curl -s -L -b cookie.txt "http://localhost/_get_instance_details.php?person_id=9&process_id=4"