Files
Task-Management/views/notes.php
2026-03-27 00:03:48 +07:00

466 lines
26 KiB
PHP

<?php
// ── DB migrations ─────────────────────────────────────────────────────────────
try { $pdo->exec("ALTER TABLE notes ADD COLUMN color TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN image_path TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE notes ADD COLUMN updated_at DATETIME DEFAULT NULL"); } catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
owner_id INTEGER,
color TEXT DEFAULT NULL,
image_path TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT NULL,
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS note_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
share_type TEXT,
share_id INTEGER,
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
)");
} catch(Exception $e) {}
// ── Handle POST ───────────────────────────────────────────────────────────────
if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(isset($_POST['create_note'])) {
$image_path = null;
if(isset($_FILES['note_image']) && $_FILES['note_image']['error'] === 0) {
$allowed = ['image/jpeg','image/png','image/gif','image/webp'];
if(in_array($_FILES['note_image']['type'], $allowed)) {
$ext = pathinfo($_FILES['note_image']['name'], PATHINFO_EXTENSION);
$fname = 'note_' . $user_id . '_' . time() . '.' . $ext;
$dest = 'uploads/' . $fname;
if(move_uploaded_file($_FILES['note_image']['tmp_name'], $dest)) $image_path = $dest;
}
}
$stmt = $pdo->prepare("INSERT INTO notes (title, content, owner_id, color, image_path) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $user_id, $_POST['color'] ?? null, $image_path]);
}
if(isset($_POST['update_note'])) {
$n = query("SELECT owner_id, image_path FROM notes WHERE id=?", [$_POST['note_id']])->fetch();
if($n && $n['owner_id'] == $user_id) {
$image_path = $n['image_path'];
if(isset($_FILES['note_image']) && $_FILES['note_image']['error'] === 0) {
$allowed = ['image/jpeg','image/png','image/gif','image/webp'];
if(in_array($_FILES['note_image']['type'], $allowed)) {
$ext = pathinfo($_FILES['note_image']['name'], PATHINFO_EXTENSION);
$fname = 'note_' . $user_id . '_' . time() . '.' . $ext;
$dest = 'uploads/' . $fname;
if(move_uploaded_file($_FILES['note_image']['tmp_name'], $dest)) {
if($image_path && file_exists($image_path)) @unlink($image_path);
$image_path = $dest;
}
}
}
if(isset($_POST['remove_image']) && $_POST['remove_image']) {
if($image_path && file_exists($image_path)) @unlink($image_path);
$image_path = null;
}
$stmt = $pdo->prepare("UPDATE notes SET title=?, content=?, color=?, image_path=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND owner_id=?");
$stmt->execute([$_POST['title'] ?? 'Untitled', $_POST['content'], $_POST['color'] ?? null, $image_path, $_POST['note_id'], $user_id]);
}
}
if(isset($_POST['delete_note'])) {
$n = query("SELECT owner_id, image_path FROM notes WHERE id=?", [$_POST['note_id']])->fetch();
if($n && $n['owner_id'] == $user_id) {
if($n['image_path'] && file_exists($n['image_path'])) @unlink($n['image_path']);
query("DELETE FROM notes WHERE id=? AND owner_id=?", [$_POST['note_id'], $user_id]);
}
}
if(isset($_POST['share_note'])) {
$n_id = $_POST['note_id'];
$share = $_POST['share_target'];
$parts = explode('_', $share, 2);
if(count($parts)===2) {
$stmt = $pdo->prepare("INSERT INTO note_shares (note_id, share_type, share_id) VALUES (?, ?, ?)");
$stmt->execute([$n_id, $parts[0], $parts[1]]);
$n = query("SELECT title FROM notes WHERE id=?", [$n_id])->fetch();
$msg = "A note '{$n['title']}' has been shared with you.";
if($parts[0] === 'user') notify_user($parts[1], $msg);
else notify_group($parts[1], $msg);
}
}
}
// ── Fetch Notes ───────────────────────────────────────────────────────────────
$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';
$sql = "SELECT n.*, (n.owner_id = ?) as is_mine, u.username as owner_name FROM notes n
LEFT JOIN note_shares ns ON n.id = ns.note_id
LEFT JOIN users u ON n.owner_id = u.id
WHERE n.owner_id = ?
OR (ns.share_type='user' AND ns.share_id = ?)
OR (ns.share_type='group' AND ns.share_id IN ($group_in))
GROUP BY n.id ORDER BY n.updated_at DESC, n.created_at DESC";
$params = array_merge([$user_id, $user_id, $user_id], $my_group_ids);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$notes = $stmt->fetchAll();
$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll();
$all_groups = query("SELECT * FROM groups")->fetchAll();
// Define note colors
$note_colors = [
null => ['bg'=>'var(--surface)', 'label'=>'Default'],
'#fff9c4' => ['bg'=>'#fff9c4', 'label'=>'Yellow'],
'#d4edda' => ['bg'=>'#d4edda', 'label'=>'Green'],
'#cce5ff' => ['bg'=>'#cce5ff', 'label'=>'Blue'],
'#f8d7da' => ['bg'=>'#f8d7da', 'label'=>'Pink'],
'#e2d9f3' => ['bg'=>'#e2d9f3', 'label'=>'Purple'],
'#fde8d8' => ['bg'=>'#fde8d8', 'label'=>'Orange'],
'#f5f5f5' => ['bg'=>'#f5f5f5', 'label'=>'Gray'],
];
// dark mode color overrides handled via JS
?>
<div class="header" style="margin-bottom:1rem;">
<h1 style="font-size:1.5rem; font-weight:700;">📝 Notes</h1>
<button class="btn" onclick="document.getElementById('new-note-modal').classList.add('active')">
<i class="fa fa-plus"></i> New Note
</button>
</div>
<!-- ── New Note Modal ─────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="new-note-modal">
<div class="modal-content" style="max-width:600px; max-height:90vh; overflow-y:auto;">
<h3 style="margin-bottom:1rem;">New Note</h3>
<form method="POST" enctype="multipart/form-data" id="new-note-form">
<input type="hidden" name="create_note" value="1">
<div class="form-group">
<input type="text" name="title" class="form-control" placeholder="Title..." id="new-note-title" style="font-weight:600; font-size:1rem;">
</div>
<!-- WYSIWYG Toolbar -->
<div class="note-editor-toolbar" id="new-note-toolbar">
<button type="button" class="note-toolbar-btn" onclick="formatNote('new-note-content','bold')" title="Bold"><b>B</b></button>
<button type="button" class="note-toolbar-btn" onclick="formatNote('new-note-content','italic')" title="Italic"><i>I</i></button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','# ')" title="H1">H1</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','## ')" title="H2">H2</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','### ')" title="H3">H3</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','- ')" title="Bullet">• List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','1. ')" title="Numbered">1. List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','- [ ] ')" title="Todo">☑ Todo</button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('new-note-content','`','`')" title="Code"><code>Code</code></button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('new-note-content','```\n','\n```')" title="Code Block">{ } Block</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','> ')" title="Quote">❝ Quote</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('new-note-content','---\n')" title="Divider">─ Line</button>
<button type="button" class="note-toolbar-btn" id="new-preview-btn" onclick="togglePreview('new-note-content','new-note-preview','new-preview-btn')">👁 Preview</button>
</div>
<div class="form-group">
<textarea name="content" id="new-note-content" class="note-content-area" placeholder="Write your note... (Markdown supported)" rows="8"></textarea>
<div id="new-note-preview" class="markdown-body" style="display:none; min-height:160px; border:1px solid var(--border); border-radius:0.5rem; padding:0.75rem; background:var(--bg); font-size:0.9rem;"></div>
</div>
<!-- Color picker -->
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Note Color</label>
<div class="note-color-palette" id="new-note-colors">
<?php foreach($note_colors as $hex => $info): ?>
<div class="color-swatch <?= !$hex ? 'selected' : '' ?>"
style="background:<?= $hex ?: '#fff' ?>; border:2px solid var(--border);"
onclick="selectColor(this,'new-note-color-input','<?= $hex ?>')"
title="<?= $info['label'] ?>"></div>
<?php endforeach; ?>
</div>
<input type="hidden" name="color" id="new-note-color-input" value="">
</div>
<!-- Image upload -->
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Attach Image (optional)</label>
<input type="file" name="note_image" accept="image/*" class="form-control">
</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('new-note-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn"><i class="fa fa-save"></i> Save Note</button>
</div>
</form>
</div>
</div>
<!-- ── Edit Note Modal ────────────────────────────────────────────────────── -->
<div class="modal-overlay" id="edit-note-modal">
<div class="modal-content" style="max-width:600px; max-height:90vh; overflow-y:auto;">
<h3 style="margin-bottom:1rem;">Edit Note</h3>
<form method="POST" enctype="multipart/form-data" id="edit-note-form">
<input type="hidden" name="update_note" value="1">
<input type="hidden" name="note_id" id="edit-note-id">
<div class="form-group">
<input type="text" name="title" id="edit-note-title" class="form-control" placeholder="Title..." style="font-weight:600; font-size:1rem;">
</div>
<div class="note-editor-toolbar">
<button type="button" class="note-toolbar-btn" onclick="formatNote('edit-note-content','bold')"><b>B</b></button>
<button type="button" class="note-toolbar-btn" onclick="formatNote('edit-note-content','italic')"><i>I</i></button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','# ')">H1</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','## ')">H2</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','### ')">H3</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','- ')">• List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','1. ')">1. List</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','- [ ] ')">☑ Todo</button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('edit-note-content','`','`')"><code>Code</code></button>
<button type="button" class="note-toolbar-btn" onclick="wrapMarkdown('edit-note-content','```\n','\n```')">{ } Block</button>
<button type="button" class="note-toolbar-btn" onclick="insertMarkdown('edit-note-content','> ')">❝ Quote</button>
<button type="button" class="note-toolbar-btn" id="edit-preview-btn" onclick="togglePreview('edit-note-content','edit-note-preview','edit-preview-btn')">👁 Preview</button>
</div>
<div class="form-group">
<textarea name="content" id="edit-note-content" class="note-content-area" rows="8"></textarea>
<div id="edit-note-preview" class="markdown-body" style="display:none; min-height:160px; border:1px solid var(--border); border-radius:0.5rem; padding:0.75rem; background:var(--bg); font-size:0.9rem;"></div>
</div>
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Note Color</label>
<div class="note-color-palette" id="edit-note-colors">
<?php foreach($note_colors as $hex => $info): ?>
<div class="color-swatch"
style="background:<?= $hex ?: '#fff' ?>; border:2px solid var(--border);"
onclick="selectColor(this,'edit-note-color-input','<?= $hex ?>')"
title="<?= $info['label'] ?>"></div>
<?php endforeach; ?>
</div>
<input type="hidden" name="color" id="edit-note-color-input" value="">
</div>
<div id="edit-current-image" style="display:none; margin-bottom:0.75rem;">
<label style="font-size:0.8rem; color:var(--text-light);">Current Image:</label>
<div style="display:flex; align-items:center; gap:0.5rem; margin-top:0.25rem;">
<img id="edit-current-img-preview" src="" alt="Current" style="height:60px; border-radius:0.5rem; object-fit:cover;">
<label style="display:flex; align-items:center; gap:0.25rem; font-size:0.8rem; cursor:pointer;">
<input type="checkbox" name="remove_image" value="1"> Remove image
</label>
</div>
</div>
<div class="form-group">
<label style="font-size:0.8rem; color:var(--text-light);">Replace Image (optional)</label>
<input type="file" name="note_image" accept="image/*" class="form-control">
</div>
<!-- Share section (only for owner) -->
<div id="edit-share-section" style="border-top:1px solid var(--border); padding-top:0.75rem; margin-top:0.5rem;">
<label style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.4rem; display:block;">Share this note</label>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
<select name="share_target" id="edit-share-target" class="form-control" style="flex:1; min-width:150px; padding:0.4rem;">
<?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>
<button type="submit" name="share_note" value="1" class="btn btn-sm" onclick="this.form.action=''; document.querySelector('[name=update_note]').disabled=true; this.form.onsubmit=null;" formaction="">
<i class="fa fa-share-nodes"></i> Share
</button>
</div>
</div>
<div style="display:flex; gap:0.5rem; justify-content:space-between; margin-top:1rem; flex-wrap:wrap;">
<button type="button" class="btn btn-danger btn-sm" onclick="deleteNote(document.getElementById('edit-note-id').value)"><i class="fa fa-trash"></i> Delete</button>
<div style="display:flex; gap:0.5rem;">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('edit-note-modal').classList.remove('active')">Cancel</button>
<button type="submit" class="btn"><i class="fa fa-save"></i> Save Changes</button>
</div>
</div>
</form>
</div>
</div>
<!-- Delete Note Form -->
<form method="POST" id="delete-note-form" style="display:none;">
<input type="hidden" name="delete_note" value="1">
<input type="hidden" name="note_id" id="delete-note-id">
</form>
<!-- Share Note Form (separate POST for share_note) -->
<form method="POST" id="share-note-standalone-form" style="display:none;">
<input type="hidden" name="share_note" value="1">
<input type="hidden" name="note_id" id="share-note-standalone-id">
<input type="hidden" name="share_target" id="share-note-standalone-target">
</form>
<!-- Markdown Libs -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- ── Notes Grid ─────────────────────────────────────────────────────────── -->
<?php if(empty($notes)): ?>
<div style="text-align:center; padding:4rem 2rem; color:var(--text-light);">
<div style="font-size:3rem; margin-bottom:1rem;">📝</div>
<h3 style="margin-bottom:0.5rem;">No notes yet</h3>
<p>Click <strong>New Note</strong> to start writing something.</p>
</div>
<?php else: ?>
<div class="notes-grid">
<?php foreach($notes as $n):
$bg = $n['color'] ?: 'var(--surface)';
$preview = strip_tags(isset($n['content']) ? (function_exists('marked') ? $n['content'] : $n['content']) : '');
$preview = mb_substr($preview, 0, 120) . (mb_strlen($preview) > 120 ? '...' : '');
?>
<div class="note-card"
style="background:<?= htmlspecialchars($bg) ?>;"
data-id="<?= $n['id'] ?>"
data-title="<?= htmlspecialchars(addslashes((string)$n['title'])) ?>"
data-content="<?= htmlspecialchars(addslashes((string)$n['content'])) ?>"
data-color="<?= htmlspecialchars((string)$n['color']) ?>"
data-image="<?= htmlspecialchars((string)($n['image_path'] ?? '')) ?>"
data-mine="<?= $n['is_mine'] ?>"
onclick="openNote(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; ?>
<?php if($n['title']): ?>
<div class="note-card-title"><?= htmlspecialchars($n['title']) ?></div>
<?php endif; ?>
<div class="note-card-preview"><?= htmlspecialchars($preview) ?></div>
<div class="note-card-footer">
<div style="display:flex; align-items:center; gap:0.4rem;">
<?php if($n['color']): ?><div class="note-color-dot" style="background:<?= htmlspecialchars($n['color']) ?>;"></div><?php endif; ?>
<span style="font-size:0.7rem; color:var(--text-light);"><?= date('M j', strtotime($n['updated_at'] ?: $n['created_at'])) ?></span>
<?php if(!$n['is_mine']): ?>
<span class="note-card-shared-badge">Shared</span>
<?php endif; ?>
</div>
<div class="note-card-actions" onclick="event.stopPropagation()">
<?php if($n['is_mine']): ?>
<button class="btn btn-sm btn-danger" style="padding:3px 7px;" onclick="deleteNote(<?= $n['id'] ?>)" title="Delete"><i class="fa fa-trash"></i></button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<script>
// ── WYSIWYG Markdown Tools ────────────────────────────────────────────────────
function formatNote(textareaId, cmd) {
const ta = document.getElementById(textareaId);
const start = ta.selectionStart, end = ta.selectionEnd;
const sel = ta.value.substring(start, end);
let wrap = cmd === 'bold' ? '**' : '_';
const replacement = `${wrap}${sel || 'text'}${wrap}`;
ta.value = ta.value.substring(0, start) + replacement + ta.value.substring(end);
ta.focus();
ta.selectionStart = start + wrap.length;
ta.selectionEnd = start + wrap.length + (sel || 'text').length;
}
function insertMarkdown(textareaId, prefix) {
const ta = document.getElementById(textareaId);
const start = ta.selectionStart;
// Go to start of line
const before = ta.value.substring(0, start);
const lineStart = before.lastIndexOf('\n') + 1;
ta.value = ta.value.substring(0, lineStart) + prefix + ta.value.substring(lineStart);
ta.focus();
const newPos = lineStart + prefix.length + (start - lineStart);
ta.selectionStart = ta.selectionEnd = newPos;
}
function wrapMarkdown(textareaId, before, after) {
const ta = document.getElementById(textareaId);
const start = ta.selectionStart, end = ta.selectionEnd;
const sel = ta.value.substring(start, end);
ta.value = ta.value.substring(0, start) + before + (sel || 'code') + after + ta.value.substring(end);
ta.focus();
}
function togglePreview(contentId, previewId, btnId) {
const content = document.getElementById(contentId);
const preview = document.getElementById(previewId);
const btn = document.getElementById(btnId);
if(preview.style.display === 'none') {
preview.innerHTML = marked.parse(content.value || '');
preview.style.display = 'block';
content.style.display = 'none';
btn.textContent = '✏️ Edit';
} else {
preview.style.display = 'none';
content.style.display = 'block';
btn.textContent = '👁 Preview';
}
}
function selectColor(el, inputId, color) {
el.closest('.note-color-palette').querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
el.classList.add('selected');
document.getElementById(inputId).value = color;
}
function openNote(card) {
const id = card.dataset.id;
const title = card.dataset.title;
const content = card.dataset.content;
const color = card.dataset.color;
const image = card.dataset.image;
const isMine = card.dataset.mine === '1';
document.getElementById('edit-note-id').value = id;
document.getElementById('edit-note-title').value = title;
document.getElementById('edit-note-content').value = content;
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';
// Color swatches
document.querySelectorAll('#edit-note-colors .color-swatch').forEach(s => {
s.classList.remove('selected');
const swatchColor = s.getAttribute('onclick').match(/'([^']*)'/g);
if(swatchColor && swatchColor[1] && swatchColor[1].replace(/'/g,'') === color) s.classList.add('selected');
if(!color && s.title === 'Default') s.classList.add('selected');
});
// Image preview
const imgBlock = document.getElementById('edit-current-image');
if(image) {
imgBlock.style.display = 'block';
document.getElementById('edit-current-img-preview').src = image;
} else {
imgBlock.style.display = 'none';
}
// Share section
document.getElementById('edit-share-section').style.display = isMine ? 'block' : 'none';
// Wire up share button
const shareBtn = document.querySelector('#edit-note-form button[name="share_note"]');
if(shareBtn) {
shareBtn.onclick = (e) => {
e.preventDefault();
const target = document.getElementById('edit-share-target').value;
document.getElementById('share-note-standalone-id').value = id;
document.getElementById('share-note-standalone-target').value = target;
document.getElementById('share-note-standalone-form').submit();
};
}
document.getElementById('edit-note-modal').classList.add('active');
}
function deleteNote(id) {
if(!confirm('Delete this note permanently?')) return;
document.getElementById('delete-note-id').value = id;
document.getElementById('delete-note-form').submit();
}
// Fix note card colors in dark mode
function applyDarkNoteColors() {
const isDark = document.body.classList.contains('dark-mode');
document.querySelectorAll('.note-card').forEach(card => {
const origColor = card.dataset.color;
if(!origColor && isDark) {
card.style.background = 'var(--surface)';
}
});
}
const observer = new MutationObserver(applyDarkNoteColors);
observer.observe(document.body, {attributes: true, attributeFilter: ['class']});
applyDarkNoteColors();
</script>