Compare commits

..

14 Commits

Author SHA1 Message Date
9fd7fbd2e5 Merge pull request 'Added username to accounts' (#6) from feature/add-username into main
Reviewed-on: #6
2026-02-16 08:49:59 +00:00
Garcia-Gomez
26bacd8928 Added username to accounts 2026-02-16 00:45:57 -08:00
fe610b094d Merge pull request 'Applied profile fix for login and logout' (#5) from feature/fix-profiles-on-load into main
Reviewed-on: #5
2026-02-13 07:47:56 +00:00
Garcia-Gomez
efc1fc3906 Applied profile fix for login and logout 2026-02-12 23:45:24 -08:00
457170a1ae Merge pull request 'Add profile functionality' (#4) from feature/profile_functionality into main
Reviewed-on: #4
2026-02-13 00:48:19 +00:00
03fb096317 Fixed functionality issue for profiles 2026-02-12 14:28:04 -08:00
8fc8d737e5 Added UI and functionality for profiles 2026-02-12 14:17:24 -08:00
f879d25bc7 Merge pull request 'Clean user button and user dropdown UI' (#3) from feature/logout-functionality into main
Reviewed-on: #3
2026-02-12 01:03:13 +00:00
71a6b44890 Fixed user button and dropdown UI 2026-02-11 16:59:32 -08:00
fd93da618c Merge pull request 'Add authentication system' (#2) from feature/authentication-system into main
Reviewed-on: #2
2026-02-11 23:58:06 +00:00
a0d00ac21c Fixed several display and authentication issues 2026-02-11 15:50:45 -08:00
11889e3f93 Added authentication functionality and simple role based model 2026-02-11 15:02:37 -08:00
c247631de6 Merge pull request 'Add three-state toggle' (#1) from feature/three-state-toggle into main
Reviewed-on: #1
2026-02-11 20:43:10 +00:00
6be1cfb6ab Finalize three-state toggle feature 2026-02-11 12:27:52 -08:00
22 changed files with 976 additions and 246 deletions

41
add_profile.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$data = json_decode(file_get_contents('php://input'), true);
$name = trim($data['name'] ?? '');
if ($name === '') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Profile name is required']);
exit;
}
// Prevent duplicate profile names per user
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE user_id = ? AND name = ?");
$stmt->execute([$user_id, $name]);
if ($stmt->fetchColumn()) {
http_response_code(409);
echo json_encode(['success' => false, 'error' => 'Profile already exists']);
exit;
}
// Insert new profile
$stmt = $pdo->prepare("INSERT INTO profiles (user_id, name, is_default) VALUES (?, ?, 0)");
$stmt->execute([$user_id, $name]);
$newProfileId = $pdo->lastInsertId();
// Make it active immediately
$_SESSION['active_profile_id'] = (int)$newProfileId;
echo json_encode([
'success' => true,
'profile_id' => $newProfileId
]);

View File

@@ -1,15 +1,29 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
$data = json_decode(file_get_contents('php://input'), true); header('Content-Type: application/json');
if (!isset($data['name']) || empty(trim($data['name']))) { require_login();
$user_id = current_user_id();
$profile_id = $_SESSION['active_profile_id'] ?? null;
if (!$profile_id) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Project name is required']); echo json_encode(['success' => false, 'error' => 'No active profile']);
exit; exit;
} }
$stmt = $pdo->prepare("INSERT INTO projects (name) VALUES (?)"); $data = json_decode(file_get_contents('php://input'), true);
$stmt->execute([trim($data['name'])]); $name = trim($data['name'] ?? '');
if ($name === '') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Project name is required']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO projects (user_id, profile_id, name) VALUES (?, ?, ?)");
$stmt->execute([$user_id, $profile_id, $name]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]); echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
?>

View File

@@ -1,18 +1,32 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$name = trim($data['name'] ?? ''); $name = trim($data['name'] ?? '');
$task_id = intval($data['task_id'] ?? 0); $task_id = intval($data['task_id'] ?? 0);
if ($name === '' || $task_id <= 0) { if ($name === '' || $task_id <= 0) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Invalid input']); echo json_encode(['success' => false, 'error' => 'Invalid input']);
exit; exit;
} }
$stmt = $pdo->prepare("INSERT INTO subtasks (task_id, name) VALUES (?, ?)"); // Ensure task belongs to this user
$stmt->execute([$task_id, $name]); $stmt = $pdo->prepare("SELECT id FROM tasks WHERE id = ? AND user_id = ? LIMIT 1");
$stmt->execute([$task_id, $user_id]);
if (!$stmt->fetchColumn()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Forbidden']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO subtasks (user_id, task_id, name) VALUES (?, ?, ?)");
$stmt->execute([$user_id, $task_id, $name]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]); echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
?>

View File

@@ -1,5 +1,11 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$name = trim($data['name'] ?? ''); $name = trim($data['name'] ?? '');
@@ -7,12 +13,20 @@ $project_id = intval($data['project_id'] ?? 0);
if ($name === '' || $project_id <= 0) { if ($name === '' || $project_id <= 0) {
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'Invalid input']); echo json_encode(['success' => false, 'error' => 'Invalid input']);
exit; exit;
} }
$stmt = $pdo->prepare("INSERT INTO tasks (project_id, name) VALUES (?, ?)"); // Ensure project belongs to this user
$stmt->execute([$project_id, $name]); $stmt = $pdo->prepare("SELECT id FROM projects WHERE id = ? AND user_id = ? LIMIT 1");
$stmt->execute([$project_id, $user_id]);
if (!$stmt->fetchColumn()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Forbidden']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO tasks (user_id, project_id, name) VALUES (?, ?, ?)");
$stmt->execute([$user_id, $project_id, $name]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]); echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
?>

36
auth.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
// auth.php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function is_logged_in(): bool {
return isset($_SESSION['user']) && isset($_SESSION['user']['id']);
}
function current_user_id(): int {
return intval($_SESSION['user']['id'] ?? 0);
}
function current_user_can_manage_settings(): bool {
return !empty($_SESSION['user']['can_manage_settings']);
}
function require_login(): void {
if (!is_logged_in()) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Not authenticated']);
exit;
}
}
function require_can_manage_settings(): void {
require_login();
if (!current_user_can_manage_settings()) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Forbidden']);
exit;
}
}

2
db.php
View File

@@ -1,5 +1,5 @@
<?php <?php
$host = '192.168.0.201'; $host = 'localhost';
$db = 'tinytask'; $db = 'tinytask';
$user = 'tinytask'; // change this if needed $user = 'tinytask'; // change this if needed
$pass = 'GD3AVEZRNnE@A3P]'; // change this if needed $pass = 'GD3AVEZRNnE@A3P]'; // change this if needed

View File

@@ -1,6 +1,11 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
require_login();
$user_id = current_user_id();
$id = intval($_GET['id'] ?? 0); $id = intval($_GET['id'] ?? 0);
if ($id > 0) { if ($id > 0) {
$pdo->prepare("DELETE FROM projects WHERE id = ?")->execute([$id]); $pdo->prepare("DELETE FROM projects WHERE id = ? AND user_id = ?")->execute([$id, $user_id]);
} }

View File

@@ -1,6 +1,11 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
require_login();
$user_id = current_user_id();
$id = intval($_GET['id'] ?? 0); $id = intval($_GET['id'] ?? 0);
if ($id > 0) { if ($id > 0) {
$pdo->prepare("DELETE FROM subtasks WHERE id = ?")->execute([$id]); $pdo->prepare("DELETE FROM subtasks WHERE id = ? AND user_id = ?")->execute([$id, $user_id]);
} }

View File

@@ -1,6 +1,11 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
require_login();
$user_id = current_user_id();
$id = intval($_GET['id'] ?? 0); $id = intval($_GET['id'] ?? 0);
if ($id > 0) { if ($id > 0) {
$pdo->prepare("DELETE FROM tasks WHERE id = ?")->execute([$id]); $pdo->prepare("DELETE FROM tasks WHERE id = ? AND user_id = ?")->execute([$id, $user_id]);
} }

View File

@@ -1,19 +1,33 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
$projects = $pdo->query("SELECT * FROM projects ORDER BY sort_order ASC")->fetchAll(); header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$profile_id = $_SESSION['active_profile_id'] ?? null;
if (!$profile_id) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'No active profile']);
exit;
}
$stmt = $pdo->prepare("SELECT * FROM projects WHERE user_id = ? AND profile_id = ? ORDER BY sort_order ASC");
$stmt->execute([$user_id, $profile_id]);
$projects = $stmt->fetchAll();
foreach ($projects as &$project) { foreach ($projects as &$project) {
$stmt = $pdo->prepare("SELECT * FROM tasks WHERE project_id = ? ORDER BY created_at"); $stmt = $pdo->prepare("SELECT * FROM tasks WHERE project_id = ? AND user_id = ? ORDER BY created_at");
$stmt->execute([$project['id']]); $stmt->execute([$project['id'], $user_id]);
$project['tasks'] = $stmt->fetchAll(); $project['tasks'] = $stmt->fetchAll();
foreach ($project['tasks'] as &$task) { foreach ($project['tasks'] as &$task) {
$stmt = $pdo->prepare("SELECT * FROM subtasks WHERE task_id = ? ORDER BY created_at"); $stmt = $pdo->prepare("SELECT * FROM subtasks WHERE task_id = ? AND user_id = ? ORDER BY created_at");
$stmt->execute([$task['id']]); $stmt->execute([$task['id'], $user_id]);
$task['subtasks'] = $stmt->fetchAll(); $task['subtasks'] = $stmt->fetchAll();
} }
} }
echo json_encode($projects); echo json_encode($projects);
?>

18
get_profiles.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$stmt = $pdo->prepare("SELECT id, name, is_default FROM profiles WHERE user_id = ? ORDER BY is_default DESC, name ASC");
$stmt->execute([$user_id]);
$profiles = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'active_profile_id' => $_SESSION['active_profile_id'] ?? null,
'profiles' => $profiles
]);

689
index.php
View File

@@ -46,19 +46,19 @@
font-size: 1.4rem; font-size: 1.4rem;
} }
.modal { .modal {
padding: 2rem; /* or try 3rem or a % like 5% */ padding: 2rem; /* or try 3rem or a % like 5% */
} }
.modal.show { .modal.show {
display: flex !important; display: flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.modal-dialog { .modal-dialog {
margin: 0; margin: 0;
} }
.modal-content { .modal-content {
font-size: 2.0rem; /* slightly larger than Bootstrap's default (~0.875rem) */ font-size: 2.0rem; /* slightly larger than Bootstrap's default (~0.875rem) */
@@ -78,11 +78,6 @@
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
} }
.add-project-btn {
font-size: 1.5rem; /* increase to your desired size */
line-height: 1;
}
.settings-btn { .settings-btn {
font-size: 1.5rem; /* increase to your desired size */ font-size: 1.5rem; /* increase to your desired size */
line-height: 1; line-height: 1;
@@ -104,6 +99,11 @@
background-color: #transparent; background-color: #transparent;
} }
.completed {
text-decoration: line-through;
opacity: 0.5;
}
.btn-icon { .btn-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -131,6 +131,52 @@
color: #d00; color: #d00;
} }
.user-menu-btn {
font-size: 1.4rem;
padding: 0.5rem 0.9rem;
}
.header-action-btn {
font-size: 1.4rem;
padding: 0.6rem 0.9rem;
display: flex;
align-items: center;
justify-content: center;
}
.profile-select {
height: 32px; /* match your header-action-btn buttons */
font-size: 1.5rem;
font-weight: 700; /* or 700 for stronger bold */
padding-right: 2.5rem; /* key: reserve space for the arrow */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.add-project-btn {
font-size: 1.5rem;
font-weight: 600; /* or 700 for stronger bold */
padding: 0.5rem 1rem;
}
.custom-dropdown {
min-width: 120px;
text-align: right;
}
.custom-dropdown .dropdown-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-dropdown .dropdown-item,
.custom-dropdown .dropdown-item-text {
font-size: 1.5rem !important;
text-align: right;
}
.project-color-0 { background-color: #f9f9fc; } .project-color-0 { background-color: #f9f9fc; }
.project-color-1 { background-color: #eefaf5; } .project-color-1 { background-color: #eefaf5; }
.project-color-2 { background-color: #fef7e0; } .project-color-2 { background-color: #fef7e0; }
@@ -154,9 +200,9 @@
.project-color-9 .project-header { background-color: #d0e4ff; } .project-color-9 .project-header { background-color: #d0e4ff; }
.project-column { .project-column {
width: 100%; /* 1 per row */ width: 100%; /* 1 per row */
} }
/* Small devices (landscape phones, ≥576px) */ /* Small devices (landscape phones, ≥576px) */
@@ -223,9 +269,37 @@
<i class="bi bi-gear-fill"></i> <i class="bi bi-gear-fill"></i>
</button> </button>
</span> </span>
<button class="btn btn-sm btn-success add-project-btn" data-bs-toggle="modal" data-bs-target="#addProjectModal" title="Add Project">
<i class="bi bi-plus"></i> <div class="d-flex align-items-center gap-3">
</button> <select id="profileSelect" class="form-select btn-lg profile-select" style="width: 140px;"></select>
<!-- Add Project -->
<button class="btn btn-success btn-lg header-action-btn"
data-bs-toggle="modal"
data-bs-target="#addProjectModal">
<i class="bi bi-plus-lg"></i>
</button>
<!-- User Dropdown -->
<div class="dropdown">
<button class="btn btn-outline-secondary btn-lg header-action-btn"
type="button"
id="menuButton"
data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end custom-dropdown" aria-labelledby="menuButton">
<li class="dropdown-item-text fw-semibold fs-5" id="dropdownUserEmail"></li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item fs-5" type="button" id="logoutBtn">
<i class="bi bi-box-arrow-right"></i>
<span>Logout</span>
</button>
</li>
</ul>
</div>
</div>
</h1> </h1>
@@ -338,24 +412,82 @@
</div> </div>
</div> </div>
<!-- Modal: Confirmation --> <!-- Modal: Confirmation -->
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="confirmationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Confirm Action</h5> <h5 class="modal-title">Confirm Action</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body" id="confirmationModalBody"> <div class="modal-body" id="confirmationModalBody">
Are you sure you want to continue? Are you sure you want to continue?
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmModalYes">Yes</button> <button type="button" class="btn btn-danger" id="confirmModalYes">Yes</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Modal: Login -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<form id="loginForm" class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Sign In</h5>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Email or Username</label>
<input type="text" class="form-control" id="loginEmail" required>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" id="loginPassword" required>
</div>
<div class="text-danger" id="loginError" style="display:none;"></div>
</div>
<div class="modal-footer d-flex justify-content-between w-100">
<button type="button" class="btn btn-outline-secondary" id="showRegisterBtn">Create account</button>
<button type="submit" class="btn btn-primary">Sign In</button>
</div>
</form>
</div>
</div>
<!-- Modal: Register -->
<div class="modal fade" id="registerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="registerForm" class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create Account</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="registerEmail" required>
</div>
<div class="mb-3">
<label class="form-label">Username (optional)</label>
<input type="text" class="form-control" id="registerUsername">
</div>
<div class="mb-3">
<label class="form-label">Password (8+ chars)</label>
<input type="password" class="form-control" id="registerPassword" required minlength="8">
</div>
<div class="text-danger" id="registerError" style="display:none;"></div>
<div class="text-success" id="registerSuccess" style="display:none;"></div>
</div>
<div class="modal-footer d-flex justify-content-between w-100">
<button type="button" class="btn btn-outline-secondary" id="backToLoginBtn">Back</button>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@@ -420,118 +552,149 @@
favicon.type = "image/svg+xml"; favicon.type = "image/svg+xml";
}); });
}); });
} }
function loadProfiles() {
fetch('get_profiles.php')
.then(r => r.json())
.then(data => {
if (!data.success) return;
const sel = document.getElementById('profileSelect');
sel.innerHTML = '';
data.profiles.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
sel.appendChild(opt);
});
if (data.active_profile_id) {
sel.value = String(data.active_profile_id);
}
});
}
function fetchProjects() {
fetch('get_data.php')
.then(res => res.json())
.then(data => {
if (!Array.isArray(data)) {
if (data && data.error === 'Not authenticated') {
new bootstrap.Modal(document.getElementById('loginModal')).show();
return;
}
console.error('get_data.php returned non-array:', data);
return;
}
const container = document.getElementById('projectGrid');
container.innerHTML = '';
data.forEach((project, idx) => {
const projectId = `project-${project.id}`;
const colorClass = `project-color-${idx % 10}`;
const col = document.createElement('div');
col.className = `col-12 col-sm-6 col-md-4 col-lg-2 col-xl-2 col-xxl-1 g-2 m-0 project-column`;
col.dataset.projectId = project.id;
col.classList.add("draggable-project");
col.innerHTML = `
<div class="project-card ${colorClass}">
<div class="project-header">
<span class="project-title">${project.name}</span>
<div class="d-flex align-items-center">
<button class="btn-icon add-task-btn" data-project-id="${project.id}" title="Add Task">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-project-btn" data-project-id="${project.id}" title="Delete Project">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="project-body">
${project.tasks.map(task => `
<div class="d-flex justify-content-between align-items-center small-text mb-1 task-text">
<span class="${task.highlighted == 1 ? 'highlighted' : ''} ${task.highlighted == 2 ? 'completed' : ''}".trim()
data-task-id="${task.id}">${task.name}</span>
<div class="d-flex">
<button class="btn-icon add-subtask-btn" data-task-id="${task.id}" title="Add Subtask">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-task-btn" data-task-id="${task.id}" title="Delete Task">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="ms-3">
${task.subtasks.map(sub => `
<div class="d-flex justify-content-between align-items-center small-text mb-1">
<span class="${sub.highlighted == 1 ? 'highlighted' : ''} ${sub.highlighted == 2 ? 'completed' : ''}".trim()
data-subtask-id="${sub.id}">${sub.name}</span>
<button class="btn-icon text-danger delete-subtask-btn" data-subtask-id="${sub.id}" title="Delete Subtask">
<i class="bi bi-dash"></i>
</button>
</div>
`).join('')}
</div>
`).join('')}
</div>
</div>
`;
container.appendChild(col);
});
// Create the drop indicator element
const placeholder = document.createElement('div');
placeholder.className = 'project-column';
placeholder.innerHTML = `<div class="project-placeholder"></div>`;
// Setup drag events for project reordering
document.querySelectorAll('.project-column').forEach(col => {
col.setAttribute('draggable', 'true');
function fetchProjects() { col.addEventListener('dragstart', (e) => {
fetch('get_data.php') e.dataTransfer.setData('text/plain', col.dataset.projectId);
.then(res => res.json()) col.classList.add('dragging');
.then(data => { });
const container = document.getElementById('projectGrid');
container.innerHTML = '';
data.forEach((project, idx) => { col.addEventListener('dragend', () => {
col.classList.remove('dragging');
placeholder.remove();
});
const projectId = `project-${project.id}`; col.addEventListener('dragover', (e) => {
const colorClass = `project-color-${idx % 10}`; e.preventDefault();
const dragging = document.querySelector('.dragging');
if (!dragging || dragging === col) return;
const col = document.createElement('div'); const bounding = col.getBoundingClientRect();
col.className = `col-12 col-sm-6 col-md-4 col-lg-2 col-xl-2 col-xxl-1 g-2 m-0 project-column`; const offset = e.clientY - bounding.top;
col.dataset.projectId = project.id;
col.classList.add("draggable-project");
col.innerHTML = `
<div class="project-card ${colorClass}">
<div class="project-header">
<span class="project-title">${project.name}</span>
<div class="d-flex align-items-center">
<button class="btn-icon add-task-btn" data-project-id="${project.id}" title="Add Task">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-project-btn" data-project-id="${project.id}" title="Delete Project">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="project-body">
${project.tasks.map(task => `
<div class="d-flex justify-content-between align-items-center small-text mb-1 task-text">
<span class="${task.highlighted ? 'highlighted' : ''}" data-task-id="${task.id}">${task.name}</span>
<div class="d-flex">
<button class="btn-icon add-subtask-btn" data-task-id="${task.id}" title="Add Subtask">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-task-btn" data-task-id="${task.id}" title="Delete Task">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="ms-3">
${task.subtasks.map(sub => `
<div class="d-flex justify-content-between align-items-center small-text mb-1">
<span class="${sub.highlighted ? 'highlighted' : ''}" data-subtask-id="${sub.id}">${sub.name}</span>
<button class="btn-icon text-danger delete-subtask-btn" data-subtask-id="${sub.id}" title="Delete Subtask">
<i class="bi bi-dash"></i>
</button>
</div>
`).join('')}
</div>
`).join('')}
</div>
</div>
`;
container.appendChild(col); if (offset < bounding.height / 2) {
}); col.parentNode.insertBefore(placeholder, col);
} else {
col.parentNode.insertBefore(placeholder, col.nextSibling);
}
});
col.addEventListener('drop', (e) => {
// Create the drop indicator element e.preventDefault();
const placeholder = document.createElement('div'); const dragging = document.querySelector('.dragging');
placeholder.className = 'project-column'; if (placeholder && placeholder.parentNode) {
placeholder.innerHTML = `<div class="project-placeholder"></div>`; placeholder.parentNode.insertBefore(dragging, placeholder);
placeholder.remove();
updateProjectOrder(); // This already exists in your code
// Setup drag events for project reordering }
document.querySelectorAll('.project-column').forEach(col => { });
col.setAttribute('draggable', 'true'); });
col.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', col.dataset.projectId);
col.classList.add('dragging');
});
col.addEventListener('dragend', () => {
col.classList.remove('dragging');
placeholder.remove();
});
col.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (!dragging || dragging === col) return;
const bounding = col.getBoundingClientRect();
const offset = e.clientY - bounding.top;
if (offset < bounding.height / 2) {
col.parentNode.insertBefore(placeholder, col);
} else {
col.parentNode.insertBefore(placeholder, col.nextSibling);
}
});
col.addEventListener('drop', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.insertBefore(dragging, placeholder);
placeholder.remove();
updateProjectOrder(); // This already exists in your code
}
});
});
// Enable drag-and-drop sorting // Enable drag-and-drop sorting
@@ -615,48 +778,44 @@ document.querySelectorAll('.project-column').forEach(col => {
}); });
}); });
document.querySelectorAll('[data-task-id]').forEach(span => { document.querySelectorAll('[data-task-id]').forEach(span => {
span.addEventListener('click', () => { span.addEventListener('click', () => {
const taskId = span.dataset.taskId; const taskId = span.dataset.taskId;
const isHighlighted = span.classList.toggle('highlighted');
fetch('toggle_highlight.php', { fetch('toggle_highlight.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'task', id: taskId, highlighted: isHighlighted }) body: JSON.stringify({ type: 'task', id: taskId })
}); })
}); .then(res => res.json())
}); .then(data => {
if (!data.success) return;
document.querySelectorAll('[data-subtask-id]').forEach(span => { span.classList.toggle('highlighted', data.highlighted === 1);
span.addEventListener('click', () => { span.classList.toggle('completed', data.highlighted === 2);
const subtaskId = span.dataset.subtaskId; });
const isHighlighted = span.classList.toggle('highlighted'); });
});
fetch('toggle_highlight.php', { document.querySelectorAll('[data-subtask-id]').forEach(span => {
method: 'POST', span.addEventListener('click', () => {
headers: { 'Content-Type': 'application/json' }, const subtaskId = span.dataset.subtaskId;
body: JSON.stringify({ type: 'subtask', id: subtaskId, highlighted: isHighlighted })
});
});
});
let itemToDelete = null; fetch('toggle_highlight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'subtask', id: subtaskId })
})
.then(res => res.json())
.then(data => {
if (!data.success) return;
document.querySelectorAll('.delete-task-btn').forEach(btn => { span.classList.toggle('highlighted', data.highlighted === 1);
btn.addEventListener('click', () => { span.classList.toggle('completed', data.highlighted === 2);
itemToDelete = btn.dataset.taskId; });
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal')); });
modal.show(); });
});
});
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
if (itemToDelete) {
fetch(`delete_task.php?id=${itemToDelete}`)
.then(() => location.reload()); // or re-fetch your UI
}
});
}); });
} }
@@ -725,23 +884,6 @@ document.querySelectorAll('.project-column').forEach(col => {
}); });
}); });
document.getElementById('settingsForm').addEventListener('submit', function (e) {
e.preventDefault();
const newTitle = document.getElementById('titleText').value.trim();
const newIcon = document.getElementById('iconClass').value.trim();
if (newTitle) {
document.getElementById('appTitle').childNodes[1].textContent = newTitle;
}
if (newIcon) {
const icon = document.getElementById('appIcon');
icon.className = `bi ${newIcon} me-2`;
}
bootstrap.Modal.getInstance(document.getElementById('settingsModal')).hide();
});
document.getElementById('settingsForm').addEventListener('submit', function (e) { document.getElementById('settingsForm').addEventListener('submit', function (e) {
e.preventDefault(); e.preventDefault();
@@ -763,6 +905,7 @@ document.querySelectorAll('.project-column').forEach(col => {
}); });
}); });
document.getElementById('resetSettings').addEventListener('click', () => { document.getElementById('resetSettings').addEventListener('click', () => {
fetch('reset_settings.php') fetch('reset_settings.php')
.then(() => { .then(() => {
@@ -770,11 +913,165 @@ document.querySelectorAll('.project-column').forEach(col => {
}); });
}); });
window.addEventListener('DOMContentLoaded', () => {
loadSettings();
});
document.addEventListener('DOMContentLoaded', fetchProjects); document.getElementById('showRegisterBtn').addEventListener('click', () => {
bootstrap.Modal.getInstance(document.getElementById('loginModal')).hide();
new bootstrap.Modal(document.getElementById('registerModal')).show();
});
document.getElementById('backToLoginBtn').addEventListener('click', () => {
bootstrap.Modal.getInstance(document.getElementById('registerModal')).hide();
new bootstrap.Modal(document.getElementById('loginModal')).show();
});
document.getElementById('loginForm').addEventListener('submit', (e) => {
e.preventDefault();
const email = document.getElementById('loginEmail').value.trim();
const password = document.getElementById('loginPassword').value;
const err = document.getElementById('loginError');
err.style.display = 'none';
err.textContent = '';
fetch('login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
.then(async r => {
const data = await r.json();
if (!r.ok) throw new Error(data.error || 'Login failed');
return data;
})
.then(data => {
bootstrap.Modal.getInstance(document.getElementById('loginModal')).hide();
// Show/hide settings button according to permissions
if (!data.user.can_manage_settings) {
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = 'none');
} else {
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = '');
}
// Update user email or username if exists in the dropdown and refresh profiles + projects
if (data.user) {
const userEl = document.getElementById('dropdownUserEmail');
if (userEl) userEl.textContent = (data.user.username && data.user.username.trim())
? data.user.username
: data.user.email;
}
loadProfiles();
fetchProjects();
})
.catch(ex => {
err.textContent = ex.message;
err.style.display = 'block';
});
});
document.getElementById('registerForm').addEventListener('submit', (e) => {
e.preventDefault();
const email = document.getElementById('registerEmail').value.trim();
const username = document.getElementById('registerUsername').value.trim();
const password = document.getElementById('registerPassword').value;
const err = document.getElementById('registerError');
const ok = document.getElementById('registerSuccess');
err.style.display = 'none'; err.textContent = '';
ok.style.display = 'none'; ok.textContent = '';
fetch('register.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, username: username || null, password })
})
.then(async r => {
const data = await r.json();
if (!r.ok) throw new Error(data.error || 'Registration failed');
return data;
})
.then(() => {
ok.textContent = 'Account created. You can sign in now.';
ok.style.display = 'block';
})
.catch(ex => {
err.textContent = ex.message;
err.style.display = 'block';
});
});
document.getElementById('profileSelect').addEventListener('change', () => {
const profileId = parseInt(document.getElementById('profileSelect').value, 10);
fetch('set_profile.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile_id: profileId })
})
.then(r => r.json())
.then(data => {
if (!data.success) return;
fetchProjects();
});
});
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
fetch('me.php')
.then(r => r.json())
.then(me => {
if (!me.logged_in) {
const modal = new bootstrap.Modal(document.getElementById('loginModal'));
modal.show();
return;
}
// Add user name to user menu
if (me.logged_in) {
document.getElementById('dropdownUserEmail').textContent =
(me.user.username && me.user.username.trim()) ? me.user.username : me.user.email;
}
// Hide settings button if user can't manage settings
if (!me.user.can_manage_settings) {
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = 'none');
}
loadProfiles();
fetchProjects();
});
});
document.addEventListener('DOMContentLoaded', () => {
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
fetch('logout.php')
.then(res => res.json())
.then(data => {
if (!data.success) return;
// Clear UI
document.getElementById('projectGrid').innerHTML = '';
// Clear Profile List
document.getElementById('profileSelect').innerHTML = '';
// Clear User Email
document.getElementById('dropdownUserEmail').textContent = '';
// Show login modal
new bootstrap.Modal(document.getElementById('loginModal')).show();
})
.catch(err => console.error('Logout failed:', err));
});
}
});
</script> </script>

79
login.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
// Keep front-end field name "email" for compatibility; it can now be email OR username
$identifierRaw = trim($data['email'] ?? '');
$identifier = strtolower($identifierRaw);
$password = strval($data['password'] ?? '');
if ($identifier === '') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Email or username is required']);
exit;
}
$isEmail = filter_var($identifier, FILTER_VALIDATE_EMAIL) !== false;
$sql = "
SELECT u.id, u.email, u.username, u.password_hash,
r.name AS role_name,
r.can_manage_settings
FROM users u
JOIN roles r ON r.id = u.role_id
WHERE " . ($isEmail ? "u.email = ?" : "u.username = ?") . "
LIMIT 1
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$identifier]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, $user['password_hash'])) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Invalid credentials']);
exit;
}
$_SESSION['user'] = [
'id' => (int)$user['id'],
'email' => $user['email'],
'username' => $user['username'], // NEW
'role' => $user['role_name'],
'can_manage_settings' => (int)$user['can_manage_settings'],
];
// Set active profile for this session (default profile if available)
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE user_id = ? AND is_default = 1 LIMIT 1");
$stmt->execute([$_SESSION['user']['id']]);
$profileId = $stmt->fetchColumn();
if (!$profileId) {
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE user_id = ? ORDER BY id ASC LIMIT 1");
$stmt->execute([$_SESSION['user']['id']]);
$profileId = $stmt->fetchColumn();
}
if (!$profileId) {
$stmt = $pdo->prepare("INSERT INTO profiles (user_id, name, is_default) VALUES (?, 'Default', 1)");
$stmt->execute([$_SESSION['user']['id']]);
$profileId = $pdo->lastInsertId();
}
$_SESSION['active_profile_id'] = (int)$profileId;
echo json_encode([
'success' => true,
'user' => [
'id' => $_SESSION['user']['id'],
'email' => $_SESSION['user']['email'],
'username' => $_SESSION['user']['username'], // NEW
'role' => $_SESSION['user']['role'],
'can_manage_settings' => $_SESSION['user']['can_manage_settings'],
],
'active_profile_id' => $_SESSION['active_profile_id']
]);

8
logout.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
require 'auth.php';
header('Content-Type: application/json');
$_SESSION = [];
session_destroy();
echo json_encode(['success' => true]);

18
me.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
require 'auth.php';
header('Content-Type: application/json');
if (!is_logged_in()) {
echo json_encode(['logged_in' => false]);
exit;
}
echo json_encode([
'logged_in' => true,
'user' => [
'id' => intval($_SESSION['user']['id']),
'email' => $_SESSION['user']['email'],
'role' => $_SESSION['user']['role'],
'can_manage_settings' => intval($_SESSION['user']['can_manage_settings']),
]
]);

98
register.php Normal file
View File

@@ -0,0 +1,98 @@
<?php
require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$email = strtolower(trim($data['email'] ?? ''));
$usernameRaw = trim($data['username'] ?? '');
$username = $usernameRaw === '' ? null : strtolower($usernameRaw);
$password = strval($data['password'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid email']);
exit;
}
if (strlen($password) < 8) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Password must be at least 8 characters']);
exit;
}
// Optional username validation
if ($username !== null) {
if (strlen($username) < 3 || strlen($username) > 50) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Username must be between 3 and 50 characters']);
exit;
}
// letters/numbers + . _ - ; must start/end with letter/number
if (!preg_match('/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/', $username)) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => 'Username may contain letters, numbers, underscore, hyphen, dot, and must start/end with a letter or number'
]);
exit;
}
}
$hash = password_hash($password, PASSWORD_DEFAULT);
// Standard role id from roles table
$stmt = $pdo->prepare("SELECT id FROM roles WHERE name = 'standard' LIMIT 1");
$stmt->execute();
$role_id = (int)$stmt->fetchColumn();
if ($role_id <= 0) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => "Role 'standard' not found"]);
exit;
}
try {
$pdo->beginTransaction();
// Friendly conflict checks (DB unique constraints should still exist)
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE email = ? LIMIT 1');
$stmt->execute([$email]);
if ($stmt->fetchColumn()) {
$pdo->rollBack();
http_response_code(409);
echo json_encode(['success' => false, 'error' => 'Email already in use']);
exit;
}
if ($username !== null) {
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE username = ? LIMIT 1');
$stmt->execute([$username]);
if ($stmt->fetchColumn()) {
$pdo->rollBack();
http_response_code(409);
echo json_encode(['success' => false, 'error' => 'Username already in use']);
exit;
}
}
// Create user (username is nullable)
$stmt = $pdo->prepare("INSERT INTO users (email, username, password_hash, role_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$email, $username, $hash, $role_id]);
$userId = (int)$pdo->lastInsertId();
// Create default profile for this user
$stmt = $pdo->prepare("INSERT INTO profiles (user_id, name, is_default) VALUES (?, 'Default', 1)");
$stmt->execute([$userId]);
$pdo->commit();
echo json_encode(['success' => true]);
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
http_response_code(409);
echo json_encode(['success' => false, 'error' => 'Account already exists']);
}

View File

@@ -1,6 +1,11 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_can_manage_settings();
$stmt = $pdo->prepare("UPDATE settings SET title = 'tinyTask', icon_class = 'kanban', icon_color = '#ff0000' WHERE id = 1"); $stmt = $pdo->prepare("UPDATE settings SET title = 'tinyTask', icon_class = 'kanban', icon_color = '#ff0000' WHERE id = 1");
$stmt->execute(); $stmt->execute();
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
?>

30
set_profile.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$input = json_decode(file_get_contents('php://input'), true);
$profile_id = (int)($input['profile_id'] ?? 0);
if ($profile_id <= 0) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid profile_id']);
exit;
}
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE id = ? AND user_id = ?");
$stmt->execute([$profile_id, $user_id]);
if (!$stmt->fetchColumn()) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Profile not allowed']);
exit;
}
$_SESSION['active_profile_id'] = $profile_id;
echo json_encode(['success' => true, 'active_profile_id' => $profile_id]);

View File

@@ -3,19 +3,28 @@ require 'db.php';
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$type = $data['type']; $type = $data['type'] ?? '';
$id = intval($data['id']); $id = intval($data['id'] ?? 0);
$highlighted = isset($data['highlighted']) && $data['highlighted'] ? 1 : 0;
if ($id <= 0 || ($type !== 'task' && $type !== 'subtask')) {
echo json_encode(['success' => false, 'error' => 'Invalid request']);
exit;
}
if ($type === 'task') { if ($type === 'task') {
$stmt = $pdo->prepare("UPDATE tasks SET highlighted = ? WHERE id = ?"); $stmt = $pdo->prepare("UPDATE tasks SET highlighted = (highlighted + 1) % 3 WHERE id = ?");
$stmt->execute([$highlighted, $id]); $stmt->execute([$id]);
echo json_encode(['success' => true]);
} elseif ($type === 'subtask') { $stmt = $pdo->prepare("SELECT highlighted FROM tasks WHERE id = ?");
$stmt = $pdo->prepare("UPDATE subtasks SET highlighted = ? WHERE id = ?"); $stmt->execute([$id]);
$stmt->execute([$highlighted, $id]); $newState = intval($stmt->fetchColumn());
echo json_encode(['success' => true]);
} else { } else {
echo json_encode(['success' => false, 'error' => 'Invalid type']); $stmt = $pdo->prepare("UPDATE subtasks SET highlighted = (highlighted + 1) % 3 WHERE id = ?");
$stmt->execute([$id]);
$stmt = $pdo->prepare("SELECT highlighted FROM subtasks WHERE id = ?");
$stmt->execute([$id]);
$newState = intval($stmt->fetchColumn());
} }
?>
echo json_encode(['success' => true, 'highlighted' => $newState]);

View File

@@ -1,25 +1,27 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true); require_login();
$user_id = current_user_id();
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !is_array($data)) { if (!$data || !is_array($data)) {
echo json_encode(['success' => false, 'message' => 'Invalid input']); echo json_encode(['success' => false, 'message' => 'Invalid input']);
exit; exit;
} }
$stmt = $pdo->prepare("UPDATE projects SET sort_order = ? WHERE id = ?"); $stmt = $pdo->prepare("UPDATE projects SET sort_order = ? WHERE id = ? AND user_id = ?");
foreach ($data as $item) { foreach ($data as $item) {
$id = $item['id']; $id = $item['id'] ?? null;
$order = $item['order']; $order = $item['order'] ?? null;
if (!is_numeric($id) || !is_numeric($order)) continue; if (!is_numeric($id) || !is_numeric($order)) continue;
$stmt->execute([$order, $id]); $stmt->execute([(int)$order, (int)$id, $user_id]);
} }
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
exit;

View File

@@ -1,13 +1,17 @@
<?php <?php
require 'db.php'; require 'db.php';
require 'auth.php';
header('Content-Type: application/json');
require_can_manage_settings();
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$title = $data['title'] ?? 'tinyTask'; $title = $data['title'] ?? 'tinyTask';
$icon = $data['icon_class'] ?? 'kanban'; $icon = $data['icon_class'] ?? 'kanban';
$color = $data['icon_color'] ?? '#ff0000'; $color = $data['icon_color'] ?? '#ff0000';
$stmt = $pdo->prepare("UPDATE settings SET title = ?, icon_class = ?, icon_color = ? WHERE id = 1"); $stmt = $pdo->prepare("UPDATE settings SET title = ?, icon_class = ?, icon_color = ? WHERE id = 1");
$stmt->execute([$title, $icon, $color]); $stmt->execute([$title, $icon, $color]);
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
?>

View File

@@ -1,11 +1,25 @@
<?php <?php
require 'db.php'; // Adjust path if needed require 'db.php';
require 'auth.php';
$data = json_decode(file_get_contents("php://input"), true); header('Content-Type: application/json');
if (isset($data['task_ids']) && is_array($data['task_ids'])) { require_login();
$stmt = $pdo->prepare("UPDATE tasks SET task_order = ? WHERE id = ?"); $user_id = current_user_id();
foreach ($data['task_ids'] as $order => $id) {
$stmt->execute([$order, $id]); $data = json_decode(file_get_contents('php://input'), true);
} if (!$data || !is_array($data)) {
echo json_encode echo json_encode(['success' => false, 'message' => 'Invalid input']);
exit;
}
$stmt = $pdo->prepare("UPDATE tasks SET task_order = ? WHERE id = ? AND user_id = ?");
foreach ($data as $item) {
$id = $item['id'] ?? null;
$order = $item['order'] ?? null;
if (!is_numeric($id) || !is_numeric($order)) continue;
$stmt->execute([(int)$order, (int)$id, $user_id]);
}
echo json_encode(['success' => true]);