Tryb komentowania ekranów

This commit is contained in:
2026-02-18 21:17:24 +01:00
parent c0beb640e6
commit a282dd2080
7 changed files with 1028 additions and 155 deletions

55
api-comments.php Normal file
View File

@@ -0,0 +1,55 @@
<?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';
$action = $_GET['action'] ?? '';
// 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();
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') {
// 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;
}
?>

225
assets/css/comments.css Normal file
View File

@@ -0,0 +1,225 @@
/* 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) 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: 10002;
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 */
}

358
assets/js/comments.js Normal file
View File

@@ -0,0 +1,358 @@
// 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="font-weight: bold; font-size: 14px;">
<span style="color: #696cff;">Magico</span> Feedback
</div>
<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>
`;
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>
<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>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Listenery UI
document.getElementById('comment-toggle').addEventListener('change', function (e) {
isCommentMode = e.target.checked;
toggleCommentMode(isCommentMode);
});
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);
}
// --- 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')) {
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(() => 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;
if (!text || !pendingElement) {
closeInputBox();
return;
}
const selector = getCssSelector(pendingElement);
fetch(API_URL + '?action=add', {
method: 'POST',
body: JSON.stringify({
page_path: CURRENT_PATH,
selector: selector,
comment: text,
author: 'User'
})
})
.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() {
// Usuń stare
document.querySelectorAll('.comment-marker').forEach(el => el.remove());
if (activePopover) { activePopover.remove(); activePopover = null; }
markersData.forEach((c, index) => {
try {
const el = document.querySelector(c.dom_selector);
if (el && el.offsetParent !== null) { // Tylko widoczne elementy
const marker = document.createElement('div');
marker.className = 'comment-marker';
marker.dataset.index = index; // Żeby łatwo znaleźć dane
// Zdarzenie kliknięcia w marker
marker.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
showPopover(marker, c);
});
document.body.appendChild(marker);
}
} catch (err) { }
});
// Po stworzeniu od razu ustaw pozycje
updateMarkerPositions();
}
function showPopover(marker, data) {
if (activePopover) activePopover.remove();
const pop = document.createElement('div');
pop.className = 'comment-popover';
// Escape HTML - XSS Protection
const safeAuthor = document.createElement('div');
safeAuthor.textContent = data.author || 'Anonim';
const safeComment = document.createElement('div');
safeComment.textContent = data.comment || '';
pop.innerHTML = `
<h6>${safeAuthor.innerHTML}</h6>
<p>${safeComment.innerHTML}</p>
`;
document.body.appendChild(pop);
activePopover = pop;
pop._associatedMarker = marker;
// 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 - 10) + 'px'; // Trochę wyżej
pop.style.left = (rect.right + scrollLeft + 10) + 'px'; // Obok markera
// Jeśli wychodzi poza ekran z prawej, daj na lewo
if (rect.right + 260 > window.innerWidth) {
pop.style.left = (rect.left + scrollLeft - 260) + 'px';
}
}
// Zamknij popover przy kliknięciu w tło
window.addEventListener('click', (e) => {
if (activePopover && !e.target.closest('.comment-popover') && !e.target.closest('.comment-marker')) {
activePopover.remove();
activePopover = null;
}
});
// 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() {
const markers = document.querySelectorAll('.comment-marker');
markers.forEach(marker => {
const index = marker.dataset.index;
const data = markersData[index];
if (!data) return;
try {
const el = document.querySelector(data.dom_selector);
if (el && el.offsetParent !== null) {
const rect = el.getBoundingClientRect();
// Jeśli element wyjechał poza ekran (jest w scrollowanym divie ale schowany)
// Można go ukryć, ale na razie po prostu przesuwamy
marker.style.top = (rect.top + window.scrollY) + 'px';
marker.style.left = (rect.left + window.scrollX + (rect.width / 2)) + 'px';
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 - 10);
let left = (rect.right + scrollLeft + 10);
if (rect.right + 260 > window.innerWidth) {
left = (rect.left + scrollLeft - 260); // Flip to left
}
activePopover.style.top = top + 'px';
activePopover.style.left = left + 'px';
}
} else {
marker.style.display = 'none'; // Ukryj jeśli element zniknął
}
} catch (e) { }
});
// Jeśli jest aktywny popover, zamknij go przy szybkim scrollu żeby nie pływał dziwnie?
// Albo po prostu zostaw.
}
// START
initUI();
loadComments();
// Odświeżanie przy zmianie rozmiaru okna
window.addEventListener('resize', updateMarkerPositions);
// MutationObserver - odświeżaj pozycje jak coś się zmieni w DOM (np otwarcie taba)
const observer = new MutationObserver(() => {
// Debounce dla wydajności
if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout);
window.commentUpdateTimeout = setTimeout(renderMarkers, 200);
});
observer.observe(document.body, {
childList: true, subtree: true, attributes: true,
attributeFilter: ['class', 'style', 'hidden']
});
})();

21
db_connect.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
$host = 'localhost';
$db = 'srv105686_magicoprototype';
$user = 'srv105686_magicoprototype';
$pass = 'pMMdmYHXSBs6wM5LHNjd';
$charset = 'utf8mb4';
$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());
}
?>

View File

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

View File

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

View File

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