+
+ +
diff --git a/views/login.php b/views/login.php new file mode 100644 index 0000000..3ae1770 --- /dev/null +++ b/views/login.php @@ -0,0 +1,30 @@ + + + + + + Login - TaskManage + + + + + + + diff --git a/views/notes.php b/views/notes.php new file mode 100644 index 0000000..9451e75 --- /dev/null +++ b/views/notes.php @@ -0,0 +1,83 @@ + +prepare("INSERT INTO notes (title, content, owner_id) VALUES (?, ?, ?)"); + $stmt->execute([$_POST['title'], $_POST['content'], $user_id]); +} + +if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_note'])) { + $n_id = $_POST['note_id']; + $share = $_POST['share_target']; // 'user_1' + $parts = explode('_', $share); + + $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 FROM notes n + LEFT JOIN note_shares ns ON n.id = ns.note_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.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(); +?> +
+

Markdown Notes

+
+ + + +
+
+

Create Note

+
+
+
+ +
+ +
+
+ +
+

My Notes

+ +
+
+

Shared' : '' ?>

+ +
+ + + +
+ +
+
+ +
+
+ +

No notes written yet.

+
+
diff --git a/views/notifications.php b/views/notifications.php new file mode 100644 index 0000000..567f68a --- /dev/null +++ b/views/notifications.php @@ -0,0 +1,36 @@ + +prepare("UPDATE notifications SET is_read = 1 WHERE user_id = ?"); + $stmt->execute([$user_id]); + header("Location: ?action=notifications"); + exit; +} + +$stmt = $pdo->prepare("SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC"); +$stmt->execute([$user_id]); +$notifications = $stmt->fetchAll(); +?> +
+

Notifications

+ 0): ?> +
+ +
+ +
+ +
+ +

No notifications yet.

+ +
    + +
  • + +
    +
  • + +
+ +
diff --git a/views/passwords.php b/views/passwords.php new file mode 100644 index 0000000..f2ddb45 --- /dev/null +++ b/views/passwords.php @@ -0,0 +1,165 @@ + +prepare("INSERT INTO passwords (title, username, password, url, notes, owner_id) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt->execute([$_POST['title'], $_POST['username'], $_POST['password'], $_POST['url'], $_POST['notes'], $user_id]); +} + +if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_action'])) { + if(isset($_POST['selected_passwords'])) { + $selected = $_POST['selected_passwords']; + $action = $_POST['bulk_action']; + + if($action === 'delete') { + foreach($selected as $p_id) { + // Check if owner + $p = query("SELECT owner_id FROM passwords WHERE id=?", [$p_id])->fetch(); + if($p && $p['owner_id']==$user_id) { + query("DELETE FROM passwords WHERE id=?", [$p_id]); + } + } + } elseif($action === 'share') { + $share_target = $_POST['share_target']; // 'user_1' + $parts = explode('_', $share_target); + foreach($selected as $p_id) { + $p = query("SELECT owner_id, title FROM passwords WHERE id=?", [$p_id])->fetch(); + if($p && $p['owner_id']==$user_id) { + $stmt = $pdo->prepare("INSERT INTO password_shares (password_id, share_type, share_id) VALUES (?, ?, ?)"); + $stmt->execute([$p_id, $parts[0], $parts[1]]); + $msg = "A password '{$p['title']}' has been shared with you."; + if($parts[0] === 'user') notify_user($parts[1], $msg); + else notify_group($parts[1], $msg); + } + } + } + } +} + +// Fetch passwords (owned + shared) +$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'; +$params = array_merge([$user_id, $user_id], $my_group_ids); + +$sql = "SELECT p.*, (p.owner_id = ?) as is_mine FROM passwords p + LEFT JOIN password_shares ps ON p.id = ps.password_id + WHERE 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.title"; +$params = array_merge([$user_id, $user_id, $user_id], $my_group_ids); +$stmt = $pdo->prepare($sql); +$stmt->execute($params); +$passwords = $stmt->fetchAll(); + +$all_users = query("SELECT id, username FROM users WHERE id != ?", [$user_id])->fetchAll(); +$all_groups = query("SELECT * FROM groups")->fetchAll(); +?> +
+

Passwords Vault

+
+ + + +
+
+

Add Password

+
+
+
+
+ + +
+
+
+ +
+
+ +
+

My Vault

+
+
+
+ +
+ + +
+
+
Shortcuts:
Ctrl+B (Copy User), Ctrl+C (Copy Password)
Hover row to activate
+
+ + + + + + + + + + + + +
TitleUsernameActions
Shared' : '' ?> + + +
+
+
+
+ + diff --git a/views/projects.php b/views/projects.php new file mode 100644 index 0000000..5974966 --- /dev/null +++ b/views/projects.php @@ -0,0 +1,174 @@ + +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']; + $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; // For AJAX +} + +if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_project'])) { + $pr_id = $_POST['project_id']; + $share = $_POST['share_target']; // 'user_1' + $parts = explode('_', $share); + + $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 items in current folder +$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(); +?> +
+

Project Files Explorer

+
+ +
+ fetch(); + ?> + ← Back + +
Root Directory
+ +
+
+ + +
+
+
+ +
+ +
+ SVG Drop files here to upload instantly +

(HTML5 Drag & Drop) +
+ +
+ +
+
+ + + + + + +
+
+ Owner: + Shared' : '' ?> +
+ +
+ +
+ + + +
+
+ + + +
+ +
+
+ +

Empty folder.

+
+
+ + + + + diff --git a/views/tasks.php b/views/tasks.php new file mode 100644 index 0000000..ed17404 --- /dev/null +++ b/views/tasks.php @@ -0,0 +1,957 @@ +exec("ALTER TABLE tasks ADD COLUMN assignor_type TEXT DEFAULT 'user'"); } catch(Exception $e) {} +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("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)"); + $pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('completed_days', '7')"); + $pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('archived_days', '90')"); +} catch(Exception $e) {} + +// Auto-cleanup lifecycle hooks +$conf_comp = query("SELECT v FROM system_config WHERE k='completed_days'")->fetchColumn() ?: 7; +$conf_arch = query("SELECT v FROM system_config WHERE k='archived_days'")->fetchColumn() ?: 90; + +if(is_numeric($conf_comp)) { + $pdo->exec("UPDATE tasks SET status='archived', status_updated_at=CURRENT_TIMESTAMP WHERE status='completed' AND datetime(COALESCE(status_updated_at, created_at), '+$conf_comp days') <= CURRENT_TIMESTAMP"); +} +if(is_numeric($conf_arch)) { + $pdo->exec("DELETE FROM tasks WHERE status='archived' AND datetime(COALESCE(status_updated_at, created_at), '+$conf_arch days') <= CURRENT_TIMESTAMP"); +} + +// Handle Task Deletions +if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_task'])) { + $del_id = $_POST['delete_task_id']; + $task_check = query("SELECT assignor_type, assignor_id, assignor_group_id FROM tasks WHERE id=?", [$del_id])->fetch(); + $user_groups = get_user_groups($user_id); + $my_group_ids = array_column($user_groups, 'id'); + + $can_delete = false; + if($task_check) { + if($task_check['assignor_type'] === 'user' && $task_check['assignor_id'] == $user_id) $can_delete = true; + if($task_check['assignor_type'] === 'group' && in_array($task_check['assignor_group_id'], $my_group_ids)) $can_delete = true; + if(!empty($_SESSION['is_admin'])) $can_delete = true; + } + + if($can_delete) { + query("DELETE FROM tasks WHERE id=?", [$del_id]); + } +} + +// Handle attachment deletion +if($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['delete_attachment']) || isset($_POST['delete_attachment_ajax']))) { + $att_id = $_POST['attachment_id']; + $att = query("SELECT owner_id, file_path FROM task_attachments WHERE id=?", [$att_id])->fetch(); + if($att && ($att['owner_id'] == $user_id || !empty($_SESSION['is_admin']))) { + @unlink($att['file_path']); + query("DELETE FROM task_attachments WHERE id=?", [$att_id]); + } + if(isset($_POST['delete_attachment_ajax'])) { + echo "OK"; exit; + } +} + +// Handle form submission +if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_task'])) { + $title = $_POST['title']; + $desc = $_POST['description'] ?? ''; + $due_date = $_POST['due_date'] ?? null; + if(trim((string)$due_date) === '') $due_date = null; + + // Parse From + $from = $_POST['from_assignor']; // 'user_1' or 'group_2' + $f_parts = explode('_', $from); + $assignor_type = $f_parts[0]; // 'user' or 'group' + $assignor_group_id = ($assignor_type === 'group') ? $f_parts[1] : null; + + // Parse To Array (multi-assignee fallback to custom string) + $to_raw = trim((string)($_POST['to_assignee'] ?? '')); + $to_list = array_filter(array_map('trim', explode(',', $to_raw))); + + $a_type = 'custom'; + $a_id = null; + $custom_name = $to_raw; + + if(count($to_list) === 1) { + $single = reset($to_list); + $u = query("SELECT id FROM users WHERE username=?", [$single])->fetch(); + if($u) { + $a_type = 'user'; + $a_id = $u['id']; + $custom_name = null; + } else { + $g = query("SELECT id FROM groups WHERE name=?", [$single])->fetch(); + if($g) { + $a_type = 'group'; + $a_id = $g['id']; + $custom_name = null; + } + } + } + + $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])) { + if(isset($_POST['ajax_submit'])) { echo json_encode(['success'=>false, 'error'=>'Task insert failed']); exit; } + } + $new_task_id = $pdo->lastInsertId(); + + // Process attachments bundled in this single request + if(isset($_FILES['attachment_file'])) { + $files = $_FILES['attachment_file']; + $names = is_array($files['name']) ? $files['name'] : [$files['name']]; + $tmps = is_array($files['tmp_name']) ? $files['tmp_name'] : [$files['tmp_name']]; + $errors = is_array($files['error']) ? $files['error'] : [$files['error']]; + + for($i=0; $iprepare("INSERT INTO task_attachments (task_id, file_name, file_path, owner_id) VALUES (?, ?, ?, ?)"); + $stmt->execute([$new_task_id, $names[$i], $path, $user_id]); + } + } + } + } + + // Handle notifications + foreach($to_list as $rec) { + $u = query("SELECT id FROM users WHERE username=?", [$rec])->fetch(); + if($u) notify_user($u['id'], "You have been assigned a new task: $title"); + else { + $g = query("SELECT id FROM groups WHERE name=?", [$rec])->fetch(); + if($g) notify_group($g['id'], "Your group has been assigned a new task: $title"); + } + } + + if(isset($_POST['ajax_submit'])) { + echo json_encode(['success'=>true, 'redirect'=>'?action=tasks&msg_title='.urlencode('Task Created').'&msg_body='.urlencode('Task successfully created.')]); + exit; + } + header('Location: ?action=tasks'); + exit; +} + +// Fetch Assignable Entities for "From" +if(!isset($user_groups)) $user_groups = get_user_groups($user_id); +if(!isset($my_group_ids)) $my_group_ids = array_column($user_groups, 'id'); + +$from_options = []; +$from_options[] = ['val' => 'user_'.$user_id, 'label' => 'Myself ('.htmlspecialchars($current_user['username']).')']; +foreach($user_groups as $g) { + $from_options[] = ['val' => 'group_'.$g['id'], 'label' => htmlspecialchars($g['name']) . ' (Group)']; +} + +$all_u = query("SELECT id, username FROM users")->fetchAll(); +$all_g = query("SELECT id, name FROM groups")->fetchAll(); + +$group_in = implode(',', array_fill(0, count($my_group_ids), '?')) ?: '0'; + +$params = array_merge([$user_id], $my_group_ids, $my_group_ids, [$user_id]); + +$sql = "SELECT t.*, u.username as assignor_user_name, g.name as assignor_group_name, au.username as assignee_user_name, ag.name as assignee_group_name + FROM tasks t + LEFT JOIN users u ON t.assignor_id = u.id + LEFT JOIN groups g ON t.assignor_group_id = g.id + LEFT JOIN users au ON (t.assignee_type='user' AND t.assignee_id = au.id) + LEFT JOIN groups ag ON (t.assignee_type='group' AND t.assignee_id = ag.id) + WHERE (t.assignee_type='user' AND t.assignee_id = ?) + OR (t.assignee_type='group' AND t.assignee_id IN ($group_in)) + OR (t.assignor_type='group' AND t.assignor_group_id IN ($group_in)) + OR (t.assignor_type='user' AND t.assignor_id = ?) + ORDER BY t.created_at DESC"; +$stmt = $pdo->prepare($sql); +$stmt->execute($params); +$tasks = $stmt->fetchAll(); + +$t_ids = array_column($tasks, 'id'); +$attachments = []; +if($t_ids) { + $in = str_repeat('?,', count($t_ids) - 1) . '?'; + $stmt = $pdo->prepare("SELECT ta.*, u.username as owner_name FROM task_attachments ta LEFT JOIN users u ON ta.owner_id = u.id WHERE ta.task_id IN ($in) ORDER BY ta.created_at ASC"); + $stmt->execute($t_ids); + foreach($stmt->fetchAll() as $row) { + $attachments[$row['task_id']][] = $row; + } +} + +$kanban = ['pending' => [], 'in_progress' => [], 'completed' => [], 'archived' => []]; +foreach($tasks as $t) { + if(isset($kanban[$t['status']])) $kanban[$t['status']][] = $t; +} + +// Sort Pending by Due Date (Earliest first) +usort($kanban['pending'], function($a, $b) { + $da = $a['due_date'] ? strtotime($a['due_date']) : PHP_INT_MAX; + $db = $b['due_date'] ? strtotime($b['due_date']) : PHP_INT_MAX; + if($da == $db) return strtotime($b['created_at']) - strtotime($a['created_at']); + return $da - $db; +}); +?> + + +
+
+
+ +
+ 📝 + +
+ +
+ + +
+
+ +
+ +
+
+ +
+ +
+ + + + + +
+
+ + + + + +
+
+
+ +
+ ['Pending','🟠'], 'in_progress'=>['In Progress','🔵'], 'completed'=>['Completed','🟢'], 'archived'=>['Archived','🗄️']] as $col_id => $col_data): ?> +
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ + + +
+ + ▼ +
+
+ +
+ '; + if($t['assignee_type'] == 'custom') echo '👤 '.htmlspecialchars((string)$t['custom_assignee_name']); + elseif($t['assignee_type'] == 'group') echo '👥 '.htmlspecialchars($t['assignee_group_name']); + else echo '👤 '.htmlspecialchars($t['assignee_user_name'] ?? ''); + echo ''; + + echo '
'; + + $att_count = empty($attachments[$t['id']]) ? 0 : count($attachments[$t['id']]); + if($att_count > 0) { + echo ' '.$att_count.''; + } + + if($t['due_date']) { + $days_diff = (strtotime($t['due_date']) - strtotime(date('Y-m-d'))) / 86400; + if($days_diff < 0) { + echo 'Overdue by '.abs($days_diff).'d'; + } elseif($days_diff == 0) { + echo 'Due Today'; + } else { + echo ''.$days_diff.'d left'; + } + } + echo '
'; + ?> +
+
+ +
+ +
+ + +
+
📅 Created:
+ +
⏰ Due Date:
+ +
+ +
+
+ From: + + + +
+
+ To: + + + +
+
+ +
+
+
Attachments
+ +
+ + + +
+ +
+
+ + + +
+ +
+ +
📄
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ +
+
+ +
+
+ +
+ +