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 strength requirements
|
||||||
- Password change interface
|
- 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
|
## Security Best Practices
|
||||||
|
|
||||||
### 1. Password Security
|
### 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",
|
"leaflet": "^1.9.4",
|
||||||
"next": "15.1.8",
|
"next": "15.1.8",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"proj4": "^2.19.3",
|
"proj4": "^2.19.3",
|
||||||
"proj4leaflet": "^1.0.2",
|
"proj4leaflet": "^1.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -4163,6 +4164,14 @@
|
|||||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/data-urls": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
|
||||||
@@ -5311,6 +5320,28 @@
|
|||||||
"bser": "2.1.1"
|
"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": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -5423,6 +5454,17 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
@@ -7843,6 +7885,42 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@@ -10495,6 +10573,14 @@
|
|||||||
"makeerror": "1.0.12"
|
"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": {
|
"node_modules/web-worker": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
||||||
@@ -13720,6 +13806,11 @@
|
|||||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||||
"dev": true
|
"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": {
|
"data-urls": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
|
||||||
@@ -14562,6 +14653,15 @@
|
|||||||
"bser": "2.1.1"
|
"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": {
|
"file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -14643,6 +14743,14 @@
|
|||||||
"mime-types": "^2.1.12"
|
"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": {
|
"fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
@@ -16292,6 +16400,21 @@
|
|||||||
"semver": "^7.3.5"
|
"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": {
|
"node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@@ -18103,6 +18226,11 @@
|
|||||||
"makeerror": "1.0.12"
|
"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": {
|
"web-worker": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "15.1.8",
|
"next": "15.1.8",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"next-auth": "^5.0.0-beta.29",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"proj4": "^2.19.3",
|
"proj4": "^2.19.3",
|
||||||
"proj4leaflet": "^1.0.2",
|
"proj4leaflet": "^1.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
@@ -3,22 +3,41 @@ import {
|
|||||||
updateProject,
|
updateProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
} from "@/lib/queries/projects";
|
} from "@/lib/queries/projects";
|
||||||
|
import initializeDatabase from "@/lib/init-db";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
||||||
|
|
||||||
async function getProjectHandler(_, { params }) {
|
// Make sure the DB is initialized before queries run
|
||||||
const project = getProjectById(params.id);
|
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);
|
return NextResponse.json(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateProjectHandler(req, { params }) {
|
async function updateProjectHandler(req, { params }) {
|
||||||
|
const { id } = await params;
|
||||||
const data = await req.json();
|
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 }) {
|
async function deleteProjectHandler(req, { params }) {
|
||||||
deleteProject(params.id);
|
const { id } = await params;
|
||||||
|
deleteProject(parseInt(id));
|
||||||
return NextResponse.json({ success: true });
|
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 initializeDatabase from "@/lib/init-db";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
|
||||||
@@ -9,15 +13,37 @@ initializeDatabase();
|
|||||||
async function getProjectsHandler(req) {
|
async function getProjectsHandler(req) {
|
||||||
const { searchParams } = new URL(req.url);
|
const { searchParams } = new URL(req.url);
|
||||||
const contractId = searchParams.get("contract_id");
|
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);
|
return NextResponse.json(projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProjectHandler(req) {
|
async function createProjectHandler(req) {
|
||||||
const data = await req.json();
|
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
|
// 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 ProjectForm from "@/components/ProjectForm";
|
||||||
import PageContainer from "@/components/ui/PageContainer";
|
import PageContainer from "@/components/ui/PageContainer";
|
||||||
import PageHeader from "@/components/ui/PageHeader";
|
import PageHeader from "@/components/ui/PageHeader";
|
||||||
import Button from "@/components/ui/Button";
|
import Button from "@/components/ui/Button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { LoadingState } from "@/components/ui/States";
|
||||||
|
|
||||||
export default async function EditProjectPage({ params }) {
|
export default function EditProjectPage() {
|
||||||
const { id } = await params;
|
const params = useParams();
|
||||||
const res = await fetch(`http://localhost:3000/api/projects/${id}`, {
|
const id = params.id;
|
||||||
cache: "no-store",
|
const [project, setProject] = useState(null);
|
||||||
});
|
const [loading, setLoading] = useState(true);
|
||||||
const project = await res.json();
|
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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
|
|||||||
@@ -195,7 +195,13 @@ export default function ProjectListPage() {
|
|||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
|
||||||
Status
|
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">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20">
|
||||||
Actions
|
Actions
|
||||||
</th>
|
</th>
|
||||||
@@ -275,6 +281,18 @@ export default function ProjectListPage() {
|
|||||||
? "Zakończony"
|
? "Zakończony"
|
||||||
: "-"}
|
: "-"}
|
||||||
</td>
|
</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">
|
<td className="px-2 py-3">
|
||||||
<Link href={`/projects/${project.project_id}`}>
|
<Link href={`/projects/${project.project_id}`}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -22,22 +22,59 @@ export default function ProjectForm({ initialData = null }) {
|
|||||||
contact: "",
|
contact: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
coordinates: "",
|
coordinates: "",
|
||||||
project_type: initialData?.project_type || "design",
|
project_type: "design",
|
||||||
// project_status is not included in the form for creation or editing
|
assigned_to: "",
|
||||||
...initialData,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [contracts, setContracts] = useState([]);
|
const [contracts, setContracts] = useState([]);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isEdit = !!initialData;
|
const isEdit = !!initialData;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Fetch contracts
|
||||||
fetch("/api/contracts")
|
fetch("/api/contracts")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(setContracts);
|
.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) {
|
function handleChange(e) {
|
||||||
setForm({ ...form, [e.target.name]: e.target.value });
|
setForm({ ...form, [e.target.name]: e.target.value });
|
||||||
}
|
}
|
||||||
@@ -83,7 +120,7 @@ export default function ProjectForm({ initialData = null }) {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* Contract and Project Type Section */}
|
{/* 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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Contract <span className="text-red-500">*</span>
|
Contract <span className="text-red-500">*</span>
|
||||||
@@ -125,6 +162,25 @@ export default function ProjectForm({ initialData = null }) {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Basic Information Section */}
|
{/* Basic Information Section */}
|
||||||
|
|||||||
@@ -163,6 +163,49 @@ export default function initializeDatabase() {
|
|||||||
// Column already exists, ignore error
|
// 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
|
// Authorization tables
|
||||||
db.exec(`
|
db.exec(`
|
||||||
-- Users table
|
-- Users table
|
||||||
|
|||||||
@@ -1,21 +1,48 @@
|
|||||||
import db from "../db.js";
|
import db from "../db.js";
|
||||||
|
|
||||||
export function getAllProjects(contractId = null) {
|
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) {
|
if (contractId) {
|
||||||
return db
|
return db
|
||||||
.prepare(
|
.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);
|
.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) {
|
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
|
// 1. Get the contract number and count existing projects
|
||||||
const contractInfo = db
|
const contractInfo = db
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -37,12 +64,16 @@ export function createProject(data) {
|
|||||||
|
|
||||||
// 2. Generate sequential number and project number
|
// 2. Generate sequential number and project number
|
||||||
const sequentialNumber = (contractInfo.project_count || 0) + 1;
|
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 (
|
INSERT INTO projects (
|
||||||
contract_id, project_name, project_number, address, plot, district, unit, city, investment_number, finish_date,
|
contract_id, project_name, project_number, address, plot, district, unit, city, investment_number, finish_date,
|
||||||
wp, contact, notes, project_type, project_status, coordinates
|
wp, contact, notes, project_type, project_status, coordinates, created_by, assigned_to, created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
`);stmt.run(
|
`);
|
||||||
|
|
||||||
|
const result = stmt.run(
|
||||||
data.contract_id,
|
data.contract_id,
|
||||||
data.project_name,
|
data.project_name,
|
||||||
projectNumber,
|
projectNumber,
|
||||||
@@ -55,16 +86,23 @@ export function createProject(data) {
|
|||||||
data.finish_date,
|
data.finish_date,
|
||||||
data.wp,
|
data.wp,
|
||||||
data.contact,
|
data.contact,
|
||||||
data.notes, data.project_type || "design",
|
data.notes,
|
||||||
|
data.project_type || "design",
|
||||||
data.project_status || "registered",
|
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
|
UPDATE projects SET
|
||||||
contract_id = ?, project_name = ?, project_number = ?, address = ?, plot = ?, district = ?, unit = ?, city = ?,
|
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 = ?
|
WHERE project_id = ?
|
||||||
`);
|
`);
|
||||||
stmt.run(
|
stmt.run(
|
||||||
@@ -80,9 +118,11 @@ export function updateProject(id, data) { const stmt = db.prepare(`
|
|||||||
data.finish_date,
|
data.finish_date,
|
||||||
data.wp,
|
data.wp,
|
||||||
data.contact,
|
data.contact,
|
||||||
data.notes, data.project_type || "design",
|
data.notes,
|
||||||
|
data.project_type || "design",
|
||||||
data.project_status || "registered",
|
data.project_status || "registered",
|
||||||
data.coordinates || null,
|
data.coordinates || null,
|
||||||
|
data.assigned_to || null,
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -91,6 +131,75 @@ export function deleteProject(id) {
|
|||||||
db.prepare("DELETE FROM projects WHERE project_id = ?").run(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) {
|
export function getProjectWithContract(id) {
|
||||||
return db
|
return db
|
||||||
.prepare(
|
.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