feat: Implement user tracking in projects

- Added user tracking features to the projects module, including:
  - Database schema updates to track project creator and assignee.
  - API enhancements for user management and project filtering by user.
  - UI components for user assignment in project forms and listings.
  - New query functions for retrieving users and filtering projects.
  - Security integration with role-based access and authentication requirements.

chore: Create utility scripts for database checks and project testing

- Added scripts to check the structure of the projects table.
- Created tests for project creation and user tracking functionality.
- Implemented API tests to verify project retrieval and user assignment.

fix: Update project creation and update functions to include user tracking

- Modified createProject and updateProject functions to handle user IDs for creator and assignee.
- Ensured that project updates reflect the correct user assignments and timestamps.
This commit is contained in:
Chop
2025-06-25 23:08:15 +02:00
parent 81afa09f3a
commit 294d8343d3
19 changed files with 790 additions and 35 deletions

View File

@@ -735,6 +735,99 @@ export function withRateLimit(
- Password strength requirements
- Password change interface
## User Tracking in Projects - NEW FEATURE ✅
### 📊 Project User Management Implementation
We've successfully implemented comprehensive user tracking for projects:
#### Database Schema Updates ✅
- **created_by**: Tracks who created the project (user ID)
- **assigned_to**: Tracks who is assigned to work on the project (user ID)
- **created_at**: Timestamp when project was created
- **updated_at**: Timestamp when project was last modified
- **Indexes**: Performance optimized with proper foreign key indexes
#### API Enhancements ✅
- **Enhanced Queries**: Projects now include user names and emails via JOIN operations
- **User Assignment**: New `/api/projects/users` endpoint for user management
- **Query Filters**: Support for filtering projects by assigned user or creator
- **User Context**: Create/update operations automatically capture authenticated user ID
#### UI Components ✅
- **Project Form**: User assignment dropdown in create/edit forms
- **Project Listing**: "Created By" and "Assigned To" columns in project table
- **User Selection**: Dropdown populated with active users for assignment
#### New Query Functions ✅
- `getAllUsersForAssignment()`: Get active users for assignment dropdown
- `getProjectsByAssignedUser(userId)`: Filter projects by assignee
- `getProjectsByCreator(userId)`: Filter projects by creator
- `updateProjectAssignment(projectId, userId)`: Update project assignment
#### Security Integration ✅
- **Authentication Required**: All user operations require valid session
- **Role-Based Access**: User assignment respects role hierarchy
- **Audit Ready**: Infrastructure prepared for comprehensive user action logging
### Usage Examples
#### Creating Projects with User Tracking
```javascript
// Projects are automatically assigned to the authenticated user as creator
POST /api/projects
{
"project_name": "New Project",
"assigned_to": "user-id-here", // Optional assignment
// ... other project data
}
```
#### Filtering Projects by User
```javascript
// Get projects assigned to specific user
GET /api/projects?assigned_to=user-id
// Get projects created by specific user
GET /api/projects?created_by=user-id
```
#### Updating Project Assignment
```javascript
POST /api/projects/users
{
"projectId": 123,
"assignedToUserId": "new-user-id"
}
```
### Next Enhancements
1. **Dashboard Views** (Recommended)
- "My Projects" dashboard showing assigned projects
- Project creation history per user
- Workload distribution reports
2. **Advanced Filtering** (Future)
- Multi-user assignment support
- Team-based project assignments
- Role-based project visibility
3. **Notifications** (Future)
- Email alerts on project assignment
- Deadline reminders for assigned users
- Status change notifications
## Security Best Practices
### 1. Password Security

13
check-columns.mjs Normal file
View File

@@ -0,0 +1,13 @@
import db from "./src/lib/db.js";
console.log("Checking projects table structure:");
const tableInfo = db.prepare("PRAGMA table_info(projects)").all();
console.log(JSON.stringify(tableInfo, null, 2));
// Check if created_at and updated_at columns exist
const hasCreatedAt = tableInfo.some((col) => col.name === "created_at");
const hasUpdatedAt = tableInfo.some((col) => col.name === "updated_at");
console.log("\nColumn existence check:");
console.log("created_at exists:", hasCreatedAt);
console.log("updated_at exists:", hasUpdatedAt);

5
check-projects-table.mjs Normal file
View File

@@ -0,0 +1,5 @@
import db from "./src/lib/db.js";
console.log("Current projects table structure:");
const tableInfo = db.prepare("PRAGMA table_info(projects)").all();
console.log(JSON.stringify(tableInfo, null, 2));

32
check-projects.mjs Normal file
View File

@@ -0,0 +1,32 @@
import Database from "better-sqlite3";
const db = new Database("./data/database.sqlite");
// Check table structures first
console.log("Users table structure:");
const usersSchema = db.prepare("PRAGMA table_info(users)").all();
console.log(usersSchema);
console.log("\nProjects table structure:");
const projectsSchema = db.prepare("PRAGMA table_info(projects)").all();
console.log(projectsSchema);
// Check if there are any projects
const projects = db
.prepare(
`
SELECT p.*,
creator.name as created_by_name,
assignee.name as assigned_to_name
FROM projects p
LEFT JOIN users creator ON p.created_by = creator.id
LEFT JOIN users assignee ON p.assigned_to = assignee.id
LIMIT 5
`
)
.all();
console.log("\nProjects in database:");
console.log(JSON.stringify(projects, null, 2));
db.close();

128
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"leaflet": "^1.9.4",
"next": "15.1.8",
"next-auth": "^5.0.0-beta.29",
"node-fetch": "^3.3.2",
"proj4": "^2.19.3",
"proj4leaflet": "^1.0.2",
"react": "^19.0.0",
@@ -4163,6 +4164,14 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -5311,6 +5320,28 @@
"bser": "2.1.1"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -5423,6 +5454,17 @@
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -7843,6 +7885,42 @@
"node": ">=10"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -10495,6 +10573,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"engines": {
"node": ">= 8"
}
},
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
@@ -13720,6 +13806,11 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
},
"data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -14562,6 +14653,15 @@
"bser": "2.1.1"
}
},
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -14643,6 +14743,14 @@
"mime-types": "^2.1.12"
}
},
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -16292,6 +16400,21 @@
"semver": "^7.3.5"
}
},
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
},
"node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"requires": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
}
},
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -18103,6 +18226,11 @@
"makeerror": "1.0.12"
}
},
"web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
},
"web-worker": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",

View File

@@ -21,6 +21,7 @@
"leaflet": "^1.9.4",
"next": "15.1.8",
"next-auth": "^5.0.0-beta.29",
"node-fetch": "^3.3.2",
"proj4": "^2.19.3",
"proj4leaflet": "^1.0.2",
"react": "^19.0.0",

View File

@@ -3,22 +3,41 @@ import {
updateProject,
deleteProject,
} from "@/lib/queries/projects";
import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
async function getProjectHandler(_, { params }) {
const project = getProjectById(params.id);
// Make sure the DB is initialized before queries run
initializeDatabase();
async function getProjectHandler(req, { params }) {
const { id } = await params;
const project = getProjectById(parseInt(id));
if (!project) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
return NextResponse.json(project);
}
async function updateProjectHandler(req, { params }) {
const { id } = await params;
const data = await req.json();
updateProject(params.id, data);
return NextResponse.json({ success: true });
// Get user ID from authenticated request
const userId = req.user?.id;
updateProject(parseInt(id), data, userId);
// Return the updated project
const updatedProject = getProjectById(parseInt(id));
return NextResponse.json(updatedProject);
}
async function deleteProjectHandler(_, { params }) {
deleteProject(params.id);
async function deleteProjectHandler(req, { params }) {
const { id } = await params;
deleteProject(parseInt(id));
return NextResponse.json({ success: true });
}

View File

@@ -1,4 +1,8 @@
import { getAllProjects, createProject } from "@/lib/queries/projects";
import {
getAllProjects,
createProject,
getAllUsersForAssignment,
} from "@/lib/queries/projects";
import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
@@ -9,15 +13,37 @@ initializeDatabase();
async function getProjectsHandler(req) {
const { searchParams } = new URL(req.url);
const contractId = searchParams.get("contract_id");
const assignedTo = searchParams.get("assigned_to");
const createdBy = searchParams.get("created_by");
let projects;
if (assignedTo) {
const { getProjectsByAssignedUser } = await import(
"@/lib/queries/projects"
);
projects = getProjectsByAssignedUser(assignedTo);
} else if (createdBy) {
const { getProjectsByCreator } = await import("@/lib/queries/projects");
projects = getProjectsByCreator(createdBy);
} else {
projects = getAllProjects(contractId);
}
const projects = getAllProjects(contractId);
return NextResponse.json(projects);
}
async function createProjectHandler(req) {
const data = await req.json();
createProject(data);
return NextResponse.json({ success: true });
// Get user ID from authenticated request
const userId = req.user?.id;
const result = createProject(data, userId);
return NextResponse.json({
success: true,
projectId: result.lastInsertRowid,
});
}
// Protected routes - require authentication

View File

@@ -0,0 +1,33 @@
import {
getAllUsersForAssignment,
updateProjectAssignment,
} from "@/lib/queries/projects";
import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server";
import { withUserAuth } from "@/lib/middleware/auth";
// Make sure the DB is initialized before queries run
initializeDatabase();
async function getUsersHandler(req) {
const users = getAllUsersForAssignment();
return NextResponse.json(users);
}
async function updateAssignmentHandler(req) {
const { projectId, assignedToUserId } = await req.json();
if (!projectId) {
return NextResponse.json(
{ error: "Project ID is required" },
{ status: 400 }
);
}
updateProjectAssignment(projectId, assignedToUserId);
return NextResponse.json({ success: true });
}
// Protected routes - require authentication
export const GET = withUserAuth(getUsersHandler);
export const POST = withUserAuth(updateAssignmentHandler);

View File

@@ -1,17 +1,52 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import ProjectForm from "@/components/ProjectForm";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import Button from "@/components/ui/Button";
import Link from "next/link";
import { LoadingState } from "@/components/ui/States";
export default async function EditProjectPage({ params }) {
const { id } = await params;
const res = await fetch(`http://localhost:3000/api/projects/${id}`, {
cache: "no-store",
});
const project = await res.json();
export default function EditProjectPage() {
const params = useParams();
const id = params.id;
const [project, setProject] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
if (!project) {
useEffect(() => {
const fetchProject = async () => {
try {
const res = await fetch(`/api/projects/${id}`);
if (res.ok) {
const projectData = await res.json();
setProject(projectData);
} else {
setError("Project not found");
}
} catch (err) {
setError("Failed to load project");
} finally {
setLoading(false);
}
};
if (id) {
fetchProject();
}
}, [id]);
if (loading) {
return (
<PageContainer>
<LoadingState />
</PageContainer>
);
}
if (error || !project) {
return (
<PageContainer>
<div className="text-center py-12">

View File

@@ -195,7 +195,13 @@ export default function ProjectListPage() {
</th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
Status
</th>{" "}
</th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
Created By
</th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
Assigned To
</th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20">
Actions
</th>
@@ -275,6 +281,18 @@ export default function ProjectListPage() {
? "Zakończony"
: "-"}
</td>
<td
className="px-2 py-3 text-xs text-gray-600 truncate"
title={project.created_by_name || "Unknown"}
>
{project.created_by_name || "Unknown"}
</td>
<td
className="px-2 py-3 text-xs text-gray-600 truncate"
title={project.assigned_to_name || "Unassigned"}
>
{project.assigned_to_name || "Unassigned"}
</td>
<td className="px-2 py-3">
<Link href={`/projects/${project.project_id}`}>
<Button

View File

@@ -22,22 +22,59 @@ export default function ProjectForm({ initialData = null }) {
contact: "",
notes: "",
coordinates: "",
project_type: initialData?.project_type || "design",
// project_status is not included in the form for creation or editing
...initialData,
project_type: "design",
assigned_to: "",
});
const [contracts, setContracts] = useState([]);
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const router = useRouter();
const isEdit = !!initialData;
useEffect(() => {
// Fetch contracts
fetch("/api/contracts")
.then((res) => res.json())
.then(setContracts);
// Fetch users for assignment
fetch("/api/projects/users")
.then((res) => res.json())
.then(setUsers);
}, []);
// Update form state when initialData changes (for edit mode)
useEffect(() => {
if (initialData) {
setForm({
contract_id: "",
project_name: "",
address: "",
plot: "",
district: "",
unit: "",
city: "",
investment_number: "",
finish_date: "",
wp: "",
contact: "",
notes: "",
coordinates: "",
project_type: "design",
assigned_to: "",
...initialData,
// Ensure these defaults are preserved if not in initialData
project_type: initialData.project_type || "design",
assigned_to: initialData.assigned_to || "",
// Format finish_date for input if it exists
finish_date: initialData.finish_date
? formatDateForInput(initialData.finish_date)
: "",
});
}
}, [initialData]);
function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}
@@ -83,7 +120,7 @@ export default function ProjectForm({ initialData = null }) {
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Contract and Project Type Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Contract <span className="text-red-500">*</span>
@@ -125,6 +162,25 @@ export default function ProjectForm({ initialData = null }) {
</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Assigned To
</label>
<select
name="assigned_to"
value={form.assigned_to || ""}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Unassigned</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name} ({user.email})
</option>
))}
</select>
</div>
</div>
{/* Basic Information Section */}

View File

@@ -163,6 +163,49 @@ export default function initializeDatabase() {
// Column already exists, ignore error
}
// Migration: Add user tracking columns to projects table
try {
db.exec(`
ALTER TABLE projects ADD COLUMN created_by TEXT;
`);
} catch (e) {
// Column already exists, ignore error
}
try {
db.exec(`
ALTER TABLE projects ADD COLUMN assigned_to TEXT;
`);
} catch (e) {
// Column already exists, ignore error
}
try {
db.exec(`
ALTER TABLE projects ADD COLUMN created_at TEXT;
`);
} catch (e) {
// Column already exists, ignore error
}
try {
db.exec(`
ALTER TABLE projects ADD COLUMN updated_at TEXT;
`);
} catch (e) {
// Column already exists, ignore error
}
// Add foreign key indexes for performance
try {
db.exec(`
CREATE INDEX IF NOT EXISTS idx_projects_created_by ON projects(created_by);
CREATE INDEX IF NOT EXISTS idx_projects_assigned_to ON projects(assigned_to);
`);
} catch (e) {
// Index already exists, ignore error
}
// Authorization tables
db.exec(`
-- Users table

View File

@@ -1,21 +1,48 @@
import db from "../db.js";
export function getAllProjects(contractId = null) {
const baseQuery = `
SELECT
p.*,
creator.name as created_by_name,
creator.email as created_by_email,
assignee.name as assigned_to_name,
assignee.email as assigned_to_email
FROM projects p
LEFT JOIN users creator ON p.created_by = creator.id
LEFT JOIN users assignee ON p.assigned_to = assignee.id
`;
if (contractId) {
return db
.prepare(
"SELECT * FROM projects WHERE contract_id = ? ORDER BY finish_date DESC"
baseQuery + " WHERE p.contract_id = ? ORDER BY p.finish_date DESC"
)
.all(contractId);
}
return db.prepare("SELECT * FROM projects ORDER BY finish_date DESC").all();
return db.prepare(baseQuery + " ORDER BY p.finish_date DESC").all();
}
export function getProjectById(id) {
return db.prepare("SELECT * FROM projects WHERE project_id = ?").get(id);
return db
.prepare(
`
SELECT
p.*,
creator.name as created_by_name,
creator.email as created_by_email,
assignee.name as assigned_to_name,
assignee.email as assigned_to_email
FROM projects p
LEFT JOIN users creator ON p.created_by = creator.id
LEFT JOIN users assignee ON p.assigned_to = assignee.id
WHERE p.project_id = ?
`
)
.get(id);
}
export function createProject(data) {
export function createProject(data, userId = null) {
// 1. Get the contract number and count existing projects
const contractInfo = db
.prepare(
@@ -37,12 +64,16 @@ export function createProject(data) {
// 2. Generate sequential number and project number
const sequentialNumber = (contractInfo.project_count || 0) + 1;
const projectNumber = `${sequentialNumber}/${contractInfo.contract_number}`; const stmt = db.prepare(`
const projectNumber = `${sequentialNumber}/${contractInfo.contract_number}`;
const stmt = db.prepare(`
INSERT INTO projects (
contract_id, project_name, project_number, address, plot, district, unit, city, investment_number, finish_date,
wp, contact, notes, project_type, project_status, coordinates
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);stmt.run(
wp, contact, notes, project_type, project_status, coordinates, created_by, assigned_to, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`);
const result = stmt.run(
data.contract_id,
data.project_name,
projectNumber,
@@ -55,16 +86,23 @@ export function createProject(data) {
data.finish_date,
data.wp,
data.contact,
data.notes, data.project_type || "design",
data.notes,
data.project_type || "design",
data.project_status || "registered",
data.coordinates || null
data.coordinates || null,
userId,
data.assigned_to || null
);
return result;
}
export function updateProject(id, data) { const stmt = db.prepare(`
export function updateProject(id, data, userId = null) {
const stmt = db.prepare(`
UPDATE projects SET
contract_id = ?, project_name = ?, project_number = ?, address = ?, plot = ?, district = ?, unit = ?, city = ?,
investment_number = ?, finish_date = ?, wp = ?, contact = ?, notes = ?, project_type = ?, project_status = ?, coordinates = ?
investment_number = ?, finish_date = ?, wp = ?, contact = ?, notes = ?, project_type = ?, project_status = ?,
coordinates = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ?
`);
stmt.run(
@@ -80,9 +118,11 @@ export function updateProject(id, data) { const stmt = db.prepare(`
data.finish_date,
data.wp,
data.contact,
data.notes, data.project_type || "design",
data.notes,
data.project_type || "design",
data.project_status || "registered",
data.coordinates || null,
data.assigned_to || null,
id
);
}
@@ -91,6 +131,75 @@ export function deleteProject(id) {
db.prepare("DELETE FROM projects WHERE project_id = ?").run(id);
}
// Get all users for assignment dropdown
export function getAllUsersForAssignment() {
return db
.prepare(
`
SELECT id, name, email, role
FROM users
WHERE is_active = 1
ORDER BY name
`
)
.all();
}
// Get projects assigned to a specific user
export function getProjectsByAssignedUser(userId) {
return db
.prepare(
`
SELECT
p.*,
creator.name as created_by_name,
creator.email as created_by_email,
assignee.name as assigned_to_name,
assignee.email as assigned_to_email
FROM projects p
LEFT JOIN users creator ON p.created_by = creator.id
LEFT JOIN users assignee ON p.assigned_to = assignee.id
WHERE p.assigned_to = ?
ORDER BY p.finish_date DESC
`
)
.all(userId);
}
// Get projects created by a specific user
export function getProjectsByCreator(userId) {
return db
.prepare(
`
SELECT
p.*,
creator.name as created_by_name,
creator.email as created_by_email,
assignee.name as assigned_to_name,
assignee.email as assigned_to_email
FROM projects p
LEFT JOIN users creator ON p.created_by = creator.id
LEFT JOIN users assignee ON p.assigned_to = assignee.id
WHERE p.created_by = ?
ORDER BY p.finish_date DESC
`
)
.all(userId);
}
// Update project assignment
export function updateProjectAssignment(projectId, assignedToUserId) {
return db
.prepare(
`
UPDATE projects
SET assigned_to = ?, updated_at = CURRENT_TIMESTAMP
WHERE project_id = ?
`
)
.run(assignedToUserId, projectId);
}
export function getProjectWithContract(id) {
return db
.prepare(

40
test-create-function.mjs Normal file
View File

@@ -0,0 +1,40 @@
import { createProject } from "./src/lib/queries/projects.js";
import initializeDatabase from "./src/lib/init-db.js";
// Initialize database
initializeDatabase();
console.log("Testing createProject function...\n");
const testProjectData = {
contract_id: 1, // Assuming contract 1 exists
project_name: "Test Project - User Tracking",
address: "Test Address 123",
plot: "123/456",
district: "Test District",
unit: "Test Unit",
city: "Test City",
investment_number: "TEST-2025-001",
finish_date: "2025-12-31",
wp: "TEST/2025/001",
contact: "test@example.com",
notes: "Test project with user tracking",
project_type: "design",
project_status: "registered",
coordinates: "50.0,20.0",
assigned_to: "e42a4b036074ff7233942a0728557141", // admin user ID
};
try {
console.log("Creating test project with admin user as creator...");
const result = createProject(
testProjectData,
"e42a4b036074ff7233942a0728557141"
);
console.log("✅ Project created successfully!");
console.log("Result:", result);
console.log("Project ID:", result.lastInsertRowid);
} catch (error) {
console.error("❌ Error creating project:", error.message);
console.error("Stack:", error.stack);
}

27
test-project-api.mjs Normal file
View File

@@ -0,0 +1,27 @@
import fetch from "node-fetch";
async function testProjectAPI() {
const baseURL = "http://localhost:3000";
console.log("Testing project API endpoints...\n");
try {
// Test fetching project 1
console.log("1. Fetching project 1:");
const response = await fetch(`${baseURL}/api/projects/1`);
console.log("Status:", response.status);
if (response.ok) {
const project = await response.json();
console.log("Project data received:");
console.log(JSON.stringify(project, null, 2));
} else {
const error = await response.text();
console.log("Error:", error);
}
} catch (error) {
console.error("Error testing API:", error.message);
}
}
testProjectAPI();

43
test-project-creation.mjs Normal file
View File

@@ -0,0 +1,43 @@
// Test project creation
const BASE_URL = "http://localhost:3001";
async function testProjectCreation() {
console.log("🧪 Testing project creation...\n");
try {
// First, login to get session
console.log("1. Logging in...");
const loginResponse = await fetch(
`${BASE_URL}/api/auth/signin/credentials`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "admin@localhost.com",
password: "admin123456",
}),
}
);
console.log("Login response status:", loginResponse.status);
const loginResult = await loginResponse.text();
console.log("Login result:", loginResult.substring(0, 200));
// Try a simple API call to see the auth system
console.log("\n2. Testing projects API...");
const projectsResponse = await fetch(`${BASE_URL}/api/projects`);
console.log("Projects API status:", projectsResponse.status);
if (projectsResponse.status === 401) {
console.log("❌ Authentication required (expected for this test)");
} else {
const projectsData = await projectsResponse.json();
console.log("✅ Projects API accessible");
console.log("Number of projects:", projectsData.length);
}
} catch (error) {
console.error("❌ Test failed:", error.message);
}
}
testProjectCreation();

27
test-user-tracking.mjs Normal file
View File

@@ -0,0 +1,27 @@
import {
getAllProjects,
getAllUsersForAssignment,
} from "./src/lib/queries/projects.js";
import initializeDatabase from "./src/lib/init-db.js";
// Initialize database
initializeDatabase();
console.log("Testing user tracking in projects...\n");
console.log("1. Available users for assignment:");
const users = getAllUsersForAssignment();
console.log(JSON.stringify(users, null, 2));
console.log("\n2. Current projects with user information:");
const projects = getAllProjects();
console.log("Total projects:", projects.length);
if (projects.length > 0) {
console.log("\nFirst project details:");
console.log(JSON.stringify(projects[0], null, 2));
} else {
console.log("No projects found.");
}
console.log("\n✅ User tracking implementation test completed!");

7
verify-project.mjs Normal file
View File

@@ -0,0 +1,7 @@
import { getProjectById } from "./src/lib/queries/projects.js";
console.log("Checking the created project with user tracking...\n");
const project = getProjectById(17);
console.log("Project details:");
console.log(JSON.stringify(project, null, 2));