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:
@@ -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
13
check-columns.mjs
Normal 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
5
check-projects-table.mjs
Normal 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
32
check-projects.mjs
Normal 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
128
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
33
src/app/api/projects/users/route.js
Normal file
33
src/app/api/projects/users/route.js
Normal 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);
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
40
test-create-function.mjs
Normal 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
27
test-project-api.mjs
Normal 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
43
test-project-creation.mjs
Normal 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
27
test-user-tracking.mjs
Normal 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
7
verify-project.mjs
Normal 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));
|
||||
Reference in New Issue
Block a user