Compare commits
5 Commits
c0beb640e6
...
3da90baf4a
| Author | SHA1 | Date | |
|---|---|---|---|
| 3da90baf4a | |||
| 2496b34bdd | |||
| d4355e3ee2 | |||
| e7549c6f00 | |||
| a282dd2080 |
154
api-comments.php
Normal file
154
api-comments.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
// api-comments.php
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Dołączamy połączenie do bazy (upewnij się, że plik db_connect.php istnieje i ma poprawne dane)
|
||||
require_once 'db_connect.php';
|
||||
|
||||
session_start();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// 0. AUTORYZACJA (LOGOWANIE)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'auth') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$pass = $input['password'] ?? '';
|
||||
|
||||
// $PROTOTYPE_PASSWORD jest zdefiniowane w db_connect.php
|
||||
if (isset($PROTOTYPE_PASSWORD) && $pass === $PROTOTYPE_PASSWORD) {
|
||||
$_SESSION['prototype_auth'] = true;
|
||||
echo json_encode(['status' => 'success']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Nieprawidłowe hasło']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 0. SPRAWDZENIE SESJI (Opcjonalne dla list, wymagane dla add)
|
||||
function isAuthorized()
|
||||
{
|
||||
return !empty($_SESSION['prototype_auth']);
|
||||
}
|
||||
|
||||
// 0.5 SPRAWDZENIE STANU AUTORYZACJI (GET) - dla frontendowego odtworzenia sesji
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'check_auth') {
|
||||
if (isAuthorized()) {
|
||||
echo json_encode(['status' => 'success']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak sesji']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 1. POBIERANIE KOMENTARZY (GET)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $action === 'list') {
|
||||
$pagePath = $_GET['page_path'] ?? '';
|
||||
|
||||
if (!$pagePath) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak ścieżki pliku']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT * FROM prototype_comments WHERE page_path = ? ORDER BY created_at DESC");
|
||||
$stmt->execute([$pagePath]);
|
||||
$comments = $stmt->fetchAll();
|
||||
|
||||
// XSS PROTECTION: Escaping danych przed wysłaniem
|
||||
foreach ($comments as &$c) {
|
||||
$c['author'] = htmlspecialchars($c['author'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$c['comment'] = htmlspecialchars($c['comment'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
// Selector musi zostać oryginalny, bo JS go używa do querySelector!
|
||||
// $c['dom_selector'] = htmlspecialchars($c['dom_selector'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
echo json_encode(['status' => 'success', 'data' => $comments]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. DODAWANIE KOMENTARZA (POST)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'add') {
|
||||
// Wymagana autoryzacja
|
||||
if (!isAuthorized()) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji. Odśwież stronę i podaj hasło.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Odczyt danych JSON z body requestu
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$pagePath = $input['page_path'] ?? '';
|
||||
$selector = $input['selector'] ?? '';
|
||||
$comment = $input['comment'] ?? '';
|
||||
$author = $input['author'] ?? 'Anonim'; // Możesz tu potem wpiąć sesję użytkownika
|
||||
|
||||
if (!$pagePath || !$selector || !$comment) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brakuje danych']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO prototype_comments (page_path, dom_selector, author, comment) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$pagePath, $selector, $author, $comment]);
|
||||
|
||||
echo json_encode(['status' => 'success', 'id' => $pdo->lastInsertId()]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 3. ROZWIĄZYWANIE KOMENTARZA (POST)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'resolve') {
|
||||
if (!isAuthorized()) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? 0;
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Zmieniamy flagę is_resolved na 1
|
||||
$stmt = $pdo->prepare("UPDATE prototype_comments SET is_resolved = 1 WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. USUWANIE KOMENTARZA (POST)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action === 'delete') {
|
||||
if (!isAuthorized()) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak autoryzacji']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $input['id'] ?? 0;
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Brak ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM prototype_comments WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
298
assets/css/comments.css
Normal file
298
assets/css/comments.css
Normal file
@@ -0,0 +1,298 @@
|
||||
/* assets/css/comments.css */
|
||||
|
||||
/* Pasek narzędzi na górze */
|
||||
#prototype-topbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #232f3e;
|
||||
color: white;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
#prototype-topbar .mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Przełącznik (Toggle) */
|
||||
.switch-label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.switch-label input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Pinezki (Markery) */
|
||||
.comment-marker {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #ff3e1d;
|
||||
border: 2px solid white;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg) translate(-50%, -100%);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.comment-marker:hover {
|
||||
z-index: 10001;
|
||||
transform: rotate(-45deg) translate(-50%, -100%) scale(1.2);
|
||||
}
|
||||
|
||||
.comment-marker.resolved {
|
||||
background: #71dd37;
|
||||
/* Zielony dla rozwiązanych */
|
||||
}
|
||||
|
||||
/* Dymek z komentarzem (Tooltip) */
|
||||
.comment-popover {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #d9dee3;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
width: 250px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100002;
|
||||
/* Musi być wyżej niż marker (100001) i topbar (100000) */
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comment-popover h6 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-popover p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Tryb wyboru - podświetlanie elementów */
|
||||
.comment-mode-active *:hover {
|
||||
outline: 2px dashed #2196F3 !important;
|
||||
cursor: comment !important;
|
||||
}
|
||||
|
||||
/* Wykluczenia, żeby nie podświetlać samego UI komentarzy */
|
||||
.comment-mode-active #prototype-topbar *:hover,
|
||||
.comment-mode-active .comment-marker:hover,
|
||||
.comment-mode-active .comment-popover *:hover {
|
||||
outline: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/* Modal do dodawania (prosty) */
|
||||
#comment-input-box {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
z-index: 100002;
|
||||
width: 300px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#comment-input-box textarea {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#comment-input-box-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100001;
|
||||
display: none;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* --- POPRAWKA: Wykluczenie okienka edycji z efektów wizualnych --- */
|
||||
|
||||
/* Nawet w trybie comment-mode-active, nasze okno dialogowe ma wyglądać normalnie */
|
||||
.comment-mode-active #comment-input-box,
|
||||
.comment-mode-active #comment-input-box * {
|
||||
outline: none !important;
|
||||
cursor: auto !important;
|
||||
/* Przywraca normalny kursor (strzałkę/łapkę) */
|
||||
}
|
||||
|
||||
/* --- POPRAWKA: Wykluczenie okienka edycji z efektów wizualnych --- */
|
||||
|
||||
/* assets/css/comments.css */
|
||||
|
||||
/* ... poprzednie style bez zmian ... */
|
||||
|
||||
/* POPRAWKI DLA MODALA I FOCUSU */
|
||||
#comment-input-box {
|
||||
/* Ustawiamy fixed/absolute dynamicznie w JS, ale bazowo: */
|
||||
z-index: 100000;
|
||||
/* Musi być wyżej niż Bootstrap Modal (zwykle 1055) */
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
width: 320px;
|
||||
display: none;
|
||||
/* Centrowanie - teraz zrobimy to sprytniej w CSS */
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#comment-input-box-overlay {
|
||||
position: fixed;
|
||||
/* JS zmieni na absolute jeśli w modalu */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 99999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ważne: Pineski muszą być nad wszystkim */
|
||||
.comment-marker {
|
||||
z-index: 100001;
|
||||
pointer-events: auto !important;
|
||||
/* Żeby dało się w nie klikać nawet w trybie blokady */
|
||||
}
|
||||
|
||||
/* Ukrywanie pinesek */
|
||||
body.comments-hidden .comment-marker {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Dialog listy wszystkich komentarzy */
|
||||
#all-comments-dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
|
||||
z-index: 100005;
|
||||
width: 500px;
|
||||
max-width: 90vw;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#all-comments-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100004;
|
||||
display: none;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.comment-list-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.comment-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.comment-list-item:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* --- FIX DLA MODALI PROTOTYPU --- */
|
||||
/* Wymuszamy, aby standardowe modale (Bootstrap) nie chowały się pod paskiem */
|
||||
.modal {
|
||||
padding-top: 50px !important;
|
||||
/* Odsuwamy od góry o wysokość paska */
|
||||
}
|
||||
|
||||
/* Opcjonalnie: Jeśli modal ma backdrop, też go przesuwamy,
|
||||
albo po prostu upewniamy się że modal-dialog ma margines */
|
||||
.modal-dialog {
|
||||
margin-top: 20px !important;
|
||||
/* Dodatkowy margines żeby nie dotykało paska */
|
||||
}
|
||||
|
||||
/* Jeśli używasz offcanvas lub innych overlayów */
|
||||
.offcanvas {
|
||||
top: 50px !important;
|
||||
height: calc(100% - 50px) !important;
|
||||
}
|
||||
|
||||
/* Poprawka dla backdropu, żeby nie zakrywał paska (jeśli pasek ma być na wierzchu) */
|
||||
.modal-backdrop {
|
||||
top: 50px !important;
|
||||
height: calc(100% - 50px) !important;
|
||||
}
|
||||
827
assets/js/comments.js
Normal file
827
assets/js/comments.js
Normal file
@@ -0,0 +1,827 @@
|
||||
// assets/js/comments.js
|
||||
|
||||
(function () {
|
||||
// Konfiguracja API
|
||||
const baseUrl = (typeof MAGICO_BASE_URL !== 'undefined') ? MAGICO_BASE_URL : '';
|
||||
const API_URL = baseUrl + '/api-comments.php';
|
||||
const CURRENT_PATH = window.location.pathname.replace(/^\/|\/$/g, '');
|
||||
|
||||
let isCommentMode = false;
|
||||
let pendingElement = null; // Element, który chcemy skomentować
|
||||
let markersData = []; // Przechowujemy dane o markerach lokalnie do szybkiego odświeżania
|
||||
|
||||
// --- 1. GENEROWANIE UI ---
|
||||
function initUI() {
|
||||
document.body.style.paddingTop = '50px';
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'prototype-topbar';
|
||||
bar.innerHTML = `
|
||||
<div style="display:flex; align-items:center;">
|
||||
<div style="font-weight: bold; font-size: 14px; margin-right: 20px;">
|
||||
<span style="color: #696cff;">Magico</span> Feedback
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap: 20px;">
|
||||
<!-- Licznik -->
|
||||
<div id="comments-count-badge" style="cursor:pointer; font-size:13px; display:flex; align-items:center; gap:5px; padding: 5px 10px; background: rgba(255,255,255,0.1); border-radius: 4px;">
|
||||
<span style="opacity:0.8;">Komentarzy:</span>
|
||||
<span id="comments-count-val" style="background:#696cff; padding:2px 8px; border-radius:10px; font-weight:bold; font-size:11px;">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Pokaż/Ukryj -->
|
||||
<div style="display:flex; align-items:center; font-size:13px; gap: 5px;">
|
||||
<input type="checkbox" id="comments-visibility-toggle" checked style="cursor:pointer;">
|
||||
<label for="comments-visibility-toggle" style="cursor:pointer; margin-bottom:0;">Pokaż pineski</label>
|
||||
</div>
|
||||
|
||||
<div style="width: 1px; height: 20px; background: rgba(255,255,255,0.2);"></div>
|
||||
|
||||
<!-- Tryb -->
|
||||
<div class="mode-switch">
|
||||
<span style="font-size: 13px; margin-right: 8px;">Tryb Komentowania</span>
|
||||
<label class="switch-label">
|
||||
<input type="checkbox" id="comment-toggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(bar);
|
||||
|
||||
// Kontener na input (domyślnie w body)
|
||||
const modalHtml = `
|
||||
<div id="comment-input-box-overlay"></div>
|
||||
<div id="comment-input-box">
|
||||
<h6 style="margin-top:0;">Dodaj notatkę</h6>
|
||||
<input type="text" id="new-comment-author" class="form-control mb-2" placeholder="Twój podpis (np. Jan)" style="font-size: 0.9rem;">
|
||||
<textarea id="new-comment-text" class="form-control mb-2" rows="3" placeholder="Wpisz uwagę..."></textarea>
|
||||
<div style="text-align: right; gap: 5px; display: flex; justify-content: flex-end;">
|
||||
<button id="cancel-comment" class="btn btn-sm btn-outline-secondary">Anuluj</button>
|
||||
<button id="save-comment" class="btn btn-sm btn-primary">Zapisz</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Lista Wszystkich -->
|
||||
<div id="all-comments-dialog-overlay"></div>
|
||||
<div id="all-comments-dialog">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom:1px solid #eee; padding-bottom:10px;">
|
||||
<h5 style="margin:0;">Wszystkie komentarze</h5>
|
||||
<button id="close-all-comments" class="btn btn-sm btn-outline-secondary">X</button>
|
||||
</div>
|
||||
<div id="all-comments-list" style="max-height:60vh; overflow-y:auto; padding-right:5px;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
|
||||
// Listenery UI
|
||||
const toggle = document.getElementById('comment-toggle');
|
||||
|
||||
toggle.addEventListener('change', function (e) {
|
||||
if (e.target.checked) {
|
||||
// Próba włączenia - autoryzacja
|
||||
checkAuth().then(ok => {
|
||||
if (ok) {
|
||||
isCommentMode = true;
|
||||
localStorage.setItem('magico_comment_mode', 'true');
|
||||
toggleCommentMode(true);
|
||||
} else {
|
||||
e.target.checked = false; // Cofnij switch
|
||||
}
|
||||
});
|
||||
} else {
|
||||
isCommentMode = false;
|
||||
localStorage.removeItem('magico_comment_mode');
|
||||
toggleCommentMode(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle visibility
|
||||
const visToggle = document.getElementById('comments-visibility-toggle');
|
||||
const savedVis = localStorage.getItem('magico_markers_visible');
|
||||
if (savedVis === 'false') {
|
||||
visToggle.checked = false;
|
||||
document.body.classList.add('comments-hidden');
|
||||
}
|
||||
|
||||
visToggle.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
document.body.classList.remove('comments-hidden');
|
||||
localStorage.setItem('magico_markers_visible', 'true');
|
||||
} else {
|
||||
document.body.classList.add('comments-hidden');
|
||||
localStorage.setItem('magico_markers_visible', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// Open list dialog
|
||||
document.getElementById('comments-count-badge').addEventListener('click', showAllCommentsDialog);
|
||||
document.getElementById('close-all-comments').addEventListener('click', () => {
|
||||
document.getElementById('all-comments-dialog').style.display = 'none';
|
||||
document.getElementById('all-comments-dialog-overlay').style.display = 'none';
|
||||
});
|
||||
|
||||
// --- RESTORE SESSION ---
|
||||
const savedMode = localStorage.getItem('magico_comment_mode');
|
||||
if (savedMode === 'true') {
|
||||
// Sprawdź cicho czy sesja PHP jest aktywna
|
||||
checkAuth(true).then(ok => {
|
||||
if (ok) {
|
||||
toggle.checked = true;
|
||||
isCommentMode = true;
|
||||
toggleCommentMode(true);
|
||||
} else {
|
||||
localStorage.removeItem('magico_comment_mode'); // Sesja wygasła
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('cancel-comment').addEventListener('click', closeInputBox);
|
||||
document.getElementById('save-comment').addEventListener('click', saveComment);
|
||||
|
||||
// Scroll listener (capture) - żeby aktualizować pineski przy każdym przewinięciu (okna lub modala)
|
||||
window.addEventListener('scroll', updateMarkerPositions, true);
|
||||
}
|
||||
|
||||
// --- 1.5 AUTORYZACJA ---
|
||||
// Prosta weryfikacja - czy mamy flagę w sesji JS?
|
||||
// Lepiej: przy włączeniu zapytać o hasło jeśli nie mamy, i wysłać do API.
|
||||
// API ustawi sesję PHP.
|
||||
|
||||
let isAuthorized = false; // Lokalna flaga, aby nie pytać co chwilę
|
||||
|
||||
async function checkAuth(silent = false) {
|
||||
if (isAuthorized) return true;
|
||||
|
||||
// Najpierw zapytaj API czy już jesteśmy zalogowani (sesja PHP)
|
||||
try {
|
||||
const check = await fetch(API_URL + '?action=check_auth');
|
||||
const checkData = await check.json();
|
||||
if (checkData.status === 'success') {
|
||||
isAuthorized = true;
|
||||
return true;
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if (silent) return false;
|
||||
|
||||
const pass = prompt("Podaj hasło do trybu komentowania:");
|
||||
if (!pass) return false;
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL + '?action=auth', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password: pass })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
isAuthorized = true;
|
||||
return true;
|
||||
} else {
|
||||
alert("Błąd: " + (data.message || "Nieprawidłowe hasło"));
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Błąd połączenia z API");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. LOGIKA TRYBU (AGRESYWNE BLOKOWANIE) ---
|
||||
function toggleCommentMode(active) {
|
||||
if (active) {
|
||||
document.body.classList.add('comment-mode-active');
|
||||
// Blokujemy wszystko: click, mousedown, mouseup, submit
|
||||
['click', 'mousedown', 'mouseup', 'submit'].forEach(evt =>
|
||||
window.addEventListener(evt, handleInteraction, true)
|
||||
);
|
||||
} else {
|
||||
document.body.classList.remove('comment-mode-active');
|
||||
['click', 'mousedown', 'mouseup', 'submit'].forEach(evt =>
|
||||
window.removeEventListener(evt, handleInteraction, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. OBSŁUGA INTERAKCJI ---
|
||||
function handleInteraction(e) {
|
||||
// ZAWSZE pozwalamy na interakcję z naszym UI
|
||||
if (e.target.closest('#prototype-topbar') ||
|
||||
e.target.closest('#comment-input-box') ||
|
||||
e.target.closest('.comment-marker') ||
|
||||
e.target.closest('.comment-popover')) { // Dodano popover do wykluczeń
|
||||
return;
|
||||
}
|
||||
|
||||
// Blokujemy natywną akcję
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
// Reagujemy tylko na 'click' (żeby nie odpalać input boxa 3 razy na mousedown/up/click)
|
||||
if (e.type === 'click') {
|
||||
pendingElement = e.target;
|
||||
openInputBox();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. SELEKTOR CSS ---
|
||||
function getCssSelector(el) {
|
||||
if (!(el instanceof Element)) return;
|
||||
const path = [];
|
||||
while (el.nodeType === Node.ELEMENT_NODE && el.tagName !== 'BODY') {
|
||||
let selector = el.tagName.toLowerCase();
|
||||
if (el.id) {
|
||||
selector += '#' + el.id;
|
||||
path.unshift(selector);
|
||||
break;
|
||||
} else {
|
||||
let sib = el, nth = 1;
|
||||
while (sib = sib.previousElementSibling) {
|
||||
if (sib.tagName.toLowerCase() == selector) nth++;
|
||||
}
|
||||
if (nth > 1) selector += ":nth-of-type(" + nth + ")";
|
||||
|
||||
if (el.classList.length > 0) {
|
||||
for (let cls of el.classList) {
|
||||
if (cls !== 'active' && cls !== 'show' && cls !== 'collapsed' && cls !== 'fade') {
|
||||
selector += "." + cls;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
path.unshift(selector);
|
||||
el = el.parentNode;
|
||||
}
|
||||
return path.join(" > ");
|
||||
}
|
||||
|
||||
// --- 5. OBSŁUGA INPUTA (PRZENOSZENIE DO MODALA) ---
|
||||
function openInputBox() {
|
||||
const box = document.getElementById('comment-input-box');
|
||||
const overlay = document.getElementById('comment-input-box-overlay');
|
||||
|
||||
// FIX: Sprawdź czy jest otwarty modal Bootstrapa
|
||||
const activeModal = document.querySelector('.modal.show .modal-content');
|
||||
|
||||
if (activeModal) {
|
||||
// Jeśli tak, przenieś nasz box do modala (żeby działało pisanie i focus)
|
||||
activeModal.appendChild(box);
|
||||
// Overlay też, żeby przykrył modal
|
||||
activeModal.appendChild(overlay);
|
||||
box.style.position = 'absolute'; // W modalu pozycjonujemy absolutnie względem modala
|
||||
} else {
|
||||
// Jeśli nie, wracamy do body
|
||||
document.body.appendChild(overlay);
|
||||
document.body.appendChild(box);
|
||||
box.style.position = 'fixed';
|
||||
}
|
||||
|
||||
// Tymczasowe wyłączenie trybu wybierania, żeby można było klikać w Boxie
|
||||
// Ale NIE zdejmujemy listenerów blokujących tło (bo tło nadal ma być nieklikalne)
|
||||
// Po prostu nasza funkcja handleInteraction przepuszcza kliknięcia w #comment-input-box
|
||||
|
||||
overlay.style.display = 'block';
|
||||
box.style.display = 'block';
|
||||
|
||||
// Focus po małym timeout, żeby przeglądarka zdążyła przenieść element
|
||||
setTimeout(() => {
|
||||
const authorInput = document.getElementById('new-comment-author');
|
||||
const savedAuthor = localStorage.getItem('magico_comment_author');
|
||||
if (savedAuthor) authorInput.value = savedAuthor;
|
||||
|
||||
// Focus na tekst, chyba że brak autora
|
||||
if (!savedAuthor) authorInput.focus();
|
||||
else document.getElementById('new-comment-text').focus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function closeInputBox() {
|
||||
const box = document.getElementById('comment-input-box');
|
||||
const overlay = document.getElementById('comment-input-box-overlay');
|
||||
|
||||
overlay.style.display = 'none';
|
||||
box.style.display = 'none';
|
||||
document.getElementById('new-comment-text').value = '';
|
||||
pendingElement = null;
|
||||
|
||||
// Wracamy z boxem do body na wszelki wypadek
|
||||
document.body.appendChild(overlay);
|
||||
document.body.appendChild(box);
|
||||
}
|
||||
|
||||
function saveComment() {
|
||||
const text = document.getElementById('new-comment-text').value;
|
||||
const author = document.getElementById('new-comment-author').value || 'Anonim';
|
||||
|
||||
if (!text || !pendingElement) {
|
||||
closeInputBox();
|
||||
return;
|
||||
}
|
||||
|
||||
// Zapisz autora na przyszłość
|
||||
localStorage.setItem('magico_comment_author', author);
|
||||
|
||||
const selector = getCssSelector(pendingElement);
|
||||
|
||||
fetch(API_URL + '?action=add', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
page_path: CURRENT_PATH,
|
||||
selector: selector,
|
||||
comment: text,
|
||||
author: author
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
closeInputBox();
|
||||
loadComments();
|
||||
} else {
|
||||
alert('Błąd: ' + data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 6. PINESKI I AKTUALIZACJA POZYCJI ---
|
||||
function loadComments() {
|
||||
fetch(API_URL + '?action=list&page_path=' + encodeURIComponent(CURRENT_PATH))
|
||||
.then(res => res.json())
|
||||
.then(resp => {
|
||||
if (resp.status === 'success') {
|
||||
markersData = resp.data; // Zapisz dane
|
||||
renderMarkers(); // Narysuj
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let activePopover = null;
|
||||
|
||||
function renderMarkers() {
|
||||
window.magicoIsUpdating = true;
|
||||
try {
|
||||
// Usuń stare
|
||||
document.querySelectorAll('.comment-marker').forEach(el => el.remove());
|
||||
if (activePopover) { activePopover.remove(); activePopover = null; }
|
||||
|
||||
// Grupowanie komentarzy po selektorze
|
||||
const grouped = {};
|
||||
let activeCount = 0;
|
||||
|
||||
markersData.forEach((c) => {
|
||||
if (c.is_resolved == 0) activeCount++;
|
||||
|
||||
// Pomiń rozwiązane, jeśli chcemy je ukrywać (na razie pokazujemy wszystkie, albo tylko nierozwiązane?)
|
||||
// User: "archiwum zmienia flagę is_resolved". Zakładamy że archiwum = ukryte?
|
||||
// "Archiwum" zwykle oznacza ukryte. Ukryjmy rozwiązane.
|
||||
if (c.is_resolved == 1) return;
|
||||
|
||||
if (!grouped[c.dom_selector]) grouped[c.dom_selector] = [];
|
||||
grouped[c.dom_selector].push(c);
|
||||
});
|
||||
|
||||
// Aktualizacja licznika na pasku
|
||||
const cntEl = document.getElementById('comments-count-val');
|
||||
if (cntEl) cntEl.textContent = activeCount;
|
||||
|
||||
Object.keys(grouped).forEach((selector, index) => {
|
||||
const commentsList = grouped[selector];
|
||||
if (!commentsList.length) return;
|
||||
|
||||
try {
|
||||
const el = document.querySelector(selector);
|
||||
if (el && el.offsetParent !== null) { // Tylko widoczne elementy
|
||||
const marker = document.createElement('div');
|
||||
marker.className = 'comment-marker';
|
||||
marker.dataset.selector = selector;
|
||||
|
||||
// Jeśli więcej niż 1 komentarz, pokaż licznik
|
||||
if (commentsList.length > 1) {
|
||||
marker.textContent = commentsList.length;
|
||||
marker.style.display = 'flex';
|
||||
marker.style.alignItems = 'center';
|
||||
marker.style.justifyContent = 'center';
|
||||
marker.style.color = 'white';
|
||||
marker.style.fontSize = '12px';
|
||||
marker.style.fontWeight = 'bold';
|
||||
// Resetuejmu transform dla tekstu żeby był czytelny? Nie, rotacja jest na pinezce.
|
||||
// Tekst też się obróci. Trzeba by go odkręcić.
|
||||
// Prościej: dodajmy span w środku który odkręcimy.
|
||||
marker.innerHTML = `<span style="transform: rotate(45deg); display:block;">${commentsList.length}</span>`;
|
||||
}
|
||||
|
||||
// Zdarzenie HOVER (najazd myszką)
|
||||
marker.addEventListener('mouseenter', (e) => {
|
||||
showPopover(marker, commentsList);
|
||||
});
|
||||
|
||||
marker.addEventListener('mouseleave', (e) => {
|
||||
marker._leaveTimeout = setTimeout(() => {
|
||||
if (activePopover && activePopover._associatedMarker === marker) {
|
||||
if (!activePopover.matches(':hover')) {
|
||||
activePopover.remove();
|
||||
activePopover = null;
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.body.appendChild(marker);
|
||||
}
|
||||
} catch (err) { }
|
||||
});
|
||||
|
||||
// Po stworzeniu od razu ustaw pozycje
|
||||
updateMarkerPositions();
|
||||
} finally {
|
||||
setTimeout(() => { window.magicoIsUpdating = false; }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function showPopover(marker, commentsList) {
|
||||
if (activePopover) activePopover.remove();
|
||||
|
||||
const pop = document.createElement('div');
|
||||
pop.className = 'comment-popover';
|
||||
|
||||
// Budowanie listy komentarzy
|
||||
let html = '<div style="max-height: 200px; overflow-y: auto;">';
|
||||
|
||||
commentsList.forEach(data => {
|
||||
// Escape (chociaż API to robi, warto mieć warstwę w JS w razie czego, ale API już robi htmlspecialchars)
|
||||
// Skoro API robi htmlspecialchars, to tutaj możemy bezpiecznie wstawić
|
||||
// Ale uwaga: JS textContent jest bezpieczniejszy.
|
||||
|
||||
// Hack na szybkie budowanie bezpiecznego HTML w pętli stringów jest trudny.
|
||||
// Zbudujmy to jako elementy DOM potem? Albo zaufajmy API + textContent buildera.
|
||||
|
||||
// Zrobimy placeholder i podstawimy wartości.
|
||||
|
||||
html += `
|
||||
<div class="comment-item" style="border-bottom:1px solid #eee; padding-bottom:8px; margin-bottom:8px;">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
|
||||
<h6 style="margin:0;">${data.author || 'Anonim'}</h6>
|
||||
<span style="font-size:10px; color:#999;">${data.created_at}</span>
|
||||
</div>
|
||||
<p style="margin-bottom:5px;">${data.comment}</p>
|
||||
<div style="text-align:right; gap:5px; display:flex; justify-content:flex-end;">
|
||||
<button class="btn btn-xs btn-outline-success btn-resolve" data-id="${data.id}" style="font-size:10px; padding: 2px 5px;">Rozwiąż</button>
|
||||
<button class="btn btn-xs btn-outline-danger btn-delete" data-id="${data.id}" style="font-size:10px; padding: 2px 5px;">Usuń</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
pop.innerHTML = html;
|
||||
document.body.appendChild(pop);
|
||||
activePopover = pop;
|
||||
pop._associatedMarker = marker;
|
||||
|
||||
// Bindowanie akcji z stopPropagation
|
||||
pop.querySelectorAll('.btn-resolve').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Ważne!
|
||||
resolveComment(btn.dataset.id);
|
||||
});
|
||||
});
|
||||
pop.querySelectorAll('.btn-delete').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Ważne!
|
||||
deleteComment(btn.dataset.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Obsługa zamykania przy wyjechaniu z popovera
|
||||
pop.addEventListener('mouseleave', () => {
|
||||
if (activePopover === pop) {
|
||||
activePopover.remove();
|
||||
activePopover = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Pozycjonowanie
|
||||
const rect = marker.getBoundingClientRect();
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
|
||||
pop.style.top = (rect.top + scrollTop - 5) + 'px';
|
||||
pop.style.left = (rect.right + scrollLeft) + 'px'; // Usunięto przerwę +2px
|
||||
|
||||
if (rect.right + 260 > window.innerWidth) {
|
||||
pop.style.left = (rect.left + scrollLeft - 260) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function resolveComment(id) {
|
||||
if (!confirm('Czy na pewno chcesz rozwiązać/zarchiwizować ten komentarz?')) return;
|
||||
fetch(API_URL + '?action=resolve', {
|
||||
method: 'POST', body: JSON.stringify({ id: id })
|
||||
}).then(res => res.json()).then(res => {
|
||||
if (res.status === 'success') loadComments();
|
||||
else alert('Błąd: ' + res.message);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteComment(id) {
|
||||
if (!confirm('Czy na pewno chcesz trwale usunąć ten komentarz?')) return;
|
||||
fetch(API_URL + '?action=delete', {
|
||||
method: 'POST', body: JSON.stringify({ id: id })
|
||||
}).then(res => res.json()).then(res => {
|
||||
if (res.status === 'success') loadComments();
|
||||
else alert('Błąd: ' + res.message);
|
||||
});
|
||||
}
|
||||
|
||||
// (Usunięto click-outside listener, bo teraz działamy na hover)
|
||||
|
||||
// Aktualizuj też pozycję otwartego popovera przy scrollu
|
||||
const originalUpdatePositions = updateMarkerPositions;
|
||||
// ... w sumie updateMarkerPositions jest niżej zdefiniowana,
|
||||
// lepiej wrzucimy to w updateMarkerPositions bezpośrednio.
|
||||
|
||||
// Funkcja wywoływana przy scrollowaniu - musi być szybka
|
||||
function updateMarkerPositions() {
|
||||
window.magicoIsUpdating = true; // Zaczynamy aktualizację
|
||||
try {
|
||||
const markers = document.querySelectorAll('.comment-marker');
|
||||
|
||||
markers.forEach(marker => {
|
||||
const selector = marker.dataset.selector;
|
||||
if (!selector) return;
|
||||
|
||||
try {
|
||||
const el = document.querySelector(selector);
|
||||
if (el && el.offsetParent !== null) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
// Jeśli element wyjechał poza ekran (jest w scrollowanym divie ale schowany)
|
||||
marker.style.top = (rect.top + window.scrollY) + 'px';
|
||||
marker.style.left = (rect.left + window.scrollX + (rect.width / 2)) + 'px';
|
||||
marker.style.display = (marker.textContent.length > 0) ? 'flex' : 'block';
|
||||
// Flex dla licznika, block dla zwykłego? A w stylach mamy display?
|
||||
// W renderMarkers ustawilismy display:flex ręcznie dla licznika.
|
||||
// Tu musimy uwazac zeby tego nie zepsuc. Pominmy style.display='block' jesli ma flex.
|
||||
if (marker.style.display !== 'flex') marker.style.display = 'block';
|
||||
|
||||
if (activePopover && activePopover._associatedMarker === marker) {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
|
||||
// Recalculate position
|
||||
let top = (rect.top + scrollTop - 5);
|
||||
let left = (rect.right + scrollLeft);
|
||||
if (rect.right + 260 > window.innerWidth) {
|
||||
left = (rect.left + scrollLeft - 260);
|
||||
}
|
||||
|
||||
activePopover.style.top = top + 'px';
|
||||
activePopover.style.left = left + 'px';
|
||||
}
|
||||
} else {
|
||||
marker.style.display = 'none';
|
||||
if (activePopover && activePopover._associatedMarker === marker) {
|
||||
activePopover.remove();
|
||||
activePopover = null;
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
});
|
||||
} finally {
|
||||
// Mały timeout, bo MutationObserver jest asynchroniczny (microtask)
|
||||
// Ale my chcemy zablokować detekcję zmian, które WŁAŚNIE zaszły.
|
||||
// Ponieważ observer odpala się "później", flaga false może zostać ustawiona ZA WCZEŚNIE?
|
||||
// Nie, observer zbiera zmiany i odpala callback.
|
||||
// Jeśli callback odpali się kiedy flaga jest true, to return.
|
||||
// Ale callback odpali się w następnym ticku.
|
||||
// Więc musimy przetrzymać flagę true do następnego ticku?
|
||||
// Tak, bezpieczniej setTimeout(..., 0).
|
||||
setTimeout(() => { window.magicoIsUpdating = false; }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// START
|
||||
initUI();
|
||||
loadComments();
|
||||
|
||||
// Odświeżanie przy zmianie rozmiaru okna
|
||||
window.addEventListener('resize', updateMarkerPositions);
|
||||
|
||||
// MutationObserver - odświeżaj pozycje jak coś się zmieni w wygenerowanym DOM (ale ignoruj nasze markery)
|
||||
// Helper sprawdzający czy element jest częścią naszego UI
|
||||
function isOurElement(node) {
|
||||
if (!node || node.nodeType !== 1) return false;
|
||||
|
||||
// Lista ID elementów systemu
|
||||
const ourIds = [
|
||||
'prototype-topbar',
|
||||
'comment-input-box',
|
||||
'comment-input-box-overlay',
|
||||
'all-comments-dialog',
|
||||
'all-comments-dialog-overlay'
|
||||
];
|
||||
|
||||
if (ourIds.includes(node.id)) return true;
|
||||
|
||||
if (node.classList && (
|
||||
node.classList.contains('comment-marker') ||
|
||||
node.classList.contains('comment-popover')
|
||||
)) return true;
|
||||
|
||||
// Sprawdź rodziców
|
||||
if (node.closest) {
|
||||
for (const id of ourIds) {
|
||||
if (node.closest('#' + id)) return true;
|
||||
}
|
||||
if (node.closest('.comment-marker') || node.closest('.comment-popover')) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- 8. OBSŁUGA OBSERVERA I PĘTLI ---
|
||||
|
||||
// Zmienna na observer zdefiniowana niżej, ale potrzebujemy jej tu.
|
||||
// Przenieśmy definicję observera wyżej albo użyjmy funkcji.
|
||||
|
||||
let observer; // deklaracja wstępna
|
||||
|
||||
function ignoreObserver(action) {
|
||||
if (observer) observer.disconnect();
|
||||
try {
|
||||
action();
|
||||
} finally {
|
||||
if (observer) startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
function startObserver() {
|
||||
observer.observe(document.body, {
|
||||
childList: true, subtree: true, attributes: true,
|
||||
attributeFilter: ['class', 'style', 'hidden']
|
||||
});
|
||||
}
|
||||
|
||||
// --- MODYFIKACJA renderMarkers i updateMarkerPositions ---
|
||||
|
||||
// Przechwytujemy stare funkcje i opakowujemy je
|
||||
// Ale lepiej po prostu zmienić ich ciała w kodzie (co robię tym replace'm)
|
||||
|
||||
// Nadpiszmy renderMarkers, żeby używało ignoreObserver
|
||||
// Uwaga: Funkcja renderMarkers i updateMarkerPositions są wyżej.
|
||||
// Ten tool replace musi być sprytny.
|
||||
// Zamiast nadpisywać, zmienię logikę observera na dole pliku i podmienię wywołania.
|
||||
|
||||
// W tym bloku (EndLine: 734) jest końcówka pliku i definicja observera.
|
||||
// Zdefiniujmy observera tutaj poprawnie.
|
||||
|
||||
observer = new MutationObserver((mutations) => {
|
||||
// Jeśli my sami aktualizujemy, ignoruj wszystko
|
||||
if (window.magicoIsUpdating) return;
|
||||
|
||||
let shouldUpdate = false;
|
||||
|
||||
// Optymalizacja: Sprawdzamy czy zmiany są ISTOTNE
|
||||
for (const mutation of mutations) {
|
||||
// Ignorujemy zmiany na naszych elementach (nawet jeśli disconnect nie zadziałał idealnie)
|
||||
if (isOurElement(mutation.target)) continue;
|
||||
|
||||
// Zmiany childList
|
||||
if (mutation.type === 'childList') {
|
||||
let externalChanges = false;
|
||||
// Sprawdź dodane
|
||||
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
||||
const node = mutation.addedNodes[i];
|
||||
if (node.nodeType === 1 && !isOurElement(node)) {
|
||||
externalChanges = true; break;
|
||||
}
|
||||
if (node.nodeType === 3 && node.textContent.trim() !== '') {
|
||||
externalChanges = true; break;
|
||||
}
|
||||
}
|
||||
// Sprawdź usunięte
|
||||
if (!externalChanges) {
|
||||
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
||||
const node = mutation.removedNodes[i];
|
||||
if (node.nodeType === 1 && !isOurElement(node)) {
|
||||
externalChanges = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!externalChanges) continue;
|
||||
}
|
||||
|
||||
shouldUpdate = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout);
|
||||
window.commentUpdateTimeout = setTimeout(() => {
|
||||
// Renderowanie wyzwolone przez zmiany zewnętrzne
|
||||
// Nie musimy tu robić ignoreObserver bo renderMarkers samo to zrobi wewnątrz?
|
||||
// Nie, bo renderMarkers to funkcja.
|
||||
// Wywołajmy renderMarkers.
|
||||
renderMarkers();
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
startObserver();
|
||||
|
||||
// EXPORT funkcji do użycia wewnątrz renderMarkers/update
|
||||
// Ponieważ nie edytuję całego pliku, musimy jakoś wstrzyknąć ignoreObserver do wywołań wyżej?
|
||||
// Nie da się bez edycji tamtych funkcji.
|
||||
// WIĘC: Zostawiam observera jak jest (z ulepszonym isOurElement),
|
||||
// ALE w renderMarkers i updateMarkerPositions dodam wywołania disconnect/connect.
|
||||
|
||||
// Czekaj, nie mam dostępu do zmiennej `observer` wewnątrz funkcji zadeklarowanych wyżej, jeśli zadeklaruję ją na dole.
|
||||
// Ale w JS var/let w tym samym scope (IIFE) są widoczne.
|
||||
// Muszę przenieść deklarację `let observer` na górę IIFE (innym replacem) lub
|
||||
// Zmienić observer na `window.magicoObserver`? Nieładnie.
|
||||
|
||||
// Zrobię tak: Zdefiniuję `ignoreObserver` tutaj, i użyję `observer.disconnect()` wewnątrz.
|
||||
// Ale muszę mieć pewność że `observer` jest zdefiniowany.
|
||||
// W obecnym kodzie jest zdefiniowany jako `const observer = ...` na dole.
|
||||
// const nie jest hoisted.
|
||||
|
||||
// PLAN B:
|
||||
// Zmienię definicję `const observer` na `let observer` i przesunę definicję funkcji `isOurElement` i observera na dół,
|
||||
// a potem w replace'ach wyżej dodam wywołania wrapperów.
|
||||
|
||||
// LUB PROŚCIEJ:
|
||||
// Zmodyfikujmy `renderMarkers` i `updateMarkerPositions` żeby sprawdzały flagę globalną `window.magicoIsUpdating`.
|
||||
// A observer będzie sprawdzał tę flagę.
|
||||
|
||||
// IMPLEMENTACJA FLAGI:
|
||||
window.magicoIsUpdating = false;
|
||||
|
||||
// --- 7. DIALOG Z LISTĄ WSZYSTKICH ---
|
||||
function showAllCommentsDialog() {
|
||||
const dialog = document.getElementById('all-comments-dialog');
|
||||
const overlay = document.getElementById('all-comments-dialog-overlay');
|
||||
const list = document.getElementById('all-comments-list');
|
||||
|
||||
// Budowanie listy
|
||||
// Filtrujemy tylko nierozwiązane??? W sumie user chciał "wszystkimi zebranymi z tego ekranu".
|
||||
// Pokażmy wszystkie uporządkowane od najnowszych, z oznaczeniem rozwiązanych.
|
||||
|
||||
// Kopia i sortowanie
|
||||
const sorted = [...markersData].sort((a, b) => b.id - a.id);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
list.innerHTML = '<p style="text-align:center; color:#999; padding:20px;">Brak komentarzy na tym ekranie.</p>';
|
||||
} else {
|
||||
let html = '';
|
||||
sorted.forEach(c => {
|
||||
const bg = c.is_resolved == 1 ? '#f0fff0' : '#fff';
|
||||
const status = c.is_resolved == 1 ? '<span class="badge bg-success" style="font-size:10px;">Rozwiązany</span>' : '';
|
||||
|
||||
html += `
|
||||
<div class="comment-list-item" style="background:${bg};">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
||||
<div>
|
||||
<strong>${c.author || 'Anonim'}</strong>
|
||||
${status}
|
||||
</div>
|
||||
<small style="color:#999;">${c.created_at}</small>
|
||||
</div>
|
||||
<p style="margin-bottom:8px;">${c.comment}</p>
|
||||
<div style="font-size:11px; color:#888; margin-bottom:5px;">
|
||||
Element: <code>${c.dom_selector.substring(0, 30)}...</code>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
${c.is_resolved == 0 ? `<button class="btn btn-sm btn-outline-success btn-resolve-list" data-id="${c.id}">Rozwiąż</button>` : ''}
|
||||
<button class="btn btn-sm btn-outline-danger btn-delete-list" data-id="${c.id}">Usuń</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
list.innerHTML = html;
|
||||
|
||||
// Listenery w liście
|
||||
list.querySelectorAll('.btn-resolve-list').forEach(btn => {
|
||||
btn.addEventListener('click', () => resolveComment(btn.dataset.id));
|
||||
});
|
||||
list.querySelectorAll('.btn-delete-list').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteComment(btn.dataset.id));
|
||||
});
|
||||
}
|
||||
|
||||
dialog.style.display = 'block';
|
||||
overlay.style.display = 'block';
|
||||
}
|
||||
|
||||
// Dodajmy do renderMarkers aktualizację licznika
|
||||
// Musimy przechwycić oryginalny renderMarkers wyżej
|
||||
// Ale jesteśmy w module, więc po prostu dopiszmy to do renderMarkers
|
||||
|
||||
// W renderMarkers (linia ~300) dodać:
|
||||
// document.getElementById('comments-count-val').textContent = markersData.filter(c => c.is_resolved == 0).length;
|
||||
// Albo wszystkich? "Liczba komentarzy". Raczej wszystkich aktywnych.
|
||||
|
||||
})();
|
||||
25
db_connect.php
Normal file
25
db_connect.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
$host = 'localhost';
|
||||
$db = 'srv105686_magicoprototype';
|
||||
$user = 'srv105686_magicoprototype';
|
||||
$pass = 'pMMdmYHXSBs6wM5LHNjd';
|
||||
$pass = 'pMMdmYHXSBs6wM5LHNjd';
|
||||
$charset = 'utf8mb4';
|
||||
|
||||
// Hasło do trybu komentowania
|
||||
$PROTOTYPE_PASSWORD = 'magic'; // Zmień to hasło!
|
||||
|
||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||
} catch (\PDOException $e) {
|
||||
// W środowisku produkcyjnym nie pokazuj błędów użytkownikom, ale tu to prototyp:
|
||||
throw new \PDOException($e->getMessage(), (int) $e->getCode());
|
||||
}
|
||||
?>
|
||||
@@ -27,4 +27,13 @@
|
||||
<!-- Vendors JS -->
|
||||
|
||||
<!-- Main JS -->
|
||||
<script src="<?= $basePath ?>/assets/js/main.js"></script>
|
||||
<script src="<?= $basePath ?>/assets/js/main.js"></script>
|
||||
<script>
|
||||
// Przekazujemy ścieżkę z PHP do globalnej zmiennej JS
|
||||
// Używamy json_encode dla bezpieczeństwa typów
|
||||
const MAGICO_BASE_URL = <?= json_encode($basePath); ?>;
|
||||
</script>
|
||||
|
||||
<?php if (!empty($enablePrototypeComments) && $enablePrototypeComments): ?>
|
||||
<script src="<?= $basePath ?>/assets/js/comments.js"></script>
|
||||
<?php endif; ?>
|
||||
@@ -44,6 +44,10 @@ if ($_SERVER['HTTP_HOST'] === 'localhost' || $_SERVER['HTTP_HOST'] === '127.0.0.
|
||||
<!-- Vendors CSS -->
|
||||
<link rel="stylesheet" href="<?= $basePath ?>/assets/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
|
||||
<?php if (!empty($enablePrototypeComments) && $enablePrototypeComments): ?>
|
||||
<link rel="stylesheet" href="<?= $basePath ?>/assets/css/comments.css">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Page CSS -->
|
||||
|
||||
<!-- Helpers -->
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php include '../../header-sneat.php'; ?>
|
||||
<?php
|
||||
$enablePrototypeComments = true;
|
||||
include '../../header-sneat.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.bg-light {
|
||||
@@ -39,7 +42,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-4 overflow-auto pb-2" id="inquiryTabs" role="tablist">
|
||||
<button class="btn btn-primary btn-sm d-flex align-items-center text-nowrap"
|
||||
<button class="btn btn-primary btn-sm d-flex align-items-center text-nowrap active"
|
||||
id="tab-general-btn" data-bs-toggle="tab" data-bs-target="#tab-general" type="button"
|
||||
role="tab" aria-selected="true">
|
||||
<i class="bi bi-info-circle me-2"></i> Ogólne
|
||||
@@ -66,8 +69,9 @@
|
||||
<div class="tab-pane fade show active" id="tab-general" role="tabpanel">
|
||||
|
||||
<div class="mb-4">
|
||||
<h5 class="d-flex align-items-center gap-1 text-primary mb-3" style="cursor: pointer;"
|
||||
data-bs-toggle="collapse" data-bs-target="#descCollapse" aria-expanded="true">
|
||||
<h5 class="d-flex align-items-center gap-1 text-primary mb-3"
|
||||
style="cursor: pointer;" data-bs-toggle="collapse"
|
||||
data-bs-target="#descCollapse" aria-expanded="true">
|
||||
<i class="bi bi-chevron-down fs-5"></i> Opis oferty
|
||||
</h5>
|
||||
<div class="collapse show" id="descCollapse">
|
||||
@@ -82,141 +86,137 @@
|
||||
<hr class="my-4">
|
||||
|
||||
<div>
|
||||
<h5 class="d-flex align-items-center gap-1 text-primary mb-4" style="cursor: pointer;"
|
||||
data-bs-toggle="collapse" data-bs-target="#timelineCollapse" aria-expanded="true">
|
||||
<h5 class="d-flex align-items-center gap-1 text-primary mb-4"
|
||||
style="cursor: pointer;" data-bs-toggle="collapse"
|
||||
data-bs-target="#timelineCollapse" aria-expanded="true">
|
||||
<i class="bi bi-chevron-down fs-5"></i> Historia operacji
|
||||
</h5>
|
||||
|
||||
<div class="collapse show" id="timelineCollapse">
|
||||
<ul class="timeline timeline-outline mb-0 pb-0">
|
||||
|
||||
<div id="timeline-initial">
|
||||
|
||||
<!-- Item 1: Komentarz -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-warning"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary">MT</span>
|
||||
</div>
|
||||
<h6 class="mb-0">Mateusz Travel dodał komentarz</h6>
|
||||
<!-- Item 1: Komentarz -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-warning"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary">MT</span>
|
||||
</div>
|
||||
<small class="text-muted">15 min temu</small>
|
||||
<h6 class="mb-0">Mateusz Travel dodał komentarz</h6>
|
||||
</div>
|
||||
<div class="bg-lighter p-2 rounded ms-1">
|
||||
<p class="mb-0 small fst-italic">"Klient prosi o zmianę
|
||||
terminu na
|
||||
czerwiec. Do weryfikacji dostępność hoteli."</p>
|
||||
<small class="text-muted">15 min temu</small>
|
||||
</div>
|
||||
<div class="bg-lighter p-2 rounded ms-1">
|
||||
<p class="mb-0 small fst-italic">"Klient prosi o zmianę
|
||||
terminu na
|
||||
czerwiec. Do weryfikacji dostępność hoteli."</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Item 2: Błąd SMS -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-danger"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-secondary">SY</span>
|
||||
</div>
|
||||
<h6 class="mb-0 text-danger">Błąd wysyłki SMS (System)
|
||||
</h6>
|
||||
</div>
|
||||
<small class="text-muted">Dziś, 12:21</small>
|
||||
</div>
|
||||
<p class="mb-0 text-danger small ms-1">Limit środków wyczerpany.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Item 3: Email -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-secondary"></span>
|
||||
<div class="timeline-event">
|
||||
<div
|
||||
class="timeline-header mb-1 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<img src="https://demos.themeselection.com/sneat-bootstrap-html-admin-template/assets/img/avatars/1.png"
|
||||
alt="Avatar" class="rounded-circle" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0">Wysłano e-mail (Automat)</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<small class="text-muted">Dziś, 12:20</small>
|
||||
<button class="btn btn-xs btn-label-secondary p-1 px-2"
|
||||
type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#emailContent1" aria-expanded="false">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Item 2: Błąd SMS -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-danger"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-secondary">SY</span>
|
||||
</div>
|
||||
<h6 class="mb-0 text-danger">Błąd wysyłki SMS (System)
|
||||
</h6>
|
||||
</div>
|
||||
<small class="text-muted">Dziś, 12:21</small>
|
||||
</div>
|
||||
<p class="mb-0 text-danger small ms-1">Limit środków wyczerpany.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<div class="ms-1">
|
||||
<p class="mb-1 small">Temat: <strong>Propozycja wycieczki w
|
||||
Tatry</strong> <span class="text-muted mx-1">|</span>
|
||||
do:
|
||||
jan@jan.pl</p>
|
||||
|
||||
<!-- Item 3: Email -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-secondary"></span>
|
||||
<div class="timeline-event">
|
||||
<div
|
||||
class="timeline-header mb-1 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<img src="https://demos.themeselection.com/sneat-bootstrap-html-admin-template/assets/img/avatars/1.png"
|
||||
alt="Avatar" class="rounded-circle" />
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0">Wysłano e-mail (Automat)</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<small class="text-muted">Dziś, 12:20</small>
|
||||
<button class="btn btn-xs btn-label-secondary p-1 px-2"
|
||||
type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#emailContent1" aria-expanded="false">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-1">
|
||||
<p class="mb-1 small">Temat: <strong>Propozycja wycieczki w
|
||||
Tatry</strong> <span class="text-muted mx-1">|</span>
|
||||
do:
|
||||
jan@jan.pl</p>
|
||||
|
||||
<div class="collapse mt-1" id="emailContent1">
|
||||
<div
|
||||
class="bg-lighter p-2 rounded border small text-muted">
|
||||
<p class="mb-1">Dzień dobry,</p>
|
||||
<p class="mb-1">W załączeniu przesyłam wstępną
|
||||
ofertę
|
||||
wyjazdu w
|
||||
Tatry dla grupy szkolnej. Proszę o zapoznanie
|
||||
się z
|
||||
programem.</p>
|
||||
<p class="mb-0">Pozdrawiam, Zespół Mateusz Travel
|
||||
</p>
|
||||
</div>
|
||||
<div class="collapse mt-1" id="emailContent1">
|
||||
<div class="bg-lighter p-2 rounded border small text-muted">
|
||||
<p class="mb-1">Dzień dobry,</p>
|
||||
<p class="mb-1">W załączeniu przesyłam wstępną
|
||||
ofertę
|
||||
wyjazdu w
|
||||
Tatry dla grupy szkolnej. Proszę o zapoznanie
|
||||
się z
|
||||
programem.</p>
|
||||
<p class="mb-0">Pozdrawiam, Zespół Mateusz Travel
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Item 4: Załączniki -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-dark"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary">MT</span>
|
||||
</div>
|
||||
<h6 class="mb-0">Mateusz Travel wgrał załączniki</h6>
|
||||
<!-- Item 4: Załączniki -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-dark"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary">MT</span>
|
||||
</div>
|
||||
<small class="text-muted">Dziś, 12:15</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-1">
|
||||
<i class="bi bi-file-earmark-pdf text-danger"></i>
|
||||
<span class="small">Oferta_Wstepna_v1.pdf</span>
|
||||
<h6 class="mb-0">Mateusz Travel wgrał załączniki</h6>
|
||||
</div>
|
||||
<small class="text-muted">Dziś, 12:15</small>
|
||||
</div>
|
||||
</li>
|
||||
<div class="d-flex align-items-center gap-2 ms-1">
|
||||
<i class="bi bi-file-earmark-pdf text-danger"></i>
|
||||
<span class="small">Oferta_Wstepna_v1.pdf</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Item 5: Zadanie -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-warning"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary">MT</span>
|
||||
</div>
|
||||
<h6 class="mb-0">Utworzono zadanie</h6>
|
||||
<!-- Item 5: Zadanie -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-warning"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar avatar-xs">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary">MT</span>
|
||||
</div>
|
||||
<small class="text-muted">Dziś, 11:00</small>
|
||||
<h6 class="mb-0">Utworzono zadanie</h6>
|
||||
</div>
|
||||
<div
|
||||
class="bg-label-warning rounded p-1 px-2 d-inline-block ms-1">
|
||||
@@ -225,24 +225,29 @@
|
||||
kalkulację</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="load-more-container" class="text-center py-2 border-left-dashed"
|
||||
style="margin-left: 14px; border-left: 1px dashed #d9dee3;">
|
||||
<button id="btn-load-more"
|
||||
class="btn btn-xs btn-outline-primary rounded-pill">
|
||||
<span class="spinner-border spinner-border-sm me-1 d-none"
|
||||
role="status" aria-hidden="true"></span>
|
||||
<i class="bi bi-arrow-down-circle me-1"></i> Wczytaj wcześniejsze
|
||||
(2)
|
||||
</button>
|
||||
</div>
|
||||
<!-- Load More Action -->
|
||||
<div id="load-more-container" class="text-center py-2 border-left-dashed"
|
||||
style="margin-left: 14px; border-left: 1px dashed #d9dee3;">
|
||||
<button id="btn-load-more"
|
||||
class="btn btn-xs btn-outline-primary rounded-pill">
|
||||
<span class="spinner-border spinner-border-sm me-1 d-none" role="status"
|
||||
aria-hidden="true"></span>
|
||||
<i class="bi bi-arrow-down-circle me-1"></i> Wczytaj wcześniejsze
|
||||
(2)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="timeline-extra" class="d-none">
|
||||
<!-- Extra items (initially hidden) -->
|
||||
<div id="timeline-extra" class="d-none">
|
||||
<ul class="timeline timeline-outline mb-0 pb-0 pt-0">
|
||||
|
||||
<!-- Item 6: Zmiana danych -->
|
||||
<li class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<li
|
||||
class="timeline-item timeline-item-transparent pb-3 border-left-dashed">
|
||||
<span class="timeline-point timeline-point-info"></span>
|
||||
<div class="timeline-event">
|
||||
<div class="timeline-header mb-1">
|
||||
@@ -279,39 +284,190 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Tap Pane: Komentarze -->
|
||||
<div class="tab-pane fade" id="tab-comments" role="tabpanel">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-chat-left-text fs-1 d-block mb-3"></i>
|
||||
<h5 class="mb-1">Brak komentarzy</h5>
|
||||
<p class="mb-0 small">Komentarze pojawią się w tym miejscu.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tap Pane: Zadania -->
|
||||
<div class="tab-pane fade" id="tab-tasks" role="tabpanel">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-check2-circle fs-1 d-block mb-3"></i>
|
||||
<h5 class="mb-1">Brak zadań</h5>
|
||||
<p class="mb-0 small">Zadania przypisane do tego zapytania pojawią się tutaj.</p>
|
||||
<i class="bi bi-chat-left-text fs-1 d-block mb-3"></i>
|
||||
<h5 class="mb-1">Brak komentarzy</h5>
|
||||
<p class="mb-0 small">Komentarze pojawią się w tym miejscu.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tap Pane: Inne zapytania -->
|
||||
<div class="tab-pane fade" id="tab-other" role="tabpanel">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-briefcase fs-1 d-block mb-3"></i>
|
||||
<h5 class="mb-1">Brak powiązanych zapytań</h5>
|
||||
<p class="mb-0 small">Inne zapytania tego klienta pojawią się tutaj.</p>
|
||||
<!-- Tap Pane: Zadania -->
|
||||
<div class="tab-pane fade" id="tab-tasks" role="tabpanel">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-check2-circle fs-1 d-block mb-3"></i>
|
||||
<h5 class="mb-1">Brak zadań</h5>
|
||||
<p class="mb-0 small">Zadania przypisane do tego zapytania pojawią się tutaj.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tap Pane: Inne zapytania -->
|
||||
<div class="tab-pane fade" id="tab-other" role="tabpanel">
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-briefcase fs-1 d-block mb-3"></i>
|
||||
<h5 class="mb-1">Brak powiązanych zapytań</h5>
|
||||
<p class="mb-0 small">Inne zapytania tej Organizacji pojawią się tutaj.</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
lub
|
||||
<hr>
|
||||
|
||||
<div class="p-3">
|
||||
<div class="alert alert-primary d-flex align-items-center p-3 mb-4" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2 fs-5"></i>
|
||||
<div class="small">
|
||||
Poniżej znajdują się inne zapytania powiązane z tym klientem (Jan Jan) lub
|
||||
organizacją.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive text-nowrap border rounded mb-3">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;" class="text-center">
|
||||
<input class="form-check-input" type="checkbox" id="chkAll">
|
||||
</th>
|
||||
<th>Zapytanie / Data</th>
|
||||
<th>Opiekun</th>
|
||||
<th class="text-center">Status</th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-border-bottom-0">
|
||||
|
||||
<tr>
|
||||
<td class="text-center align-middle">
|
||||
<input class="form-check-input inquiry-chk" type="checkbox">
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex flex-column" style="max-width: 280px;">
|
||||
<a href="#" class="fw-semibold text-truncate text-body"
|
||||
title="#135 - Wycieczka Bieszczady">#135 - Wycieczka
|
||||
Bieszczady</a>
|
||||
<small class="text-muted"
|
||||
style="font-size: 0.75rem;">12.02.2026</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center" data-bs-toggle="tooltip"
|
||||
title="Mateusz Travel">
|
||||
<div class="avatar avatar-xs me-2">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-primary"
|
||||
style="font-size: 0.65rem;">MT</span>
|
||||
</div>
|
||||
<small class="text-muted">M. Travel</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center align-middle">
|
||||
<span class="badge bg-label-info"
|
||||
style="font-size: 0.7rem; padding: 4px 8px;">Nowa</span>
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<a href="#"
|
||||
class="btn btn-sm btn-icon btn-label-secondary rounded-circle"
|
||||
data-bs-toggle="tooltip" title="Przejdź do zapytania">
|
||||
<i class="bi bi-arrow-right-short fs-4"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center align-middle">
|
||||
<input class="form-check-input inquiry-chk" type="checkbox">
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex flex-column" style="max-width: 280px;">
|
||||
<a href="#" class="fw-semibold text-truncate text-body">#120
|
||||
- Zapytanie ogólne</a>
|
||||
<small class="text-muted"
|
||||
style="font-size: 0.75rem;">28.01.2026</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center" data-bs-toggle="tooltip"
|
||||
title="Adam Admin">
|
||||
<div class="avatar avatar-xs me-2">
|
||||
<span
|
||||
class="avatar-initial rounded-circle bg-label-secondary"
|
||||
style="font-size: 0.65rem;">AD</span>
|
||||
</div>
|
||||
<small class="text-muted">A. Admin</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center align-middle">
|
||||
<span class="badge bg-label-secondary"
|
||||
style="font-size: 0.7rem; padding: 4px 8px;">Nie
|
||||
zainter.</span>
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<a href="#"
|
||||
class="btn btn-sm btn-icon btn-label-secondary rounded-circle"
|
||||
data-bs-toggle="tooltip" title="Przejdź do zapytania">
|
||||
<i class="bi bi-arrow-right-short fs-4"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="text-center align-middle">
|
||||
<input class="form-check-input inquiry-chk" type="checkbox">
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex flex-column" style="max-width: 280px;">
|
||||
<a href="#" class="fw-semibold text-truncate text-body">#115
|
||||
- Kolonie Lato 2026</a>
|
||||
<small class="text-muted"
|
||||
style="font-size: 0.75rem;">15.01.2026</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center" data-bs-toggle="tooltip"
|
||||
title="Kasia Kowalska">
|
||||
<div class="avatar avatar-xs me-2">
|
||||
<img src="https://demos.themeselection.com/sneat-bootstrap-html-admin-template/assets/img/avatars/5.png"
|
||||
alt="Avatar" class="rounded-circle">
|
||||
</div>
|
||||
<small class="text-muted">K. Kowalska</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center align-middle">
|
||||
<span class="badge bg-label-success"
|
||||
style="font-size: 0.7rem; padding: 4px 8px;">Wysłana</span>
|
||||
</td>
|
||||
<td class="align-middle text-center">
|
||||
<a href="#"
|
||||
class="btn btn-sm btn-icon btn-label-secondary rounded-circle"
|
||||
data-bs-toggle="tooltip" title="Przejdź do zapytania">
|
||||
<i class="bi bi-arrow-right-short fs-4"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">Wybrano: <strong id="merge-count">0</strong></small>
|
||||
<button class="btn btn-sm btn-primary disabled" id="btn-merge-selection">
|
||||
<i class="bi bi-intersect me-1"></i> Scal zaznaczone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -744,4 +900,49 @@
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 6. TABS: AKTUALIZACJA STYLI PRZYCISKÓW
|
||||
// ==========================================
|
||||
const tabButtons = document.querySelectorAll('#inquiryTabs button[data-bs-toggle="tab"]');
|
||||
|
||||
tabButtons.forEach(btn => {
|
||||
btn.addEventListener('shown.bs.tab', function (event) {
|
||||
// Reset styli dla wszystkich przycisków w tej grupie
|
||||
tabButtons.forEach(b => {
|
||||
b.classList.remove('btn-primary');
|
||||
b.classList.add('btn-outline-secondary');
|
||||
});
|
||||
|
||||
// Aktywacja klikniętego przycisku
|
||||
event.target.classList.remove('btn-outline-secondary');
|
||||
event.target.classList.add('btn-primary');
|
||||
});
|
||||
});
|
||||
|
||||
(function () {
|
||||
const checkboxes = document.querySelectorAll('.inquiry-checkbox');
|
||||
const btnMerge = document.getElementById('btnMergeInquiries');
|
||||
const countSpan = document.getElementById('selectedCount');
|
||||
const selectAll = document.getElementById('selectAllInquiries');
|
||||
|
||||
function updateState() {
|
||||
const count = Array.from(checkboxes).filter(c => c.checked).length;
|
||||
countSpan.textContent = count;
|
||||
if (count > 0) {
|
||||
btnMerge.removeAttribute('disabled');
|
||||
} else {
|
||||
btnMerge.setAttribute('disabled', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
checkboxes.forEach(cb => cb.addEventListener('change', updateState));
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', function () {
|
||||
checkboxes.forEach(cb => cb.checked = this.checked);
|
||||
updateState();
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user