thumbnail improvement
This commit is contained in:
parent
cc5d6146bf
commit
0f6b05982a
13
expenses.php
13
expenses.php
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
107
includes/media_helper.php
Normal 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/');
|
||||
}
|
||||
14
labour.php
14
labour.php
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
226
reports_media.php
Normal 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'; ?>
|
||||
Loading…
x
Reference in New Issue
Block a user