Files
Task-Management/views/projects.php

392 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$parent_id = $_GET['folder'] ?? null;
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_folder'])) {
$stmt = $pdo->prepare("INSERT INTO projects (name, is_folder, parent_id, owner_id) VALUES (?, 1, ?, ?)");
$stmt->execute([$_POST['name'], $parent_id, $user_id]);
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_item'])) {
$stmt = $pdo->prepare("DELETE FROM projects WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['item_id'], $user_id]);
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_item'])) {
$stmt = $pdo->prepare("UPDATE projects SET name=? WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['new_name'], $_POST['item_id'], $user_id]);
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_file']) && isset($_FILES['file'])) {
$name = $_FILES['file']['name'];
$tmp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$path = 'uploads/' . time() . '_' . $name;
move_uploaded_file($tmp_name, $path);
$stmt = $pdo->prepare("INSERT INTO projects (name, is_folder, parent_id, file_path, owner_id) VALUES (?, 0, ?, ?, ?)");
$stmt->execute([$name, $parent_id, $path, $user_id]);
echo "OK"; exit;
}
if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_project'])) {
$pr_id = $_POST['project_id'];
$share = $_POST['share_target'];
$parts = explode('_', $share, 2);
if(count($parts)===2) {
$stmt = $pdo->prepare("INSERT INTO project_shares (project_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$pr_id, $parts[0], $parts[1]]);
$pr = query("SELECT name FROM projects WHERE id=?", [$pr_id])->fetch();
$msg = "A project/file '{$pr['name']}' has been shared with you.";
if($parts[0] === 'user') notify_user($parts[1], $msg);
else notify_group($parts[1], $msg);
}
}
// ── Fetch current folder content ──────────────────────────────────────────────
$user_groups = get_user_groups($user_id);
$my_group_ids = array_column($user_groups, 'id');
$group_in = implode(',', array_fill(0, count($my_group_ids), '?')) ?: '0';
$parent_cond = $parent_id ? "p.parent_id = ?" : "p.parent_id IS NULL";
$sql = "SELECT p.*, (p.owner_id = ?) as is_mine, u.username as owner_name FROM projects p
LEFT JOIN project_shares ps ON p.id = ps.project_id
LEFT JOIN users u ON p.owner_id = u.id
WHERE ($parent_cond) AND (
p.owner_id = ?
OR (ps.share_type='user' AND ps.share_id = ?)
OR (ps.share_type='group' AND ps.share_id IN ($group_in))
) GROUP BY p.id ORDER BY p.is_folder DESC, p.name ASC";
$params = [$user_id];
if($parent_id) $params[] = $parent_id;
$params = array_merge($params, [$user_id, $user_id], $my_group_ids);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$items = $stmt->fetchAll();
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
$all_groups = query("SELECT * FROM groups")->fetchAll();
// ── Build breadcrumb path ─────────────────────────────────────────────────────
$breadcrumbs = [];
$cur_id = $parent_id;
while($cur_id) {
$node = query("SELECT id, name, parent_id FROM projects WHERE id=?", [$cur_id])->fetch();
if(!$node) break;
array_unshift($breadcrumbs, $node);
$cur_id = $node['parent_id'];
}
// ── Helper: file size ─────────────────────────────────────────────────────────
function fmt_size($bytes) {
if(!$bytes) return '—';
if($bytes < 1024) return $bytes . ' B';
if($bytes < 1048576) return round($bytes/1024,1) . ' KB';
return round($bytes/1048576,1) . ' MB';
}
function get_file_icon($name, $is_folder) {
if($is_folder) return '📁';
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$map = [
'pdf'=>'📄','doc'=>'📝','docx'=>'📝','xls'=>'📊','xlsx'=>'📊','ppt'=>'📑','pptx'=>'📑',
'jpg'=>'🖼','jpeg'=>'🖼','png'=>'🖼','gif'=>'🖼','webp'=>'🖼','svg'=>'🖼',
'mp4'=>'🎬','avi'=>'🎬','mov'=>'🎬','mp3'=>'🎵','wav'=>'🎵',
'zip'=>'🗜','rar'=>'🗜','7z'=>'🗜','tar'=>'🗜',
'php'=>'⚙','js'=>'⚙','css'=>'🎨','html'=>'🌐','txt'=>'📃','md'=>'📃',
'sh'=>'💻','py'=>'🐍',
];
return $map[$ext] ?? '📄';
}
?>
<!-- Rename form -->
<form method="POST" id="rename-form" style="display:none;">
<input type="hidden" name="edit_item" value="1">
<input type="hidden" name="item_id" id="rn-item_id">
<input type="hidden" name="new_name" id="rn-new_name">
</form>
<!-- Share Modal -->
<div class="modal-overlay" id="share-modal">
<div class="modal-content" style="max-width:400px;">
<h3 style="margin-bottom:1rem; font-size:1.05rem;"><i class="fa fa-share-nodes"></i> Share Item</h3>
<form method="POST" id="share-form">
<input type="hidden" name="share_project" value="1">
<input type="hidden" name="project_id" id="share-project-id">
<div class="form-group">
<label>Share with</label>
<select name="share_target" class="form-control">
<?php foreach($all_users as $u): ?>
<option value="user_<?= $u['id'] ?>">👤 <?= htmlspecialchars($u['username']) ?></option>
<?php endforeach; ?>
<?php foreach($all_groups as $g): ?>
<option value="group_<?= $g['id'] ?>">👥 <?= htmlspecialchars($g['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="display:flex; gap:0.5rem; justify-content:flex-end; margin-top:1rem;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('share-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn"><i class="fa fa-share-nodes"></i> Share</button>
</div>
</form>
</div>
</div>
<!-- Delete form -->
<form method="POST" id="delete-item-form" style="display:none;">
<input type="hidden" name="delete_item" value="1">
<input type="hidden" name="item_id" id="delete-item-id">
</form>
<!-- Hidden folder create form -->
<form method="POST" id="new-folder-form" style="display:none;">
<input type="hidden" name="create_folder" value="1">
<input type="hidden" name="name" id="new-folder-name">
</form>
<!-- Explorer Toolbar / Breadcrumb -->
<div class="explorer-toolbar" style="margin-bottom:0.75rem;">
<a href="?action=projects" class="btn btn-secondary btn-sm" title="Home">
<i class="fa fa-house"></i>
</a>
<a href="javascript:history.back()" class="btn btn-secondary btn-sm" title="Back">
<i class="fa fa-arrow-left"></i>
</a>
<div class="explorer-path">
<a class="explorer-path-part <?= !$parent_id?'current':'' ?>" href="?action=projects">Root</a>
<?php foreach($breadcrumbs as $bc): ?>
<span class="explorer-path-sep">›</span>
<a class="explorer-path-part <?= $bc['id']==$parent_id?'current':'' ?>" href="?action=projects&folder=<?= $bc['id'] ?>">
<?= htmlspecialchars($bc['name']) ?>
</a>
<?php endforeach; ?>
</div>
<input type="text" id="explorer-search" placeholder="Search..." class="form-control" style="width:180px; padding:0.4rem 0.6rem; font-size:0.85rem;" oninput="filterExplorer(this.value)">
<label class="btn btn-secondary btn-sm" style="cursor:pointer;">
<i class="fa fa-arrow-up-from-bracket"></i> Upload File
<input type="file" id="file-upload-input" style="display:none;" onchange="handleFileSelect(this)">
</label>
<button class="btn btn-sm" onclick="showNewFolderDialog()"><i class="fa fa-folder-plus"></i> New Folder</button>
</div>
<div id="drag-upload-overlay" style="position:fixed; inset:0; z-index:1200; display:none; align-items:center; justify-content:center; pointer-events:none; background:rgba(15,23,42,0.45); backdrop-filter:blur(2px);">
<div style="background:var(--surface); border:2px dashed var(--primary); border-radius:1rem; padding:2rem 2.5rem; color:var(--text); text-align:center; box-shadow:0 12px 30px rgba(0,0,0,0.2);">
<i class="fa fa-cloud-arrow-up" style="font-size:2rem; margin-bottom:0.6rem; display:block; color:var(--primary);"></i>
<div style="font-weight:700; margin-bottom:0.2rem;">Drop files to upload</div>
<div style="font-size:0.84rem; color:var(--text-light);">Release your file anywhere on this page</div>
</div>
</div>
<!-- Detail Table -->
<div class="card" style="padding:0; overflow:hidden; min-height:calc(100vh - 190px);">
<table class="explorer-table" id="explorer-table">
<thead>
<tr>
<th style="width:30px;"><input type="checkbox" id="explorer-select-all" onclick="explorerSelectAll(this)"></th>
<th onclick="sortExplorer('name')" style="min-width:200px;">
<i class="fa fa-sort" style="font-size:0.7rem; margin-right:4px;"></i>Name
</th>
<th onclick="sortExplorer('type')" style="width:80px;">Type</th>
<th onclick="sortExplorer('owner')" style="width:120px;">Owner</th>
<th onclick="sortExplorer('size')" style="width:80px;">Size</th>
<th style="width:140px;">Shared</th>
<th style="width:160px; text-align:right;">Actions</th>
</tr>
</thead>
<tbody id="explorer-tbody">
<?php if(empty($items)): ?>
<tr><td colspan="7" style="text-align:center; padding:3rem; color:var(--text-light);">
<i class="fa fa-folder-open" style="font-size:2rem; display:block; margin-bottom:0.5rem;"></i>
This folder is empty.
</td></tr>
<?php endif; ?>
<?php foreach($items as $item):
$ext = $item['is_folder'] ? 'folder' : strtoupper(pathinfo($item['name'], PATHINFO_EXTENSION) ?: 'File');
$size = (!$item['is_folder'] && $item['file_path'] && file_exists($item['file_path'])) ? filesize($item['file_path']) : null;
$icon = get_file_icon($item['name'], $item['is_folder']);
?>
<tr class="explorer-row" data-name="<?= htmlspecialchars($item['name']) ?>" data-type="<?= $item['is_folder']?'folder':'file' ?>">
<td><input type="checkbox" class="explorer-check" value="<?= $item['id'] ?>"></td>
<td>
<div class="explorer-name">
<span class="explorer-icon"><?= $icon ?></span>
<?php if($item['is_folder']): ?>
<a href="?action=projects&folder=<?= $item['id'] ?>"><?= htmlspecialchars($item['name']) ?></a>
<?php else: ?>
<a href="<?= htmlspecialchars($item['file_path']) ?>" target="_blank"><?= htmlspecialchars($item['name']) ?></a>
<?php endif; ?>
</div>
</td>
<td style="color:var(--text-light); font-size:0.8rem;"><?= htmlspecialchars($ext) ?></td>
<td style="font-size:0.8rem;">
<?= htmlspecialchars($item['owner_name']) ?>
<?php if(!$item['is_mine']): ?><span class="badge" style="font-size:0.6rem; padding:1px 5px; margin-left:2px;">Shared</span><?php endif; ?>
</td>
<td style="color:var(--text-light); font-size:0.8rem;"><?= $size !== null ? fmt_size($size) : '—' ?></td>
<td style="font-size:0.8rem; color:var(--text-light);">
<?php
// Show shares
$shares = query("SELECT ps.share_type, ps.share_id, u.username, g.name as gname FROM project_shares ps LEFT JOIN users u ON (ps.share_type='user' AND ps.share_id=u.id) LEFT JOIN groups g ON (ps.share_type='group' AND ps.share_id=g.id) WHERE ps.project_id=?", [$item['id']])->fetchAll();
foreach($shares as $sh) {
echo '<span style="font-size:0.72rem; background:rgba(var(--primary-rgb),0.1); color:var(--primary); padding:1px 5px; border-radius:8px; margin:1px; display:inline-block;">';
echo $sh['share_type']==='user' ? ('👤 '.htmlspecialchars($sh['username']??'')) : ('👥 '.htmlspecialchars($sh['gname']??''));
echo '</span>';
}
if(!$shares) echo '<span style="color:var(--text-light); font-size:0.8rem;">—</span>';
?>
</td>
<td>
<div class="explorer-actions" style="justify-content:flex-end;">
<?php if($item['is_mine']): ?>
<button class="btn btn-sm btn-secondary" onclick="openShare(<?= $item['id'] ?>)" title="Share"><i class="fa fa-share-nodes"></i></button>
<button class="btn btn-sm btn-secondary" onclick="renameItem(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Rename"><i class="fa fa-pen"></i></button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(<?= $item['id'] ?>, '<?= htmlspecialchars(addslashes($item['name'])) ?>')" title="Delete"><i class="fa fa-trash"></i></button>
<?php else: ?>
<span style="font-size:0.75rem; color:var(--text-light); padding:0 0.5rem;">View only</span>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Status Bar -->
<div class="explorer-status-bar">
<span id="explorer-count"><?= count($items) ?> item<?= count($items)!=1?'s':'' ?></span>
<span id="explorer-selected-count" style="display:none; color:var(--primary); font-weight:600;"></span>
<span style="margin-left:auto;">
<?= $parent_id ? 'Folder: '.htmlspecialchars(end($breadcrumbs)['name'] ?? '') : 'Root directory' ?>
</span>
</div>
<script>
function renameItem(id, currName) {
let nn = prompt("Enter new name:", currName);
if(nn && nn.trim()!=='' && nn !== currName) {
document.getElementById('rn-item_id').value = id;
document.getElementById('rn-new_name').value = nn;
document.getElementById('rename-form').submit();
}
}
function deleteItem(id, name) {
if(!confirm(`Delete "${name}"? This cannot be undone.`)) return;
document.getElementById('delete-item-id').value = id;
document.getElementById('delete-item-form').submit();
}
function openShare(id) {
document.getElementById('share-project-id').value = id;
document.getElementById('share-modal').classList.add('active');
}
function showNewFolderDialog() {
const name = prompt('New folder name:');
if(name && name.trim()) {
document.getElementById('new-folder-name').value = name.trim();
document.getElementById('new-folder-form').submit();
}
}
function handleFileSelect(input) {
if(input.files.length > 0) uploadFile(input.files[0]);
}
function handleDrop(e) {
e.preventDefault();
hideDragOverlay();
if(e.dataTransfer.files.length > 0) uploadFile(e.dataTransfer.files[0]);
}
function uploadFile(file) {
showToast('Uploading ' + file.name + '...');
let formData = new FormData();
formData.append('upload_file', '1');
formData.append('file', file);
fetch('?action=projects&folder=<?= $parent_id ?>', { method: 'POST', body: formData })
.then(() => window.location.reload());
}
let dragCounter = 0;
const dragOverlay = document.getElementById('drag-upload-overlay');
function showDragOverlay() {
if(dragOverlay) dragOverlay.style.display = 'flex';
}
function hideDragOverlay() {
if(dragOverlay) dragOverlay.style.display = 'none';
}
document.addEventListener('dragenter', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
dragCounter++;
showDragOverlay();
});
document.addEventListener('dragover', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
e.preventDefault();
});
document.addEventListener('dragleave', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
dragCounter = Math.max(0, dragCounter - 1);
if(dragCounter === 0) hideDragOverlay();
});
document.addEventListener('drop', (e) => {
if(!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes('Files')) return;
e.preventDefault();
dragCounter = 0;
handleDrop(e);
});
// ── Select all ────────────────────────────────────────────────────────────────
function explorerSelectAll(chk) {
document.querySelectorAll('.explorer-check').forEach(c => c.checked = chk.checked);
updateSelectedCount();
}
document.querySelectorAll('.explorer-check').forEach(c => c.addEventListener('change', updateSelectedCount));
function updateSelectedCount() {
const n = document.querySelectorAll('.explorer-check:checked').length;
const el = document.getElementById('explorer-selected-count');
if(n > 0) {
el.textContent = n + ' selected';
el.style.display = 'inline';
} else {
el.style.display = 'none';
}
}
// ── Search filter ─────────────────────────────────────────────────────────────
function filterExplorer(q) {
q = q.toLowerCase();
let visible = 0;
document.querySelectorAll('#explorer-tbody tr.explorer-row').forEach(row => {
const name = row.dataset.name || '';
if(name.toLowerCase().includes(q)) { row.style.display = ''; visible++; }
else row.style.display = 'none';
});
document.getElementById('explorer-count').textContent = visible + ' item' + (visible!==1?'s':'');
}
// ── Sort ──────────────────────────────────────────────────────────────────────
let sortDir = {};
function sortExplorer(col) {
sortDir[col] = !sortDir[col];
const tbody = document.getElementById('explorer-tbody');
const rows = Array.from(tbody.querySelectorAll('tr.explorer-row'));
rows.sort((a, b) => {
let av = '', bv = '';
if(col==='name') { av=a.dataset.name||''; bv=b.dataset.name||''; }
if(col==='type') { av=a.dataset.type||''; bv=b.dataset.type||''; }
// folders first for type sort
if(col==='type' && av===bv) return 0;
const cmp = av.localeCompare(bv);
return sortDir[col] ? cmp : -cmp;
});
// Folders always first
const folders = rows.filter(r => r.dataset.type==='folder');
const files = rows.filter(r => r.dataset.type!=='folder');
[...folders,...files].forEach(r => tbody.appendChild(r));
}
</script>