Proces spotkania wersja 1

This commit is contained in:
Flatlogic Bot 2026-01-11 15:08:42 +00:00
parent c52190436a
commit 6714d8259a
12 changed files with 461 additions and 15 deletions

View File

@ -762,4 +762,70 @@ class WorkflowEngine {
throw $e; throw $e;
} }
} }
public function getOrCreateMeeting(int $groupId, string $meetingDatetime): int {
$meetingKey = $groupId . '_' . $meetingDatetime;
$stmt = $this->pdo->prepare("SELECT id FROM meetings WHERE meeting_key = ?");
$stmt->execute([$meetingKey]);
$meetingId = $stmt->fetchColumn();
if (!$meetingId) {
$stmt = $this->pdo->prepare("INSERT INTO meetings (bni_group_id, meeting_datetime, meeting_key) VALUES (?, ?, ?)");
$stmt->execute([$groupId, $meetingDatetime, $meetingKey]);
$meetingId = $this->pdo->lastInsertId();
}
return (int)$meetingId;
}
public function getMeetingAttendance(int $meetingId): array {
$stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ?");
$stmt->execute([$meetingId]);
$attendance_raw = $stmt->fetchAll(PDO::FETCH_ASSOC);
$attendance = [];
foreach ($attendance_raw as $att) {
$attendance[$att['person_id']] = $att;
}
return $attendance;
}
public function updateMeetingAttendance(int $meetingId, int $personId, string $status, int $userId, ?string $guestSurvey = null): void {
$sql = "INSERT INTO meeting_attendance (meeting_id, person_id, attendance_status, guest_survey, updated_by) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE attendance_status = VALUES(attendance_status), guest_survey = VALUES(guest_survey), updated_by = VALUES(updated_by)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$meetingId, $personId, $status, $guestSurvey, $userId]);
}
public function getMeetingAttendanceByGroupAndDate(int $groupId, string $meetingDatetime): array {
$meetingId = $this->getOrCreateMeeting($groupId, $meetingDatetime);
return $this->getMeetingAttendance($meetingId);
}
public function isMemberOfGroup(int $personId, int $bniGroupId): bool {
$stmt = $this->pdo->prepare("SELECT COUNT(*) FROM people WHERE id = ? AND bni_group_id = ?");
$stmt->execute([$personId, $bniGroupId]);
return (int)$stmt->fetchColumn() > 0;
}
public function getMeetingDetails(int $personId, int $bniGroupId, string $meetingDatetime): array {
$meetingId = $this->getOrCreateMeeting($bniGroupId, $meetingDatetime);
$stmt = $this->pdo->prepare("SELECT * FROM meeting_attendance WHERE meeting_id = ? AND person_id = ?");
$stmt->execute([$meetingId, $personId]);
$attendance = $stmt->fetch(PDO::FETCH_ASSOC);
if ($attendance) {
return $attendance;
}
// If no record, return default state
$isMember = $this->isMemberOfGroup($personId, $bniGroupId);
return [
'meeting_id' => $meetingId,
'person_id' => $personId,
'attendance_status' => $isMember ? 'present' : 'none',
'guest_survey' => null,
];
}
} }

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);

View File

@ -11,7 +11,7 @@ if (!isset($_SESSION['user_id'])) {
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo getenv('PROJECT_NAME') ?: 'My App'; ?> - Dashboard</title> <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.'; ?>"> <meta name="description" content="<?php echo getenv('PROJECT_DESCRIPTION') ?: 'A modern web application.'; ?>">
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
@ -28,7 +28,7 @@ if (!isset($_SESSION['user_id'])) {
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>"> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<!-- OG Meta Tags --> <!-- OG Meta Tags -->
<meta property="og:title" content="<?php echo getenv('PROJECT_NAME') ?: 'My App'; ?>"> <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: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: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:url" content="<?php echo (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; ?>">
@ -36,9 +36,10 @@ if (!isset($_SESSION['user_id'])) {
<!-- Twitter Card --> <!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<?php echo getenv('PROJECT_NAME') ?: 'My App'; ?>"> <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: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'; ?>"> <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> </head>
<body> <body>

View File

@ -1,5 +1,5 @@
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#"><?php echo getenv('PROJECT_NAME') ?: 'My App'; ?></a> <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"> <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> <span class="navbar-toggler-icon"></span>
</button> </button>

View File

@ -0,0 +1,47 @@
<?php
require_once 'WorkflowEngine.php';
session_start();
header('Content-Type: application/json');
$response = ['success' => false, 'message' => 'An error occurred.'];
if (!isset($_SESSION['user_id'])) {
$response['message'] = 'You must be logged in to perform this action.';
echo json_encode($response);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$response['message'] = 'Invalid request method.';
echo json_encode($response);
exit;
}
$personId = $_POST['person_id'] ?? null;
$groupId = $_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 || !$groupId || !$meetingDate || !$status) {
$response['message'] = 'Missing required parameters.';
echo json_encode($response);
exit;
}
try {
$workflowEngine = new WorkflowEngine();
$meetingId = $workflowEngine->getOrCreateMeeting((int)$groupId, $meetingDate);
$workflowEngine->updateMeetingAttendance($meetingId, (int)$personId, $status, (int)$userId, $guestSurvey);
$response['success'] = true;
$response['message'] = 'Attendance updated successfully.';
} catch (Exception $e) {
error_log($e->getMessage());
$response['message'] = 'Error updating attendance: ' . $e->getMessage();
}
echo json_encode($response);

View File

@ -140,3 +140,9 @@ body {
justify-content: flex-end; justify-content: flex-end;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.navbar-logo {
height: 30px;
width: auto;
margin-right: 50px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

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) UNSIGNED NOT NULL,
meeting_datetime DATETIME NOT NULL,
meeting_key VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY meeting_key (meeting_key),
FOREIGN KEY (bni_group_id) REFERENCES bni_groups(id) ON DELETE CASCADE
)";
$pdoconn->exec($sql);
echo "Table 'meetings' created successfully." . PHP_EOL;
} catch (PDOException $e) {
echo "Error creating table: " . $e->getMessage() . PHP_EOL;
exit(1);
}

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

234
index.php
View File

@ -182,18 +182,26 @@ $status_colors = [
<?php // Spotkania Columns ?> <?php // Spotkania Columns ?>
<?php foreach ($spotkania_cols as $col): ?> <?php foreach ($spotkania_cols as $col): ?>
<td class="text-center align-middle"> <td class="text-center align-middle meeting-cell" data-group-id="<?= $col['group_id'] ?>">
<?php <?php
// Placeholder Status: Logic for meeting attendance is not yet defined. // Use the new isMemberOfGroup function to determine the default status
// Display icon only if the person belongs to the group for that column. $isMember = $workflowEngine->isMemberOfGroup($person['id'], $col['group_id']);
if ($person['bni_group_id'] == $col['group_id']) { $status = $isMember ? 'present' : 'none';
$status = 'none'; // Default/placeholder status
$color = $status_colors[$status]; // The meeting date will be determined by JS, we will add it to the data attribute
echo "<span class=\"badge rounded-circle bg-$color\" style=\"width: 20px; height: 20px; display: inline-block;\" title=\"Status nieokreślony\"></span>"; // The initial meeting date is the first one in the list.
} else { $meeting_datetime = $col['meetings'][0] ?? '';
echo ''; // Empty cell if person is not in this group
} $color = $status_colors[$status] ?? 'secondary';
?> ?>
<span class="badge rounded-circle bg-<?= $color ?> meeting-dot"
style="width: 20px; height: 20px; display: inline-block; cursor: pointer;"
data-person-id="<?= $person['id'] ?>"
data-person-name="<?= htmlspecialchars($person['first_name'] . ' ' . $person['last_name']) ?>"
data-bni-group-id="<?= $col['group_id'] ?>"
data-meeting-datetime="<?= $meeting_datetime ?>"
data-initial-status="<?= $status ?>"
title="Status: <?= ucfirst($status) ?>"></span>
</td> </td>
<?php endforeach; ?> <?php endforeach; ?>
@ -488,6 +496,212 @@ $status_colors = [
</div> </div>
<!-- Meeting Attendance Modal -->
<div class="modal fade" id="meetingAttendanceModal" tabindex="-1" aria-labelledby="meetingAttendanceModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form id="meetingAttendanceForm">
<div class="modal-header">
<h5 class="modal-title" id="meetingAttendanceModalLabel">Update Attendance</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="person_id" id="meetingPersonId">
<input type="hidden" name="group_id" id="meetingGroupId">
<input type="hidden" name="meeting_date" id="meetingDate">
<p><strong>Person:</strong> <span id="meetingPersonName"></span></p>
<div class="mb-3">
<label for="attendanceStatus" class="form-label">Status</label>
<select class="form-select" id="attendanceStatus" name="attendance_status">
<option value="present">Present</option>
<option value="absent">Absent</option>
<option value="substitute">Substitute</option>
<option value="none">None</option>
</select>
</div>
<div class="mb-3" id="guestSurveySection" style="display: none;">
<label class="form-label">Guest Survey</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="guest_survey" id="guestSurvey1" value="1">
<label class="form-check-label" for="guestSurvey1">1 (Positive)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="guest_survey" id="guestSurvey2" value="2">
<label class="form-check-label" for="guestSurvey2">2 (Neutral)</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="guest_survey" id="guestSurvey3" value="3">
<label class="form-check-label" for="guestSurvey3">3 (Negative)</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const meetingModal = new bootstrap.Modal(document.getElementById('meetingAttendanceModal'));
const meetingForm = document.getElementById('meetingAttendanceForm');
const meetingPersonName = document.getElementById('meetingPersonName');
const meetingPersonId = document.getElementById('meetingPersonId');
const meetingGroupId = document.getElementById('meetingGroupId');
const meetingDateInput = document.getElementById('meetingDate');
const attendanceStatus = document.getElementById('attendanceStatus');
const guestSurveySection = document.getElementById('guestSurveySection');
const statusColors = {
'none': 'secondary',
'absent': 'danger',
'substitute': 'warning',
'present': 'success',
};
function updateDot(dot, status) {
const color = statusColors[status] || 'secondary';
dot.className = `badge rounded-circle bg-${color} meeting-dot`;
dot.dataset.initialStatus = status;
dot.title = `Status: ${status.charAt(0).toUpperCase() + status.slice(1)}`;
}
function fetchAndUpdateAllDotsForGroup(groupId, meetingDate) {
fetch(`_get_meeting_attendance.php?group_id=${groupId}&meeting_date=${meetingDate}`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the data-meeting-datetime for all dots in the column
document.querySelectorAll(`.meeting-dot[data-bni-group-id='${groupId}']`).forEach(dot => {
dot.dataset.meetingDatetime = meetingDate;
const personId = dot.dataset.personId;
let status = dot.dataset.initialStatus; // Keep default if no record
if (data.attendance[personId]) {
status = data.attendance[personId].attendance_status;
}
updateDot(dot, status);
});
}
});
}
// Event delegation for meeting dot clicks
document.querySelector('tbody').addEventListener('click', function(event) {
if (!event.target.classList.contains('meeting-dot')) {
return;
}
const dot = event.target;
const personId = dot.dataset.personId;
const personName = dot.dataset.personName;
const bniGroupId = dot.dataset.bniGroupId;
const meetingDatetime = dot.dataset.meetingDatetime;
const initialStatus = dot.dataset.initialStatus;
// The person's own group is on the TR element
const personGroupId = dot.closest('tr').dataset.groupId;
const isMember = personGroupId == bniGroupId;
// Set form values
meetingPersonId.value = personId;
meetingPersonName.textContent = personName;
meetingGroupId.value = bniGroupId;
meetingDateInput.value = meetingDatetime;
attendanceStatus.value = initialStatus;
// Show/hide guest survey
guestSurveySection.style.display = !isMember ? 'block' : 'none';
// Clear previous survey selection
document.querySelectorAll('input[name="guest_survey"]').forEach(radio => radio.checked = false);
// Fetch full details to populate the modal correctly, including saved status and survey
fetch(`_get_meeting_details.php?person_id=${personId}&bni_group_id=${bniGroupId}&meeting_datetime=${meetingDatetime}`)
.then(response => response.json())
.then(data => {
if(data.success) {
attendanceStatus.value = data.details.attendance_status;
if(data.details.guest_survey) {
const surveyRadio = document.getElementById('guestSurvey' + data.details.guest_survey);
if(surveyRadio) surveyRadio.checked = true;
}
}
meetingModal.show();
})
.catch(err => {
console.error('Error fetching meeting details:', err);
// Still show modal with defaults if fetch fails
meetingModal.show();
});
});
meetingForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const personId = formData.get('person_id');
const bniGroupId = formData.get('group_id');
fetch('_update_meeting_attendance.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
meetingModal.hide();
// Find the correct dot to update. Note the attribute is data-bni-group-id
const dot = document.querySelector(`.meeting-dot[data-person-id='${personId}'][data-bni-group-id='${bniGroupId}']`);
if (dot) {
updateDot(dot, formData.get('attendance_status'));
}
} else {
alert(data.message || 'An error occurred.');
}
});
});
// Initial load and navigation for meeting attendance
document.querySelectorAll('th[data-group-id][data-meetings]').forEach(headerCell => {
const groupId = headerCell.dataset.groupId;
let meetings = JSON.parse(headerCell.dataset.meetings);
let currentIndex = 0;
function updateMeetingView() {
if (meetings.length > 0) {
const currentMeetingDate = meetings[currentIndex];
headerCell.querySelector('.meeting-date').textContent = new Date(currentMeetingDate).toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' });
fetchAndUpdateAllDotsForGroup(groupId, currentMeetingDate);
} else {
headerCell.querySelector('.meeting-date').textContent = 'Brak';
}
headerCell.querySelector('.meeting-prev-btn').disabled = currentIndex === 0;
headerCell.querySelector('.meeting-next-btn').disabled = currentIndex >= meetings.length - 1;
}
if (meetings.length > 0) {
updateMeetingView();
}
headerCell.querySelector('.meeting-prev-btn').addEventListener('click', function() {
if (currentIndex > 0) {
currentIndex--;
updateMeetingView();
}
});
headerCell.querySelector('.meeting-next-btn').addEventListener('click', function() {
if (currentIndex < meetings.length - 1) {
currentIndex++;
updateMeetingView();
}
});
});
});
</script>
<?php include '_footer.php'; ?> <?php include '_footer.php'; ?>
<!-- Instance Modal --> <!-- Instance Modal -->