Merge pull request 'Add profile functionality' (#4) from feature/profile_functionality into main

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-02-13 00:48:19 +00:00
8 changed files with 323 additions and 139 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

@@ -6,6 +6,13 @@ 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;
}
$data = json_decode(file_get_contents('php://input'), true);
$name = trim($data['name'] ?? '');
@@ -16,7 +23,7 @@ if ($name === '') {
exit;
}
$stmt = $pdo->prepare("INSERT INTO projects (user_id, name) VALUES (?, ?)");
$stmt->execute([$user_id, $name]);
$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()]);

View File

@@ -6,9 +6,16 @@ header('Content-Type: application/json');
require_login();
$user_id = current_user_id();
$profile_id = $_SESSION['active_profile_id'] ?? null;
$stmt = $pdo->prepare("SELECT * FROM projects WHERE user_id = ? ORDER BY sort_order ASC");
$stmt->execute([$user_id]);
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) {
@@ -23,4 +30,4 @@ foreach ($projects as &$project) {
}
}
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
]);

311
index.php
View File

@@ -144,8 +144,19 @@
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.4rem;
font-size: 1.5rem;
font-weight: 600; /* or 700 for stronger bold */
padding: 0.5rem 1rem;
}
@@ -259,37 +270,36 @@
</button>
</span>
<div class="d-flex align-items-center gap-3">
<!-- 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>
<div class="d-flex align-items-center gap-3">
<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>
<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>
<!-- 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>
</div>
<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>
@@ -538,131 +548,149 @@
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 => {
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);
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 = '';
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');
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>
`;
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);
});
container.appendChild(col);
});
// Create the drop indicator element
const placeholder = document.createElement('div');
placeholder.className = 'project-column';
placeholder.innerHTML = `<div class="project-placeholder"></div>`;
// 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');
// 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('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('dragend', () => {
col.classList.remove('dragging');
placeholder.remove();
});
col.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (!dragging || dragging === col) return;
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;
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);
}
});
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
}
});
});
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
@@ -956,7 +984,23 @@ document.querySelectorAll('.project-column').forEach(col => {
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();
@@ -979,6 +1023,7 @@ document.querySelectorAll('.project-column').forEach(col => {
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = 'none');
}
loadProfiles();
fetchProjects();
});
});

View File

@@ -33,6 +33,27 @@ $_SESSION['user'] = [
'can_manage_settings' => intval($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) {
// Fallback to first profile
$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) {
// Last-resort: create a default profile if none exist (useful for new users)
$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' => [
@@ -40,5 +61,6 @@ echo json_encode([
'email' => $_SESSION['user']['email'],
'role' => $_SESSION['user']['role'],
'can_manage_settings' => $_SESSION['user']['can_manage_settings'],
]
],
'active_profile_id' => $_SESSION['active_profile_id']
]);

View File

@@ -33,10 +33,24 @@ if ($role_id <= 0) {
}
try {
$pdo->beginTransaction();
// Create user
$stmt = $pdo->prepare("INSERT INTO users (email, password_hash, role_id) VALUES (?, ?, ?)");
$stmt->execute([$email, $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']);
}

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]);