feat: integrate Tagify for multi-tag support and add a new view-note modal across the application
This commit is contained in:
@@ -256,7 +256,7 @@ $active_tab = $_GET['tab'] ?? 'overview';
|
||||
</button>
|
||||
</form>
|
||||
<!-- Delete -->
|
||||
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete user <?= htmlspecialchars(addslashes($u['username'])) ?>?')">
|
||||
<form method="POST" style="display:inline;" onsubmit="event.preventDefault(); appConfirm('Delete user <?= htmlspecialchars(addslashes($u['username'])) ?>?').then(y => { if(y) { this.onsubmit=null; this.submit(); } })">
|
||||
<input type="hidden" name="del_user_id" value="<?= $u['id'] ?>">
|
||||
<button type="submit" name="delete_user" class="btn btn-sm btn-danger" title="Delete user">
|
||||
<i class="fa fa-trash"></i>
|
||||
@@ -327,7 +327,7 @@ $active_tab = $_GET['tab'] ?? 'overview';
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:var(--text-light);"><?= count($g_users) ?> member<?= count($g_users)!=1?'s':'' ?></div>
|
||||
</div>
|
||||
<form method="POST" onsubmit="return confirm('Delete group <?= htmlspecialchars(addslashes($g['name'])) ?>?')">
|
||||
<form method="POST" onsubmit="event.preventDefault(); appConfirm('Delete group <?= htmlspecialchars(addslashes($g['name'])) ?>?').then(y => { if(y) { this.onsubmit=null; this.submit(); } })">
|
||||
<input type="hidden" name="del_group_id" value="<?= $g['id'] ?>">
|
||||
<button type="submit" name="delete_group" class="btn btn-sm btn-danger"><i class="fa fa-trash"></i></button>
|
||||
</form>
|
||||
@@ -443,10 +443,10 @@ $active_tab = $_GET['tab'] ?? 'overview';
|
||||
<h3 style="font-size:1rem; font-weight:700; margin-bottom:0.5rem; color:#EF4444;"><i class="fa fa-triangle-exclamation"></i> Danger Zone</h3>
|
||||
<p style="font-size:0.85rem; color:var(--text-light); margin-bottom:1rem;">These actions are irreversible. Proceed with extreme caution.</p>
|
||||
<div style="display:flex; gap:0.75rem; flex-wrap:wrap;">
|
||||
<button class="btn btn-danger" onclick="if(confirm('Really purge all archive tasks?')) alert('Not implemented in this demo.')">
|
||||
<button class="btn btn-danger" onclick="appConfirm('Really purge all archive tasks?').then(y => { if(y) appAlert('Not implemented in this demo.'); })">
|
||||
<i class="fa fa-trash"></i> Purge Archived Tasks
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="if(confirm('Really clear all activity logs?')) alert('Not implemented in this demo.')">
|
||||
<button class="btn btn-danger" onclick="appConfirm('Really clear all activity logs?').then(y => { if(y) appAlert('Not implemented in this demo.'); })">
|
||||
<i class="fa fa-eraser"></i> Clear Activity Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -52,61 +52,69 @@ try {
|
||||
} catch(Exception $e) {}
|
||||
|
||||
// ── Current user mood ─────────────────────────────────────────────────────────
|
||||
$my_mood = $current_user['mood'] ?? null;
|
||||
$me = query("SELECT username, profile_pic, mood, mood_updated_at FROM users WHERE id=?", [$user_id])->fetch();
|
||||
$my_mood = $me['mood'] ?? null;
|
||||
|
||||
// ── Team moods (all users + their mood) ──────────────────────────────────────
|
||||
$team_moods = query("SELECT id, username, mood, mood_updated_at FROM users WHERE mood IS NOT NULL AND id != ? ORDER BY mood_updated_at DESC", [$user_id])->fetchAll();
|
||||
$team_moods = query("SELECT id, username, mood, mood_updated_at, profile_pic FROM users WHERE mood IS NOT NULL AND id != ? ORDER BY mood_updated_at DESC", [$user_id])->fetchAll();
|
||||
|
||||
// ── Activity log ──────────────────────────────────────────────────────────────
|
||||
// Admin: sees ALL activity across all users & groups
|
||||
// Standard user: sees own activity + group members activity
|
||||
// Admin: No longer sees all activity by default.
|
||||
// Standard user/Admin: sees own activity + group members activity, BUT we hide admin activity from non-self.
|
||||
$is_admin_user = !empty($_SESSION['is_admin']);
|
||||
|
||||
$peer_ids = [$user_id];
|
||||
if ($my_group_ids) {
|
||||
$gin = implode(',', array_fill(0, count($my_group_ids), '?'));
|
||||
$peers = $pdo->prepare("SELECT DISTINCT user_id FROM user_groups WHERE group_id IN ($gin)");
|
||||
$peers->execute($my_group_ids);
|
||||
$peer_ids = array_unique(array_merge($peer_ids, array_column($peers->fetchAll(), 'user_id')));
|
||||
}
|
||||
|
||||
// Ensure at least the user themselves is present
|
||||
if(empty($peer_ids)) { $peer_ids = [$user_id]; }
|
||||
|
||||
// Get all admins except the current user
|
||||
$other_admins = [];
|
||||
try {
|
||||
$other_admins = $pdo->prepare("SELECT id FROM users WHERE is_admin = 1 AND id != ?");
|
||||
$other_admins->execute([$user_id]);
|
||||
$other_admins = $other_admins->fetchAll(PDO::FETCH_COLUMN);
|
||||
} catch(Exception $e) {}
|
||||
|
||||
// Filter out other admins from peer ids
|
||||
$peer_ids = array_filter($peer_ids, function($id) use ($other_admins) {
|
||||
return !in_array($id, $other_admins);
|
||||
});
|
||||
|
||||
$uid_in = implode(',', array_fill(0, count($peer_ids), '?'));
|
||||
$recent_logs = [];
|
||||
try {
|
||||
if ($is_admin_user) {
|
||||
// Admin: fetch all activity
|
||||
$stmt = $pdo->prepare("SELECT al.*, u.username FROM activity_log al LEFT JOIN users u ON al.user_id=u.id ORDER BY al.created_at DESC LIMIT 40");
|
||||
$stmt->execute();
|
||||
} else {
|
||||
// Standard: only own + group peers
|
||||
$peer_ids = [$user_id];
|
||||
if ($my_group_ids) {
|
||||
$gin = implode(',', array_fill(0, count($my_group_ids), '?'));
|
||||
$peers = $pdo->prepare("SELECT DISTINCT user_id FROM user_groups WHERE group_id IN ($gin)");
|
||||
$peers->execute($my_group_ids);
|
||||
$peer_ids = array_unique(array_merge($peer_ids, array_column($peers->fetchAll(), 'user_id')));
|
||||
}
|
||||
$uid_in = implode(',', array_fill(0, count($peer_ids), '?'));
|
||||
$stmt = $pdo->prepare("SELECT al.*, u.username FROM activity_log al LEFT JOIN users u ON al.user_id=u.id WHERE al.user_id IN ($uid_in) ORDER BY al.created_at DESC LIMIT 20");
|
||||
$stmt->execute($peer_ids);
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT al.*, u.username FROM activity_log al LEFT JOIN users u ON al.user_id=u.id WHERE al.user_id IN ($uid_in) ORDER BY al.created_at DESC LIMIT 30");
|
||||
$stmt->execute(array_values($peer_ids));
|
||||
$recent_logs = $stmt->fetchAll();
|
||||
} catch(Exception $e) {}
|
||||
|
||||
// ── Task logs ─────────────────────────────────────────────────────────────────
|
||||
$task_logs = [];
|
||||
try {
|
||||
if ($is_admin_user) {
|
||||
// Admin: all recent task status changes
|
||||
$stmt = $pdo->prepare("SELECT t.title, t.status, t.status_updated_at, u.username as assignor
|
||||
FROM tasks t LEFT JOIN users u ON t.assignor_id=u.id
|
||||
WHERE t.status_updated_at IS NOT NULL
|
||||
ORDER BY t.status_updated_at DESC LIMIT 40");
|
||||
$stmt->execute();
|
||||
} else {
|
||||
$group_in_tl = $my_group_ids ? implode(',', array_fill(0, count($my_group_ids), '?')) : '0';
|
||||
$tl_params = array_merge([$user_id, $user_id], $my_group_ids, $my_group_ids);
|
||||
$stmt = $pdo->prepare("SELECT t.title, t.status, t.status_updated_at, u.username as assignor
|
||||
FROM tasks t LEFT JOIN users u ON t.assignor_id=u.id
|
||||
WHERE t.status_updated_at IS NOT NULL
|
||||
AND (
|
||||
t.assignee_id=? OR t.assignor_id=?
|
||||
OR (t.assignee_type='group' AND t.assignee_id IN ($group_in_tl))
|
||||
OR (t.assignor_type='group' AND t.assignor_group_id IN ($group_in_tl))
|
||||
)
|
||||
ORDER BY t.status_updated_at DESC LIMIT 30");
|
||||
$stmt->execute($tl_params);
|
||||
}
|
||||
$group_in_tl = $my_group_ids ? implode(',', array_fill(0, count($my_group_ids), '?')) : '0';
|
||||
$tl_params = array_merge([$user_id, $user_id], $my_group_ids, $my_group_ids);
|
||||
|
||||
$admin_excl = $other_admins ? "AND t.assignor_id NOT IN (" . implode(',', array_fill(0, count($other_admins), '?')) . ")" : "";
|
||||
if($other_admins) { $tl_params = array_merge($tl_params, $other_admins); }
|
||||
|
||||
$stmt = $pdo->prepare("SELECT t.title, t.status, t.status_updated_at, u.username as assignor
|
||||
FROM tasks t LEFT JOIN users u ON t.assignor_id=u.id
|
||||
WHERE t.status_updated_at IS NOT NULL
|
||||
AND (
|
||||
t.assignee_id=? OR t.assignor_id=?
|
||||
OR (t.assignee_type='group' AND t.assignee_id IN ($group_in_tl))
|
||||
OR (t.assignor_type='group' AND t.assignor_group_id IN ($group_in_tl))
|
||||
)
|
||||
$admin_excl
|
||||
ORDER BY t.status_updated_at DESC LIMIT 30");
|
||||
$stmt->execute($tl_params);
|
||||
$task_logs = $stmt->fetchAll();
|
||||
} catch(Exception $e) {}
|
||||
?>
|
||||
@@ -293,6 +301,19 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
</div>
|
||||
<div class="db-stat-link">Open Notes →</div>
|
||||
</a>
|
||||
|
||||
<!-- Tools -->
|
||||
<a href="?action=tools" class="db-stat-card" style="border-top:3px solid #EC4899;">
|
||||
<div class="db-stat-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 9.36l-7.1 7.1a1 1 0 0 1-1.4 0l-2.8-2.8a1 1 0 0 1 0-1.4l7.1-7.1a6 6 0 0 1 9.36-7.94z"></path></svg>
|
||||
Tools
|
||||
</div>
|
||||
<div class="db-stat-value" style="color:#EC4899;">3</div>
|
||||
<div class="db-stat-sub">
|
||||
<span style="background:rgba(236,72,153,0.12); color:#EC4899;">🛠 Utilities</span>
|
||||
</div>
|
||||
<div class="db-stat-link">Open Tools →</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- ── Team Moods ───────────────────────────────────────────── -->
|
||||
@@ -301,19 +322,46 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
<h3 style="font-size:1rem; font-weight:700;">👥 Team Mood</h3>
|
||||
<span style="font-size:0.75rem; color:var(--text-light); font-style:italic;">Live from colleagues</span>
|
||||
</div>
|
||||
<?php if(empty($team_moods)): ?>
|
||||
<p style="color:var(--text-light); font-size:0.85rem;">No one else has set a mood yet.</p>
|
||||
<?php if(empty($team_moods) && empty($my_mood)): ?>
|
||||
<p style="color:var(--text-light); font-size:0.85rem;">No team mood activity yet.</p>
|
||||
<?php else: ?>
|
||||
<div class="team-mood-row" style="display:flex; flex-wrap:nowrap; overflow-x:auto; gap:1rem; padding-bottom:0.5rem;">
|
||||
<?php
|
||||
$avatar_colors_tm = ['#4F46E5','#10B981','#F59E0B','#EF4444','#8B5CF6','#06B6D4','#EC4899','#84CC16'];
|
||||
|
||||
if(!empty($my_mood)):
|
||||
$m_initial = strtoupper(mb_substr($me['username'], 0, 1));
|
||||
$m_color = $avatar_colors_tm[crc32($me['username']) % count($avatar_colors_tm)];
|
||||
?>
|
||||
<div style="position:relative; flex-shrink:0; cursor:pointer;" title="You (Updated: <?= date('M j, H:i', strtotime($me['mood_updated_at'])) ?>)">
|
||||
<div style="width:40px; height:40px; border-radius:50%; background:<?= $m_color ?>; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:1rem; overflow:hidden;">
|
||||
<?php if(!empty($me['profile_pic']) && file_exists($me['profile_pic'])): ?>
|
||||
<img src="<?= htmlspecialchars($me['profile_pic']) ?>?v=<?= time() ?>" style="width:100%; height:100%; object-fit:cover;">
|
||||
<?php else: ?>
|
||||
<?= $m_initial ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="position:absolute; top:-4px; right:-4px; background:var(--surface); border-radius:50%; width:20px; height:20px; display:flex; align-items:center; justify-content:center; font-size:0.8rem; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
|
||||
<?= $my_mood ?>
|
||||
</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-light); text-align:center; margin-top:4px; max-width:40px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-style:italic;">
|
||||
You
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
foreach($team_moods as $tm):
|
||||
$initial = strtoupper(mb_substr($tm['username'], 0, 1));
|
||||
$color = $avatar_colors_tm[crc32($tm['username']) % count($avatar_colors_tm)];
|
||||
?>
|
||||
<div style="position:relative; flex-shrink:0; cursor:pointer;" title="<?= htmlspecialchars($tm['username']) ?> (Updated: <?= date('M j, H:i', strtotime($tm['mood_updated_at'])) ?>)">
|
||||
<div style="width:40px; height:40px; border-radius:50%; background:<?= $color ?>; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:1rem;">
|
||||
<?= $initial ?>
|
||||
<div style="width:40px; height:40px; border-radius:50%; background:<?= $color ?>; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:1rem; overflow:hidden;">
|
||||
<?php if(!empty($tm['profile_pic']) && file_exists($tm['profile_pic'])): ?>
|
||||
<img src="<?= htmlspecialchars($tm['profile_pic']) ?>?v=<?= time() ?>" style="width:100%; height:100%; object-fit:cover;">
|
||||
<?php else: ?>
|
||||
<?= $initial ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="position:absolute; top:-4px; right:-4px; background:var(--surface); border-radius:50%; width:20px; height:20px; display:flex; align-items:center; justify-content:center; font-size:0.8rem; box-shadow:0 2px 4px rgba(0,0,0,0.1);">
|
||||
<?= $tm['mood'] ?>
|
||||
@@ -325,11 +373,6 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if($my_mood): ?>
|
||||
<div style="margin-top:0.5rem; font-size:0.8rem; color:var(--text-light);">
|
||||
Your mood is visible to your team: <strong style="color:var(--text);"><?= $my_mood ?></strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ── Activity Log ───────────────────────────────────────────── -->
|
||||
@@ -337,12 +380,9 @@ if (empty($_SESSION['dash_logged_today'])) {
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<div style="display:flex; align-items:center; gap:0.65rem;">
|
||||
<h3 style="font-size:1rem; font-weight:700;">🕓 Recent Activity</h3>
|
||||
<?php if($is_admin_user): ?>
|
||||
<span class="admin-activity-badge"><i class="fa fa-shield-halved" style="font-size:0.65rem;"></i> All Users</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<span style="font-size:0.75rem; color:var(--text-light);">
|
||||
<?= $is_admin_user ? 'All activity across platform' : 'You & your groups' ?>
|
||||
You & your groups
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
@@ -34,6 +34,61 @@
|
||||
setTimeout(() => t.classList.remove('show'), 3000);
|
||||
}
|
||||
|
||||
// ── Custom Alert / Confirm ─────────────────────────────────────────────
|
||||
function appAlert(msg) {
|
||||
return new Promise(resolve => {
|
||||
let modal = document.getElementById('app-alert-modal');
|
||||
if(!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'app-alert-modal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.zIndex = '10005';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width:320px; text-align:center;">
|
||||
<h3 style="margin-bottom:1rem; font-size:1.1rem;">Notice</h3>
|
||||
<p style="margin-bottom:1.5rem; font-size:0.9rem; color:var(--text);" id="app-alert-msg"></p>
|
||||
<button class="btn" style="width:100%;" id="app-alert-ok">OK</button>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
document.getElementById('app-alert-msg').textContent = msg;
|
||||
modal.classList.add('active');
|
||||
document.getElementById('app-alert-ok').onclick = () => { modal.classList.remove('active'); resolve(true); };
|
||||
});
|
||||
}
|
||||
window.alert = function(msg) { appAlert(msg); };
|
||||
|
||||
function appConfirm(msg) {
|
||||
return new Promise(resolve => {
|
||||
let modal = document.getElementById('app-confirm-modal');
|
||||
if(!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'app-confirm-modal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.zIndex = '10005';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width:350px;">
|
||||
<h3 style="margin-bottom:0.75rem; font-size:1.1rem; text-align:center;">Confirmation</h3>
|
||||
<p style="margin-bottom:1.5rem; font-size:0.95rem; color:var(--text); text-align:center; line-height:1.4;" id="app-confirm-msg"></p>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<button class="btn btn-secondary" style="flex:1;" id="app-confirm-no">Cancel</button>
|
||||
<button class="btn btn-danger" style="flex:1;" id="app-confirm-yes">Confirm</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
document.getElementById('app-confirm-msg').textContent = msg;
|
||||
modal.classList.add('active');
|
||||
document.getElementById('app-confirm-no').onclick = () => { modal.classList.remove('active'); resolve(false); };
|
||||
document.getElementById('app-confirm-yes').onclick = () => { modal.classList.remove('active'); resolve(true); };
|
||||
});
|
||||
}
|
||||
|
||||
function appConfirmUrl(event, url, msg) {
|
||||
event.preventDefault();
|
||||
appConfirm(msg).then(yes => { if(yes) window.location.href = url; });
|
||||
}
|
||||
|
||||
// ── Sidebar Collapse ──────────────────────────────────────────────────
|
||||
function toggleSidebar() {
|
||||
const collapsed = document.body.classList.toggle('sidebar-collapsed');
|
||||
@@ -87,6 +142,19 @@
|
||||
document.getElementById('change-pw-modal').classList.add('active');
|
||||
});
|
||||
<?php endif; ?>
|
||||
|
||||
// ── Tagify globally ───────────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if(typeof Tagify !== 'undefined' && window.APP_GLOBAL_TAGS) {
|
||||
document.querySelectorAll('.tagify-input').forEach(el => {
|
||||
new Tagify(el, {
|
||||
whitelist: window.APP_GLOBAL_TAGS,
|
||||
dropdown: { maxItems: 20, classname: "tags-look", enabled: 0, closeOnSelect: false },
|
||||
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',')
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,6 +7,26 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="public/css/style.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />
|
||||
<style>
|
||||
.tags-look .tagify__dropdown__item{ display: inline-block; border-radius: 3px; padding: .3em .5em; border: 1px solid var(--border); background: var(--surface); margin: .2em; font-size: .85em; transition: 0s; color: var(--text); }
|
||||
.tags-look .tagify__dropdown__item--active{ color: black; }
|
||||
.tags-look .tagify__dropdown__item:hover{ background: var(--primary); color: white; border-color: var(--primary); }
|
||||
</style>
|
||||
<?php
|
||||
$all_app_tags = [];
|
||||
try {
|
||||
$rows = $pdo->query("SELECT label_name as tag FROM notes WHERE label_name IS NOT NULL AND label_name != ''")->fetchAll();
|
||||
foreach($rows as $r) { foreach(explode(',', $r['tag']) as $p) { $all_app_tags[trim($p)] = 1; } }
|
||||
$rows = $pdo->query("SELECT tags as tag FROM tasks WHERE tags IS NOT NULL AND tags != ''")->fetchAll();
|
||||
foreach($rows as $r) { foreach(explode(',', $r['tag']) as $p) { $all_app_tags[trim($p)] = 1; } }
|
||||
} catch(Exception $e) {}
|
||||
$global_tags = array_keys($all_app_tags);
|
||||
?>
|
||||
<script>
|
||||
window.APP_GLOBAL_TAGS = <?= json_encode(array_filter($global_tags)) ?>;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
@@ -45,7 +65,7 @@
|
||||
<form method="POST">
|
||||
<input type="hidden" name="delete_profile_pic" value="1">
|
||||
<input type="hidden" name="redirect_to" value="<?= htmlspecialchars($action) ?>">
|
||||
<button type="submit" class="btn btn-danger" style="width:100%;" onclick="return confirm('Delete profile picture?')">Delete Picture</button>
|
||||
<button type="button" class="btn btn-danger" style="width:100%;" onclick="appConfirm('Delete profile picture?').then(y => { if(y) this.form.submit(); })">Delete Picture</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-secondary" style="width:100%; margin-top:0.5rem;" onclick="document.getElementById('profile-pic-modal').classList.remove('active')">Close</button>
|
||||
@@ -234,7 +254,7 @@
|
||||
<i class="fa fa-key"></i> Change Password
|
||||
</button>
|
||||
<div class="user-dropdown-divider"></div>
|
||||
<a href="?action=logout" class="user-dropdown-item user-dropdown-logout" onclick="return confirm('Logout?')">
|
||||
<a href="?action=logout" class="user-dropdown-item user-dropdown-logout" onclick="appConfirmUrl(event, this.href, 'Logout?')">
|
||||
<i class="fa fa-right-from-bracket"></i> Logout
|
||||
</a>
|
||||
</div>
|
||||
|
||||
135
views/notes.php
135
views/notes.php
@@ -122,8 +122,11 @@ $all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->
|
||||
$all_groups = query("SELECT * FROM groups")->fetchAll();
|
||||
$label_names = [];
|
||||
foreach($notes as $n) {
|
||||
$lbl = trim((string)($n['label_name'] ?? ''));
|
||||
if($lbl !== '') $label_names[strtolower($lbl)] = $lbl;
|
||||
$lbls = explode(',', (string)($n['label_name'] ?? ''));
|
||||
foreach($lbls as $lbl) {
|
||||
$lbl = trim($lbl);
|
||||
if($lbl !== '') $label_names[strtolower($lbl)] = $lbl;
|
||||
}
|
||||
}
|
||||
natcasesort($label_names);
|
||||
|
||||
@@ -182,7 +185,8 @@ $note_colors = [
|
||||
<input type="text" name="title" class="form-control" placeholder="Title..." id="new-note-title" style="font-weight:600; font-size:1rem;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="label_name" class="form-control" id="new-note-label" placeholder="Tag/Label name">
|
||||
<label style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.25rem; display:block;">Tags</label>
|
||||
<input type="text" name="label_name" class="form-control tagify-input" id="new-note-label" placeholder="Add tags...">
|
||||
</div>
|
||||
</div>
|
||||
<!-- WYSIWYG Toolbar -->
|
||||
@@ -231,6 +235,36 @@ $note_colors = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── View Note Modal ────────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="view-note-modal">
|
||||
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:92vh; overflow-y:auto; padding:0; background:var(--bg);">
|
||||
<div id="view-note-header" style="padding:1.5rem; border-bottom:1px solid var(--border); background:var(--surface); position:sticky; top:0; z-index:10;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:1rem;">
|
||||
<div style="flex:1; min-width:0;">
|
||||
<h2 id="view-note-title" style="margin:0 0 0.5rem 0; font-size:1.4rem; font-weight:800; line-height:1.3; color:var(--text); word-wrap:break-word;"></h2>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
|
||||
<span id="view-note-label-container" style="display:none; align-items:center; gap:0.35rem; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.75rem; font-weight:600;">
|
||||
<i class="fa fa-tag" style="font-size:0.7rem;"></i> <span id="view-note-label"></span>
|
||||
</span>
|
||||
<span id="view-note-date" style="font-size:0.8rem; color:var(--text-light);"></span>
|
||||
<span id="view-note-shared-badge" class="note-card-shared-badge" style="display:none;">Shared</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem; flex-shrink:0;">
|
||||
<button class="btn btn-sm btn-secondary" id="view-note-edit-btn" onclick="transToEditNote()"><i class="fa fa-pencil"></i> Edit</button>
|
||||
<button type="button" style="background:none;border:none;color:var(--text-light);cursor:pointer;font-size:1.6rem; line-height:1; padding:0 4px;" onclick="document.getElementById('view-note-modal').classList.remove('active')">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="view-note-image-container" style="display:none; margin-top:1.25rem; border-radius:0.75rem; overflow:hidden; border:1px solid var(--border);">
|
||||
<img id="view-note-image" src="" style="width:100%; max-height:450px; object-fit:cover; display:block;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:1.5rem;">
|
||||
<div id="view-note-content" class="markdown-body" style="font-size:1rem; line-height:1.7; color:var(--text);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Edit Note Modal ────────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="edit-note-modal">
|
||||
<div class="modal-content note-modal-compact" style="max-width:760px; max-height:90vh; overflow-y:auto;">
|
||||
@@ -246,7 +280,8 @@ $note_colors = [
|
||||
<input type="text" name="title" id="edit-note-title" class="form-control" placeholder="Title..." style="font-weight:600; font-size:1rem;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="label_name" id="edit-note-label" class="form-control" placeholder="Tag/Label name">
|
||||
<label style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.25rem; display:block;">Tags</label>
|
||||
<input type="text" name="label_name" id="edit-note-label" class="form-control tagify-input" placeholder="Add tags...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-editor-toolbar">
|
||||
@@ -374,7 +409,8 @@ $note_colors = [
|
||||
data-image="<?= htmlspecialchars((string)($n['image_path'] ?? '')) ?>"
|
||||
data-shares="<?= $shares_json ?>"
|
||||
data-mine="<?= $n['is_mine'] ?>"
|
||||
onclick="openNote(this)">
|
||||
data-updated="<?= date('M j, Y, H:i', strtotime($n['updated_at'] ?: $n['created_at'])) ?>"
|
||||
onclick="viewNote(this)">
|
||||
<?php if($n['image_path'] && file_exists($n['image_path'])): ?>
|
||||
<img src="<?= htmlspecialchars($n['image_path']) ?>" alt="Note image" class="note-card-img">
|
||||
<?php endif; ?>
|
||||
@@ -382,8 +418,12 @@ $note_colors = [
|
||||
<div class="note-card-title"><?= htmlspecialchars($n['title']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if(!empty($n['label_name'])): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:0.35rem; width:max-content; max-width:100%; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.7rem; font-weight:600;">
|
||||
<i class="fa fa-tag" style="font-size:0.65rem;"></i> <?= htmlspecialchars($n['label_name']) ?>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:0.35rem; margin-bottom:0.5rem;">
|
||||
<?php foreach(explode(',', $n['label_name']) as $lbl): $lbl=trim($lbl); if(!$lbl) continue; ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:0.35rem; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.7rem; font-weight:600;">
|
||||
<i class="fa fa-tag" style="font-size:0.65rem;"></i> <?= htmlspecialchars($lbl) ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="note-card-preview"><?= htmlspecialchars($preview) ?></div>
|
||||
@@ -478,7 +518,7 @@ function openNote(card) {
|
||||
document.getElementById('edit-note-color-input').value = color;
|
||||
document.getElementById('edit-note-preview').style.display = 'none';
|
||||
document.getElementById('edit-note-content').style.display = 'block';
|
||||
document.getElementById('edit-preview-btn').textContent = '👁 Preview';
|
||||
document.getElementById('edit-preview-btn').innerHTML = '👁 Preview';
|
||||
|
||||
// Color swatches
|
||||
document.querySelectorAll('#edit-note-colors .color-swatch').forEach(s => {
|
||||
@@ -518,6 +558,60 @@ function openNote(card) {
|
||||
document.getElementById('edit-note-modal').classList.add('active');
|
||||
}
|
||||
|
||||
let currentViewedCard = null;
|
||||
|
||||
function viewNote(card) {
|
||||
currentViewedCard = card;
|
||||
const title = card.dataset.title || 'Untitled Note';
|
||||
const content = card.dataset.content || '';
|
||||
const label = card.dataset.labelRaw || '';
|
||||
const image = card.dataset.image;
|
||||
const updatedAt = card.dataset.updated;
|
||||
const isMine = card.dataset.mine === '1';
|
||||
|
||||
document.getElementById('view-note-title').textContent = title;
|
||||
|
||||
|
||||
// Clear old tags
|
||||
document.getElementById('view-note-label-container').innerHTML = '';
|
||||
|
||||
if(label) {
|
||||
document.getElementById('view-note-label-container').style.display = 'flex';
|
||||
const lbls = label.split(',');
|
||||
lbls.forEach(l => {
|
||||
const span = document.createElement('span');
|
||||
span.style.cssText = 'display:inline-flex; align-items:center; gap:0.35rem; padding:3px 8px; border-radius:999px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-size:0.75rem; font-weight:600;';
|
||||
span.innerHTML = `<i class="fa fa-tag" style="font-size:0.7rem;"></i> ${l.trim()}`;
|
||||
document.getElementById('view-note-label-container').appendChild(span);
|
||||
});
|
||||
} else {
|
||||
document.getElementById('view-note-label-container').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('view-note-date').textContent = 'Updated ' + updatedAt;
|
||||
document.getElementById('view-note-shared-badge').style.display = !isMine ? 'inline-block' : 'none';
|
||||
|
||||
if(image) {
|
||||
document.getElementById('view-note-image-container').style.display = 'block';
|
||||
document.getElementById('view-note-image').src = image;
|
||||
} else {
|
||||
document.getElementById('view-note-image-container').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('view-note-content').innerHTML = marked.parse(content);
|
||||
|
||||
document.getElementById('view-note-edit-btn').style.display = isMine ? 'inline-flex' : 'none';
|
||||
|
||||
document.getElementById('view-note-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function transToEditNote() {
|
||||
if(currentViewedCard) {
|
||||
document.getElementById('view-note-modal').classList.remove('active');
|
||||
openNote(currentViewedCard);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNoteShares(jsonShares) {
|
||||
const list = document.getElementById('note-shares-list');
|
||||
list.innerHTML = '';
|
||||
@@ -542,9 +636,12 @@ function renderNoteShares(jsonShares) {
|
||||
|
||||
function removeNoteShare(shareId) {
|
||||
if(!shareId) return;
|
||||
if(!confirm('Unshare this note access?')) return;
|
||||
document.getElementById('remove-note-share-id').value = String(shareId);
|
||||
document.getElementById('remove-note-share-form').submit();
|
||||
appConfirm('Unshare this note access?').then(y => {
|
||||
if(y) {
|
||||
document.getElementById('remove-note-share-id').value = String(shareId);
|
||||
document.getElementById('remove-note-share-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterNotes() {
|
||||
@@ -553,17 +650,21 @@ function filterNotes() {
|
||||
document.querySelectorAll('.note-filter-item').forEach(card => {
|
||||
const t = (card.dataset.title || '').toLowerCase();
|
||||
const c = (card.dataset.content || '').toLowerCase();
|
||||
const cl = (card.dataset.label || '').toLowerCase();
|
||||
const okText = !q || t.includes(q) || c.includes(q) || cl.includes(q);
|
||||
const okLabel = !lbl || cl === lbl;
|
||||
// data-label is now comma separated
|
||||
const cardLabels = (card.dataset.label || '').split(',').map(s=>s.trim());
|
||||
const okText = !q || t.includes(q) || c.includes(q) || cardLabels.join(' ').includes(q);
|
||||
const okLabel = !lbl || cardLabels.includes(lbl);
|
||||
card.style.display = (okText && okLabel) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function deleteNote(id) {
|
||||
if(!confirm('Delete this note permanently?')) return;
|
||||
document.getElementById('delete-note-id').value = id;
|
||||
document.getElementById('delete-note-form').submit();
|
||||
appConfirm('Delete this note permanently?').then(y => {
|
||||
if(y) {
|
||||
document.getElementById('delete-note-id').value = id;
|
||||
document.getElementById('delete-note-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fix note card colors in dark mode
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
// ── DB migrations ─────────────────────────────────────────────────────────────
|
||||
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN folder_id INTEGER DEFAULT NULL"); } catch(Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE passwords ADD COLUMN tags TEXT DEFAULT NULL"); } catch(Exception $e) {}
|
||||
try {
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS password_folders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -25,15 +26,15 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
if(isset($_POST['create_password'])) {
|
||||
$stmt = $pdo->prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id, folder_id) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id, $_POST['folder_id'] ?: null]);
|
||||
$stmt = $pdo->prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id, folder_id, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id, $_POST['folder_id'] ?: null, trim($_POST['tags'] ?? '')]);
|
||||
}
|
||||
|
||||
if(isset($_POST['update_password'])) {
|
||||
$p = query("SELECT owner_id FROM passwords WHERE id=?", [$_POST['password_id']])->fetch();
|
||||
if($p && $p['owner_id'] == $user_id) {
|
||||
$stmt = $pdo->prepare("UPDATE passwords SET title=?, username=?, password=?, url=?, notes=?, folder_id=? WHERE id=? AND owner_id=?");
|
||||
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $_POST['folder_id'] ?: null, $_POST['password_id'], $user_id]);
|
||||
$stmt = $pdo->prepare("UPDATE passwords SET title=?, username=?, password=?, url=?, notes=?, folder_id=?, tags=? WHERE id=? AND owner_id=?");
|
||||
$stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $_POST['folder_id'] ?: null, trim($_POST['tags'] ?? ''), $_POST['password_id'], $user_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +145,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
||||
</div>
|
||||
</div>
|
||||
<div id="pw-view-notes" style="font-size:0.8rem; color:var(--text-light); margin-top:0.5rem; white-space:pre-wrap;"></div>
|
||||
<div id="pw-view-tags-container" style="display:none; flex-wrap:wrap; gap:0.25rem; margin-top:1rem; padding-top:0.5rem; border-top:1px dashed var(--border);"></div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:0.5rem; margin-top:1rem;">
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('password-view-modal').classList.remove('active')">Close</button>
|
||||
</div>
|
||||
@@ -194,6 +196,10 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
||||
<label>Notes</label>
|
||||
<textarea name="notes" id="pw-edit-notes" class="form-control" rows="2" placeholder="Optional notes..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tags</label>
|
||||
<input type="text" name="tags" id="pw-edit-tags" class="form-control tagify-input" placeholder="Add tags...">
|
||||
</div>
|
||||
<!-- Shares management section (populated by JS) -->
|
||||
<div id="pw-shares-section" style="display:none; border-top:1px solid var(--border); padding-top:0.75rem; margin-top:0.5rem;">
|
||||
<label style="font-size:0.8rem; color:var(--text-light); display:block; margin-bottom:0.4rem;">Shared with</label>
|
||||
@@ -298,7 +304,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" name="bulk_action" value="move_folder" class="btn btn-sm btn-secondary"><i class="fa fa-folder-arrow-down"></i> Move</button>
|
||||
<button type="submit" name="bulk_action" value="delete" class="btn btn-sm btn-danger" onclick="return confirm('Delete selected entries?')"><i class="fa fa-trash"></i> Delete</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="appConfirm('Delete selected entries?').then(y => { if(y) { const el = document.createElement('input'); el.type='hidden'; el.name='bulk_action'; el.value='delete'; this.form.appendChild(el); this.form.submit(); } })"><i class="fa fa-trash"></i> Delete</button>
|
||||
</div>
|
||||
|
||||
<!-- Entries List -->
|
||||
@@ -364,6 +370,7 @@ $active_folder = $_GET['vault_folder'] ?? 'all';
|
||||
data-notes="<?= htmlspecialchars(addslashes((string)($p['notes'] ?? ''))) ?>"
|
||||
data-folder-id="<?= htmlspecialchars((string)($p['folder_id'] ?? '')) ?>"
|
||||
data-shares="<?= $shares_json ?>"
|
||||
data-tags="<?= htmlspecialchars((string)($p['tags'] ?? '')) ?>"
|
||||
onclick="openViewEntry(this)">
|
||||
<!-- Checkbox (only for owned items) -->
|
||||
<?php if($p['is_mine']): ?>
|
||||
@@ -473,10 +480,33 @@ function openViewEntry(row) {
|
||||
document.getElementById('pw-view-pw').value = row.dataset.pw;
|
||||
document.getElementById('pw-view-pw').type = 'password';
|
||||
document.getElementById('pw-view-toggle').innerHTML = '<i class="fa fa-eye"></i>';
|
||||
const urlEl = document.getElementById('pw-view-url');
|
||||
urlEl.textContent = '';
|
||||
urlEl.textContent = row.dataset.url || '';
|
||||
const urlEl = document.createElement('div');
|
||||
document.getElementById('pw-view-url').innerHTML = ''; // Clear existing
|
||||
if(row.dataset.url) {
|
||||
urlEl.innerHTML = `<a href="${row.dataset.url}" target="_blank" style="color:var(--primary);">${row.dataset.url}</a>`;
|
||||
document.getElementById('pw-view-url').appendChild(urlEl);
|
||||
}
|
||||
|
||||
document.getElementById('pw-view-notes').textContent = row.dataset.notes || '';
|
||||
|
||||
// Tag rendering
|
||||
const tagsContainer = document.getElementById('pw-view-tags-container');
|
||||
tagsContainer.innerHTML = '';
|
||||
const tagsStr = row.dataset.tags || '';
|
||||
if(tagsStr) {
|
||||
tagsContainer.style.display = 'flex';
|
||||
tagsStr.split(',').forEach(t => {
|
||||
t = t.trim();
|
||||
if(!t) return;
|
||||
const span = document.createElement('span');
|
||||
span.style.cssText = 'font-size:0.65rem; padding:2px 8px; border-radius:12px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-weight:600; display:inline-flex; align-items:center; gap:0.2rem;';
|
||||
span.innerHTML = `<i class="fa fa-tag" style="font-size:0.6rem;"></i> ${t}`;
|
||||
tagsContainer.appendChild(span);
|
||||
});
|
||||
} else {
|
||||
tagsContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('password-view-modal').classList.add('active');
|
||||
}
|
||||
|
||||
@@ -504,6 +534,7 @@ function openAddPassword() {
|
||||
document.getElementById('pw-edit-url').value = '';
|
||||
document.getElementById('pw-edit-notes').value = '';
|
||||
document.getElementById('pw-edit-folder').value = '';
|
||||
document.getElementById('pw-edit-tags').value = '';
|
||||
document.getElementById('pw-shares-section').style.display = 'none';
|
||||
document.getElementById('pw-shares-list').innerHTML = '';
|
||||
document.getElementById('pw-edit-delete-wrap').style.display = 'none';
|
||||
@@ -534,6 +565,7 @@ function openEditEntry(row) {
|
||||
document.getElementById('pw-edit-url').value = row.dataset.url || '';
|
||||
document.getElementById('pw-edit-notes').value = row.dataset.notes || '';
|
||||
document.getElementById('pw-edit-folder').value = row.dataset.folderId || '';
|
||||
document.getElementById('pw-edit-tags').value = row.dataset.tags || '';
|
||||
document.getElementById('pw-edit-delete-wrap').style.display = 'block';
|
||||
renderPasswordShares(row.dataset.shares || '[]');
|
||||
document.getElementById('password-edit-modal').classList.add('active');
|
||||
@@ -568,15 +600,21 @@ function renderPasswordShares(jsonShares) {
|
||||
|
||||
function removePasswordShare(shareId) {
|
||||
if(!shareId) return;
|
||||
if(!confirm('Unshare this password access?')) return;
|
||||
document.getElementById('remove-share-id').value = String(shareId);
|
||||
document.getElementById('remove-share-form').submit();
|
||||
appConfirm('Unshare this password access?').then(y => {
|
||||
if(y) {
|
||||
document.getElementById('remove-share-id').value = String(shareId);
|
||||
document.getElementById('remove-share-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletePassword() {
|
||||
if(!confirm('Delete this entry permanently?')) return;
|
||||
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
|
||||
document.getElementById('delete-pw-form').submit();
|
||||
appConfirm('Delete this entry permanently?').then(y => {
|
||||
if(y) {
|
||||
document.getElementById('delete-pw-id').value = document.getElementById('pw-edit-id').value;
|
||||
document.getElementById('delete-pw-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Toggles ───────────────────────────────────────────────────────────────────
|
||||
@@ -588,7 +626,8 @@ function filterVault(q) {
|
||||
q = q.toLowerCase();
|
||||
document.querySelectorAll('.vault-entry').forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(q) ? '' : 'none';
|
||||
const tags = (row.dataset.tags || '').toLowerCase();
|
||||
row.style.display = (text.includes(q) || tags.includes(q)) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -602,9 +641,12 @@ function showAddFolderPrompt() {
|
||||
}
|
||||
|
||||
function deleteFolder(id, name) {
|
||||
if(!confirm(`Delete folder "${name}"? Entries will not be deleted.`)) return;
|
||||
document.getElementById('del-folder-id').value = id;
|
||||
document.getElementById('del-folder-form').submit();
|
||||
appConfirm(`Delete folder "${name}"? Entries will not be deleted.`).then(y => {
|
||||
if(y) {
|
||||
document.getElementById('del-folder-id').value = id;
|
||||
document.getElementById('del-folder-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Hotkeys on hovered row ────────────────────────────────────────────────────
|
||||
|
||||
@@ -289,9 +289,12 @@ function renameItem(id, currName) {
|
||||
}
|
||||
|
||||
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();
|
||||
appConfirm(`Delete "${name}"? This cannot be undone.`).then(y => {
|
||||
if(y) {
|
||||
document.getElementById('delete-item-id').value = id;
|
||||
document.getElementById('delete-item-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openShare(id) {
|
||||
@@ -300,9 +303,12 @@ function openShare(id) {
|
||||
}
|
||||
|
||||
function removeProjectShare(shareId) {
|
||||
if(!confirm('Unshare this item?')) return;
|
||||
document.getElementById('remove-share-id').value = shareId;
|
||||
document.getElementById('remove-share-form').submit();
|
||||
appConfirm('Unshare this item?').then(y => {
|
||||
if(y) {
|
||||
document.getElementById('remove-share-id').value = shareId;
|
||||
document.getElementById('remove-share-form').submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showNewFolderDialog() {
|
||||
|
||||
@@ -4,6 +4,7 @@ try { $pdo->exec("ALTER TABLE tasks ADD COLUMN assignor_type TEXT DEFAULT 'user'
|
||||
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN assignor_group_id INTEGER"); } catch(Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN status_updated_at DATETIME"); } catch(Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN due_date DATE"); } catch(Exception $e) {}
|
||||
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT NULL"); } catch(Exception $e) {}
|
||||
try { $pdo->exec("CREATE TABLE IF NOT EXISTS task_attachments (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, file_name TEXT, file_path TEXT, owner_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); } catch(Exception $e) {}
|
||||
try {
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS system_config (k TEXT PRIMARY KEY, v TEXT)");
|
||||
@@ -117,8 +118,9 @@ if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_task'])) {
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO tasks (title, description, assignor_id, assignor_type, assignor_group_id, assignee_type, assignee_id, custom_assignee_name, status_updated_at, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)");
|
||||
if(!$stmt->execute([$title, $desc, $user_id, $assignor_type, $assignor_group_id, $a_type, $a_id, $custom_name, $due_date])) {
|
||||
$tags = trim($_POST['tags'] ?? '');
|
||||
$stmt = $pdo->prepare("INSERT INTO tasks (title, description, assignor_id, assignor_type, assignor_group_id, assignee_type, assignee_id, custom_assignee_name, status_updated_at, due_date, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)");
|
||||
if(!$stmt->execute([$title, $desc, $user_id, $assignor_type, $assignor_group_id, $a_type, $a_id, $custom_name, $due_date, $tags])) {
|
||||
if(isset($_POST['ajax_submit'])) { echo json_encode(['success'=>false, 'error'=>'Task insert failed']); exit; }
|
||||
}
|
||||
$new_task_id = $pdo->lastInsertId();
|
||||
@@ -434,6 +436,16 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
<i class="fas fa-paperclip" style="font-size:0.85rem;"></i>
|
||||
<input type="file" multiple style="display:none;" onchange="handleTaskFilesSelect(this)">
|
||||
</label>
|
||||
|
||||
<div style="position:relative;" title="Tags">
|
||||
<div id="tag-icon-btn" style="cursor:pointer; display:flex; align-items:center; justify-content:center; width:30px; height:30px; border-radius:50%; background:var(--surface); border:1px solid var(--border); color:var(--text-light); transition:background 0.2s;" onclick="document.getElementById('tags-dropdown-container').style.display='block';">
|
||||
<i class="fas fa-tag" style="font-size:0.85rem;"></i>
|
||||
</div>
|
||||
<div id="tags-dropdown-container" style="display:none; position:absolute; top:35px; right:-20px; background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:0.75rem; z-index:100; box-shadow:0 10px 15px -3px rgba(0,0,0,0.1); width:280px; align-items:stretch;">
|
||||
<label style="font-size:0.75rem; font-weight:600; color:var(--text); margin-bottom:0.4rem; display:block; text-align:left;">Tags</label>
|
||||
<input type="text" name="tags" class="form-control tagify-input" style="font-size:0.8rem; border:1px solid var(--border); border-radius:4px; padding:4px; max-width:100%;" placeholder="Add tags...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn" style="padding:0.3rem 1rem; border-radius:20px; font-weight:600; font-size:0.8rem;" onclick="submitCreateTaskForm()">Create</button>
|
||||
<button type="submit" id="hidden-create-btn" name="create_task" style="display:none;"></button>
|
||||
@@ -494,7 +506,15 @@ body.dark-mode .kanban-task { border: 1px solid rgba(255,255,255,0.05); border-l
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem;">
|
||||
<?php if(!empty($t['tags'])): ?>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:0.25rem; margin-top:0.5rem;">
|
||||
<?php foreach(explode(',', $t['tags']) as $tag): $tag=trim($tag); if(!$tag) continue; ?>
|
||||
<span style="font-size:0.65rem; padding:2px 8px; border-radius:12px; background:rgba(var(--primary-rgb),0.12); color:var(--primary); font-weight:600;"><i class="fa fa-tag" style="font-size:0.6rem;"></i> <?= htmlspecialchars($tag) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem; margin-top:0.5rem;">
|
||||
<?php
|
||||
echo '<span class="badge-role role-to" style="padding:2px 8px;">';
|
||||
if($t['assignee_type'] == 'custom') echo '👤 '.htmlspecialchars((string)$t['custom_assignee_name']);
|
||||
@@ -773,9 +793,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.addEventListener('mousedown', function(e) {
|
||||
let container = document.getElementById('to-dropdown-container');
|
||||
if(container && !container.contains(e.target) && e.target.id !== 'to-input' && !e.target.closest('label')) {
|
||||
if(container && !container.contains(e.target) && e.target.id !== 'to-input' && !e.target.closest('#to-icon-btn')) {
|
||||
container.style.display = 'none';
|
||||
}
|
||||
|
||||
let tagContainer = document.getElementById('tags-dropdown-container');
|
||||
if(tagContainer && !tagContainer.contains(e.target) && !e.target.closest('#tag-icon-btn') && !e.target.closest('.tagify')) {
|
||||
tagContainer.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
let pendingTaskFiles = [];
|
||||
|
||||
Reference in New Issue
Block a user