TKK_E32230273/history.php

697 lines
25 KiB
PHP

<?php
include 'koneksi.php';
// Coba tambahkan kolom status jika belum ada (is_read, is_starred, is_deleted, deleted_at, location)
try {
$pdo->exec("ALTER TABLE history
ADD COLUMN is_read TINYINT(1) DEFAULT 0,
ADD COLUMN is_starred TINYINT(1) DEFAULT 0,
ADD COLUMN is_deleted TINYINT(1) DEFAULT 0,
ADD COLUMN occupant_name VARCHAR(255) DEFAULT NULL,
ADD COLUMN deleted_at DATETIME DEFAULT NULL");
} catch (PDOException $e) {
// Abaikan jika kolom sudah ada
}
try {
// Create trigger for occupant deletion
$pdo->exec("DROP TRIGGER IF EXISTS trg_history_occupant_delete");
$pdo->exec("
CREATE TRIGGER trg_history_occupant_delete
BEFORE DELETE ON occupant
FOR EACH ROW
BEGIN
UPDATE history
SET occupant_name = OLD.name,
is_deleted = 1,
is_starred = 0,
is_read = 1,
deleted_at = IFNULL(deleted_at, NOW())
WHERE occupant_id = OLD.id_occupant;
END
");
// Auto-delete history placed in trash older than 30 days
$oldHistories = $pdo->query("SELECT id_history, schedule_id FROM history WHERE is_deleted = 1 AND deleted_at < DATE_SUB(NOW(), INTERVAL 30 DAY)")->fetchAll(PDO::FETCH_ASSOC);
if (!empty($oldHistories)) {
$delHistIds = array_column($oldHistories, 'id_history');
if (!empty($delHistIds)) {
$inQueryHist = implode(',', array_fill(0, count($delHistIds), '?'));
$pdo->prepare("DELETE FROM history WHERE id_history IN ($inQueryHist)")->execute($delHistIds);
}
$delSchedIds = array_column($oldHistories, 'schedule_id');
if (!empty($delSchedIds)) {
$inQuerySched = implode(',', array_fill(0, count($delSchedIds), '?'));
$pdo->prepare("DELETE FROM schedule WHERE id_schedule IN ($inQuerySched)")->execute($delSchedIds);
}
}
} catch (PDOException $e) {
// Abaikan jika ada error trigger
}
// Konfigurasi Folder & Pagination
$limit = 12; // Grid view limit
$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
$offset = ($page - 1) * $limit;
// Pencarian
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$searchQuery = "";
$searchParams = [];
// Only show non-deleted
$folderCondition = "h.is_deleted = 0";
if (!empty($search)) {
$searchQuery .= " AND (COALESCE(o.name, h.occupant_name) LIKE ? OR o.nik_nip LIKE ? OR o.no_hp LIKE ? OR s.kegiatan LIKE ?)";
$searchParamValue = "%$search%";
$searchParams = array_merge($searchParams, [$searchParamValue, $searchParamValue, $searchParamValue, $searchParamValue]);
}
$searchUrl = "&search=" . urlencode($search);
$totalData = 0;
$totalPages = 1;
try {
$stmtTotal = $pdo->prepare("
SELECT COUNT(*)
FROM history h
LEFT JOIN occupant o ON h.occupant_id = o.id_occupant
JOIN schedule s ON h.schedule_id = s.id_schedule
WHERE $folderCondition $searchQuery
");
$stmtTotal->execute($searchParams);
$totalData = $stmtTotal->fetchColumn();
$totalPages = ceil($totalData / $limit);
} catch (PDOException $e) {
$errorMsg = $e->getMessage();
}
// Ambil data
$query = "
SELECT
h.id_history, h.foto AS history_foto, h.is_read, h.is_starred, h.is_deleted, h.occupant_name,
o.name, o.nik_nip, o.no_hp, o.foto AS occupant_foto,
s.date AS schedule_date, s.kegiatan
FROM history h
LEFT JOIN occupant o ON h.occupant_id = o.id_occupant
JOIN schedule s ON h.schedule_id = s.id_schedule
WHERE $folderCondition $searchQuery
ORDER BY s.date DESC
LIMIT $limit OFFSET $offset
";
$histories = [];
try {
$stmt = $pdo->prepare($query);
$stmt->execute($searchParams);
$histories = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$errorMsg = $e->getMessage();
}
include 'header.php';
?>
<style>
/* CSS matching profile.php */
.history-container {
padding: 0;
font-family: 'Inter', -apple-system, sans-serif;
}
.history-search {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.875rem;
}
.history-search label {
margin-bottom: 0;
font-weight: 400;
color: #333;
white-space: nowrap;
}
.history-search input[type="search"],
.history-search input[type="text"] {
background: transparent !important;
border: 1px solid #d1d5db !important;
border-radius: 3px;
color: #333 !important;
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
height: 28px;
transition: 0.2s ease;
outline: none;
}
.history-search input:focus {
outline: none !important;
border: 1px solid #60a5fa !important;
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.15);
}
/* Grid Layout for Cards */
.history-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 24px;
}
.history-card {
background: #ffffff;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e5e9f0;
box-shadow: none;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
cursor: pointer;
position: relative;
}
.history-card:hover {
border-color: #d1d5db;
transform: translateY(-2px);
}
/* Unread indicator */
.unread-indicator {
position: absolute;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
background-color: #ffaf02ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.28);
}
.unread-indicator i {
color: #ffffff;
font-size: 16px;
margin: 0;
}
.delete-confirm-cb:checked ~ .custom-control-label::before {
background-color: #ff4747 !important;
border-color: #ff4747 !important;
}
.history-card-img {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
background: #e2e8f0;
border-bottom: 1px solid #e5e9f0;
}
.history-card-body {
padding: 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.activity-desc {
font-size: 14px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.occupant-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px dashed #e5e9f0;
}
.occupant-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
border: 1px solid #e5e9f0;
}
.occupant-details {
flex-grow: 1;
overflow: hidden;
}
.occupant-name {
font-size: 13px;
font-weight: 700;
color: #1a1a1a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0 2px 0;
}
.occupant-nik {
font-size: 11px;
color: #888;
margin: 0;
}
.card-footer-flex {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.history-time {
font-size: 12px;
font-weight: 500;
color: #888;
display: flex;
align-items: center;
gap: 6px;
}
.history-time i {
font-size: 14px;
color: #94a3b8;
}
.btn-delete-card {
background: transparent;
color: #d4d4d8;
border: none;
padding: 6px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.btn-delete-card:hover {
background: #fef2f2;
color: #ef4444;
}
/* DataTables-style pagination */
.pagination-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
}
.page-info {
font-size: 0.875rem;
color: #333;
}
.pagination-btns {
display: flex;
align-items: center;
gap: 0;
}
.pagination-btns .paginate_button {
padding: 0.25rem 0.6rem;
font-size: 0.875rem;
color: #333;
text-decoration: none;
cursor: pointer;
display: inline-block;
border: 1px solid transparent;
border-radius: 4px;
margin: 0 4px;
transition: background 0.15s, border-color 0.15s;
background: transparent;
}
.pagination-btns .paginate_button:hover:not(.disabled):not(.current) {
background: #f5f6f8 !important;
border: 1px solid #ddd !important;
color: #333 !important;
border-radius: 4px;
text-decoration: none;
}
.pagination-btns .paginate_button.current {
background: transparent !important;
border: 1px solid #ddd !important;
color: #7b7979 !important;
border-radius: 4px;
cursor: default;
}
.pagination-btns .paginate_button.disabled {
color: #aaa;
cursor: not-allowed;
opacity: 0.5;
}
.pagination-btns .paginate_button.disabled:hover {
background: transparent !important;
border-color: transparent !important;
}
.pagination-btns a.paginate_button {
color: #333;
text-decoration: none;
}
.pagination-btns a.paginate_button:hover {
text-decoration: none;
}
/* Empty state */
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-state i {
font-size: 48px;
color: #d4d4d8;
margin-bottom: 16px;
}
.empty-state h3 {
font-size: 18px;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 8px;
}
.empty-state p {
font-size: 14px;
color: #888;
}
/* Modal styling matching profile.php */
.mdl-card { border: none; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }
.modal-detail-img {
width: 100%;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid #e5e9f0;
}
.pf-lbl { display: block; font-size: 12px; font-weight: 500; color: #4B49AC; margin-bottom: 4px; }
.pf-val { font-size: 14px; font-weight: 400; color: #1a1a1a; margin-bottom: 0; }
</style>
<div class="main-panel">
<div class="content-wrapper">
<div class="history-container">
<!-- HEADER PROFILE.PHP STYLE -->
<div class="row mb-4 align-items-center">
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
<h3 class="font-weight-bold">Activity Feed</h3>
<h6 class="font-weight-normal mb-0">List of occupant activities reported, <span class="text-primary">Review them below!</span></h6>
</div>
</div>
<!-- MAIN CARD CONTAINER FOR THE GRID -->
<div class="row">
<div class="col-md-12 grid-margin stretch-card">
<div class="card" style="border:1px solid #e5e9f0;box-shadow:none;border-radius:12px;">
<div class="card-body" style="padding: 32px;">
<!-- Header Inside Card (optional, but requested search bar here) -->
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap" style="gap: 16px;">
<p class="card-title mb-0">All Reported Activities</p>
<div class="history-search">
<form action="history.php" method="GET" class="history-search m-0">
<label>Search:</label>
<input type="search" name="search" value="<?= htmlspecialchars($search) ?>">
</form>
</div>
</div>
<?php if(empty($histories) && !isset($errorMsg)): ?>
<div class="empty-state">
<i class="mdi mdi-clipboard-text-outline"></i>
<h3>No activities found</h3>
<p>When someone submits an activity report, it will appear here.</p>
</div>
<?php elseif(isset($errorMsg)): ?>
<div class="alert alert-danger border-0 rounded" style="border-radius: 8px;">Error: <?= $errorMsg ?></div>
<?php else: ?>
<div class="history-grid">
<?php foreach($histories as $idx => $h):
$timestamp = strtotime($h['schedule_date']);
$timeDate = date('M d, Y • h:i A', $timestamp);
$dispName = !empty($h['name']) ? $h['name'] : (!empty($h['occupant_name']) ? $h['occupant_name'] : 'Deleted Occupant');
$dispFoto = !empty($h['occupant_foto']) ? $h['occupant_foto'] : 'images/admin.png';
$dispNik = !empty($h['nik_nip']) ? $h['nik_nip'] : '-';
$dispHp = !empty($h['no_hp']) ? $h['no_hp'] : '-';
$mainImg = !empty($h['history_foto']) ? $h['history_foto'] : 'images/placeholder.jpg';
$jsonData = htmlspecialchars(json_encode([
'id' => $h['id_history'],
'name' => $dispName,
'nik' => $dispNik,
'hp' => $dispHp,
'kegiatan' => $h['kegiatan'],
'time' => $timeDate,
'img' => $mainImg,
'avatar' => $dispFoto,
'is_read' => $h['is_read']
]), ENT_QUOTES, 'UTF-8');
?>
<div class="history-card" data-id="<?= $h['id_history'] ?>" onclick="openHistoryDetail(<?= $jsonData ?>)">
<?php if ($h['is_read'] == 0): ?>
<div class="unread-indicator" id="unread-<?= $h['id_history'] ?>" title="Unread">
<i class="mdi mdi-bell-ring"></i>
</div>
<?php endif; ?>
<img src="<?= htmlspecialchars($mainImg) ?>" class="history-card-img" alt="Activity Photo">
<div class="history-card-body">
<div class="activity-desc" title="<?= htmlspecialchars($h['kegiatan']) ?>">
<?= htmlspecialchars($h['kegiatan']) ?>
</div>
<div class="occupant-info">
<img src="<?= htmlspecialchars($dispFoto) ?>" class="occupant-avatar" alt="Avatar">
<div class="occupant-details">
<h4 class="occupant-name"><?= htmlspecialchars($dispName) ?></h4>
<p class="occupant-nik">NIK: <?= htmlspecialchars($dispNik) ?></p>
</div>
</div>
<div class="card-footer-flex">
<div class="history-time">
<i class="mdi mdi-clock-outline"></i> <?= $timeDate ?>
</div>
<button class="btn-delete-card" onclick="event.stopPropagation(); deleteHistory(<?= $h['id_history'] ?>)" title="Delete Report">
<i class="mdi mdi-delete-outline"></i>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination Inside Card (DataTables style) -->
<div class="pagination-wrapper">
<div class="page-info">
Showing <?= min($offset + 1, $totalData) ?> to <?= min($offset + $limit, $totalData) ?> of <?= $totalData ?> entries
</div>
<div class="pagination-btns">
<?php if($page > 1): ?>
<a href="?page=<?= $page - 1 ?><?= $searchUrl ?>" class="paginate_button previous">Previous</a>
<?php else: ?>
<span class="paginate_button previous disabled">Previous</span>
<?php endif; ?>
<?php
$maxVisible = 5;
$startPage = max(1, $page - floor($maxVisible / 2));
$endPage = min($totalPages, $startPage + $maxVisible - 1);
if ($endPage - $startPage + 1 < $maxVisible) {
$startPage = max(1, $endPage - $maxVisible + 1);
}
for ($i = $startPage; $i <= $endPage; $i++): ?>
<?php if($i == $page): ?>
<span class="paginate_button current"><?= $i ?></span>
<?php else: ?>
<a href="?page=<?= $i ?><?= $searchUrl ?>" class="paginate_button"><?= $i ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if($page < $totalPages): ?>
<a href="?page=<?= $page + 1 ?><?= $searchUrl ?>" class="paginate_button next">Next</a>
<?php else: ?>
<span class="paginate_button next disabled">Next</span>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal Detail -->
<div class="modal fade" id="modalDetailHistory" tabindex="-1">
<div class="modal-dialog modal-dialog" style="max-width: 400px;">
<div class="modal-content mdl-card">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title font-weight-bold" style="color: #1a1a1a; font-size: 16px;"><i class="mdi mdi-text-box-search-outline text-primary mr-2"></i>Activity Detail</h5>
<button type="button" class="close" data-dismiss="modal" style="outline: none;">&times;</button>
</div>
<div class="modal-body p-4">
<div class="text-center mb-3">
<img src="" id="detImg" class="modal-detail-img shadow-sm" alt="Activity Image" style="margin-bottom: 0; max-height: 250px; object-fit: cover;">
</div>
<div style="background: #ffffffff; border: 1px solid #e5e9f0; border-radius: 8px; padding: 15px;">
<div class="d-flex align-items-center mb-3 pb-2" style="border-bottom: 1px solid #e5e9f0;">
<img src="" id="detAvatar" style="width:36px; height:36px; border-radius:50%; object-fit:cover; border:1px solid #d4d4d8; margin-right:12px;">
<div>
<h6 class="font-weight-bold mb-1" id="detName" style="color:#1a1a1a; font-size: 13px;"></h6>
<p class="text-muted mb-0" id="detTime" style="font-size:11px;"></p>
</div>
</div>
<div class="mb-2">
<span class="pf-lbl" style="font-size: 11px;">Activity Description</span>
<p class="pf-val font-weight-bold" id="detKegiatan" style="color: #4B49AC; font-size: 13px;"></p>
</div>
<div class="row">
<div class="col-6 mb-2">
<span class="pf-lbl" style="font-size: 11px;">NIK / NIP</span>
<p class="pf-val" id="detNik" style="font-size: 12px;"></p>
</div>
<div class="col-6 mb-2">
<span class="pf-lbl" style="font-size: 11px;">Phone (WhatsApp)</span>
<p class="pf-val" id="detHp" style="font-size: 12px;"></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== MODAL: DELETE ==================== -->
<div class="modal fade" id="modalDeleteHistory" tabindex="-1">
<div class="modal-dialog modal-dialog">
<div class="modal-content mdl-card">
<form id="formDeleteHistory">
<div class="modal-body p-4" style="text-align: left;">
<input type="hidden" name="id_history" id="delete_id_history">
<h5 class="font-weight-bold mb-3">Delete Report</h5>
<p style="font-size:14px;color:#666;margin-bottom:20px;">Are you sure you want to delete this activity report?</p>
<div class="custom-control custom-checkbox mb-4" style="display:flex; align-items:center;">
<input type="checkbox" class="custom-control-input delete-confirm-cb" id="deleteConfirmCheckHistory" required>
<label class="custom-control-label" for="deleteConfirmCheckHistory" style="font-size:14px; color:#1e1f23; padding-top:2px;">I understand that this report will be permanently removed</label>
</div>
<div class="d-flex justify-content-end" style="gap:8px;">
<button type="button" class="btn btn-light font-weight-bold" style="padding:8px 18px;font-size:13px;" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger font-weight-bold" style="padding:8px 18px;font-size:13px;" id="btnDeleteHistory" disabled>Delete</button>
</div>
</div>
</form>
</div>
</div>
</div>
<?php include 'footer.php'; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
function openHistoryDetail(data) {
// Populate Modal
document.getElementById('detImg').src = data.img;
document.getElementById('detAvatar').src = data.avatar;
document.getElementById('detName').textContent = data.name;
document.getElementById('detNik').textContent = data.nik;
document.getElementById('detHp').textContent = data.hp;
document.getElementById('detKegiatan').textContent = data.kegiatan;
document.getElementById('detTime').innerHTML = '<i class="mdi mdi-clock-outline mr-1"></i>' + data.time;
$('#modalDetailHistory').modal('show');
// Mark as read if unread
if (data.is_read == 0) {
$.post('history_ajax.php', { action: 'mark_read', id: data.id }, function(res) {
if (res.status === 'success') {
// Remove indicator dot
let dot = document.getElementById('unread-' + data.id);
if(dot) {
dot.style.transition = "opacity 0.3s";
dot.style.opacity = "0";
setTimeout(() => dot.remove(), 300);
}
data.is_read = 1; // update local data so it doesn't trigger again
}
});
}
}
function deleteHistory(id) {
document.getElementById('delete_id_history').value = id;
$('#modalDeleteHistory').modal('show');
}
document.getElementById('formDeleteHistory').addEventListener('submit', function(e) {
e.preventDefault();
var btn = document.getElementById('btnDeleteHistory');
btn.disabled = true;
btn.innerHTML = '<i class="mdi mdi-loading mdi-spin mr-1"></i>Deleting...';
var id = document.getElementById('delete_id_history').value;
$.post('history_ajax.php', { action: 'hard_delete', id: id }, function(res) {
btn.disabled = false;
btn.innerHTML = 'Delete';
if (res.status === 'success') {
$('#modalDeleteHistory').modal('hide');
$(`.history-card[data-id="${id}"]`).fadeOut(300, function() {
$(this).remove();
if($('.history-card').length === 0) {
window.location.reload();
}
});
} else {
Swal.fire('Error', res.message || 'Could not delete.', 'error');
}
}, 'json').fail(function() {
btn.disabled = false;
btn.innerHTML = 'Delete';
Swal.fire('Error', 'Connection failed', 'error');
});
});
(function(){
var cb = document.getElementById('deleteConfirmCheckHistory');
var btn = document.getElementById('btnDeleteHistory');
if(cb && btn){
cb.addEventListener('change', function(){ btn.disabled = !this.checked; });
}
$('#modalDeleteHistory').on('hidden.bs.modal', function(){ if(cb){cb.checked=false;} if(btn){btn.disabled=true;} });
})();
// Ensure the alert styles match profile.php minimal borders
const style = document.createElement('style');
style.innerHTML = `
.swal2-popup { border-radius: 12px !important; box-shadow: 0 8px 30px rgba(0,0,0,0.1) !important; border: 1px solid #e5e9f0 !important; }
.swal2-confirm, .swal2-cancel { border-radius: 8px !important; padding: 8px 20px !important; font-weight: 500 !important; }
`;
document.head.appendChild(style);
</script>