thumbnail improvement

This commit is contained in:
Flatlogic Bot 2026-02-15 16:33:12 +00:00
parent cc5d6146bf
commit 0f6b05982a
6 changed files with 361 additions and 5 deletions

View File

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/media_helper.php';
$tenant_id = 1;
@ -32,8 +33,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_expense'])) {
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
if (move_uploaded_file($tmp_name, $file_path)) {
$stmt = db()->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, file_size, mime_type, uploaded_by) VALUES (?, 'expense', ?, ?, ?, ?, ?, 'John Manager')");
$stmt->execute([$tenant_id, $expense_id, $file_name, $file_path, $file_size, $mime_type]);
$thumbnail_path = null;
if (isImage($mime_type)) {
$thumb_name = 'thumb_' . $new_file_name;
$thumb_path = 'uploads/' . $thumb_name;
if (createThumbnail($file_path, $thumb_path)) {
$thumbnail_path = $thumb_path;
}
}
$stmt = db()->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, thumbnail_path, file_size, mime_type, uploaded_by) VALUES (?, 'expense', ?, ?, ?, ?, ?, ?, 'John Manager')");
$stmt->execute([$tenant_id, $expense_id, $file_name, $file_path, $thumbnail_path, $file_size, $mime_type]);
}
}
}

View File

@ -57,11 +57,12 @@ $currentPage = basename($_SERVER['PHP_SELF']);
<a class="nav-link <?= $currentPage === 'employees.php' ? 'active' : '' ?>" href="employees.php">Employees</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['reports.php', 'files.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
<a class="nav-link dropdown-toggle <?= in_array($currentPage, ['reports.php', 'files.php', 'reports_media.php']) ? 'active' : '' ?>" href="#" role="button" data-bs-toggle="dropdown">
Reports
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-menu-item dropdown-item" href="reports.php">Summary Reports</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="reports_media.php">Media Gallery</a></li>
<li><a class="dropdown-menu-item dropdown-item" href="files.php">Files</a></li>
</ul>
</li>

107
includes/media_helper.php Normal file
View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* Creates a thumbnail for an image.
*
* @param string $sourcePath Path to the source image.
* @param string $targetPath Path where the thumbnail should be saved.
* @param int $maxWidth Maximum width of the thumbnail.
* @param int $maxHeight Maximum height of the thumbnail.
* @return bool True on success, false on failure.
*/
function createThumbnail(string $sourcePath, string $targetPath, int $maxWidth = 400, int $maxHeight = 400): bool {
if (!file_exists($sourcePath)) {
return false;
}
$imageInfo = getimagesize($sourcePath);
if ($imageInfo === false) {
return false;
}
list($width, $height, $type) = $imageInfo;
$sourceImage = null;
switch ($type) {
case IMAGETYPE_JPEG:
$sourceImage = @imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$sourceImage = @imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$sourceImage = @imagecreatefromgif($sourcePath);
break;
case IMAGETYPE_WEBP:
$sourceImage = @imagecreatefromwebp($sourcePath);
break;
default:
return false;
}
if (!$sourceImage) {
return false;
}
// Calculate dimensions
$ratio = min($maxWidth / $width, $maxHeight / $height);
if ($ratio >= 1) {
$newWidth = $width;
$newHeight = $height;
} else {
$newWidth = (int)($width * $ratio);
$newHeight = (int)($height * $ratio);
}
$thumbnail = imagecreatetruecolor($newWidth, $newHeight);
// Handle transparency for PNG/GIF/WebP
if ($type == IMAGETYPE_PNG || $type == IMAGETYPE_GIF || $type == IMAGETYPE_WEBP) {
imagealphablending($thumbnail, false);
imagesavealpha($thumbnail, true);
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
imagefilledrectangle($thumbnail, 0, 0, $newWidth, $newHeight, $transparent);
}
imagecopyresampled($thumbnail, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
if (!is_dir(dirname($targetPath))) {
mkdir(dirname($targetPath), 0775, true);
}
$success = false;
switch ($type) {
case IMAGETYPE_JPEG:
$success = imagejpeg($thumbnail, $targetPath, 85);
break;
case IMAGETYPE_PNG:
$success = imagepng($thumbnail, $targetPath);
break;
case IMAGETYPE_GIF:
$success = imagegif($thumbnail, $targetPath);
break;
case IMAGETYPE_WEBP:
$success = imagewebp($thumbnail, $targetPath);
break;
}
imagedestroy($sourceImage);
imagedestroy($thumbnail);
return $success;
}
/**
* Checks if a mime type is an image.
*/
function isImage(string $mimeType): bool {
return str_starts_with($mimeType, 'image/');
}
/**
* Checks if a mime type is a video.
*/
function isVideo(string $mimeType): bool {
return str_starts_with($mimeType, 'video/');
}

View File

@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/media_helper.php';
$tenant_id = 1;
@ -64,8 +65,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_labour'])) {
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
if (move_uploaded_file($tmp_name, $file_path)) {
$stmt = $db->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, file_size, mime_type, uploaded_by) VALUES (?, 'labour_entry', ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $labour_entry_id, $file_name, $file_path, $file_size, $mime_type, $currentUserName]);
$thumbnail_path = null;
if (isImage($mime_type)) {
$thumb_name = 'thumb_' . $new_file_name;
$thumb_path = 'uploads/' . $thumb_name;
if (createThumbnail($file_path, $thumb_path)) {
$thumbnail_path = $thumb_path;
}
}
$stmt = $db->prepare("INSERT INTO attachments (tenant_id, entity_type, entity_id, file_name, file_path, thumbnail_path, file_size, mime_type, uploaded_by) VALUES (?, 'labour_entry', ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $labour_entry_id, $file_name, $file_path, $thumbnail_path, $file_size, $mime_type, $currentUserName]);
}
}

View File

@ -115,6 +115,9 @@ include __DIR__ . '/includes/header.php';
<li class="nav-item">
<a class="nav-link <?= $report_type === 'calendar' ? 'active' : '' ?>" href="reports.php?report_type=calendar">Monthly Calendar</a>
</li>
<li class="nav-item">
<a class="nav-link" href="reports_media.php">Media Gallery</a>
</li>
</ul>
<?php if ($report_type === 'labour_export'): ?>

226
reports_media.php Normal file
View File

@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/media_helper.php';
$tenant_id = 1;
// Filters
$filter_author = $_GET['author'] ?? '';
$filter_project = (int)($_GET['project_id'] ?? 0);
$filter_evidence = (int)($_GET['evidence_type_id'] ?? 0);
$filter_start = $_GET['start_date'] ?? '';
$filter_end = $_GET['end_date'] ?? '';
$where = ["a.tenant_id = ?", "(a.mime_type LIKE 'image/%' OR a.mime_type LIKE 'video/%')"];
$params = [$tenant_id];
if ($filter_author) {
$where[] = "a.uploaded_by = ?";
$params[] = $filter_author;
}
if ($filter_project) {
$where[] = "COALESCE(le.project_id, ex.project_id) = ?";
$params[] = $filter_project;
}
if ($filter_evidence) {
$where[] = "le.evidence_type_id = ?";
$params[] = $filter_evidence;
}
if ($filter_start) {
$where[] = "DATE(a.created_at) >= ?";
$params[] = $filter_start;
}
if ($filter_end) {
$where[] = "DATE(a.created_at) <= ?";
$params[] = $filter_end;
}
$where_clause = implode(" AND ", $where);
$query = "
SELECT a.*,
p.name as project_name,
et.name as evidence_type_name
FROM attachments a
LEFT JOIN labour_entries le ON a.entity_type = 'labour_entry' AND a.entity_id = le.id
LEFT JOIN expenses ex ON a.entity_type = 'expense' AND a.entity_id = ex.id
LEFT JOIN projects p ON p.id = COALESCE(le.project_id, ex.project_id)
LEFT JOIN evidence_types et ON le.evidence_type_id = et.id
WHERE $where_clause
ORDER BY a.created_at DESC
";
$stmt = db()->prepare($query);
$stmt->execute($params);
$mediaItems = $stmt->fetchAll();
// Get filter options
$authors = db()->prepare("SELECT DISTINCT uploaded_by FROM attachments WHERE tenant_id = ? AND uploaded_by IS NOT NULL ORDER BY uploaded_by");
$authors->execute([$tenant_id]);
$authorList = $authors->fetchAll(PDO::FETCH_COLUMN);
$projects = db()->prepare("SELECT id, name FROM projects WHERE tenant_id = ? AND is_archived = 0 ORDER BY name");
$projects->execute([$tenant_id]);
$projectList = $projects->fetchAll();
$evidenceTypes = db()->prepare("SELECT id, name FROM evidence_types WHERE tenant_id = ? ORDER BY name");
$evidenceTypes->execute([$tenant_id]);
$evidenceTypeList = $evidenceTypes->fetchAll();
$pageTitle = "SR&ED Manager - Media Gallery";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="reports.php" class="text-decoration-none text-muted">Reports</a></li>
<li class="breadcrumb-item active" aria-current="page">Media Gallery</li>
</ol>
</nav>
<h2 class="fw-bold mb-0">Media Gallery</h2>
</div>
<div class="text-muted small">
Total Items: <?= count($mediaItems) ?>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold">Project</label>
<select name="project_id" class="form-select form-select-sm">
<option value="">All Projects</option>
<?php foreach ($projectList as $p): ?>
<option value="<?= $p['id'] ?>" <?= $filter_project == $p['id'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Author</label>
<select name="author" class="form-select form-select-sm">
<option value="">All Authors</option>
<?php foreach ($authorList as $author): ?>
<option value="<?= htmlspecialchars($author) ?>" <?= $filter_author == $author ? 'selected' : '' ?>><?= htmlspecialchars($author) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Objective Evidence</label>
<select name="evidence_type_id" class="form-select form-select-sm">
<option value="">All Evidence</option>
<?php foreach ($evidenceTypeList as $et): ?>
<option value="<?= $et['id'] ?>" <?= $filter_evidence == $et['id'] ? 'selected' : '' ?>><?= htmlspecialchars($et['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">From</label>
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_start) ?>">
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">To</label>
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($filter_end) ?>">
</div>
<div class="col-md-1">
<div class="d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1">Filter</button>
<a href="reports_media.php" class="btn btn-sm btn-outline-secondary" title="Reset"><i class="bi bi-x-lg"></i></a>
</div>
</div>
</form>
</div>
</div>
<?php if (empty($mediaItems)): ?>
<div class="card border-0 shadow-sm text-center py-5">
<div class="card-body">
<i class="bi bi-images text-muted" style="font-size: 3rem;"></i>
<h5 class="mt-3 text-muted">No media files found</h5>
<p class="text-muted small">Try adjusting your filters or upload some images/videos.</p>
<a href="reports_media.php" class="btn btn-sm btn-outline-secondary">Reset All Filters</a>
</div>
</div>
<?php else: ?>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-4">
<?php foreach ($mediaItems as $item):
$isImg = isImage($item['mime_type']);
$isVideo = isVideo($item['mime_type']);
$thumb = $item['thumbnail_path'] ?: $item['file_path'];
// If it's a video and no thumbnail, show a placeholder icon
$showThumb = true;
if ($isVideo && !$item['thumbnail_path']) {
$showThumb = false;
}
?>
<div class="col">
<div class="card h-100 border-0 shadow-sm overflow-hidden position-relative group">
<div class="ratio ratio-1x1 bg-light">
<?php if ($showThumb): ?>
<img src="<?= htmlspecialchars($thumb) ?>" class="object-fit-cover w-100 h-100 transition" alt="<?= htmlspecialchars($item['file_name']) ?>">
<?php else: ?>
<div class="d-flex align-items-center justify-content-center bg-dark">
<i class="bi bi-play-circle text-white" style="font-size: 2.5rem;"></i>
</div>
<?php endif; ?>
<div class="overlay d-flex align-items-center justify-content-center bg-dark bg-opacity-50 opacity-0 transition">
<div class="d-flex gap-2">
<a href="<?= htmlspecialchars($item['file_path']) ?>" target="_blank" class="btn btn-sm btn-light rounded-circle shadow-sm" title="View Full">
<i class="bi bi-eye"></i>
</a>
<a href="<?= htmlspecialchars($item['file_path']) ?>" download class="btn btn-sm btn-light rounded-circle shadow-sm" title="Download">
<i class="bi bi-download"></i>
</a>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="badge <?= $isImg ? 'bg-primary' : 'bg-danger' ?> extra-small">
<?= $isImg ? 'IMAGE' : 'VIDEO' ?>
</span>
<small class="text-muted extra-small"><?= date('M j, Y', strtotime($item['created_at'])) ?></small>
</div>
<div class="text-truncate small fw-bold text-dark" title="<?= htmlspecialchars($item['file_name']) ?>">
<?= htmlspecialchars($item['file_name']) ?>
</div>
<div class="extra-small text-muted text-truncate mt-1">
<i class="bi bi-folder2 me-1"></i> <?= htmlspecialchars($item['project_name'] ?: 'No Project') ?>
</div>
<?php if ($item['evidence_type_name']): ?>
<div class="extra-small text-muted text-truncate">
<i class="bi bi-shield-check me-1"></i> <?= htmlspecialchars($item['evidence_type_name']) ?>
</div>
<?php endif; ?>
<div class="d-flex align-items-center mt-2 pt-2 border-top">
<div class="bg-light rounded-circle p-1 me-2" style="width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-person extra-small"></i>
</div>
<span class="extra-small text-muted"><?= htmlspecialchars($item['uploaded_by'] ?: 'System') ?></span>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<style>
.group:hover .overlay { opacity: 1 !important; }
.transition { transition: all 0.3s ease; }
.extra-small { font-size: 0.7rem; }
.object-fit-cover { object-fit: cover; }
.bg-opacity-50 { background-color: rgba(0,0,0,0.5) !important; }
.opacity-0 { opacity: 0; }
</style>
<?php include __DIR__ . '/includes/footer.php'; ?>