Compare commits

..

58 Commits

Author SHA1 Message Date
Flatlogic Bot
3553ebe363 Brakujące procesy 2026-03-04 19:52:36 +00:00
Flatlogic Bot
55bc7456bf Generowanie pliku pdf z katalogiem 2026-03-02 20:46:20 +00:00
Flatlogic Bot
2630bc9dd8 Układ procesów 2026-03-02 19:53:56 +00:00
Flatlogic Bot
708233f9b4 Podział na typy procesów 2026-03-02 19:32:17 +00:00
Flatlogic Bot
45d975adbf Zmiana widoku checkboksów 2026-03-02 17:43:41 +00:00
Flatlogic Bot
6d5f26a42c Autosave: 20260302-173002 2026-03-02 17:30:02 +00:00
Flatlogic Bot
2e85c01115 Poprawki po aktualizacji 2026-03-02 17:03:42 +00:00
Flatlogic Bot
9ea7b9d268 Podział na procesy indywidualne i grupowe 2026-03-02 10:47:44 +00:00
Flatlogic Bot
9134470c19 Poprawki dotyczące procesów 2026-03-02 08:16:18 +00:00
Flatlogic Bot
f2aacc643f Autosave: 20260302-080601 2026-03-02 08:06:02 +00:00
Flatlogic Bot
b1016e2a54 Wiele razy ten sam proces do osoby 2026-03-02 07:38:53 +00:00
Flatlogic Bot
7717562504 Nowy proces - obsługa 3m i 7m 2026-03-02 06:38:50 +00:00
Flatlogic Bot
4c2c56f0dc Proces szkoleń 2026-03-01 20:39:33 +00:00
Flatlogic Bot
cca6219870 Poprawki błędów 2026-03-01 20:13:59 +00:00
Flatlogic Bot
dc85db3cf1 Korekty błędów 2026-03-01 19:13:33 +00:00
Flatlogic Bot
1ae32ef541 Naprawiona akcja klawiszy startu procesów 2026-03-01 18:22:03 +00:00
Flatlogic Bot
e1d38e6487 Wersja po sprzątaniu i tłumaczeniach 2026-03-01 17:31:49 +00:00
Flatlogic Bot
1c68363a11 restore 2026-02-28 20:51:00 +00:00
Flatlogic Bot
61f9e88301 Edit _bulk_print_badges.php via Editor 2026-01-12 09:01:36 +00:00
Flatlogic Bot
fb86b47003 Edit _bulk_print_badges.php via Editor 2026-01-12 08:59:52 +00:00
Flatlogic Bot
a9ab6bd592 Edit _bulk_print_badges.php via Editor 2026-01-12 08:59:09 +00:00
Flatlogic Bot
bedf2de8b1 Edit _bulk_print_badges.php via Editor 2026-01-12 08:57:56 +00:00
Flatlogic Bot
c7dc809d9e Edit _bulk_print_badges.php via Editor 2026-01-12 08:56:46 +00:00
Flatlogic Bot
1c47d12879 Edit _bulk_print_badges.php via Editor 2026-01-12 08:54:34 +00:00
Flatlogic Bot
9f1c19bf45 Edit _bulk_print_badges.php via Editor 2026-01-12 08:53:05 +00:00
Flatlogic Bot
6df581c51b Edit _bulk_print_badges.php via Editor 2026-01-12 08:51:14 +00:00
Flatlogic Bot
0b3a1c1c19 Edit _bulk_print_badges.php via Editor 2026-01-12 08:49:55 +00:00
Flatlogic Bot
a1a7a31494 Edit _bulk_print_badges.php via Editor 2026-01-12 08:48:44 +00:00
Flatlogic Bot
4e5ed7affc Edit _bulk_print_badges.php via Editor 2026-01-12 08:47:01 +00:00
Flatlogic Bot
9cbec6a128 Edit assets/css/custom.css via Editor 2026-01-11 17:47:16 +00:00
Flatlogic Bot
1066458199 Edit assets/css/custom.css via Editor 2026-01-11 17:44:25 +00:00
Flatlogic Bot
b6d5f17197 MVP z ruchomym paskiem 2026-01-11 17:41:41 +00:00
Flatlogic Bot
803226678c Wersja MVP 2026-01-11 17:25:38 +00:00
Flatlogic Bot
1639a6367c System gotowy do działania 2026-01-11 17:11:49 +00:00
Flatlogic Bot
a15baf88b4 Drukowane dokumenty pdf 2026-01-11 17:07:31 +00:00
Flatlogic Bot
274148ff8b Generowanie pdf, z błędem 2026-01-11 16:49:25 +00:00
Flatlogic Bot
eb3e723258 Procesy obsługi spotkań 2026-01-11 15:45:15 +00:00
Flatlogic Bot
2cc2c42b57 Obsługa procesów spotkania - kolejna wersja 2026-01-11 15:24:47 +00:00
Flatlogic Bot
6714d8259a Proces spotkania wersja 1 2026-01-11 15:08:42 +00:00
Flatlogic Bot
c52190436a Naprawiona większość mechanizmów przed spotkaniami 2026-01-11 14:32:24 +00:00
Flatlogic Bot
8f2aebda66 Przywrócenie działania większości funkcji 2026-01-11 14:17:21 +00:00
Flatlogic Bot
4acc5b8118 Revert to version 21fc1b6 2026-01-11 12:15:08 +00:00
Flatlogic Bot
779f35f634 Wersja z błędami 2026-01-11 12:14:53 +00:00
Flatlogic Bot
21fc1b6baa Refaktoryzacja kodu, poważne zmiany 2026-01-11 09:20:50 +00:00
Flatlogic Bot
44d4fa5a60 Duże poprawki 2026-01-11 08:54:28 +00:00
Flatlogic Bot
6460ff3ac8 Kolejna iteracja procesów w systemie 2026-01-10 22:04:53 +00:00
Flatlogic Bot
4674e7458b Pierwsze procesy 2026-01-10 21:10:13 +00:00
Flatlogic Bot
3b1a26adc9 Głęboka refactoryzacja kodu 2026-01-10 20:46:53 +00:00
Flatlogic Bot
a703aeb1e2 Wersja po posprzątaniu kodu 2026-01-10 19:52:03 +00:00
Flatlogic Bot
524c7007ab Pierwszy działający proces 2026-01-10 17:22:42 +00:00
Flatlogic Bot
ee89357279 Edycja wyglądu tabeli procesów 2026-01-10 11:21:09 +00:00
Flatlogic Bot
47c0b35493 Stan przed edycją interfejsu procesów 2026-01-10 10:40:47 +00:00
Flatlogic Bot
cdd041c172 feat: Group functions in edit modal 2026-01-10 10:21:12 +00:00
Flatlogic Bot
3da7625d06 feat: Add phone and email to dashboard 2026-01-10 10:20:23 +00:00
Flatlogic Bot
2a8ad6701c feat: Refactor process dashboard UI 2026-01-10 10:19:38 +00:00
Flatlogic Bot
a141f60df0 Mechanizm obsługi procesów 2026-01-10 10:17:15 +00:00
Flatlogic Bot
82b98582c4 Wersja stabilna, przed procesami 2026-01-10 09:20:40 +00:00
Flatlogic Bot
9234472371 Wersja bazowa 2026-01-10 07:48:27 +00:00
194 changed files with 18332 additions and 144 deletions

1257
WorkflowEngine.php Normal file

File diff suppressed because it is too large Load Diff

32
_add_bni_group.php Normal file
View File

@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/db/config.php';
if (isset($_POST['add'])) {
$name = $_POST['name'];
$city = $_POST['city'] ?? null;
$active = isset($_POST['active']) ? 1 : 0;
$display_order = $_POST['display_order'] ?? 0;
if (empty($name)) {
$_SESSION['error_message'] = 'Name is required.';
header('Location: bni_groups.php');
exit;
}
try {
$pdo = db();
$stmt = $pdo->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;

104
_add_calendar_event.php Normal file
View File

@ -0,0 +1,104 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}
require_once 'db/config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$start_datetime = $_POST['start_datetime'] ?? '';
$end_datetime = $_POST['end_datetime'] ?? '';
$event_type_id = $_POST['event_type_id'] ?? null;
$recurrence = $_POST['recurrence'] ?? '';
$recurrence_end_date = $_POST['recurrence_end_date'] ?? '';
if (empty($recurrence)) {
$recurrence = null;
}
if (empty($recurrence_end_date)) {
$recurrence_end_date = null;
}
if (empty($title) || empty($start_datetime) || empty($end_datetime) || empty($event_type_id)) {
header("Location: calendar.php?error=empty_fields");
exit();
}
$pdo = db();
try {
$pdo->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();
}
}

18
_add_event_type.php Normal file
View File

@ -0,0 +1,18 @@
<?php
require_once 'db/config.php';
if (isset($_POST['add'])) {
$name = $_POST['name'];
$color = $_POST['color'];
$display_order = $_POST['display_order'];
$pdo = db();
$stmt = $pdo->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;
}
?>

35
_add_function.php Normal file
View File

@ -0,0 +1,35 @@
<?php
session_start();
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: login.php');
exit();
}
require_once 'db/config.php';
if (isset($_POST['name'], $_POST['bni_group_id'])) {
$name = trim($_POST['name']);
$bni_group_id = trim($_POST['bni_group_id']);
if (!empty($name) && !empty($bni_group_id)) {
try {
$pdo = db();
// Get the current max display order
$stmt = $pdo->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();

28
_add_process_event.php Normal file
View File

@ -0,0 +1,28 @@
<?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
session_start();
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Unauthorized');
}
$instanceId = $_POST['instance_id'] ?? null;
$message = $_POST['message'] ?? null;
$userId = $_SESSION['user_id'];
if (!$instanceId || !$message) {
throw new WorkflowRuleFailedException('Missing parameters: instance_id and message are required.');
}
$workflowEngine = new WorkflowEngine();
$workflowEngine->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;

41
_apply_transition.php Normal file
View File

@ -0,0 +1,41 @@
<?php
session_start();
require_once 'lib/ErrorHandler.php';
register_error_handler();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new WorkflowNotAllowedException('Invalid request method.');
}
require_once 'WorkflowEngine.php';
if (!isset($_POST['instanceId']) || !isset($_POST['transitionId'])) {
throw new WorkflowNotAllowedException('Błąd: Brak wymaganych parametrów.');
}
$instanceId = (int)$_POST['instanceId'];
$transitionId = $_POST['transitionId'];
$userId = $_SESSION['user_id'] ?? null;
$payload = $_POST['payload'] ?? [];
if (!$userId) {
throw new WorkflowNotAllowedException('Błąd: Sesja wygasła.', [], 401);
}
$engine = new WorkflowEngine();
if ($transitionId === 'note') {
$message = $payload['message'] ?? '';
if (empty($message)) {
throw new WorkflowNotAllowedException('Treść notatki nie może być pusta.');
}
$engine->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);

50
_bulk_add_event.php Normal file
View File

@ -0,0 +1,50 @@
<?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
require_once 'db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Unauthorized');
}
$person_ids = json_decode($_POST['person_ids'] ?? '[]');
$process_id = $_POST['process_id'] ?? null;
$message = $_POST['description'] ?? null;
$userId = $_SESSION['user_id'];
if (empty($person_ids) || !$process_id || !$message) {
throw new WorkflowRuleFailedException('Missing parameters: person_ids, process_id, and description are required.');
}
$pdo = db();
$placeholders = implode(',', array_fill(0, count($person_ids), '?'));
$stmt = $pdo->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;

56
_bulk_init_instances.php Normal file
View File

@ -0,0 +1,56 @@
<?php
require_once 'WorkflowEngine.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
session_start();
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Unauthorized');
}
$userId = $_SESSION['user_id'];
$personIds = $_POST['personIds'] ?? '[]';
if (is_string($personIds)) {
$personIds = json_decode($personIds, true);
}
$process_id = $_POST['process_id'] ?? null;
if (empty($personIds) || !$process_id) {
throw new WorkflowRuleFailedException('Missing parameters');
}
$engine = new WorkflowEngine();
$results = [
'success' => [],
'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();

View File

@ -0,0 +1,112 @@
<?php
require_once 'lib/ErrorHandler.php';
register_error_handler();
session_start();
$correlation_id = $GLOBALS['correlation_id'];
error_log($correlation_id . ': Bulk print attendance list request started.');
require_once 'WorkflowEngine.php';
require_once 'lib/tfpdf/font/unifont/ttfonts.php';
define('FPDF_FONTPATH', 'lib/tfpdf/font/');
require_once 'lib/tfpdf/tfpdf.php';
// Authentication check
if (!isset($_SESSION['user_id'])) {
if (ob_get_length()) ob_end_clean();
http_response_code(403);
header('Content-Type: application/json');
$error_message = 'Brak uprawnień.';
error_log($correlation_id . ': ' . $error_message);
echo json_encode(['error' => ['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;

111
_bulk_print_badges.php Normal file
View File

@ -0,0 +1,111 @@
<?php
require_once 'lib/ErrorHandler.php';
register_error_handler();
session_start();
$correlation_id = $GLOBALS['correlation_id']; // Use correlation_id from the global error handler
error_log($correlation_id . ': Bulk print badges request started.');
require_once 'WorkflowEngine.php';
require_once 'lib/tfpdf/font/unifont/ttfonts.php';
define('FPDF_FONTPATH', 'lib/tfpdf/font/');
require_once 'lib/tfpdf/tfpdf.php';
// Authentication check
if (!isset($_SESSION['user_id'])) {
if (ob_get_length()) ob_end_clean();
http_response_code(403);
header('Content-Type: application/json');
$error_message = 'Brak uprawnień.';
error_log($correlation_id . ': ' . $error_message);
echo json_encode(['error' => ['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();
if(file_exists('assets/pasted-20260112-081646-4e946aad.png')) {
$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;

52
_bulk_update_status.php Normal file
View File

@ -0,0 +1,52 @@
<?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
require_once 'db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Unauthorized');
}
$person_ids = json_decode($_POST['person_ids'] ?? '[]');
$process_id = $_POST['process_id'] ?? null;
$status = $_POST['status'] ?? null;
$reason = $_POST['reason'] ?? '';
$userId = $_SESSION['user_id'];
if (empty($person_ids) || !$process_id || !$status) {
throw new WorkflowRuleFailedException('Missing parameters: person_ids, process_id, and status are required.');
}
$pdo = db();
$placeholders = implode(',', array_fill(0, count($person_ids), '?'));
$stmt = $pdo->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;

121
_create_person.php Normal file
View File

@ -0,0 +1,121 @@
<?php
require_once 'lib/ErrorHandler.php';
register_error_handler();
require_once 'lib/i18n.php';
require_once 'db/config.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => ['message' => t('error.method_not_allowed', 'Niedozwolona metoda.')], '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' => t('error.missing_fields', 'Imię, nazwisko, email i hasło są wymagane.')], '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' => t('error.email_exists', 'Konto z tym adresem email już istnieje.')], '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
}

20
_delete_bni_group.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/db/config.php';
if (isset($_GET['id'])) {
$id = $_GET['id'];
try {
$pdo = db();
$stmt = $pdo->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;

View File

@ -0,0 +1,48 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit();
}
require_once 'db/config.php';
if (isset($_GET['id'])) {
$event_id = $_GET['id'];
$pdo = db();
try {
$pdo->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();
}

16
_delete_event_type.php Normal file
View File

@ -0,0 +1,16 @@
<?php
require_once 'db/config.php';
if (isset($_GET['id'])) {
$id = $_GET['id'];
$pdo = db();
$stmt = $pdo->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;
}
?>

21
_delete_function.php Normal file
View File

@ -0,0 +1,21 @@
<?php
session_start();
if (!isset($_SESSION['user_id']) || !isset($_GET['id'])) {
header('Location: login.php');
exit();
}
include 'db/config.php';
$id = $_GET['id'];
$pdo = db();
$stmt = $pdo->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();

51
_delete_person.php Normal file
View File

@ -0,0 +1,51 @@
<?php
require_once 'db/config.php';
session_start();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['person_id'])) {
$id = $_POST['person_id'];
try {
$pdo = db();
$stmt = $pdo->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.']);
?>

28
_footer.php Normal file
View File

@ -0,0 +1,28 @@
<!-- Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- jQuery UI -->
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
<!-- Custom JS -->
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const sidebarLinks = document.querySelectorAll('#sidebar .nav-link');
const currentPath = window.location.pathname.split('/').pop();
sidebarLinks.forEach(link => {
const linkPath = link.getAttribute('href').split('/').pop();
if (linkPath === currentPath) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
});
</script>
</body>
</html>

40
_get_event_details.php Normal file
View File

@ -0,0 +1,40 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header("HTTP/1.1 401 Unauthorized");
exit();
}
require_once 'db/config.php';
if (isset($_GET['id'])) {
$event_id = $_GET['id'];
$pdo = db();
$stmt = $pdo->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");
}

41
_get_future_meetings.php Normal file
View File

@ -0,0 +1,41 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/db/config.php';
$bni_group_id = isset($_GET['bni_group_id']) ? (int)$_GET['bni_group_id'] : 0;
$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 2;
if ($bni_group_id === 0) {
echo json_encode(['error' => '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()]);
}

328
_get_instance_details.php Normal file
View File

@ -0,0 +1,328 @@
<?php
require_once 'WorkflowEngine.php';
require_once 'lib/i18n.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
session_start();
// Security check
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$person_id = $_GET['person_id'] ?? null;
$subject_id = $_GET['subject_id'] ?? $person_id;
$subject_type = $_GET['subject_type'] ?? 'person';
$cycle_key = $_GET['cycle_key'] ?? null;
$process_definition_id = $_GET['process_id'] ?? null;
$process_code = $_GET['process_code'] ?? null;
$pdo = db();
if (!$process_definition_id && $process_code) {
$stmt = $pdo->prepare("SELECT id FROM process_definitions WHERE code = ? AND is_latest = 1 LIMIT 1");
$stmt->execute([$process_code]);
$process_definition_id = $stmt->fetchColumn();
}
if (!$subject_id || !$process_definition_id) {
http_response_code(400);
echo json_encode(['error' => 'Missing subject_id or process_id']);
exit;
}
$userId = $_SESSION['user_id'];
$engine = new WorkflowEngine();
// Fetch Subject and Process Definition details first
$subjectName = 'Unknown';
if ($subject_type === 'person') {
$stmt_person = $pdo->prepare("SELECT first_name, last_name FROM people WHERE id = ?");
$stmt_person->execute([$subject_id]);
$person = $stmt_person->fetch();
if ($person) {
$subjectName = $person['first_name'] . ' ' . $person['last_name'];
}
} elseif ($subject_type === 'meeting') {
$stmt_meeting = $pdo->prepare("SELECT m.meeting_datetime, bg.name as group_name FROM meetings m JOIN bni_groups bg ON m.bni_group_id = bg.id WHERE m.id = ?");
$stmt_meeting->execute([$subject_id]);
$meeting = $stmt_meeting->fetch();
if ($meeting) {
$subjectName = $meeting['group_name'] . ' - ' . date('d.m.Y', strtotime($meeting['meeting_datetime']));
}
} elseif ($subject_type === 'bni_group') {
$stmt_group = $pdo->prepare("SELECT name FROM bni_groups WHERE id = ?");
$stmt_group->execute([$subject_id]);
$group = $stmt_group->fetch();
if ($group) {
$subjectName = 'Grupa ' . $group['name'];
}
} elseif ($subject_type === 'organization') {
$subjectName = 'Główna Organizacja';
}
$stmt_process = $pdo->prepare("SELECT * FROM process_definitions WHERE id = ?");
$stmt_process->execute([$process_definition_id]);
$process = $stmt_process->fetch();
if (!$process) {
http_response_code(404);
echo "<p class='text-danger'>Could not find process.</p>";
exit;
}
// Try to find an existing instance
$instance = $engine->getActiveInstanceForSubject($process['code'], $subject_type, $subject_id, $cycle_key);
?>
<!-- Title for the modal, to be grabbed by JS -->
<div id="instance-modal-title" class="d-none">
<?= htmlspecialchars($subjectName) ?> - <?= htmlspecialchars($process['name']) ?>
</div>
<?php if ($instance): // INSTANCE EXISTS ?>
<?php
$instanceId = $instance['id'];
$isCompleted = in_array($instance['current_status'], ['completed', 'positive', 'negative', 'error']);
?>
<?php if ($isCompleted): ?>
<div class="alert alert-info d-flex justify-content-between align-items-center">
<div>
<strong><?= t('modal.process_completed', 'Proces zakończony.') ?></strong>
Obecny status: <?= htmlspecialchars($instance['current_status']) ?>.
</div>
<button id="restartProcessBtn" class="btn btn-sm btn-primary" data-subject-type="<?= $subject_type ?>" data-subject-id="<?= $subject_id ?>" data-person-id="<?= $person_id ?>" data-process-code="<?= htmlspecialchars($process['code']) ?>" data-mode="create_new_run">
<?= t('modal.start_new_instance', 'Rozpocznij od nowa') ?>
</button>
</div>
<?php endif; ?>
<?php
$definition = $process['definition_json'] ? json_decode($process['definition_json'], true) : null;
$isChecklist = ($definition && isset($definition['type']) && $definition['type'] === 'checklist');
$events = $engine->getEvents($instanceId);
$instanceData = $instance['data_json'] ? json_decode($instance['data_json'], true) : [];
?>
<?php if ($isChecklist): ?>
<?php
$tasks = $definition['tasks'] ?? [];
?>
<div class="checklist-modal-container" data-instance-id="<?= $instanceId ?>">
<h5>Zadania do wykonania</h5>
<div class="checklist-container">
<?php foreach ($tasks as $task):
$isChecked = !empty($instanceData[$task['code']]);
?>
<div class="form-check mb-2" style="font-size: 1.15rem;">
<input class="form-check-input task-checkbox-modal" type="checkbox" value="" style="cursor: pointer; border-radius: 0.25em;"
data-task-code="<?= htmlspecialchars($task['code']) ?>" <?= $isChecked ? 'checked' : '' ?> id="chk_<?= htmlspecialchars($task['code']) ?>">
<label class="form-check-label pt-1" for="chk_<?= htmlspecialchars($task['code']) ?>" title="<?= htmlspecialchars($task['name']) ?>" style="cursor: pointer;">
<?= htmlspecialchars($task['name']) ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<?php
$currentNodeId = $instance['current_node_id'];
$all_nodes = $engine->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;
}
}
?>
<div class="process-steps-container">
<h5>Kroki procesu</h5>
<ul class="list-group">
<?php foreach ($all_nodes as $nodeId => $node):
if (!isset($node['ui_hints']['title']) || $node['ui_hints']['title'] === '') continue;
$is_current = ($currentNodeId === $nodeId);
$is_completed = isset($visited_nodes[$nodeId]) && !$is_current;
$status_icon = '';
$li_class = '';
if ($is_current) {
$li_class = 'list-group-item-primary';
$status_icon = '<i class="bi bi-arrow-right-circle-fill text-primary me-2"></i>';
} elseif ($is_completed) {
$li_class = 'list-group-item-success';
$status_icon = '<i class="bi bi-check-circle-fill text-success me-2"></i>';
} else {
$li_class = 'text-muted';
$status_icon = '<i class="bi bi-circle me-2"></i>';
}
?>
<li class="list-group-item d-flex justify-content-between align-items-center <?= $li_class ?>">
<div>
<?= $status_icon ?>
<strong><?= htmlspecialchars($node['ui_hints']['title']) ?></strong>
</div>
</li>
<?php endforeach; ?>
</ul>
<div class="mt-3">
<h5><?= t('modal.available_actions', 'Dostępne akcje') ?></h5>
<?php
$currentNode = $all_nodes[$currentNodeId] ?? null;
if ($currentNode && isset($currentNode['ui_hints']['form_schema'])):
?>
<form id="transition-form">
<?php foreach ($currentNode['ui_hints']['form_schema'] as $field):
$fieldName = $field['name'];
$currentValue = $instanceData[$fieldName] ?? null;
?>
<?php if ($field['type'] === 'checkbox'): ?>
<div class="form-check mb-3" style="font-size: 1.15rem;">
<input type="hidden" name="<?= htmlspecialchars($fieldName) ?>" value="0">
<input type="checkbox" id="<?= htmlspecialchars($fieldName) ?>" name="<?= htmlspecialchars($fieldName) ?>" class="form-check-input" value="1" style="cursor: pointer; border-radius: 0.25em;" <?= (!empty($currentValue) || (!isset($instanceData[$fieldName]) && !empty($field['default']))) ? 'checked' : '' ?>/>
<label for="<?= htmlspecialchars($fieldName) ?>" class="form-check-label pt-1" style="cursor: pointer;"><?= htmlspecialchars($field['label']) ?></label>
</div>
<?php else: ?>
<div class="mb-3">
<label for="<?= htmlspecialchars($fieldName) ?>" class="form-label"><?= htmlspecialchars($field['label']) ?></label>
<?php if ($field['type'] === 'textarea'): ?>
<textarea id="<?= htmlspecialchars($fieldName) ?>" name="<?= htmlspecialchars($fieldName) ?>" class="form-control"><?= htmlspecialchars($currentValue ?? '') ?></textarea>
<?php elseif ($field['type'] === 'select'): ?>
<select id="<?= htmlspecialchars($fieldName) ?>" name="<?= htmlspecialchars($fieldName) ?>" class="form-select">
<?php foreach ($field['options'] as $option): ?>
<option value="<?= htmlspecialchars($option['value']) ?>" <?= $currentValue == $option['value'] ? 'selected' : '' ?>><?= htmlspecialchars($option['label']) ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="<?= htmlspecialchars($field['type']) ?>" id="<?= htmlspecialchars($fieldName) ?>" name="<?= htmlspecialchars($fieldName) ?>" class="form-control" value="<?= htmlspecialchars((string)($currentValue ?? (($field['default'] ?? '') === 'now' ? date('Y-m-d\TH:i') : ($field['default'] ?? '')))) ?>">
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</form>
<?php endif; ?>
<?php if (empty($availableTransitions)): ?>
<p>No actions available.</p>
<?php else: ?>
<?php foreach ($availableTransitions as $transition): ?>
<button class="btn btn-sm btn-primary apply-transition-btn"
data-instance-id="<?= $instanceId ?>"
data-transition-id="<?= $transition['id'] ?>">
<?= htmlspecialchars($transition['name']) ?>
</button>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<hr>
<div class="add-note-container">
<h5><?= t('modal.add_note', 'Dodaj notatkę') ?></h5>
<div class="mb-3">
<textarea id="noteMessage" class="form-control" rows="2" placeholder="Wpisz treść notatki..."></textarea>
</div>
<button id="addNoteBtn" class="btn btn-secondary" data-instance-id="<?= $instanceId ?>"><?= t('modal.add_note', 'Dodaj notatkę') ?></button>
</div>
<hr>
<div class="history-container">
<h5><?= t('modal.history', 'Historia') ?></h5>
<?php if (empty($events)): ?>
<p>Brak zdarzeń.</p>
<?php else: ?>
<?php
// Prepare a map for outcome_status labels for readability
$outcomeStatusOptions = [];
if (isset($definition['nodes']['awaiting_call']['ui_hints']['form_schema'])) {
foreach ($definition['nodes']['awaiting_call']['ui_hints']['form_schema'] as $field) {
if ($field['name'] === 'outcome_status' && isset($field['options'])) {
foreach ($field['options'] as $option) {
if (!empty($option['value'])) {
$outcomeStatusOptions[$option['value']] = $option['label'];
}
}
break;
}
}
}
?>
<ul class="list-group">
<?php foreach ($events as $event): ?>
<?php
$payload = json_decode($event['payload_json'], true);
$isCallAttempt = $event['event_type'] === 'transition_completed' && isset($payload['transition_id']) && $payload['transition_id'] === 'submit_outcome';
?>
<li class="list-group-item">
<?php if ($isCallAttempt): ?>
<?php
$data = $payload['data'] ?? [];
$outcomeLabel = $outcomeStatusOptions[$data['outcome_status']] ?? ucfirst(str_replace('_', ' ', $data['outcome_status']));
?>
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">Call Attempt</h6>
<small><?= date('d.m.Y, H:i', strtotime($data['call_date'])) ?></small>
</div>
<p class="mb-1"><strong>Outcome:</strong> <?= htmlspecialchars($outcomeLabel) ?></p>
<?php if (!empty($data['note'])): ?>
<p class="mb-1 fst-italic">"<?= nl2br(htmlspecialchars($data['note'])) ?>"</p>
<?php endif; ?>
<?php if (!empty($data['next_contact_date'])): ?>
<p class="mb-1"><strong>Next follow-up:</strong> <?= date('d.m.Y, H:i', strtotime($data['next_contact_date'])) ?></p>
<?php endif; ?>
<?php else: // Generic event display ?>
<strong><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $event['event_type']))) ?></strong>
<?php
if (!empty($event['message'])) {
$message = $payload['message'] ?? $event['message'];
echo '<p class="mb-1 text-muted fst-italic">' . htmlspecialchars($message) . '</p>';
}
?>
<?php endif; ?>
<small class="text-muted">By <?= htmlspecialchars($event['first_name'] . ' ' . $event['last_name']) ?> on <?= date('d.m.Y, H:i', strtotime($event['created_at'])) ?></small>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php else: // NO INSTANCE EXISTS ?>
<?php
$eligibility = ['is_eligible' => true, 'reasons' => []];
if ($subject_type === 'person') {
$eligibility = $engine->checkEligibility($subject_id, $process_definition_id);
}
?>
<div class="text-center">
<?php if ($eligibility['is_eligible']): ?>
<h4><?= t('modal.process_not_started', 'Proces nie został uruchomiony') ?></h4>
<p>This process has not been started for this subject.</p>
<button id="startProcessBtn" class="btn btn-primary" data-subject-type="<?= $subject_type ?>" data-subject-id="<?= $subject_id ?>" data-process-id="<?= $process_definition_id ?>" data-process-code="<?= $process_code ?>" data-cycle-key="<?= $cycle_key ?>">
<?= t('modal.start_process', 'Uruchom proces') ?>
</button>
<?php else: ?>
<h4>Not Eligible</h4>
<p>This person is not eligible to start this process.</p>
<ul class="list-group list-group-flush text-start">
<?php foreach ($eligibility['reasons'] as $reason): ?>
<li class="list-group-item list-group-item-danger"><?= htmlspecialchars($reason) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>

View File

@ -0,0 +1,35 @@
<?php
require_once 'WorkflowEngine.php';
session_start();
header('Content-Type: application/json');
$response = ['success' => 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);

27
_get_meeting_details.php Normal file
View File

@ -0,0 +1,27 @@
<?php
require_once 'WorkflowEngine.php';
session_start();
header('Content-Type: application/json');
$response = ['success' => 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);

66
_get_person_details.php Normal file
View File

@ -0,0 +1,66 @@
<?php
require_once 'db/config.php';
if (isset($_GET['id'])) {
header('Content-Type: application/json');
try {
$person_id = $_GET['id'];
$pdo = db();
// Fetch person details
$stmt = $pdo->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;
}
?>

View File

@ -0,0 +1,96 @@
<?php
require_once 'db/config.php';
header('Content-Type: application/json');
if (!isset($_GET['process_id'])) {
http_response_code(400);
echo json_encode(['error' => '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()]);
}
?>

53
_header.php Normal file
View File

@ -0,0 +1,53 @@
<?php
session_start();
require_once __DIR__ . "/lib/i18n.php";
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?> - Dashboard</title>
<meta name="description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<!-- OG Meta Tags -->
<meta property="og:title" content="<?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?>">
<meta property="og:description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
<meta property="og:image" content="<?php echo getenv('PROJECT_IMAGE_URL') ?: 'https://via.placeholder.com/1200x630.png?text=Visit+My+App'; ?>">
<meta property="og:url" content="<?php echo (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; ?>">
<meta property="og:type" content="website">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?>">
<meta name="twitter:description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
<meta name="twitter:image" content="<?php echo getenv('PROJECT_IMAGE_URL') ?: 'https://via.placeholder.com/1200x630.png?text=Visit+My+App'; ?>'>
<link rel="icon" href="assets/pasted-20260111-144117-aba8ec29.jpg" type="image/jpeg">
</head>
<body class="<?php echo isset($_COOKIE['sidebar_collapsed']) && $_COOKIE['sidebar_collapsed'] === 'true' ? 'sidebar-collapsed' : ''; ?>">
<script>
(function() {
if (localStorage.getItem('sidebarCollapsed') === 'true') {
document.body.classList.add('sidebar-collapsed');
}
})();
</script>

37
_init_instances.php Normal file
View File

@ -0,0 +1,37 @@
<?php
require_once 'db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$pdo = db();
// Get all active people
$stmt_people = $pdo->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;

53
_init_single_instance.php Normal file
View File

@ -0,0 +1,53 @@
<?php
require_once 'lib/ErrorHandler.php';
register_error_handler();
require_once 'db/config.php';
require_once 'WorkflowEngine.php';
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => ['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);
$subjectType = filter_input(INPUT_POST, 'subject_type', FILTER_SANITIZE_STRING);
$subjectId = filter_input(INPUT_POST, 'subject_id', FILTER_VALIDATE_INT);
$processCode = filter_input(INPUT_POST, 'process_code', FILTER_SANITIZE_STRING);
$mode = filter_input(INPUT_POST, 'mode', FILTER_SANITIZE_STRING) ?: 'resume_or_create';
if ($personId && !$subjectType) {
$subjectType = 'person';
$subjectId = $personId;
}
$pdo = db();
if ($processDefinitionId && !$processCode) {
$stmt = $pdo->prepare("SELECT code FROM process_definitions WHERE id = ?");
$stmt->execute([$processDefinitionId]);
$processCode = $stmt->fetchColumn();
}
if (!$subjectId || !$subjectType || !$processCode) {
throw new InvalidArgumentException('Invalid or missing subject_type, subject_id, or process_code.');
}
$engine = new WorkflowEngine();
$instance = $engine->getOrCreateInstanceBySubject($subjectType, $subjectId, $processCode, $userId, $mode);
if ($instance && isset($instance['instance_id'])) {
echo json_encode(['success' => true, 'message' => 'Process initialized successfully.', 'instance_id' => $instance['instance_id']]);
} else {
throw new Exception("Failed to initialize process for an unknown reason.");
}

19
_navbar.php Normal file
View File

@ -0,0 +1,19 @@
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<button class="navbar-toggler d-none d-md-block" type="button" id="sidebar-toggler" aria-label="Toggle sidebar" aria-expanded="true">
<i class="bi bi-list"></i>
</button>
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#"><img src="assets/pasted-20260111-143449-befa41d3.png" class="d-inline-block align-top navbar-logo" alt=""> <?php echo getenv('PROJECT_NAME') ?: 'BNI obsługa regionu'; ?></a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="navbar-nav flex-row px-3">
<?php if (isset($_SESSION['user_name'])): ?>
<li class="nav-item text-nowrap">
<span class="nav-link">Witaj, <?= htmlspecialchars($_SESSION['user_name']) ?></span>
</li>
<?php endif; ?>
<li class="nav-item text-nowrap ms-2">
<a class="nav-link" href="logout.php">Wyloguj</a>
</li>
</ul>
</nav>

View File

@ -0,0 +1,241 @@
<?php
// Start output buffering to prevent any accidental output before headers
ob_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/lib/ErrorHandler.php';
require_once __DIR__ . '/lib/tfpdf/tfpdf.php';
require_once __DIR__ . '/lib/tfpdf/font/unifont/ttfonts.php';
// Disable error display to avoid corrupting PDF output
ini_set('display_errors', 0);
error_reporting(E_ALL);
$group_name = $_GET['group_name'] ?? '';
if (empty($group_name)) {
while (ob_get_level() > 0) ob_end_clean();
http_response_code(400);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Parameter group_name is required']);
exit;
}
try {
$db = db();
// 1. Fetch group by name
$stmt = $db->prepare('SELECT id, name FROM bni_groups WHERE LOWER(name) = LOWER(?)');
$stmt->execute([trim($group_name)]);
$group = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$group) {
while (ob_get_level() > 0) ob_end_clean();
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Group not found']);
exit;
}
$groupId = $group['id'];
$groupRealName = $group['name'];
// 2. Fetch members
$stmt = $db->prepare('
SELECT
p.first_name,
p.last_name,
p.person_photo_path,
p.company_name,
p.company_logo_path,
p.business_description,
p.industry,
p.company_size
FROM people p
WHERE p.bni_group_id = ?
ORDER BY p.last_name ASC, p.first_name ASC
');
$stmt->execute([$groupId]);
$members = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($members)) {
while (ob_get_level() > 0) ob_end_clean();
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'No members found in this group']);
exit;
}
// Helper to validate images before adding to PDF
$tempFiles = [];
function getVerifiedImagePath($path, &$tempFiles) {
if (empty($path)) return null;
// Local file
if (strpos($path, 'http') !== 0) {
$localPath = rtrim(realpath(__DIR__), '/') . '/' . ltrim($path, '/');
if (!file_exists($localPath)) return null;
$info = @getimagesize($localPath);
if ($info && in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG])) {
return $localPath;
}
return null;
}
// URL
$ch = curl_init($path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$data = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $data) {
$tmp = tempnam(sys_get_temp_dir(), 'pdfimg_');
file_put_contents($tmp, $data);
$info = @getimagesize($tmp);
if ($info && in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG])) {
$tempFiles[] = $tmp;
return $tmp;
}
@unlink($tmp);
}
return null;
}
// 3. Create PDF
$pdf = new tFPDF();
$pdf->AddFont('DejaVu', '', 'DejaVuSans.ttf', true);
$pdf->AddFont('DejaVu', 'B', 'DejaVuSans-Bold.ttf', true);
$pdf->SetMargins(15, 20, 15);
$pdf->SetAutoPageBreak(true, 15);
foreach ($members as $member) {
$pdf->AddPage();
// --- HEADER ---
$pdf->SetFont('DejaVu', 'B', 12);
$pdf->SetTextColor(150, 150, 150);
$pdf->Cell(0, 10, 'Grupa: ' . $groupRealName, 0, 1, 'R');
$pdf->Ln(5);
$pdf->SetTextColor(0, 0, 0);
// --- IMAGES ---
$yStart = $pdf->GetY();
$minY = $yStart;
// Left - Photo
if (!empty($member['person_photo_path'])) {
$validPath = getVerifiedImagePath($member['person_photo_path'], $tempFiles);
if ($validPath) {
@$pdf->Image($validPath, 15, $yStart, 40);
$minY = max($minY, $yStart + 50);
}
}
// Right - Logo
if (!empty($member['company_logo_path'])) {
$validPath = getVerifiedImagePath($member['company_logo_path'], $tempFiles);
if ($validPath) {
@$pdf->Image($validPath, 155, $yStart, 40);
$minY = max($minY, $yStart + 50);
}
}
// --- MAIN INFO ---
$pdf->SetY($yStart + 10);
$name = trim($member['first_name'] . ' ' . $member['last_name']);
if ($name === '') {
$name = 'Nieznany członek';
}
$pdf->SetFont('DejaVu', 'B', 22);
// Add margins for text so it doesn't overlap images
$pdf->SetLeftMargin(55);
$pdf->SetRightMargin(55);
$pdf->Cell(100, 12, $name, 0, 1, 'C');
$companyName = trim($member['company_name'] ?? '');
if ($companyName !== '') {
$pdf->SetFont('DejaVu', 'B', 16);
$pdf->SetTextColor(80, 80, 80);
$pdf->MultiCell(100, 8, $companyName, 0, 'C');
$pdf->SetTextColor(0, 0, 0);
}
// Reset margins
$pdf->SetLeftMargin(15);
$pdf->SetRightMargin(15);
$pdf->SetX(15);
// Ensure we draw details below the images
if ($pdf->GetY() < $minY) {
$pdf->SetY($minY + 5);
} else {
$pdf->Ln(10);
}
// --- DETAILS ---
$pdf->SetFont('DejaVu', 'B', 12);
// Industry
$pdf->Cell(45, 8, 'Branża:', 0, 0);
$pdf->SetFont('DejaVu', '', 12);
$industry = trim($member['industry'] ?? '');
$pdf->Cell(0, 8, $industry !== '' ? $industry : '-', 0, 1);
// Company Size
$pdf->SetFont('DejaVu', 'B', 12);
$pdf->Cell(45, 8, 'Wielkość firmy:', 0, 0);
$pdf->SetFont('DejaVu', '', 12);
$companySize = trim($member['company_size'] ?? '');
$pdf->Cell(0, 8, $companySize !== '' ? $companySize : '-', 0, 1);
$pdf->Ln(8);
// Description
$pdf->SetFont('DejaVu', 'B', 12);
$pdf->Cell(0, 8, 'Opis:', 0, 1);
$pdf->SetFont('DejaVu', '', 11);
$desc = trim($member['business_description'] ?? '');
if ($desc === '') {
$desc = 'Brak opisu.';
}
$pdf->MultiCell(0, 6, $desc);
}
// Clear buffer before sending headers
while (ob_get_level() > 0) ob_end_clean();
$safeName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', strtolower($groupRealName));
$filename = 'lista_czlonkow_' . $safeName . '.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: private, max-age=0, must-revalidate');
header('Pragma: public');
// Output directly
$pdf->Output('D', $filename);
// Cleanup
foreach ($tempFiles as $tmp) {
@unlink($tmp);
}
exit;
} catch (Exception $e) {
while (ob_get_level() > 0) ob_end_clean();
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Wystąpił błąd podczas generowania PDF', 'details' => $e->getMessage()]);
exit;
}

View File

@ -0,0 +1,154 @@
<?php
require_once 'db/config.php';
require_once 'lib/ErrorHandler.php';
register_error_handler();
session_start();
function validate_definition_json($json) {
if (empty($json)) {
http_response_code(422);
throw new WorkflowRuleFailedException('Process definition JSON cannot be empty.');
}
$data = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(422);
throw new WorkflowRuleFailedException('Invalid JSON format in definition.');
}
$allowed_statuses = ['none', 'negative', 'in_progress', 'positive', 'active', 'processing', 'paused', 'completed', 'terminated'];
if (isset($data['nodes'])) {
foreach ($data['nodes'] as $node) {
if (isset($node['ui_hints']['status']) && !in_array($node['ui_hints']['status'], $allowed_statuses)) {
http_response_code(422);
throw new WorkflowRuleFailedException('Invalid status in ui_hints. Allowed values are: ' . implode(', ', $allowed_statuses));
}
}
}
if (isset($data['transitions'])) {
foreach ($data['transitions'] as $transition) {
if (isset($transition['actions'])) {
foreach ($transition['actions'] as $action) {
if ($action['type'] === 'start_process' && isset($action['process_name'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Use process_code instead of process_name in transition actions.');
}
}
}
}
}
if (isset($data['eligibility_rules'])) {
foreach ($data['eligibility_rules'] as $rule) {
if ($rule['type'] === 'process_completed' && isset($rule['process_name'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Use process_code instead of process_name in eligibility_rules.');
}
}
}
if (!isset($data['start_node_id'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Process definition is missing start_node_id.');
}
$start_node_id = $data['start_node_id'];
if (!isset($data['nodes']) || !is_array($data['nodes'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Process definition is missing a valid "nodes" object.');
}
if (!isset($data['nodes'][$start_node_id])) {
http_response_code(422);
throw new WorkflowRuleFailedException("start_node_id '{$start_node_id}' does not exist in nodes.");
}
if (!isset($data['transitions']) || !is_array($data['transitions'])) {
http_response_code(422);
throw new WorkflowRuleFailedException('Process definition is missing a valid "transitions" array.');
}
foreach ($data['transitions'] as $index => $transition) {
if (!isset($transition['from']) || !isset($transition['to'])) {
http_response_code(422);
throw new WorkflowRuleFailedException("Transition at index {$index} is missing 'from' or 'to' property.");
}
if (!isset($data['nodes'][$transition['from']])) {
http_response_code(422);
throw new WorkflowRuleFailedException("Transition from an unknown node: '{$transition['from']}'.");
}
if (!isset($data['nodes'][$transition['to']])) {
http_response_code(422);
throw new WorkflowRuleFailedException("Transition to an unknown node: '{$transition['to']}'.");
}
}
}
try {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$processId = $_POST['process_id'] ?? null;
$name = $_POST['name'] ?? '';
$definition_json = $_POST['definition_json'] ?? '';
$subject_scope = $_POST['subject_scope'] ?? 'person';
validate_definition_json($definition_json);
// Generate a simple code from the name
$code = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $name)));
if (empty($name)) {
throw new WorkflowRuleFailedException('Process name is required.');
}
$pdo = db();
$start_node = json_decode($definition_json, true)['start_node_id'] ?? null;
if (empty($processId)) {
// Create new process
$sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, is_latest, subject_scope) VALUES (?, ?, ?, ?, 1, 1, 1, ?)';
$params = [$name, $code, $definition_json, $start_node, $subject_scope];
$message = 'Process created successfully.';
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
} else {
// "Update" existing process by creating a new version
$stmt_old = $pdo->prepare('SELECT code, version, sort_order, is_active, subject_scope FROM process_definitions WHERE id = ?');
$stmt_old->execute([$processId]);
$old = $stmt_old->fetch();
if ($old) {
$is_active = isset($_POST['is_active']) ? (int)$_POST['is_active'] : $old['is_active'];
$new_version = $old['version'] + 1;
$db_code = $old['code'];
// Mark all previous versions as not latest
$stmt_update = $pdo->prepare('UPDATE process_definitions SET is_latest = 0 WHERE code = ?');
$stmt_update->execute([$db_code]);
// Insert new version
$sql = 'INSERT INTO process_definitions (name, code, definition_json, start_node_id, is_active, version, supersedes_definition_id, is_latest, sort_order, subject_scope) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)';
$params = [$name, $db_code, $definition_json, $start_node, $is_active, $new_version, $processId, $old['sort_order'], $subject_scope];
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$message = 'Process updated successfully (new version created).';
} else {
throw new WorkflowRuleFailedException('Process not found.');
}
}
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()]);
}

74
_sidebar.php Normal file
View File

@ -0,0 +1,74 @@
<?php $current_page = basename($_SERVER['PHP_SELF']); ?>
<nav id="sidebar" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link <?= ($current_page == 'index.php' || $current_page == '') ? 'active' : '' ?>" aria-current="page" href="/">
<i class="bi bi-kanban"></i>
<span class="nav-link-text"><?= t('menu.process_dashboard', 'Pulpit procesów') ?></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= ($current_page == 'meetings_processes.php') ? 'active' : '' ?>" href="meetings_processes.php">
<i class="bi bi-camera-video"></i>
<span class="nav-link-text">Procesy spotkań</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= ($current_page == 'groups_processes.php') ? 'active' : '' ?>" href="groups_processes.php">
<i class="bi bi-collection"></i>
<span class="nav-link-text">Procesy grup BNI</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= ($current_page == 'organization_processes.php') ? 'active' : '' ?>" href="organization_processes.php">
<i class="bi bi-building"></i>
<span class="nav-link-text">Procesy organizacji</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= ($current_page == 'calendar.php') ? 'active' : '' ?>" href="calendar.php">
<i class="bi bi-calendar-event"></i>
<span class="nav-link-text"><?= t('menu.calendar', 'Kalendarz') ?></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="#settings-submenu" data-bs-toggle="collapse" aria-expanded="false">
<i class="bi bi-gear"></i>
<span class="nav-link-text"><?= t('menu.settings', 'Ustawienia') ?></span>
</a>
<ul class="nav flex-column collapse" id="settings-submenu" data-bs-parent="#sidebar">
<li class="nav-item submenu-item">
<a class="nav-link <?= ($current_page == 'event_types.php') ? 'active' : '' ?>" href="event_types.php">
<i class="bi bi-tags"></i>
<span class="nav-link-text"><?= t('menu.event_types', 'Typy zdarzeń') ?></span>
</a>
</li>
<li class="nav-item submenu-item">
<a class="nav-link <?= ($current_page == 'bni_groups.php') ? 'active' : '' ?>" href="bni_groups.php">
<i class="bi bi-people"></i>
<span class="nav-link-text"><?= t('menu.bni_groups', 'Grupy BNI') ?></span>
</a>
</li>
<li class="nav-item submenu-item">
<a class="nav-link <?= ($current_page == 'functions.php') ? 'active' : '' ?>" href="functions.php">
<i class="bi bi-person-rolodex"></i>
<span class="nav-link-text"><?= t('menu.functions', 'Funkcje') ?></span>
</a>
</li>
</ul>
</li>
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
<li class="nav-item">
<a class="nav-link <?= ($current_page == 'process_definitions.php') ? 'active' : '' ?>" href="process_definitions.php">
<i class="bi bi-diagram-3"></i>
<span class="nav-link-text"><?= t('menu.process_definitions', 'Definicje procesów') ?></span>
</a>
</li>
<?php endif; ?>
</ul>
</div>
</nav>

34
_update_bni_group.php Normal file
View File

@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/db/config.php';
if (isset($_POST['edit'])) {
$id = $_POST['id'];
$name = $_POST['name'];
$city = $_POST['city'] ?? null;
$active = isset($_POST['active']) ? 1 : 0;
$display_order = $_POST['display_order'] ?? 0;
if (empty($name) || empty($id)) {
$_SESSION['error_message'] = 'Name and ID are required.';
header('Location: bni_groups.php');
exit;
}
try {
$pdo = db();
$stmt = $pdo->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;

View File

@ -0,0 +1,67 @@
<?php
require_once 'db/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['order']) && is_array($_POST['order'])) {
$ordered_ids = $_POST['order'];
$pdo = db();
$pdo->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.']);

View File

@ -0,0 +1,81 @@
<?php
session_start();
require_once 'db/config.php';
require_once 'lib/ErrorHandler.php';
require_once 'lib/WorkflowExceptions.php';
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Authentication required.');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$event_id = $_POST['event_id'] ?? null;
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$start_datetime = $_POST['start_datetime'] ?? '';
$end_datetime = $_POST['end_datetime'] ?? '';
$event_type_id = $_POST['event_type_id'] ?? null;
$update_scope = $_POST['update_scope'] ?? 'one';
$group_ids = $_POST['group_ids'] ?? [];
if (empty($event_id) || empty($title) || empty($event_type_id) || !is_array($group_ids)) {
throw new WorkflowRuleFailedException('Empty fields');
}
$pdo = db();
try {
$pdo->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;
}
}

19
_update_event_type.php Normal file
View File

@ -0,0 +1,19 @@
<?php
require_once 'db/config.php';
if (isset($_POST['edit'])) {
$id = $_POST['id'];
$name = $_POST['name'];
$color = $_POST['color'];
$display_order = $_POST['display_order'];
$pdo = db();
$stmt = $pdo->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;
}
?>

View File

@ -0,0 +1,67 @@
<?php
require_once 'db/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['order']) && is_array($_POST['order'])) {
$ordered_ids = $_POST['order'];
$pdo = db();
$pdo->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.']);

30
_update_function.php Normal file
View File

@ -0,0 +1,30 @@
<?php
session_start();
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: login.php');
exit();
}
require_once 'db/config.php';
if (isset($_POST['id'], $_POST['name'], $_POST['bni_group_id'])) {
$id = $_POST['id'];
$name = trim($_POST['name']);
$bni_group_id = trim($_POST['bni_group_id']);
if (!empty($name) && !empty($bni_group_id)) {
try {
$pdo = db();
$stmt = $pdo->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();

View File

@ -0,0 +1,67 @@
<?php
require_once 'db/config.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['order']) && is_array($_POST['order'])) {
$ordered_ids = $_POST['order'];
$pdo = db();
$pdo->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.']);

View File

@ -0,0 +1,29 @@
<?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
session_start();
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Unauthorized');
}
$instanceId = $_POST['instance_id'] ?? null;
$status = $_POST['status'] ?? null;
$reason = $_POST['reason'] ?? '';
$userId = $_SESSION['user_id'];
if (!$instanceId || !$status) {
throw new WorkflowRuleFailedException('Missing parameters: instance_id and status are required.');
}
$workflowEngine = new WorkflowEngine();
$workflowEngine->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;

View File

@ -0,0 +1,47 @@
<?php
require_once 'lib/ErrorHandler.php';
register_error_handler();
require_once 'db/config.php';
require_once 'WorkflowEngine.php';
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
throw new WorkflowException('You must be logged in to perform this action.', 401);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new WorkflowException('Invalid request method.', 405);
}
$personId = $_POST['person_id'] ?? null;
$bniGroupId = $_POST['bni_group_id'] ?? $_POST['group_id'] ?? null;
$meetingDate = $_POST['meeting_date'] ?? null;
$status = $_POST['attendance_status'] ?? null;
$guestSurvey = $_POST['guest_survey'] ?? null;
$userId = $_SESSION['user_id'];
if (!$personId || !$bniGroupId || !$meetingDate || !$status) {
$missing_params = [];
if (!$personId) $missing_params[] = 'person_id';
if (!$bniGroupId) $missing_params[] = 'bni_group_id';
if (!$meetingDate) $missing_params[] = 'meeting_date';
if (!$status) $missing_params[] = 'status';
throw new WorkflowException('Missing required parameters: ' . implode(', ', $missing_params), 400);
}
$workflowEngine = new WorkflowEngine();
$meetingId = $workflowEngine->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);

125
_update_person.php Normal file
View File

@ -0,0 +1,125 @@
<?php
require_once 'db/config.php';
session_start();
require_once 'lib/i18n.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$personId = $_POST['id'];
$first_name = $_POST['first_name'];
$last_name = $_POST['last_name'];
$email = $_POST['email'];
$company_name = $_POST['company_name'];
$phone = $_POST['phone'];
$role = $_POST['role'] ?? 'guest';
$functions = isset($_POST['functions']) ? $_POST['functions'] : [];
$password = $_POST['password'];
$bni_group_id = isset($_POST['bni_group_id']) && !empty($_POST['bni_group_id']) ? $_POST['bni_group_id'] : null;
// New fields
$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)) {
http_response_code(422);
echo json_encode(['error' => ['message' => t('error.missing_update_fields', 'Imię, nazwisko i email są wymagane.')], '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();
}

View File

@ -0,0 +1,36 @@
<?php
require_once 'lib/ErrorHandler.php';
require_once 'WorkflowEngine.php';
session_start();
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
throw new WorkflowNotAllowedException('Unauthorized');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new WorkflowNotAllowedException('Method Not Allowed');
}
$inputJSON = file_get_contents('php://input');
$input = json_decode($inputJSON, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new WorkflowRuleFailedException('Invalid JSON');
}
$instanceId = $input['instance_id'] ?? null;
$taskCode = $input['task_code'] ?? null;
$isChecked = $input['is_checked'] ?? null;
$userId = $_SESSION['user_id'];
if (!$instanceId || !$taskCode || $isChecked === null) {
throw new WorkflowRuleFailedException('Missing required parameters: instance_id, task_code, is_checked');
}
$workflowEngine = new WorkflowEngine();
$result = $workflowEngine->updateChecklistStatus((int)$instanceId, $taskCode, (bool)$isChecked, (int)$userId);
echo json_encode($result);
exit;

52
apply_patch.py Normal file
View File

@ -0,0 +1,52 @@
import re
with open('_get_instance_details.php', 'r') as f:
content = f.read()
pattern = re.compile(r"<\?php foreach \(\dcurrentNode\['ui_hints'\]\['form_schema'\].*?endforeach;\s*\?>", re.DOTALL)
replacement_text = r"""<?php foreach ($currentNode['ui_hints']['form_schema'] as $field):
$fieldName = $field['name'];
$currentValue = $instanceData[$fieldName] ?? null;
?>
<?php if ($field['type'] === 'checkbox'): ?>
<div class="form-check mb-3">
<input type="hidden" name="<?= $fieldName ?>" value="0">
<input type="checkbox" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-check-input" value="1" <?= (!empty($currentValue) || (!isset($currentValue) && !empty($field['default']))) ? 'checked' : '' ?>>
<label for="<?= $fieldName ?>" class="form-check-label"><?= $field['label'] ?></label>
</div>
<?php else: ?>
<div class="mb-3">
<label for="<?= $fieldName ?>" class="form-label"><?= $field['label'] ?></label>
<?php if ($field['type'] === 'textarea'): ?>
<textarea id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control"><?= htmlspecialchars((string)($currentValue ?? '')) ?></textarea>
<?php elseif ($field['type'] === 'select'): ?>
<select id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-select">
<?php foreach ($field['options'] as $option):
$selected = ($currentValue !== null && (string)$currentValue === (string)$option['value']) ? 'selected' : '';
?>
<option value="<?= $option['value'] ?>" <?= $selected ?>><?= $option['label'] ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<?php
$defaultVal = ($field['default'] ?? '') === 'now' ? date('Y-m-d\\TH:i') : ($field['default'] ?? '');
$valToUse = $currentValue ?? $defaultVal;
?>
<input type="<?= $field['type'] ?>" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control" value="<?= htmlspecialchars((string)$valToUse) ?>">
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>"""
def replacer(match):
return replacement_text
matches = pattern.search(content)
if matches:
content = pattern.sub(replacer, content, count=1)
with open('_get_instance_details.php', 'w') as f:
f.write(content)
print("Patched successfully!")
else:
print("Pattern not found!")

169
assets/css/custom.css Normal file
View File

@ -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;
}

253
assets/js/main.js Normal file
View File

@ -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 = '<h5>Follow-up Process Summary</h5>';
summaryHtml += '<dl class="row">';
if (followUpSummary.last_call_outcome) {
summaryHtml += `<dt class="col-sm-4">Last Call Outcome</dt><dd class="col-sm-8">${followUpSummary.last_call_outcome.replace(/_/g, ' ')}</dd>`;
}
if (followUpSummary.last_call_date) {
summaryHtml += `<dt class="col-sm-4">Last Call Date</dt><dd class="col-sm-8">${new Date(followUpSummary.last_call_date).toLocaleString()}</dd>`;
}
if (followUpSummary.next_contact_date) {
summaryHtml += `<dt class="col-sm-4">Next Contact Date</dt><dd class="col-sm-8">${new Date(followUpSummary.next_contact_date).toLocaleString()}</dd>`;
}
if (followUpSummary.final_outcome) {
summaryHtml += `<dt class="col-sm-4">Final Status</dt><dd class="col-sm-8">${followUpSummary.final_outcome} (${followUpSummary.reason || 'N/A'})</dd>`;
}
summaryHtml += '</dl>';
summaryContainer.html(summaryHtml);
} else {
summaryContainer.html('<p class="text-muted">No Follow-up process data found for this person.</p>');
}
// 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 = $('<optgroup>').attr('label', groupName);
groupedFunctions[groupName].forEach(function(func) {
var option = $('<option></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></form>');
form.attr("method", "post");
form.attr("action", "_delete_person.php");
var field = $('<input></input>');
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();
});
});
});
});

View File

@ -0,0 +1,159 @@
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 subjectScope = button.dataset.subjectScope || 'person';
const processDefinition = button.dataset.processDefinition;
modalTitle.textContent = 'Edit Process';
processIdInput.value = processId;
processNameInput.value = processName;
if(document.getElementById('subjectScope')) document.getElementById('subjectScope').value = subjectScope;
try {
definition = JSON.parse(processDefinition || '{}');
} catch(e) {
definition = {};
}
} else {
modalTitle.textContent = 'Create Process';
processIdInput.value = '';
processNameInput.value = '';
definition = {};
}
eligibilityRulesJsonTextarea.value = '';
render();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

201
bni_groups.php Normal file
View File

@ -0,0 +1,201 @@
<?php
require_once 'db/config.php';
include '_header.php';
include '_navbar.php';
$pdo = db();
$stmt = $pdo->query("SELECT * FROM bni_groups ORDER BY display_order");
$bni_groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<div class="row">
<?php include '_sidebar.php'; ?>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<h1 class="h2 pt-3 pb-2 mb-3 border-bottom"><?php echo t('bni_groups.title', 'BNI Groups'); ?></h1>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
<?= $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<?php echo t('bni_groups.add_new', 'Add New BNI Group'); ?>
</button>
</div>
<div class="table-responsive mt-4">
<table class="table table-striped table-sm">
<thead>
<tr>
<th style="width: 30px;"></th>
<th><?php echo t('bni_groups.name', 'Name'); ?></th>
<th><?php echo t('bni_groups.city', 'City'); ?></th>
<th><?php echo t('bni_groups.active', 'Active'); ?></th>
<th><?php echo t('bni_groups.actions', 'Actions'); ?></th>
</tr>
</thead>
<tbody id="sortable-list">
<?php foreach ($bni_groups as $group): ?>
<tr data-id="<?= $group['id'] ?>">
<td class="handle"><i class="bi bi-grip-vertical"></i></td>
<td><?= htmlspecialchars($group['name']) ?></td>
<td><?= htmlspecialchars($group['city']) ?></td>
<td><?= $group['active'] ? t('bni_groups.yes', 'Yes') : t('bni_groups.no', 'No') ?></td>
<td>
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#editModal"
data-id="<?= $group['id'] ?>"
data-name="<?= htmlspecialchars($group['name']) ?>"
data-city="<?= htmlspecialchars($group['city']) ?>"
data-active="<?= $group['active'] ?>">
<?php echo t('bni_groups.edit', 'Edit'); ?>
</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal" data-id="<?= $group['id'] ?>">
<?php echo t('bni_groups.delete', 'Delete'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</main>
</div>
</div>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addModalLabel"><?php echo t('bni_groups.add_title', 'Add BNI Group'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="_add_bni_group.php" method="post">
<div class="modal-body">
<div class="mb-3">
<label for="addName" class="form-label"><?php echo t('bni_groups.name', 'Name'); ?></label>
<input type="text" class="form-control" id="addName" name="name" required>
</div>
<div class="mb-3">
<label for="addCity" class="form-label"><?php echo t('bni_groups.city', 'City'); ?></label>
<input type="text" class="form-control" id="addCity" name="city">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="1" id="addActive" name="active" checked>
<label class="form-check-label" for="addActive">
<?php echo t('bni_groups.active', 'Active'); ?>
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="submit" name="add" class="btn btn-primary"><?php echo t('bni_groups.add_button', 'Add'); ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel"><?php echo t('bni_groups.edit_title', 'Edit BNI Group'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="_update_bni_group.php" method="post">
<div class="modal-body">
<input type="hidden" id="editId" name="id">
<div class="mb-3">
<label for="editName" class="form-label"><?php echo t('bni_groups.name', 'Name'); ?></label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label for="editCity" class="form-label"><?php echo t('bni_groups.city', 'City'); ?></label>
<input type="text" class="form-control" id="editCity" name="city">
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" value="1" id="editActive" name="active">
<label class="form-check-label" for="editActive">
<?php echo t('bni_groups.active', 'Active'); ?>
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="submit" name="edit" class="btn btn-primary"><?php echo t('modal.save_changes', 'Save changes'); ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel"><?php echo t('bni_groups.delete_title', 'Delete BNI Group'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php echo t('bni_groups.delete_confirm', 'Are you sure you want to delete this BNI group?'); ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.cancel', 'Cancel'); ?></button>
<a href="#" id="deleteLink" class="btn btn-danger"><?php echo t('bni_groups.delete', 'Delete'); ?></a>
</div>
</div>
</div>
</div>
<?php include '_footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
var editModal = document.getElementById('editModal');
editModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var name = button.getAttribute('data-name');
var city = button.getAttribute('data-city');
var active = button.getAttribute('data-active');
var idInput = editModal.querySelector('#editId');
var nameInput = editModal.querySelector('#editName');
var cityInput = editModal.querySelector('#editCity');
var activeInput = editModal.querySelector('#editActive');
idInput.value = id;
nameInput.value = name;
cityInput.value = city;
activeInput.checked = (active == 1);
});
var deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var deleteLink = deleteModal.querySelector('#deleteLink');
deleteLink.href = '_delete_bni_group.php?id=' + id;
});
$(function() {
$("#sortable-list").sortable({
handle: ".handle",
update: function(event, ui) {
var order = $(this).sortable('toArray', {attribute: 'data-id'});
$.post('_update_bni_group_order.php', { order: order });
}
}).disableSelection();
});
});
</script>

352
calendar.php Normal file
View File

@ -0,0 +1,352 @@
<?php
require_once 'db/config.php';
include '_header.php';
// Get current month and year
$month = isset($_GET['month']) ? (int)$_GET['month'] : date('m');
$year = isset($_GET['year']) ? (int)$_GET['year'] : date('Y');
// Create a DateTime object for the first day of the month
$firstDayOfMonth = new DateTime("$year-$month-01");
$daysInMonth = $firstDayOfMonth->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;
?>
<?php include '_navbar.php'; ?>
<div class="container-fluid">
<div class="row">
<?php include '_sidebar.php'; ?>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<h2 class="text-center"><?php echo t('calendar.' . strtolower($firstDayOfMonth->format('F')), $firstDayOfMonth->format('F')) . ' ' . $firstDayOfMonth->format('Y'); ?></h2>
<div class="d-flex justify-content-between mb-3">
<a href="?month=<?php echo $prevMonth; ?>&year=<?php echo $prevYear; ?>" class="btn btn-primary">&lt; <?php echo t('calendar.previous', 'Previous'); ?></a>
<a href="?month=<?php echo $nextMonth; ?>&year=<?php echo $nextYear; ?>" class="btn btn-primary"><?php echo t('calendar.next', 'Next'); ?> &gt;</a>
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addEventModal">
<?php echo t('calendar.add_event', 'Add Event'); ?>
</button>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th><?php echo t('calendar.monday', 'Monday'); ?></th>
<th><?php echo t('calendar.tuesday', 'Tuesday'); ?></th>
<th><?php echo t('calendar.wednesday', 'Wednesday'); ?></th>
<th><?php echo t('calendar.thursday', 'Thursday'); ?></th>
<th><?php echo t('calendar.friday', 'Friday'); ?></th>
<th><?php echo t('calendar.saturday', 'Saturday'); ?></th>
<th><?php echo t('calendar.sunday', 'Sunday'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<?php
// Print empty cells
for ($i = 1; $i < $dayOfWeek; $i++) {
echo "<td></td>";
}
$currentDay = 1;
while ($currentDay <= $daysInMonth) {
if ($dayOfWeek > 7) {
$dayOfWeek = 1;
echo "</tr><tr>";
}
echo "<td class=\"calendar-day\" data-date=\"$year-$month-$currentDay\">";
echo "<strong>$currentDay</strong>";
if (isset($eventsByDay[$currentDay])) {
echo "<ul class=\"list-unstyled\">";
foreach ($eventsByDay[$currentDay] as $event) {
echo '<li class="badge" style="background-color: '.($event['type_color'] ?? '#007bff').'" data-event-id="'.$event['id'].'">' . htmlspecialchars($event['title']);
if (!empty($event['group_names'])) {
echo '<br><small class="fst-italic">(' . htmlspecialchars($event['group_names']) . ')</small>';
}
echo "</li>";
}
echo "</ul>";
}
echo "</td>";
$currentDay++;
$dayOfWeek++;
}
// Print remaining empty cells
while ($dayOfWeek <= 7) {
echo "<td></td>";
$dayOfWeek++;
}
?>
</tr>
</tbody>
</table>
</main>
</div>
</div>
<!-- Event Details Modal -->
<div class="modal fade" id="eventDetailsModal" tabindex="-1" aria-labelledby="eventDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="eventDetailsModalLabel"><?php echo t('calendar.event_details', 'Event Details'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h5 id="event-title"></h5>
<p id="event-description"></p>
<p><strong><?php echo t('calendar.starts', 'Starts:'); ?></strong> <span id="event-start"></span></p>
<p><strong><?php echo t('calendar.ends', 'Ends:'); ?></strong> <span id="event-end"></span></p>
<p><strong><?php echo t('calendar.type', 'Type:'); ?></strong> <span id="event-type"></span></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="button" class="btn btn-primary" id="edit-event-btn"><?php echo t('calendar.edit', 'Edit'); ?></button>
<a href="#" id="delete-event-btn" class="btn btn-danger"><?php echo t('calendar.delete', 'Delete'); ?></a>
</div>
</div>
</div>
</div>
<div class="modal fade" id="addEventModal" tabindex="-1" aria-labelledby="addEventModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addEventModalLabel"><?php echo t('calendar.add_event_title', 'Add Calendar Event'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="_add_calendar_event.php" method="post">
<div class="mb-3">
<label for="title" class="form-label"><?php echo t('calendar.title', 'Title'); ?></label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label"><?php echo t('form.description', 'Description'); ?></label>
<textarea class="form-control" id="description" name="description"></textarea>
</div>
<div class="mb-3">
<label for="start_datetime" class="form-label"><?php echo t('calendar.start_time', 'Start Time'); ?></label>
<input type="datetime-local" class="form-control" id="start_datetime" name="start_datetime" required>
</div>
<div class="mb-3">
<label for="end_datetime" class="form-label"><?php echo t('calendar.end_time', 'End Time'); ?></label>
<input type="datetime-local" class="form-control" id="end_datetime" name="end_datetime" required>
</div>
<div class="mb-3">
<label for="event_type_id" class="form-label"><?php echo t('form.type', 'Type'); ?></label>
<select class="form-select" id="event_type_id" name="event_type_id">
<?php foreach ($event_types as $type): ?>
<option value="<?= $type['id'] ?>"><?= htmlspecialchars($type['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="group_ids" class="form-label"><?php echo t('calendar.groups', 'Group(s)'); ?></label>
<select class="form-select" id="group_ids" name="group_ids[]" multiple required>
<?php foreach ($bni_groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="recurrence" class="form-label"><?php echo t('calendar.recurrence', 'Recurrence'); ?></label>
<select class="form-select" id="recurrence" name="recurrence">
<option value=""><?php echo t('calendar.no_repeat', 'Does not repeat'); ?></option>
<option value="daily"><?php echo t('calendar.daily', 'Daily'); ?></option>
<option value="weekly"><?php echo t('calendar.weekly', 'Weekly'); ?></option>
<option value="monthly"><?php echo t('calendar.monthly', 'Monthly'); ?></option>
</select>
</div>
<div class="mb-3" id="recurrence_end_date_container" style="display: none;">
<label for="recurrence_end_date" class="form-label"><?php echo t('calendar.recurrence_end_date', 'Recurrence End Date'); ?></label>
<input type="date" class="form-control" id="recurrence_end_date" name="recurrence_end_date">
</div>
<button type="submit" class="btn btn-primary"><?php echo t('calendar.save_event', 'Save Event'); ?></button>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="editEventModal" tabindex="-1" aria-labelledby="editEventModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editEventModalLabel"><?php echo t('calendar.edit_event_title', 'Edit Calendar Event'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="_update_calendar_event.php" method="post">
<input type="hidden" id="edit_event_id" name="event_id">
<div class="mb-3">
<label for="edit_title" class="form-label"><?php echo t('calendar.title', 'Title'); ?></label>
<input type="text" class="form-control" id="edit_title" name="title" required>
</div>
<div class="mb-3">
<label for="edit_description" class="form-label"><?php echo t('form.description', 'Description'); ?></label>
<textarea class="form-control" id="edit_description" name="description"></textarea>
</div>
<div class="mb-3">
<label for="edit_start_datetime" class="form-label"><?php echo t('calendar.start_time', 'Start Time'); ?></label>
<input type="datetime-local" class="form-control" id="edit_start_datetime" name="start_datetime" required>
</div>
<div class="mb-3">
<label for="edit_end_datetime" class="form-label"><?php echo t('calendar.end_time', 'End Time'); ?></label>
<input type="datetime-local" class="form-control" id="edit_end_datetime" name="end_datetime" required>
</div>
<div class="mb-3">
<label for="edit_event_type_id" class="form-label"><?php echo t('form.type', 'Type'); ?></label>
<select class="form-select" id="edit_event_type_id" name="event_type_id">
<?php foreach ($event_types as $type): ?>
<option value="<?= $type['id'] ?>"><?= htmlspecialchars($type['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="edit_group_ids" class="form-label"><?php echo t('calendar.groups', 'Group(s)'); ?></label>
<select class="form-select" id="edit_group_ids" name="group_ids[]" multiple required>
<?php foreach ($bni_groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div id="edit-recurrence-options" class="mb-3" style="display: none;">
<strong><?php echo t('calendar.recurring_notice', 'This is a recurring event.'); ?></strong><br>
<div class="form-check">
<input class="form-check-input" type="radio" name="update_scope" id="update_scope_one" value="one" checked>
<label class="form-check-label" for="update_scope_one">
<?php echo t('calendar.update_scope_one', 'Update this event only'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="update_scope" id="update_scope_all" value="all">
<label class="form-check-label" for="update_scope_all">
<?php echo t('calendar.update_scope_all', 'Update all events in the series'); ?>
</label>
</div>
</div>
<button type="submit" class="btn btn-primary"><?php echo t('modal.save_changes', 'Save Changes'); ?></button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const addEventModal = new bootstrap.Modal(document.getElementById('addEventModal'));
const eventDetailsModal = new bootstrap.Modal(document.getElementById('eventDetailsModal'));
const editEventModal = new bootstrap.Modal(document.getElementById('editEventModal'));
let currentEventData = null;
document.querySelectorAll('.calendar-day').forEach(day => {
day.addEventListener('click', function(e) {
if (e.target.classList.contains('badge')) {
const eventId = e.target.dataset.eventId;
fetch(`_get_event_details.php?id=${eventId}`)
.then(response => response.json())
.then(data => {
currentEventData = data;
document.getElementById('event-title').textContent = data.title;
document.getElementById('event-description').textContent = data.description;
document.getElementById('event-start').textContent = new Date(data.start_datetime).toLocaleString();
document.getElementById('event-end').textContent = new Date(data.end_datetime).toLocaleString();
document.getElementById('event-type').textContent = data.type_name;
document.getElementById('delete-event-btn').href = `_delete_calendar_event.php?id=${eventId}`;
eventDetailsModal.show();
});
} else {
const date = this.dataset.date;
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const currentTime = `${hours}:${minutes}`;
const startInput = document.getElementById('start_datetime');
const month = String(new Date(date).getMonth() + 1).padStart(2, '0');
const dayOfMonth = String(new Date(date).getDate()).padStart(2, '0');
const year = new Date(date).getFullYear();
startInput.value = `${year}-${month}-${dayOfMonth}T${currentTime}`;
addEventModal.show();
}
});
});
document.getElementById('edit-event-btn').addEventListener('click', function() {
if (currentEventData) {
const recurrenceOptions = document.getElementById('edit-recurrence-options');
if (currentEventData.parent_event_id !== null || currentEventData.recurrence !== null) {
recurrenceOptions.style.display = 'block';
document.getElementById('update_scope_all').checked = true;
} else {
recurrenceOptions.style.display = 'none';
}
document.getElementById('edit_event_id').value = currentEventData.id;
document.getElementById('edit_title').value = currentEventData.title;
document.getElementById('edit_description').value = currentEventData.description;
document.getElementById('edit_start_datetime').value = currentEventData.start_datetime.slice(0, 16);
document.getElementById('edit_end_datetime').value = currentEventData.end_datetime.slice(0, 16);
document.getElementById('edit_event_type_id').value = currentEventData.event_type_id;
const groupSelect = document.getElementById('edit_group_ids');
const groupIds = currentEventData.group_ids || [];
for (const option of groupSelect.options) {
option.selected = groupIds.includes(option.value);
}
eventDetailsModal.hide();
editEventModal.show();
}
});
const recurrenceSelect = document.getElementById('recurrence');
const recurrenceEndDateContainer = document.getElementById('recurrence_end_date_container');
recurrenceSelect.addEventListener('change', function() {
if (this.value) {
recurrenceEndDateContainer.style.display = 'block';
} else {
recurrenceEndDateContainer.style.display = 'none';
}
});
});
</script>
<?php
include '_footer.php';
?>

5
cookie.txt Normal file
View File

@ -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 nupuqse7tg6gsrulg298ru8sn8

130
current_definition.json Normal file
View File

@ -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" } ]
}
]
}

View File

@ -0,0 +1,8 @@
<?php
function migrate_001($pdo) {
$sql = "ALTER TABLE process_definitions ADD COLUMN definition_json TEXT NULL AFTER name;";
$pdo->exec($sql);
echo "Migration 001 applied: Added definition_json to process_definitions.\n";
}

View File

@ -0,0 +1,17 @@
<?php
function migrate_002($pdo) {
$sql = "CREATE TABLE IF NOT EXISTS calendar_events (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
start_datetime DATETIME NOT NULL,
end_datetime DATETIME NOT NULL,
type VARCHAR(50) NOT NULL, -- 'meeting', 'training'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);";
$pdo->exec($sql);
echo "Migration 002 applied: Created calendar_events table.\n";
}

View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "CREATE TABLE IF NOT EXISTS event_types (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
color VARCHAR(7) NOT NULL DEFAULT '#007bff'
);";
$pdo->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");
}

View File

@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
// First, add the column without the foreign key constraint
$sql_add_column = "ALTER TABLE calendar_events ADD COLUMN event_type_id INT NULL;";
$pdo->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");
}
}

View File

@ -0,0 +1,18 @@
<?php
require_once(__DIR__ . '/../config.php');
try {
$pdo = db();
$sql = "CREATE TABLE IF NOT EXISTS `bni_groups` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`city` VARCHAR(255),
`active` BOOLEAN NOT NULL DEFAULT 1,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);";
$pdo->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);
}

View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = <<<SQL
ALTER TABLE `calendar_events`
ADD COLUMN `recurrence` VARCHAR(20) DEFAULT NULL,
ADD COLUMN `recurrence_end_date` DATE DEFAULT NULL,
ADD COLUMN `parent_event_id` INT DEFAULT NULL;
SQL;
$pdo->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");
}

View File

@ -0,0 +1,17 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "ALTER TABLE calendar_events DROP COLUMN type;";
$pdo->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");
}
}

View File

@ -0,0 +1,16 @@
<?php
require_once(__DIR__ . '/../config.php');
try {
$pdo = db();
$sql = "CREATE TABLE IF NOT EXISTS `roles` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
$pdo->exec($sql);
echo "Table 'roles' created successfully." . PHP_EOL;
} catch (PDOException $e) {
echo "Error creating table 'roles': " . $e->getMessage() . PHP_EOL;
exit(1);
}

View File

@ -0,0 +1,18 @@
<?php
require_once(__DIR__ . '/../config.php');
try {
$pdo = db();
$sql = "CREATE TABLE IF NOT EXISTS `user_roles` (
`user_id` INT(11) UNSIGNED NOT NULL,
`role_id` INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (`user_id`, `role_id`),
FOREIGN KEY (`user_id`) REFERENCES `people`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
$pdo->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);
}

View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "ALTER TABLE event_types ADD COLUMN display_order INT NOT NULL DEFAULT 0";
$pdo->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);
}

View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "ALTER TABLE bni_groups ADD COLUMN display_order INT NOT NULL DEFAULT 0";
$pdo->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);
}

View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = "ALTER TABLE roles ADD COLUMN display_order INT NOT NULL DEFAULT 0";
$pdo->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);
}

View File

@ -0,0 +1,21 @@
<?php
require_once(__DIR__ . '/../config.php');
try {
$pdo = db();
$sql = "RENAME TABLE `roles` TO `functions`;";
$pdo->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);
}

View File

@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_014_add_role_to_people_table() {
$pdo = db();
// Check if the column already exists
try {
$result = $pdo->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();

View File

@ -0,0 +1,61 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
// First, check if the column already exists
$stmt = $pdo->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);
}

View File

@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
// Check if table calendar_event_groups already exists
$stmt = $pdo->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");
}

View File

@ -0,0 +1,19 @@
<?php
require_once(__DIR__ . '/../../db/config.php');
try {
$pdo = db();
// Add bni_group_id to people table
$stmt = $pdo->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());
}
?>

View File

@ -0,0 +1,24 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$sql = <<<SQL
ALTER TABLE people
ADD COLUMN nip VARCHAR(255) DEFAULT NULL,
ADD COLUMN industry VARCHAR(255) DEFAULT NULL,
ADD COLUMN company_size VARCHAR(255) DEFAULT NULL,
ADD COLUMN business_description TEXT DEFAULT NULL,
ADD COLUMN company_logo_path VARCHAR(255) DEFAULT NULL,
ADD COLUMN person_photo_path VARCHAR(255) DEFAULT NULL,
ADD COLUMN gains_sheet_path VARCHAR(255) DEFAULT NULL,
ADD COLUMN top_wanted_contacts_path VARCHAR(255) DEFAULT NULL,
ADD COLUMN top_owned_contacts_path VARCHAR(255) DEFAULT NULL;
SQL;
$pdo->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);
}

View File

@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../../db/config.php';
$pdo = db();
$columns = [
'nip' => '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";
}

View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
echo "Starting migration 021: Rename camelCase columns to snake_case...\n";
// Process instances table
$pdo->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());
}

View File

@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/../config.php';
function migrate_022($pdo) {
try {
$sql = "ALTER TABLE process_definitions ADD COLUMN is_active TINYINT(1) NOT NULL DEFAULT 1 AFTER definition_json";
$pdo->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);
}

View File

@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_023($pdo) {
$json_definition = '{ "nodes": { "start": { "id": "start", "name": "Start", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na wystawienie pro formy", "next_step": "Wystaw pro formę" } }, "wait_for_payment": { "id": "wait_for_payment", "name": "Oczekiwanie na płatność", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na potwierdzenie wpływu środków", "next_step": "Potwierdź wpływ środków" } }, "notify_committee": { "id": "notify_committee", "name": "Powiadomienie komitetu", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na powiadomienie komitetu", "next_step": "Powiadom komitet i przejdź do decyzji" } }, "decision_committee": { "id": "decision_committee", "name": "Decyzja komitetu", "ui_hints": { "status": "in_progress", "reason": "Oczekuje na decyzję komitetu", "next_step": "Zatwierdź lub odrzuć" } }, "end_accepted": { "id": "end_accepted", "name": "Zatwierdzony", "ui_hints": { "status": "positive", "reason": "Członek przyjęty", "next_step": "" } }, "end_rejected": { "id": "end_rejected", "name": "Odrzucony", "ui_hints": { "status": "negative", "reason": "Członek odrzucony", "next_step": "" } } }, "transitions": [ { "id": "issue_proforma", "name": "Wystaw pro formę", "from": "start", "to": "wait_for_payment", "actions": [ { "type": "set_data", "params": { "keys": ["issue_date", "document_number"] } } ] }, { "id": "confirm_payment", "name": "Potwierdź wpływ środków", "from": "wait_for_payment", "to": "notify_committee", "actions": [ { "type": "set_data", "params": { "keys": ["payment_date"] } } ] } , { "id": "proceed_to_decision", "name": "Przekaż do decyzji komitetu", "from": "notify_committee", "to": "decision_committee" }, { "id": "accept_member", "name": "Zatwierdź", "from": "decision_committee", "to": "end_accepted", "actions": [ { "type": "start_process", "params": { "process_code": "wprowadzenie-nowego-cz-onka" } } ] }, { "id": "reject_member", "name": "Odrzuć", "from": "decision_committee", "to": "end_rejected" } ], "start_node_id": "start", "eligibility_rules": [ { "type": "deny_manual_start" } ] }';
$stmt = $pdo->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";
}

View File

@ -0,0 +1,154 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_024()
{
$pdo = db();
$process_code = 'guest_handling';
$json_definition = <<<EOT
{
"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" },
{ "name": "note", "label": "Notes", "type": "textarea" }
]
}
},
"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": ""
}
}
},
"transitions": [
{
"id": "log_wants_to_join",
"from": "awaiting_call",
"to": "end_positive",
"name": "Wants to Join",
"actions": [
{
"type": "set_data",
"params": { "keys": ["call_date", "note", "outcome_status"] }
},
{
"type": "start_process",
"process_code": "obsluga-przyjecia-nowego-czlonka"
}
]
},
{
"id": "log_declined",
"from": "awaiting_call",
"to": "end_negative_declined",
"name": "Declined",
"actions": [
{
"type": "set_data",
"params": { "keys": ["call_date", "note", "outcome_status"] }
}
]
},
{
"id": "log_no_answer",
"from": "awaiting_call",
"to": "decide_after_no_answer",
"name": "No Answer",
"actions": [
{
"type": "set_data",
"params": { "keys": ["call_date", "note", "outcome_status"] }
}
]
},
{
"id": "log_call_later",
"from": "awaiting_call",
"to": "awaiting_call",
"name": "Call Later",
"actions": [
{
"type": "set_data",
"params": { "keys": ["call_date", "note", "outcome_status"] }
}
]
},
{
"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"
}
]
}
EOT;
$stmt = $pdo->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";
}
}

View File

@ -0,0 +1,161 @@
<?php
require_once __DIR__ . '/../../db/config.php';
function migrate_025()
{
$pdo = db();
$process_code = 'guest_handling';
$json_definition = <<<'EOT'
{
"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" } ]
}
]
}
EOT;
$stmt = $pdo->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";
}
}

View File

@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../../db/config.php';
$db = db();
$new_json_definition = file_get_contents(__DIR__ . '/../../new_definition.json');
$stmt = $db->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.";

View File

@ -0,0 +1,19 @@
<?php
require_once __DIR__ . '/../config.php';
function migrate_027($pdo) {
try {
$sql = "UPDATE process_definitions SET is_active = 0 WHERE name = 'Sales Pipeline'";
$pdo->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);
}

View File

@ -0,0 +1,22 @@
<?php
require_once __DIR__ . '/../config.php';
function migrate_028($pdo) {
try {
$sql = "ALTER TABLE process_definitions ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER is_active";
$pdo->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);
}

View File

@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../config.php';
function migrate_029($pdo) {
$process_order = [
'Przygotowanie spotkania grupy' => 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);
}

View File

@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdoconn = db();
$pdoconn->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) 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);
}

View File

@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdoconn = db();
$pdoconn->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);
}

View File

@ -0,0 +1,73 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$db = db();
echo "Starting migration 032.\n";
// Step 1: Add bni_group_id column
try {
echo "Step 1: Adding bni_group_id column...\n";
$db->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");
}

View File

@ -0,0 +1,71 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$db = db();
$db->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");
}

View File

@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$db = db();
$db->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");
}

View File

@ -0,0 +1,18 @@
<?php
require_once __DIR__ . '/../../db/config.php';
$db = db();
// Step 1: Back-fill bni_group_id from group_id where bni_group_id is NULL
$db->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.";

View File

@ -0,0 +1,57 @@
<?php
function migrate_036($pdo) {
echo "Starting migration 036 (Process Versioning)...
";
// 1. Process Definitions
$stmt = $pdo->query("SHOW INDEX FROM process_definitions WHERE Key_name = 'code'");
if ($stmt->fetch()) {
$pdo->exec("ALTER TABLE process_definitions DROP INDEX `code`");
echo "Dropped unique index on process_definitions.code.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_definitions LIKE 'supersedes_definition_id'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_definitions ADD COLUMN supersedes_definition_id INT(11) UNSIGNED NULL AFTER version");
echo "Added supersedes_definition_id to process_definitions.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_definitions LIKE 'is_latest'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_definitions ADD COLUMN is_latest TINYINT(1) NOT NULL DEFAULT 1 AFTER supersedes_definition_id");
echo "Added is_latest to process_definitions.
";
}
$stmt = $pdo->query("SHOW INDEX FROM process_definitions WHERE Key_name = 'idx_code_latest_active'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_definitions ADD INDEX idx_code_latest_active (code, is_latest, is_active)");
echo "Added index idx_code_latest_active on process_definitions.
";
}
// Ensure existing rows are marked as latest properly (all existing should be latest)
// Nothing needed, default is 1.
// 2. Process Instances
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'person_process'");
if ($stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances DROP INDEX `person_process`");
echo "Dropped unique index on process_instances.person_process.
";
}
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_person_definition'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_person_definition (person_id, process_definition_id)");
echo "Added index idx_person_definition on process_instances.
";
}
echo "Migration 036 completed.
";
}

View File

@ -0,0 +1,66 @@
<?php
function migrate_037($pdo) {
echo "Starting migration 037 (Process Subjects)...
";
// 1. process_instances: add subject_type, subject_id, cycle_key, bni_group_id
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'subject_type'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN subject_type VARCHAR(20) NOT NULL DEFAULT 'person' AFTER person_id");
echo "Added subject_type to process_instances.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'subject_id'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN subject_id INT NOT NULL DEFAULT 0 AFTER subject_type");
echo "Added subject_id to process_instances.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'cycle_key'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN cycle_key VARCHAR(64) NULL AFTER subject_id");
echo "Added cycle_key to process_instances.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'bni_group_id'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN bni_group_id INT NULL AFTER cycle_key");
echo "Added bni_group_id to process_instances.
";
}
// 2. Data migration
echo "Migrating existing data...
";
$pdo->exec("UPDATE process_instances SET subject_type = 'person', subject_id = person_id WHERE subject_id = 0");
// 3. Add indexes
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_subject_active'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_subject_active (subject_type, subject_id, current_status)");
echo "Added index idx_subject_active.
";
}
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_subject_cycle'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_subject_cycle (subject_type, subject_id, cycle_key)");
echo "Added index idx_subject_cycle.
";
}
$stmt = $pdo->query("SHOW INDEX FROM process_instances WHERE Key_name = 'idx_group'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD INDEX idx_group (bni_group_id)");
echo "Added index idx_group.
";
}
echo "Migration 037 completed.
";
}

View File

@ -0,0 +1,42 @@
<?php
function migrate_038($pdo) {
echo "Starting migration 038 (Subject Scope and Run Number)...
";
$stmt = $pdo->query("SHOW COLUMNS FROM process_definitions LIKE 'subject_scope'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_definitions ADD COLUMN subject_scope VARCHAR(32) NOT NULL DEFAULT 'person' AFTER name");
echo "Added subject_scope to process_definitions.
";
}
$stmt = $pdo->query("SHOW COLUMNS FROM process_instances LIKE 'run_number'");
if (!$stmt->fetch()) {
$pdo->exec("ALTER TABLE process_instances ADD COLUMN run_number INT NOT NULL DEFAULT 1 AFTER subject_id");
echo "Added run_number to process_instances.
";
}
$defJson = json_encode([
'start_node_id' => 'node_start',
'nodes' => [
['id' => 'node_start', 'name' => 'Planowanie', 'type' => 'task', 'ui_hints' => ['color' => 'blue', 'status' => 'not_started']],
['id' => 'node_end', 'name' => 'Zakończone', 'type' => 'end', 'ui_hints' => ['color' => 'green', 'status' => 'completed']]
],
'transitions' => [
['from' => 'node_start', 'to' => 'node_end', 'name' => 'Oznacz jako gotowe', 'trigger' => 'manual']
]
]);
$stmt = $pdo->prepare("SELECT id FROM process_definitions WHERE code = 'meeting_preparation_meeting'");
$stmt->execute();
if (!$stmt->fetch()) {
$stmtIns = $pdo->prepare("INSERT INTO process_definitions (code, name, version, is_active, subject_scope, definition_json) VALUES ('meeting_preparation_meeting', 'Przygotowanie spotkania (spotkanie)', 1, 1, 'meeting', :json)");
$stmtIns->execute(['json' => $defJson]);
echo "Inserted meeting_preparation_meeting definition.
";
}
echo "Migration 038 completed.
";
}

View File

@ -0,0 +1,43 @@
<?php
require_once __DIR__ . '/../config.php';
function migrate_039($pdo) {
// 1. Obsługa gościa (guest_handling)
// 2. Obsługa przyjęcia nowego członka (obsluga-przyjecia-nowego-czlonka)
// 3. Mentoring nowego członka (mentoring)
// 4. Szkolenie nowego członka (training_new_member)
// 5. Członkostwo (customer_journey)
$processes = [
'guest_handling' => ['name' => 'Obsługa gościa', 'sort_order' => 10],
'obsluga-przyjecia-nowego-czlonka' => ['name' => 'Obsługa przyjęcia nowego członka', 'sort_order' => 20],
'mentoring' => ['name' => 'Mentoring nowego członka', 'sort_order' => 30],
'training_new_member' => ['name' => 'Szkolenie nowego członka', 'sort_order' => 40],
'customer_journey' => ['name' => 'Członkostwo', 'sort_order' => 50],
// Push the other ones to the bottom
'meeting_preparation' => ['name' => 'Przygotowanie spotkania grupy', 'sort_order' => 60],
'meeting_preparation_meeting' => ['name' => 'Przygotowanie spotkania (spotkanie)', 'sort_order' => 70],
];
try {
$stmt = $pdo->prepare("UPDATE process_definitions SET name = :name, sort_order = :sort_order WHERE code = :code");
foreach ($processes as $code => $data) {
$stmt->execute([
'name' => $data['name'],
'sort_order' => $data['sort_order'],
'code' => $code
]);
}
echo "Migration 039 successful: Process names and order updated.\n";
} catch (PDOException $e) {
die("Migration 039 failed: " . $e->getMessage() . "\n");
}
}
// For manual execution if needed
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
migrate_039(db());
}

View File

@ -0,0 +1,481 @@
<?php
require_once __DIR__ . '/../config.php';
function migrate_040($pdo) {
$processes = [
[
'code' => 'training_new_member',
'name' => 'Szkolenie nowego członka',
'subject_scope' => 'person',
'version' => 1,
'is_latest' => 1,
'is_active' => 1,
'sort_order' => 40,
'start_node_id' => 'tracker',
'definition_json' => '{
"start_node_id": "tracker",
"nodes": {
"tracker": {
"id": "tracker",
"name": "Tracker szkoleń",
"ui_hints": {
"title": "Szkolenia dla młodego członka",
"status": "in_progress",
"reason": "Oczekiwanie na ukończenie wszystkich szkoleń",
"next_step": "Zaktualizuj status",
"form_schema": [
{
"name": "w1",
"label": "W1",
"type": "checkbox"
},
{
"name": "w2",
"label": "W2",
"type": "checkbox"
},
{
"name": "w3",
"label": "W3",
"type": "checkbox"
},
{
"name": "w4",
"label": "W4",
"type": "checkbox"
},
{
"name": "w5",
"label": "W5",
"type": "checkbox"
},
{
"name": "w6",
"label": "W6",
"type": "checkbox"
},
{
"name": "msp",
"label": "MSP",
"type": "checkbox"
},
{
"name": "ltt",
"label": "LTT",
"type": "checkbox"
}
]
}
},
"completed": {
"id": "completed",
"name": "Zakończony",
"ui_hints": {
"title": "Wszystkie szkolenia ukończone",
"status": "positive",
"reason": "Wszystkie szkolenia zostały ukończone",
"next_step": "Zaktualizuj status",
"form_schema": [
{
"name": "w1",
"label": "W1",
"type": "checkbox"
},
{
"name": "w2",
"label": "W2",
"type": "checkbox"
},
{
"name": "w3",
"label": "W3",
"type": "checkbox"
},
{
"name": "w4",
"label": "W4",
"type": "checkbox"
},
{
"name": "w5",
"label": "W5",
"type": "checkbox"
},
{
"name": "w6",
"label": "W6",
"type": "checkbox"
},
{
"name": "msp",
"label": "MSP",
"type": "checkbox"
},
{
"name": "ltt",
"label": "LTT",
"type": "checkbox"
}
]
}
}
},
"transitions": [
{
"id": "update_tracker",
"name": "Zapisz zmiany",
"from": "tracker",
"to": "tracker",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"w1",
"w2",
"w3",
"w4",
"w5",
"w6",
"msp",
"ltt"
]
}
}
]
},
{
"id": "auto_complete",
"name": "Automatyczne zakończenie",
"from": "tracker",
"to": "completed",
"condition": {
"type": "all_true",
"fields": [
"w1",
"w2",
"w3",
"w4",
"w5",
"w6",
"msp",
"ltt"
]
}
},
{
"id": "update_completed",
"name": "Zapisz zmiany",
"from": "completed",
"to": "tracker",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"w1",
"w2",
"w3",
"w4",
"w5",
"w6",
"msp",
"ltt"
]
}
}
]
}
]'
],
[
'code' => 'customer_journey',
'name' => 'Członkostwo',
'subject_scope' => 'person',
'version' => 1,
'is_latest' => 1,
'is_active' => 1,
'sort_order' => 50,
'start_node_id' => 'survey_3m',
'definition_json' => '{
"start_node_id": "survey_3m",
"initial_data": {
"cycle_year": 1
},
"nodes": {
"survey_3m": {
"name": "Ankieta 3 miesiąca",
"type": "user_task",
"ui_hints": {
"title": "Ankieta 3 miesiąca",
"status": "in_progress",
"reason": "Oczekuje na ankietę po 3 miesiącach",
"next_step": "Wypełnij ankietę",
"form_schema": [
{
"name": "survey_3m_done_at",
"type": "date",
"label": "Data wykonania",
"required": true,
"default": "now"
},
{
"name": "note",
"type": "text",
"label": "Notatka"
}
]
}
},
"survey_7m": {
"name": "Ankieta 7 miesiąca",
"type": "user_task",
"ui_hints": {
"title": "Ankieta 7 miesiąca",
"status": "in_progress",
"reason": "Oczekuje na ankietę po 7 miesiącach",
"next_step": "Wypełnij ankietę",
"form_schema": [
{
"name": "survey_7m_done_at",
"type": "date",
"label": "Data wykonania",
"required": true,
"default": "now"
},
{
"name": "note",
"type": "text",
"label": "Notatka"
}
]
}
},
"payment": {
"name": "Wpłata",
"type": "user_task",
"ui_hints": {
"title": "Wpłata",
"status": "in_progress",
"reason": "Oczekuje na wpłatę",
"next_step": "Zaksięguj wpłatę",
"form_schema": [
{
"name": "payment_done_at",
"type": "date",
"label": "Data wpłaty",
"required": true,
"default": "now"
},
{
"name": "note",
"type": "text",
"label": "Notatka"
}
]
}
},
"committee": {
"name": "Komitet",
"type": "user_task",
"ui_hints": {
"title": "Komitet",
"status": "in_progress",
"reason": "Decyzja komitetu",
"next_step": "Wprowadź decyzję",
"form_schema": [
{
"name": "committee_decision",
"type": "select",
"label": "Decyzja",
"options": [
{
"value": "approved",
"label": "Zatwierdzony"
},
{
"value": "rejected",
"label": "Odrzucony"
},
{
"value": "pending",
"label": "Oczekujący"
}
],
"required": true
},
{
"name": "note",
"type": "text",
"label": "Notatka"
}
]
}
},
"renewal": {
"name": "Przedłużenie członkostwa",
"type": "user_task",
"ui_hints": {
"title": "Przedłużenie członkostwa",
"status": "in_progress",
"reason": "Oczekuje na przedłużenie",
"next_step": "Przedłuż na kolejny rok",
"form_schema": [
{
"name": "renewal_done_at",
"type": "date",
"label": "Data przedłużenia",
"required": true,
"default": "now"
},
{
"name": "note",
"type": "text",
"label": "Notatka"
}
]
}
}
},
"transitions": [
{
"id": "finish_survey_3m",
"name": "Zakończ ankietę (3m)",
"from": "survey_3m",
"to": "survey_7m",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"survey_3m_done_at",
"note"
]
}
}
]
},
{
"id": "finish_survey_7m",
"name": "Zakończ ankietę (7m)",
"from": "survey_7m",
"to": "payment",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"survey_7m_done_at",
"note"
]
}
}
]
},
{
"id": "finish_payment",
"name": "Potwierdź wpłatę",
"from": "payment",
"to": "committee",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"payment_done_at",
"note"
]
}
}
]
},
{
"id": "finish_committee",
"name": "Wprowadź decyzję komitetu",
"from": "committee",
"to": "renewal",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"committee_decision",
"note"
]
}
}
]
},
{
"id": "finish_renewal",
"name": "Zamknij cykl i rozpocznij kolejny rok",
"from": "renewal",
"to": "survey_3m",
"actions": [
{
"type": "set_data",
"params": {
"keys": [
"renewal_done_at",
"note"
]
}
},
{
"type": "increment_data",
"params": {
"key": "cycle_year",
"by": 1
}
},
{
"type": "reset_data",
"params": {
"keys": [
"survey_3m_done_at",
"survey_7m_done_at",
"payment_done_at",
"committee_decision",
"renewal_done_at"
]
}
}
]
}
]'
]
];
try {
$checkStmt = $pdo->prepare("SELECT COUNT(*) FROM process_definitions WHERE code = :code");
$insertStmt = $pdo->prepare("INSERT INTO process_definitions (code, name, subject_scope, version, is_latest, is_active, sort_order, start_node_id, definition_json) VALUES (:code, :name, :subject_scope, :version, :is_latest, :is_active, :sort_order, :start_node_id, :definition_json)");
foreach ($processes as $p) {
$checkStmt->execute(['code' => $p['code']]);
$exists = $checkStmt->fetchColumn() > 0;
if (!$exists) {
$insertStmt->execute([
'code' => $p['code'],
'name' => $p['name'],
'subject_scope' => $p['subject_scope'],
'version' => $p['version'],
'is_latest' => $p['is_latest'],
'is_active' => $p['is_active'],
'sort_order' => $p['sort_order'],
'start_node_id' => $p['start_node_id'],
'definition_json' => $p['definition_json']
]);
}
}
echo "Migration 040 successful: Missing process definitions inserted.\n";
} catch (PDOException $e) {
die("Migration 040 failed: " . $e->getMessage() . "\n");
}
}
// For manual execution if needed
if (basename(__FILE__) == basename($_SERVER["SCRIPT_FILENAME"])) {
migrate_040(db());
}

134
db_setup.php Normal file
View File

@ -0,0 +1,134 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
echo "Starting database setup...\n";
// 1. People table (unified table for users and contacts)
$pdo->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());
}

63
do_patch.py Normal file
View File

@ -0,0 +1,63 @@
import re
with open('_get_instance_details.php', 'r') as f:
content = f.read()
old_block = """ <?php foreach ($currentNode['ui_hints']['form_schema'] as $field): ?>
<div class="mb-3">
<label for="<?= $field['name'] ?>" class="form-label"><?= $field['label'] ?></label>
<?php if ($field['type'] === 'textarea'): ?>
<textarea id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-control"></textarea>
<?php elseif ($field['type'] === 'select'): ?>
<select id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-select">
<?php foreach ($field['options'] as $option): ?>
<option value="<?= $option['value'] ?>"><?= $option['label'] ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="<?= $field['type'] ?>" id="<?= $field['name'] ?>" name="<?= $field['name'] ?>" class="form-control" value="<?= ($field['default'] ?? '') === 'now' ? date('Y-m-d\\TH:i') : '' ?>">
<?php endif; ?>
</div>
<?php endforeach; ?>"""
new_block = """ <?php foreach ($currentNode['ui_hints']['form_schema'] as $field):
$fieldName = $field['name'];
$currentValue = $instanceData[$fieldName] ?? null;
?>
<?php if ($field['type'] === 'checkbox'): ?>
<div class="form-check mb-3">
<input type="hidden" name="<?= $fieldName ?>" value="0">
<input type="checkbox" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-check-input" value="1" <?= (!empty($currentValue) || (!isset($currentValue) && !empty($field['default']))) ? 'checked' : '' ?>>
<label for="<?= $fieldName ?>" class="form-check-label"><?= $field['label'] ?></label>
</div>
<?php else: ?>
<div class="mb-3">
<label for="<?= $fieldName ?>" class="form-label"><?= $field['label'] ?></label>
<?php if ($field['type'] === 'textarea'): ?>
<textarea id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control"><?= htmlspecialchars((string)($currentValue ?? '')) ?></textarea>
<?php elseif ($field['type'] === 'select'): ?>
<select id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-select">
<?php foreach ($field['options'] as $option):
$selected = ($currentValue !== null && (string)$currentValue === (string)$option['value']) ? 'selected' : '';
?>
<option value="<?= $option['value'] ?>" <?= $selected ?>><?= $option['label'] ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<?php
$defaultVal = ($field['default'] ?? '') === 'now' ? date('Y-m-d\\TH:i') : ($field['default'] ?? '');
$valToUse = $currentValue ?? $defaultVal;
?>
<input type="<?= $field['type'] ?>" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control" value="<?= htmlspecialchars((string)$valToUse) ?>">
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>"""
if old_block in content:
content = content.replace(old_block, new_block)
with open('_get_instance_details.php', 'w') as f:
f.write(content)
print("Patched successfully")
else:
print("Old block not found!")

53
do_patch_base64.py Normal file
View File

@ -0,0 +1,53 @@
import re
with open('_get_instance_details.php', 'r') as f:
content = f.read()
pattern = re.compile(r"<\?php foreach \(\dcurrentNode\['ui_hints'\]\['form_schema'\] as \$field\): \?>(.*?)<\?php endforeach; \?>", re.DOTALL)
# use a function for replacement so it doesnt parse backslashes
replacement_text = r"""<?php foreach ($currentNode['ui_hints']['form_schema'] as $field):
$fieldName = $field['name'];
$currentValue = $instanceData[$fieldName] ?? null;
?>
<?php if ($field['type'] === 'checkbox'): ?>
<div class="form-check mb-3">
<input type="hidden" name="<?= $fieldName ?>" value="0">
<input type="checkbox" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-check-input" value="1" <?= (!empty($currentValue) || (!isset($currentValue) && !empty($field['default']))) ? 'checked' : '' ?>>
<label for="<?= $fieldName ?>" class="form-check-label"><?= $field['label'] ?></label>
</div>
<?php else: ?>
<div class="mb-3">
<label for="<?= $fieldName ?>" class="form-label"><?= $field['label'] ?></label>
<?php if ($field['type'] === 'textarea'): ?>
<textarea id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control"><?= htmlspecialchars((string)($currentValue ?? '')) ?></textarea>
<?php elseif ($field['type'] === 'select'): ?>
<select id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-select">
<?php foreach ($field['options'] as $option):
$selected = ($currentValue !== null && (string)$currentValue === (string)$option['value']) ? 'selected' : '';
?>
<option value="<?= $option['value'] ?>" <?= $selected ?>><?= $option['label'] ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<?ph`
$defaultVal = ($field['default'] ?? '') === 'now' ? date('Y-m-d\\TH:i') : ($field['default'] ?? '');
$valToUse = $currentValue ?? $defaultVal;
?>
<input type="<?= $field['type'] ?>" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control" value="<?= htmlspecialchars((string)$valToUse) ?>">
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>"""
def replacer(match):
return replacement_text
matches = pattern.search(content)
if matches:
content = pattern.sub(replacer, content, count=1)
with open('_get_instance_details.php', 'w') as f:
f.write(content)
print("Patched successfully!")
else:
print("Pattern not found!")

48
do_patch_final.py Normal file
View File

@ -0,0 +1,48 @@
import re
with open('_get_instance_details.php', 'r') as f:
content = f.read()
pattern = re.compile(r"<\?php foreach \(\dcurrentNode\['ui_hints'\]\['form_schema'\] as \$field\): \?>(.*?)<\?php endforeach; \?>", re.DOTALL)
replacement = r"""<?php foreach ($currentNode['ui_hints']['form_schema'] as $field):
$fieldName = $field['name'];
$currentValue = $instanceData[$fieldName] ?? null;
?>
<?php if ($field['type'] === 'checkbox'): ?>
<div class="form-check mb-3">
<input type="hidden" name="<?= $fieldName ?>" value="0">
<input type="checkbox" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-check-input" value="1" <?= (!empty($currentValue) || (!isset($currentValue) && !empty($field['default']))) ? 'checked' : '' ?>>
<label for="<?= $fieldName ?>" class="form-check-label"><?= $field['label'] ?></label>
</div>
<?php else: ?>
<div class="mb-3">
<label for="<?= $fieldName ?>" class="form-label"><?= $field['label'] ?></label>
<?php if ($field['type'] === 'textarea'): ?>
<textarea id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control"><?= htmlspecialchars((string)($currentValue ?? '')) ?></textarea>
<?php elseif ($field['type'] === 'select'): ?>
<select id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-select">
<?php foreach ($field['options'] as $option):
$selected = ($currentValue !== null && (string)$currentValue === (string)$option['value']) ? 'selected' : '';
?>
<option value="<?= $option['value'] ?>" <?= $selected ?>><?= $option['label'] ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<?php
$defaultVal = ($field['default'] ?? '') === 'now' ? date('Y-m-d\\TH:i') : ($field['default'] ?? '');
$valToUse = $currentValue ?? $defaultVal;
?>
<input type="<?= $field['type'] ?>" id="<?= $fieldName ?>" name="<?= $fieldName ?>" class="form-control" value="<?= htmlspecialchars((string)$valToUse) ?>">
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>"""
if pattern.search(content):
content = pattern.sub(lambda m: replacement, content, count=1)
with open('_get_instance_details.php', 'w') as f:
f.write(content)
print("Patched successfully!")
else:
print("Pattern not found!")

185
event_types.php Normal file
View File

@ -0,0 +1,185 @@
<?php
require_once 'db/config.php';
include '_header.php';
include '_navbar.php';
$pdo = db();
$stmt = $pdo->query("SELECT * FROM event_types ORDER BY display_order");
$event_types = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid">
<div class="row">
<?php include '_sidebar.php'; ?>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<h1 class="h2 pt-3 pb-2 mb-3 border-bottom"><?php echo t('event_types.title', 'Event Types'); ?></h1>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show mt-3" role="alert">
<?= $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<?php echo t('event_types.add_new', 'Add New Event Type'); ?>
</button>
</div>
<div class="table-responsive mt-4">
<table class="table table-striped table-sm">
<thead>
<tr>
<th style="width: 30px;"></th>
<th><?php echo t('event_types.name', 'Name'); ?></th>
<th><?php echo t('event_types.color', 'Color'); ?></th>
<th><?php echo t('event_types.actions', 'Actions'); ?></th>
</tr>
</thead>
<tbody id="sortable-list">
<?php foreach ($event_types as $type): ?>
<tr data-id="<?= $type['id'] ?>">
<td class="handle"><i class="bi bi-grip-vertical"></i></td>
<td><?= htmlspecialchars($type['name']) ?></td>
<td><span class="badge" style="background-color: <?= htmlspecialchars($type['color']) ?>"><?= htmlspecialchars($type['color']) ?></span></td>
<td>
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#editModal" data-id="<?= $type['id'] ?>" data-name="<?= htmlspecialchars($type['name']) ?>" data-color="<?= htmlspecialchars($type['color']) ?>" data-display-order="<?= htmlspecialchars($type['display_order']) ?>">
<?php echo t('event_types.edit', 'Edit'); ?>
</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal" data-id="<?= $type['id'] ?>">
<?php echo t('event_types.delete', 'Delete'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</main>
</div>
</div>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addModalLabel"><?php echo t('event_types.add_title', 'Add Event Type'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="_add_event_type.php" method="post">
<div class="modal-body">
<div class="mb-3">
<label for="addName" class="form-label"><?php echo t('event_types.name', 'Name'); ?></label>
<input type="text" class="form-control" id="addName" name="name" required>
</div>
<div class="mb-3">
<label for="addColor" class="form-label"><?php echo t('event_types.color', 'Color'); ?></label>
<input type="color" class="form-control" id="addColor" name="color" value="#007bff" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="submit" name="add" class="btn btn-primary"><?php echo t('event_types.add_button', 'Add'); ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel"><?php echo t('event_types.edit_title', 'Edit Event Type'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="_update_event_type.php" method="post">
<div class="modal-body">
<input type="hidden" id="editId" name="id">
<input type="hidden" id="editDisplayOrder" name="display_order">
<div class="mb-3">
<label for="editName" class="form-label"><?php echo t('event_types.name', 'Name'); ?></label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label for="editColor" class="form-label"><?php echo t('event_types.color', 'Color'); ?></label>
<input type="color" class="form-control" id="editColor" name="color" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="submit" name="edit" class="btn btn-primary"><?php echo t('modal.save_changes', 'Save changes'); ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel"><?php echo t('event_types.delete_title', 'Delete Event Type'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php echo t('event_types.delete_confirm', 'Are you sure you want to delete this event type?'); ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.cancel', 'Cancel'); ?></button>
<a href="#" id="deleteLink" class="btn btn-danger"><?php echo t('event_types.delete', 'Delete'); ?></a>
</div>
</div>
</div>
</div>
<?php include '_footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
var editModal = document.getElementById('editModal');
editModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var name = button.getAttribute('data-name');
var color = button.getAttribute('data-color');
var display_order = button.getAttribute('data-display-order');
var modalTitle = editModal.querySelector('.modal-title');
var idInput = editModal.querySelector('#editId');
var nameInput = editModal.querySelector('#editName');
var colorInput = editModal.querySelector('#editColor');
var displayOrderInput = editModal.querySelector('#editDisplayOrder');
idInput.value = id;
nameInput.value = name;
colorInput.value = color;
displayOrderInput.value = display_order;
});
var deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var deleteLink = deleteModal.querySelector('#deleteLink');
deleteLink.href = '_delete_event_type.php?id=' + id;
});
$(function() {
$("#sortable-list").sortable({
handle: ".handle",
update: function(event, ui) {
var order = $(this).sortable('toArray', {attribute: 'data-id'});
$.post('_update_event_type_order.php', { order: order });
}
}).disableSelection();
});
});
</script>

195
functions.php Normal file
View File

@ -0,0 +1,195 @@
<?php
require_once 'db/config.php';
include '_header.php';
include '_navbar.php';
$pdo = db();
$stmt = $pdo->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);
?>
<div class="container-fluid">
<div class="row">
<?php include '_sidebar.php'; ?>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<h1 class="h2 pt-3 pb-2 mb-3 border-bottom"><?php echo t('functions.title', 'Functions'); ?></h1>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success alert-dismissible fade show mt-3" function="alert">
<?= $_SESSION['success_message']; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<?php echo t('functions.add_new', 'Add New Function'); ?>
</button>
</div>
<div class="table-responsive mt-4">
<table class="table table-striped table-sm">
<thead>
<tr>
<th style="width: 30px;"></th>
<th><?php echo t('functions.name', 'Name'); ?></th>
<th><?php echo t('functions.group', 'Group'); ?></th>
<th><?php echo t('functions.actions', 'Actions'); ?></th>
</tr>
</thead>
<tbody id="sortable-list">
<?php foreach ($functions as $function): ?>
<tr data-id="<?= $function['id'] ?>">
<td class="handle"><i class="bi bi-grip-vertical"></i></td>
<td><?= htmlspecialchars($function['name']) ?></td>
<td><?= htmlspecialchars($function['group_name']) ?></td>
<td>
<button type="button" class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#editModal" data-id="<?= $function['id'] ?>" data-name="<?= htmlspecialchars($function['name']) ?>" data-bni_group_id="<?= $function['bni_group_id'] ?>">
<?php echo t('functions.edit', 'Edit'); ?>
</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal" data-id="<?= $function['id'] ?>">
<?php echo t('functions.delete', 'Delete'); ?>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</main>
</div>
</div>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1" aria-labelledby="addModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addModalLabel"><?php echo t('functions.add_title', 'Add Function'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="_add_function.php" method="post">
<div class="modal-body">
<div class="mb-3">
<label for="addName" class="form-label"><?php echo t('functions.name', 'Name'); ?></label>
<input type="text" class="form-control" id="addName" name="name" required>
</div>
<div class="mb-3">
<label for="addBniGroup" class="form-label"><?php echo t('functions.group', 'Group'); ?></label>
<select class="form-select" id="addBniGroup" name="bni_group_id" required>
<option value=""><?php echo t('functions.select_group', 'Select a group'); ?></option>
<?php foreach ($bni_groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="submit" name="add" class="btn btn-primary"><?php echo t('functions.add_button', 'Add'); ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel"><?php echo t('functions.edit_title', 'Edit Function'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="_update_function.php" method="post">
<div class="modal-body">
<input type="hidden" id="editId" name="id">
<div class="mb-3">
<label for="editName" class="form-label"><?php echo t('functions.name', 'Name'); ?></label>
<input type="text" class="form-control" id="editName" name="name" required>
</div>
<div class="mb-3">
<label for="editBniGroup" class="form-label"><?php echo t('functions.group', 'Group'); ?></label>
<select class="form-select" id="editBniGroup" name="bni_group_id" required>
<option value=""><?php echo t('functions.select_group', 'Select a group'); ?></option>
<?php foreach ($bni_groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.close', 'Close'); ?></button>
<button type="submit" name="edit" class="btn btn-primary"><?php echo t('modal.save_changes', 'Save changes'); ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel"><?php echo t('functions.delete_title', 'Delete Function'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<?php echo t('functions.delete_confirm', 'Are you sure you want to delete this function?'); ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo t('modal.cancel', 'Cancel'); ?></button>
<a href="#" id="deleteLink" class="btn btn-danger"><?php echo t('functions.delete', 'Delete'); ?></a>
</div>
</div>
</div>
</div>
<?php include '_footer.php'; ?>
<script>
document.addEventListener('DOMContentLoaded', function () {
var editModal = document.getElementById('editModal');
editModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var name = button.getAttribute('data-name');
var bni_group_id = button.getAttribute('data-bni_group_id');
var modalTitle = editModal.querySelector('.modal-title');
var idInput = editModal.querySelector('#editId');
var nameInput = editModal.querySelector('#editName');
var groupSelect = editModal.querySelector('#editBniGroup');
idInput.value = id;
nameInput.value = name;
groupSelect.value = bni_group_id;
});
var deleteModal = document.getElementById('deleteModal');
deleteModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var deleteLink = deleteModal.querySelector('#deleteLink');
deleteLink.href = '_delete_function.php?id=' + id;
});
$(function() {
$("#sortable-list").sortable({
handle: ".handle",
update: function(event, ui) {
var order = $(this).sortable('toArray', {attribute: 'data-id'});
$.post('_update_function_order.php', { order: order });
}
}).disableSelection();
});
});
</script>

7
get_definition.php Normal file
View File

@ -0,0 +1,7 @@
<?php
require 'db/config.php';
$db = db();
$stmt = $db->prepare("SELECT definition_json FROM process_definitions WHERE id = 4");
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
echo $result['definition_json'];

Some files were not shown because too many files have changed in this diff Show More