feat: Add user tracking to project tasks and notes
- Implemented user tracking columns in project_tasks and notes tables. - Added created_by and assigned_to fields to project_tasks. - Introduced created_by field and is_system flag in notes. - Updated API endpoints to handle user tracking during task and note creation. - Enhanced database initialization to include new columns and indexes. - Created utility functions to fetch users for task assignment. - Updated front-end components to display user information for tasks and notes. - Added tests for project-tasks API endpoints to verify functionality.
This commit is contained in:
@@ -809,6 +809,81 @@ POST /api/projects/users
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Project Tasks User Tracking - NEW FEATURE ✅
|
||||||
|
|
||||||
|
### 📋 Task User Management Implementation
|
||||||
|
|
||||||
|
We've also implemented comprehensive user tracking for project tasks:
|
||||||
|
|
||||||
|
#### Database Schema Updates ✅
|
||||||
|
|
||||||
|
- **created_by**: Tracks who created the task (user ID)
|
||||||
|
- **assigned_to**: Tracks who is assigned to work on the task (user ID)
|
||||||
|
- **created_at**: Timestamp when task was created
|
||||||
|
- **updated_at**: Timestamp when task was last modified
|
||||||
|
- **Indexes**: Performance optimized with proper foreign key indexes
|
||||||
|
|
||||||
|
#### API Enhancements ✅
|
||||||
|
|
||||||
|
- **Enhanced Queries**: Tasks now include user names and emails via JOIN operations
|
||||||
|
- **User Assignment**: New `/api/project-tasks/users` endpoint for user management
|
||||||
|
- **Query Filters**: Support for filtering tasks by assigned user or creator
|
||||||
|
- **User Context**: Create/update operations automatically capture authenticated user ID
|
||||||
|
|
||||||
|
#### UI Components ✅
|
||||||
|
|
||||||
|
- **Task Form**: User assignment dropdown in create task forms
|
||||||
|
- **Task Listing**: "Created By" and "Assigned To" columns in task table
|
||||||
|
- **User Selection**: Dropdown populated with active users for assignment
|
||||||
|
|
||||||
|
#### New Task Query Functions ✅
|
||||||
|
|
||||||
|
- `getAllUsersForTaskAssignment()`: Get active users for assignment dropdown
|
||||||
|
- `getProjectTasksByAssignedUser(userId)`: Filter tasks by assignee
|
||||||
|
- `getProjectTasksByCreator(userId)`: Filter tasks by creator
|
||||||
|
- `updateProjectTaskAssignment(taskId, userId)`: Update task assignment
|
||||||
|
|
||||||
|
#### Task Creation Behavior ✅
|
||||||
|
|
||||||
|
- **Auto-assignment**: Tasks are automatically assigned to the authenticated user as creator
|
||||||
|
- **Optional Assignment**: Users can assign tasks to other team members during creation
|
||||||
|
- **Creator Tracking**: All tasks track who created them for accountability
|
||||||
|
|
||||||
|
### Task Usage Examples
|
||||||
|
|
||||||
|
#### Creating Tasks with User Tracking
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Tasks are automatically assigned to the authenticated user as creator
|
||||||
|
POST /api/project-tasks
|
||||||
|
{
|
||||||
|
"project_id": 123,
|
||||||
|
"task_template_id": 1, // or custom_task_name for custom tasks
|
||||||
|
"assigned_to": "user-id-here", // Optional, defaults to creator
|
||||||
|
"priority": "high"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Filtering Tasks by User
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get tasks assigned to specific user
|
||||||
|
GET /api/project-tasks?assigned_to=user-id
|
||||||
|
|
||||||
|
// Get tasks created by specific user
|
||||||
|
GET /api/project-tasks?created_by=user-id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Updating Task Assignment
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
POST /api/project-tasks/users
|
||||||
|
{
|
||||||
|
"taskId": 456,
|
||||||
|
"assignedToUserId": "new-user-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Next Enhancements
|
### Next Enhancements
|
||||||
|
|
||||||
1. **Dashboard Views** (Recommended)
|
1. **Dashboard Views** (Recommended)
|
||||||
@@ -828,123 +903,69 @@ POST /api/projects/users
|
|||||||
- Deadline reminders for assigned users
|
- Deadline reminders for assigned users
|
||||||
- Status change notifications
|
- Status change notifications
|
||||||
|
|
||||||
## Security Best Practices
|
## Notes User Tracking - NEW FEATURE ✅
|
||||||
|
|
||||||
### 1. Password Security
|
### 📝 Notes User Management Implementation
|
||||||
|
|
||||||
- Minimum 8 characters
|
We've also implemented comprehensive user tracking for all notes (both project notes and task notes):
|
||||||
- Require special characters, numbers
|
|
||||||
- Hash with bcrypt (cost factor 12+)
|
|
||||||
- Implement password history
|
|
||||||
|
|
||||||
### 2. Session Security
|
#### Database Schema Updates ✅
|
||||||
|
|
||||||
- Secure cookies
|
- **created_by**: Tracks who created the note (user ID)
|
||||||
- Session rotation
|
- **is_system**: Distinguishes between user notes and system-generated notes
|
||||||
- Timeout handling
|
- **Enhanced queries**: Notes now include user names and emails via JOIN operations
|
||||||
- Device tracking
|
- **Indexes**: Performance optimized with proper indexes for user lookups
|
||||||
|
|
||||||
### 3. API Security
|
#### API Enhancements ✅
|
||||||
|
|
||||||
- Input validation on all endpoints
|
- **User Context**: All note creation operations automatically capture authenticated user ID
|
||||||
- SQL injection prevention (prepared statements)
|
- **System Notes**: Automatic system notes (task status changes) track who made the change
|
||||||
- XSS protection
|
- **User Information**: Note retrieval includes creator name and email for display
|
||||||
- CSRF tokens
|
|
||||||
|
|
||||||
### 4. Audit & Monitoring
|
#### UI Components ✅
|
||||||
|
|
||||||
- Log all authentication events
|
- **Project Notes**: Display creator name and email in project note listings
|
||||||
- Monitor failed login attempts
|
- **Task Notes**: Show who added each note with user badges and timestamps
|
||||||
- Track permission changes
|
- **System Notes**: Distinguished from user notes with special styling and "System" badge
|
||||||
- Alert on suspicious activity
|
- **User Attribution**: Clear indication of who created each note and when
|
||||||
|
|
||||||
## Testing Status
|
#### New Note Query Functions ✅
|
||||||
|
|
||||||
### ✅ Completed Tests
|
- `getAllNotesWithUsers()`: Get all notes with user and project/task context
|
||||||
|
- `getNotesByCreator(userId)`: Filter notes by creator for user activity tracking
|
||||||
|
- Enhanced `getNotesByProjectId()` and `getNotesByTaskId()` with user information
|
||||||
|
|
||||||
- **Authentication Flow**: Login/logout working correctly
|
#### Automatic User Tracking ✅
|
||||||
- **API Protection**: All endpoints properly secured
|
|
||||||
- **Role Validation**: Permission levels enforced
|
|
||||||
- **Session Management**: JWT tokens and expiration working
|
|
||||||
- **Password Security**: bcrypt hashing and verification functional
|
|
||||||
- **Account Lockout**: Failed attempt tracking and temporary lockout
|
|
||||||
|
|
||||||
### 🔧 Available Test Scripts
|
- **Note Creation**: All new notes automatically record who created them
|
||||||
|
- **System Notes**: Task status changes generate system notes attributed to the user who made the change
|
||||||
|
- **Audit Trail**: Complete history of who added what notes and when
|
||||||
|
|
||||||
- `test-auth.mjs` - Tests API route protection and auth endpoints
|
### Notes Usage Examples
|
||||||
- `test-auth-detailed.mjs` - Comprehensive authentication flow testing
|
|
||||||
- `test-complete-auth.mjs` - Full system authentication validation
|
|
||||||
- `test-logged-in-flow.mjs` - Authenticated user session testing
|
|
||||||
|
|
||||||
### ✅ Verified Security Features
|
#### Project Notes with User Tracking
|
||||||
|
|
||||||
- Unauthorized API requests return 401
|
- Notes display creator name in a blue badge next to the timestamp
|
||||||
- Role-based access control working
|
- Form automatically associates notes with the authenticated user
|
||||||
- Session tokens properly validated
|
- Clear visual distinction between different note authors
|
||||||
- Password attempts tracked and limited
|
|
||||||
- Admin user creation and management functional
|
|
||||||
|
|
||||||
## Deployment Considerations
|
#### Task Notes with User Tracking
|
||||||
|
|
||||||
### 1. Environment Variables
|
- User notes show creator name in a gray badge
|
||||||
|
- System notes show "System" badge but also track the user who triggered the action
|
||||||
|
- Full audit trail of task status changes and who made them
|
||||||
|
|
||||||
- Use strong, random secrets
|
#### System Note Generation
|
||||||
- Different keys per environment
|
|
||||||
- Secure secret management
|
|
||||||
|
|
||||||
### 2. Database Security
|
```javascript
|
||||||
|
// When a user changes a task status, a system note is automatically created:
|
||||||
|
// "Status changed from 'pending' to 'in_progress'" - attributed to the user who made the change
|
||||||
|
```
|
||||||
|
|
||||||
- Regular backups
|
### Benefits
|
||||||
- Encryption at rest
|
|
||||||
- Network security
|
|
||||||
- Access logging
|
|
||||||
|
|
||||||
### 3. Application Security
|
1. **Accountability**: Full audit trail of who added what notes
|
||||||
|
2. **Context**: Know who to contact for clarification on specific notes
|
||||||
- HTTPS enforcement
|
3. **History**: Track communication and decisions made by team members
|
||||||
- Security headers
|
4. **System Integration**: Automatic notes for system actions still maintain user attribution
|
||||||
- Content Security Policy
|
5. **User Experience**: Clear visual indicators of note authors improve team collaboration
|
||||||
- Regular security updates
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
### 1. Development Phase
|
|
||||||
|
|
||||||
- Implement on development branch
|
|
||||||
- Test thoroughly with sample data
|
|
||||||
- Document all changes
|
|
||||||
|
|
||||||
### 2. Staging Deployment
|
|
||||||
|
|
||||||
- Deploy to staging environment
|
|
||||||
- Performance testing
|
|
||||||
- Security testing
|
|
||||||
- User acceptance testing
|
|
||||||
|
|
||||||
### 3. Production Deployment
|
|
||||||
|
|
||||||
- Database backup before migration
|
|
||||||
- Gradual rollout
|
|
||||||
- Monitor for issues
|
|
||||||
- Rollback plan ready
|
|
||||||
|
|
||||||
## Resources and Documentation
|
|
||||||
|
|
||||||
### NextAuth.js
|
|
||||||
|
|
||||||
- [Official Documentation](https://next-auth.js.org/)
|
|
||||||
- [Better SQLite3 Adapter](https://authjs.dev/reference/adapter/better-sqlite3)
|
|
||||||
|
|
||||||
### Security Libraries
|
|
||||||
|
|
||||||
- [Zod Validation](https://zod.dev/)
|
|
||||||
- [bcryptjs](https://www.npmjs.com/package/bcryptjs)
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
|
|
||||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
||||||
- [Next.js Security Guidelines](https://nextjs.org/docs/advanced-features/security-headers)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Next Steps**: Choose which phase to implement first and create detailed implementation tickets for development.
|
|
||||||
|
|||||||
25
check-task-schema.mjs
Normal file
25
check-task-schema.mjs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const db = new Database("./data/database.sqlite");
|
||||||
|
|
||||||
|
console.log("Project Tasks table structure:");
|
||||||
|
const projectTasksSchema = db.prepare("PRAGMA table_info(project_tasks)").all();
|
||||||
|
console.table(projectTasksSchema);
|
||||||
|
|
||||||
|
console.log("\nSample project tasks with user tracking:");
|
||||||
|
const tasks = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT pt.*,
|
||||||
|
creator.name as created_by_name,
|
||||||
|
assignee.name as assigned_to_name
|
||||||
|
FROM project_tasks pt
|
||||||
|
LEFT JOIN users creator ON pt.created_by = creator.id
|
||||||
|
LEFT JOIN users assignee ON pt.assigned_to = assignee.id
|
||||||
|
LIMIT 3
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
console.table(tasks);
|
||||||
|
|
||||||
|
db.close();
|
||||||
49
debug-task-insert.mjs
Normal file
49
debug-task-insert.mjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const db = new Database("./data/database.sqlite");
|
||||||
|
|
||||||
|
console.log("Project Tasks table columns:");
|
||||||
|
const projectTasksSchema = db.prepare("PRAGMA table_info(project_tasks)").all();
|
||||||
|
projectTasksSchema.forEach((col) => {
|
||||||
|
console.log(
|
||||||
|
`${col.name}: ${col.type} (${col.notnull ? "NOT NULL" : "NULL"})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nChecking if created_at and updated_at columns exist...");
|
||||||
|
const hasCreatedAt = projectTasksSchema.some(
|
||||||
|
(col) => col.name === "created_at"
|
||||||
|
);
|
||||||
|
const hasUpdatedAt = projectTasksSchema.some(
|
||||||
|
(col) => col.name === "updated_at"
|
||||||
|
);
|
||||||
|
console.log("created_at exists:", hasCreatedAt);
|
||||||
|
console.log("updated_at exists:", hasUpdatedAt);
|
||||||
|
|
||||||
|
// Let's try a simple insert to see what happens
|
||||||
|
console.log("\nTesting manual insert...");
|
||||||
|
try {
|
||||||
|
const result = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO project_tasks (
|
||||||
|
project_id, task_template_id, status, priority,
|
||||||
|
created_by, assigned_to, created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(1, 1, "pending", "normal", "test-user", "test-user");
|
||||||
|
|
||||||
|
console.log("Insert successful, ID:", result.lastInsertRowid);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
db.prepare("DELETE FROM project_tasks WHERE id = ?").run(
|
||||||
|
result.lastInsertRowid
|
||||||
|
);
|
||||||
|
console.log("Test record cleaned up");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Insert failed:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
60
fix-notes-columns.mjs
Normal file
60
fix-notes-columns.mjs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const db = new Database("./data/database.sqlite");
|
||||||
|
|
||||||
|
console.log("Adding user tracking columns to notes table...\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Adding created_by column...");
|
||||||
|
db.exec(`ALTER TABLE notes ADD COLUMN created_by TEXT;`);
|
||||||
|
console.log("✓ created_by column added");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("created_by column already exists or error:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Adding is_system column...");
|
||||||
|
db.exec(`ALTER TABLE notes ADD COLUMN is_system INTEGER DEFAULT 0;`);
|
||||||
|
console.log("✓ is_system column added");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("is_system column already exists or error:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nVerifying columns were added...");
|
||||||
|
const schema = db.prepare("PRAGMA table_info(notes)").all();
|
||||||
|
const hasCreatedBy = schema.some((col) => col.name === "created_by");
|
||||||
|
const hasIsSystem = schema.some((col) => col.name === "is_system");
|
||||||
|
|
||||||
|
console.log("created_by exists:", hasCreatedBy);
|
||||||
|
console.log("is_system exists:", hasIsSystem);
|
||||||
|
|
||||||
|
if (hasCreatedBy && hasIsSystem) {
|
||||||
|
console.log("\n✅ All columns are now present!");
|
||||||
|
|
||||||
|
// Test a manual insert
|
||||||
|
console.log("\nTesting manual note insert...");
|
||||||
|
try {
|
||||||
|
const result = db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO notes (project_id, note, created_by, is_system, note_date)
|
||||||
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(1, "Test note with user tracking", "test-user-id", 0);
|
||||||
|
|
||||||
|
console.log("Insert successful, ID:", result.lastInsertRowid);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
db.prepare("DELETE FROM notes WHERE note_id = ?").run(
|
||||||
|
result.lastInsertRowid
|
||||||
|
);
|
||||||
|
console.log("Test record cleaned up");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Insert failed:", error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("\n❌ Some columns are still missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
37
fix-task-columns.mjs
Normal file
37
fix-task-columns.mjs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const db = new Database("./data/database.sqlite");
|
||||||
|
|
||||||
|
console.log("Adding missing columns to project_tasks table...\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Adding created_at column...");
|
||||||
|
db.exec(`ALTER TABLE project_tasks ADD COLUMN created_at TEXT;`);
|
||||||
|
console.log("✓ created_at column added");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("created_at column already exists or error:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("Adding updated_at column...");
|
||||||
|
db.exec(`ALTER TABLE project_tasks ADD COLUMN updated_at TEXT;`);
|
||||||
|
console.log("✓ updated_at column added");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("updated_at column already exists or error:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nVerifying columns were added...");
|
||||||
|
const schema = db.prepare("PRAGMA table_info(project_tasks)").all();
|
||||||
|
const hasCreatedAt = schema.some((col) => col.name === "created_at");
|
||||||
|
const hasUpdatedAt = schema.some((col) => col.name === "updated_at");
|
||||||
|
|
||||||
|
console.log("created_at exists:", hasCreatedAt);
|
||||||
|
console.log("updated_at exists:", hasUpdatedAt);
|
||||||
|
|
||||||
|
if (hasCreatedAt && hasUpdatedAt) {
|
||||||
|
console.log("\n✅ All columns are now present!");
|
||||||
|
} else {
|
||||||
|
console.log("\n❌ Some columns are still missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
@@ -9,14 +9,22 @@ async function createNoteHandler(req) {
|
|||||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
INSERT INTO notes (project_id, task_id, note)
|
INSERT INTO notes (project_id, task_id, note, created_by, note_date)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
`
|
`
|
||||||
).run(project_id || null, task_id || null, note);
|
).run(project_id || null, task_id || null, note, req.user?.id || null);
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating note:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to create note", details: error.message },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNoteHandler(_, { params }) {
|
async function deleteNoteHandler(_, { params }) {
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ async function updateProjectTaskHandler(req, { params }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProjectTaskStatus(params.id, status);
|
updateProjectTaskStatus(params.id, status, req.user?.id || null);
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error updating task status:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to update project task" },
|
{ error: "Failed to update project task", details: error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,20 @@ async function createProjectTaskHandler(req) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = createProjectTask(data);
|
// Add user tracking information from authenticated session
|
||||||
|
const taskData = {
|
||||||
|
...data,
|
||||||
|
created_by: req.user?.id || null,
|
||||||
|
// If no assigned_to is specified, default to the creator
|
||||||
|
assigned_to: data.assigned_to || req.user?.id || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = createProjectTask(taskData);
|
||||||
return NextResponse.json({ success: true, id: result.lastInsertRowid });
|
return NextResponse.json({ success: true, id: result.lastInsertRowid });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error creating project task:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to create project task" },
|
{ error: "Failed to create project task", details: error.message },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/app/api/project-tasks/users/route.js
Normal file
50
src/app/api/project-tasks/users/route.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
updateProjectTaskAssignment,
|
||||||
|
getAllUsersForTaskAssignment,
|
||||||
|
} from "@/lib/queries/tasks";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { withUserAuth, withReadAuth } from "@/lib/middleware/auth";
|
||||||
|
|
||||||
|
// GET: Get all users for task assignment
|
||||||
|
async function getUsersForTaskAssignmentHandler(req) {
|
||||||
|
try {
|
||||||
|
const users = getAllUsersForTaskAssignment();
|
||||||
|
return NextResponse.json(users);
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to fetch users" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Update task assignment
|
||||||
|
async function updateTaskAssignmentHandler(req) {
|
||||||
|
try {
|
||||||
|
const { taskId, assignedToUserId } = await req.json();
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "taskId is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = updateProjectTaskAssignment(taskId, assignedToUserId);
|
||||||
|
|
||||||
|
if (result.changes === 0) {
|
||||||
|
return NextResponse.json({ error: "Task not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to update task assignment" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
export const GET = withReadAuth(getUsersForTaskAssignmentHandler);
|
||||||
|
export const POST = withUserAuth(updateTaskAssignmentHandler);
|
||||||
@@ -38,7 +38,7 @@ async function addTaskNoteHandler(req) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
addNoteToTask(task_id, note, is_system);
|
addNoteToTask(task_id, note, is_system, req.user?.id || null);
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding task note:", error);
|
console.error("Error adding task note:", error);
|
||||||
|
|||||||
@@ -400,12 +400,20 @@ export default async function ProjectViewPage({ params }) {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
{" "}
|
{" "}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader> <div className="flex items-center justify-between">
|
<CardHeader>
|
||||||
|
{" "}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
Project Location
|
Project Location
|
||||||
</h2>
|
</h2>
|
||||||
{project.coordinates && (
|
{project.coordinates && (
|
||||||
<Link href={`/projects/map?lat=${project.coordinates.split(',')[0].trim()}&lng=${project.coordinates.split(',')[1].trim()}&zoom=16`}>
|
<Link
|
||||||
|
href={`/projects/map?lat=${project.coordinates
|
||||||
|
.split(",")[0]
|
||||||
|
.trim()}&lng=${project.coordinates
|
||||||
|
.split(",")[1]
|
||||||
|
.trim()}&zoom=16`}
|
||||||
|
>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 mr-2"
|
className="w-4 h-4 mr-2"
|
||||||
@@ -481,9 +489,16 @@ export default async function ProjectViewPage({ params }) {
|
|||||||
className="border border-gray-200 p-4 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors"
|
className="border border-gray-200 p-4 rounded-lg bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm font-medium text-gray-500">
|
<span className="text-sm font-medium text-gray-500">
|
||||||
{n.note_date}
|
{n.note_date}
|
||||||
</span>
|
</span>
|
||||||
|
{n.created_by_name && (
|
||||||
|
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium">
|
||||||
|
{n.created_by_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-900 leading-relaxed">{n.note}</p>
|
<p className="text-gray-900 leading-relaxed">{n.note}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import Badge from "./ui/Badge";
|
|||||||
|
|
||||||
export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
||||||
const [taskTemplates, setTaskTemplates] = useState([]);
|
const [taskTemplates, setTaskTemplates] = useState([]);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
const [taskType, setTaskType] = useState("template"); // "template" or "custom"
|
const [taskType, setTaskType] = useState("template"); // "template" or "custom"
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState("");
|
const [selectedTemplate, setSelectedTemplate] = useState("");
|
||||||
const [customTaskName, setCustomTaskName] = useState("");
|
const [customTaskName, setCustomTaskName] = useState("");
|
||||||
const [customMaxWaitDays, setCustomMaxWaitDays] = useState("");
|
const [customMaxWaitDays, setCustomMaxWaitDays] = useState("");
|
||||||
const [customDescription, setCustomDescription] = useState("");
|
const [customDescription, setCustomDescription] = useState("");
|
||||||
const [priority, setPriority] = useState("normal");
|
const [priority, setPriority] = useState("normal");
|
||||||
|
const [assignedTo, setAssignedTo] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -19,6 +21,11 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
|||||||
fetch("/api/tasks/templates")
|
fetch("/api/tasks/templates")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(setTaskTemplates);
|
.then(setTaskTemplates);
|
||||||
|
|
||||||
|
// Fetch users for assignment
|
||||||
|
fetch("/api/project-tasks/users")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(setUsers);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
@@ -34,6 +41,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
|||||||
const requestData = {
|
const requestData = {
|
||||||
project_id: parseInt(projectId),
|
project_id: parseInt(projectId),
|
||||||
priority,
|
priority,
|
||||||
|
assigned_to: assignedTo || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (taskType === "template") {
|
if (taskType === "template") {
|
||||||
@@ -56,6 +64,7 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
|||||||
setCustomMaxWaitDays("");
|
setCustomMaxWaitDays("");
|
||||||
setCustomDescription("");
|
setCustomDescription("");
|
||||||
setPriority("normal");
|
setPriority("normal");
|
||||||
|
setAssignedTo("");
|
||||||
if (onTaskAdded) onTaskAdded();
|
if (onTaskAdded) onTaskAdded();
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to add task to project.");
|
alert("Failed to add task to project.");
|
||||||
@@ -158,6 +167,24 @@ export default function ProjectTaskForm({ projectId, onTaskAdded }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Assign To <span className="text-gray-500 text-xs">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={assignedTo}
|
||||||
|
onChange={(e) => setAssignedTo(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 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>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Priority
|
Priority
|
||||||
|
|||||||
@@ -273,6 +273,28 @@ export default function ProjectTasksList() {
|
|||||||
<td className="px-4 py-3 text-sm text-gray-600">
|
<td className="px-4 py-3 text-sm text-gray-600">
|
||||||
{task.address || "N/A"}
|
{task.address || "N/A"}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-gray-600">
|
||||||
|
{task.created_by_name ? (
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{task.created_by_name}</div>
|
||||||
|
<div className="text-xs text-gray-500">{task.created_by_email}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"N/A"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-gray-600">
|
||||||
|
{task.assigned_to_name ? (
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{task.assigned_to_name}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{task.assigned_to_email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 italic">Unassigned</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
{showTimeLeft && (
|
{showTimeLeft && (
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -361,7 +383,7 @@ export default function ProjectTasksList() {
|
|||||||
const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => {
|
const TaskTable = ({ tasks, showGrouped = false, showTimeLeft = false }) => {
|
||||||
const filteredTasks = filterTasks(tasks);
|
const filteredTasks = filterTasks(tasks);
|
||||||
const groupedTasks = groupTasksByName(filteredTasks);
|
const groupedTasks = groupTasksByName(filteredTasks);
|
||||||
const colSpan = showTimeLeft ? "8" : "7";
|
const colSpan = showTimeLeft ? "10" : "9";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -379,7 +401,13 @@ export default function ProjectTasksList() {
|
|||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
Address
|
Address
|
||||||
</th>{" "}
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Created By
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
|
Assigned To
|
||||||
|
</th>
|
||||||
{showTimeLeft && (
|
{showTimeLeft && (
|
||||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700">
|
||||||
Time Left
|
Time Left
|
||||||
|
|||||||
@@ -517,6 +517,11 @@ export default function ProjectTasksSection({ projectId }) {
|
|||||||
System
|
System
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{note.created_by_name && (
|
||||||
|
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded-full font-medium">
|
||||||
|
{note.created_by_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-800">
|
<p className="text-sm text-gray-800">
|
||||||
{note.note}
|
{note.note}
|
||||||
@@ -525,6 +530,11 @@ export default function ProjectTasksSection({ projectId }) {
|
|||||||
{formatDate(note.note_date, {
|
{formatDate(note.note_date, {
|
||||||
includeTime: true,
|
includeTime: true,
|
||||||
})}
|
})}
|
||||||
|
{note.created_by_name && (
|
||||||
|
<span className="ml-2">
|
||||||
|
by {note.created_by_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{!note.is_system && (
|
{!note.is_system && (
|
||||||
|
|||||||
@@ -196,11 +196,72 @@ export default function initializeDatabase() {
|
|||||||
// Column already exists, ignore error
|
// Column already exists, ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add foreign key indexes for performance
|
// Migration: Add user tracking columns to project_tasks table
|
||||||
try {
|
try {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_created_by ON projects(created_by);
|
ALTER TABLE project_tasks ADD COLUMN created_by TEXT;
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_assigned_to ON projects(assigned_to);
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE project_tasks ADD COLUMN assigned_to TEXT;
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE project_tasks ADD COLUMN created_at TEXT;
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE project_tasks ADD COLUMN updated_at TEXT;
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes for project_tasks user tracking
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_tasks_created_by ON project_tasks(created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_tasks_assigned_to ON project_tasks(assigned_to);
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Index already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration: Add user tracking columns to notes table
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE notes ADD COLUMN created_by TEXT;
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE notes ADD COLUMN is_system INTEGER DEFAULT 0;
|
||||||
|
`);
|
||||||
|
} catch (e) {
|
||||||
|
// Column already exists, ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes for notes user tracking
|
||||||
|
try {
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_created_by ON notes(created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_project_id ON notes(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_task_id ON notes(task_id);
|
||||||
`);
|
`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Index already exists, ignore error
|
// Index already exists, ignore error
|
||||||
|
|||||||
@@ -2,29 +2,100 @@ import db from "../db.js";
|
|||||||
|
|
||||||
export function getNotesByProjectId(project_id) {
|
export function getNotesByProjectId(project_id) {
|
||||||
return db
|
return db
|
||||||
.prepare(`SELECT * FROM notes WHERE project_id = ? ORDER BY note_date DESC`)
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT n.*,
|
||||||
|
u.name as created_by_name,
|
||||||
|
u.email as created_by_email
|
||||||
|
FROM notes n
|
||||||
|
LEFT JOIN users u ON n.created_by = u.id
|
||||||
|
WHERE n.project_id = ?
|
||||||
|
ORDER BY n.note_date DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
.all(project_id);
|
.all(project_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addNoteToProject(project_id, note) {
|
export function addNoteToProject(project_id, note, created_by = null) {
|
||||||
db.prepare(`INSERT INTO notes (project_id, note) VALUES (?, ?)`).run(
|
db.prepare(
|
||||||
project_id,
|
`
|
||||||
note
|
INSERT INTO notes (project_id, note, created_by, note_date)
|
||||||
);
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
`
|
||||||
|
).run(project_id, note, created_by);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNotesByTaskId(task_id) {
|
export function getNotesByTaskId(task_id) {
|
||||||
return db
|
return db
|
||||||
.prepare(`SELECT * FROM notes WHERE task_id = ? ORDER BY note_date DESC`)
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT n.*,
|
||||||
|
u.name as created_by_name,
|
||||||
|
u.email as created_by_email
|
||||||
|
FROM notes n
|
||||||
|
LEFT JOIN users u ON n.created_by = u.id
|
||||||
|
WHERE n.task_id = ?
|
||||||
|
ORDER BY n.note_date DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
.all(task_id);
|
.all(task_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addNoteToTask(task_id, note, is_system = false) {
|
export function addNoteToTask(
|
||||||
|
task_id,
|
||||||
|
note,
|
||||||
|
is_system = false,
|
||||||
|
created_by = null
|
||||||
|
) {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO notes (task_id, note, is_system) VALUES (?, ?, ?)`
|
`INSERT INTO notes (task_id, note, is_system, created_by, note_date)
|
||||||
).run(task_id, note, is_system ? 1 : 0);
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`
|
||||||
|
).run(task_id, note, is_system ? 1 : 0, created_by);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteNote(note_id) {
|
export function deleteNote(note_id) {
|
||||||
db.prepare(`DELETE FROM notes WHERE note_id = ?`).run(note_id);
|
db.prepare(`DELETE FROM notes WHERE note_id = ?`).run(note_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all notes with user information (for admin/reporting purposes)
|
||||||
|
export function getAllNotesWithUsers() {
|
||||||
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT n.*,
|
||||||
|
u.name as created_by_name,
|
||||||
|
u.email as created_by_email,
|
||||||
|
p.project_name,
|
||||||
|
COALESCE(pt.custom_task_name, t.name) as task_name
|
||||||
|
FROM notes n
|
||||||
|
LEFT JOIN users u ON n.created_by = u.id
|
||||||
|
LEFT JOIN projects p ON n.project_id = p.project_id
|
||||||
|
LEFT JOIN project_tasks pt ON n.task_id = pt.id
|
||||||
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
|
ORDER BY n.note_date DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get notes created by a specific user
|
||||||
|
export function getNotesByCreator(userId) {
|
||||||
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT n.*,
|
||||||
|
u.name as created_by_name,
|
||||||
|
u.email as created_by_email,
|
||||||
|
p.project_name,
|
||||||
|
COALESCE(pt.custom_task_name, t.name) as task_name
|
||||||
|
FROM notes n
|
||||||
|
LEFT JOIN users u ON n.created_by = u.id
|
||||||
|
LEFT JOIN projects p ON n.project_id = p.project_id
|
||||||
|
LEFT JOIN project_tasks pt ON n.task_id = pt.id
|
||||||
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
|
WHERE n.created_by = ?
|
||||||
|
ORDER BY n.note_date DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(userId);
|
||||||
|
}
|
||||||
|
|||||||
@@ -222,9 +222,13 @@ export function getNotesForProject(projectId) {
|
|||||||
return db
|
return db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
SELECT * FROM notes
|
SELECT n.*,
|
||||||
WHERE project_id = ?
|
u.name as created_by_name,
|
||||||
ORDER BY note_date DESC
|
u.email as created_by_email
|
||||||
|
FROM notes n
|
||||||
|
LEFT JOIN users u ON n.created_by = u.id
|
||||||
|
WHERE n.project_id = ?
|
||||||
|
ORDER BY n.note_date DESC
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.all(projectId);
|
.all(projectId);
|
||||||
|
|||||||
@@ -27,10 +27,16 @@ export function getAllProjectTasks() {
|
|||||||
p.plot,
|
p.plot,
|
||||||
p.city,
|
p.city,
|
||||||
p.address,
|
p.address,
|
||||||
p.finish_date
|
p.finish_date,
|
||||||
|
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 project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
LEFT JOIN projects p ON pt.project_id = p.project_id
|
LEFT JOIN projects p ON pt.project_id = p.project_id
|
||||||
|
LEFT JOIN users creator ON pt.created_by = creator.id
|
||||||
|
LEFT JOIN users assignee ON pt.assigned_to = assignee.id
|
||||||
ORDER BY pt.date_added DESC
|
ORDER BY pt.date_added DESC
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
@@ -50,9 +56,15 @@ export function getProjectTasks(projectId) {
|
|||||||
CASE
|
CASE
|
||||||
WHEN pt.task_template_id IS NOT NULL THEN 'template'
|
WHEN pt.task_template_id IS NOT NULL THEN 'template'
|
||||||
ELSE 'custom'
|
ELSE 'custom'
|
||||||
END as task_type
|
END as task_type,
|
||||||
|
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 project_tasks pt
|
FROM project_tasks pt
|
||||||
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
|
LEFT JOIN users creator ON pt.created_by = creator.id
|
||||||
|
LEFT JOIN users assignee ON pt.assigned_to = assignee.id
|
||||||
WHERE pt.project_id = ?
|
WHERE pt.project_id = ?
|
||||||
ORDER BY pt.date_added DESC
|
ORDER BY pt.date_added DESC
|
||||||
`
|
`
|
||||||
@@ -68,14 +80,19 @@ export function createProjectTask(data) {
|
|||||||
if (data.task_template_id) {
|
if (data.task_template_id) {
|
||||||
// Creating from template - explicitly set custom_max_wait_days to NULL so COALESCE uses template value
|
// Creating from template - explicitly set custom_max_wait_days to NULL so COALESCE uses template value
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO project_tasks (project_id, task_template_id, custom_max_wait_days, status, priority)
|
INSERT INTO project_tasks (
|
||||||
VALUES (?, ?, NULL, ?, ?)
|
project_id, task_template_id, custom_max_wait_days, status, priority,
|
||||||
|
created_by, assigned_to, created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, NULL, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
`);
|
`);
|
||||||
result = stmt.run(
|
result = stmt.run(
|
||||||
data.project_id,
|
data.project_id,
|
||||||
data.task_template_id,
|
data.task_template_id,
|
||||||
data.status || "pending",
|
data.status || "pending",
|
||||||
data.priority || "normal"
|
data.priority || "normal",
|
||||||
|
data.created_by || null,
|
||||||
|
data.assigned_to || null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the template name for the log
|
// Get the template name for the log
|
||||||
@@ -85,8 +102,11 @@ export function createProjectTask(data) {
|
|||||||
} else {
|
} else {
|
||||||
// Creating custom task
|
// Creating custom task
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO project_tasks (project_id, custom_task_name, custom_max_wait_days, custom_description, status, priority)
|
INSERT INTO project_tasks (
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
project_id, custom_task_name, custom_max_wait_days, custom_description,
|
||||||
|
status, priority, created_by, assigned_to, created_at, updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
`);
|
`);
|
||||||
result = stmt.run(
|
result = stmt.run(
|
||||||
data.project_id,
|
data.project_id,
|
||||||
@@ -94,7 +114,9 @@ export function createProjectTask(data) {
|
|||||||
data.custom_max_wait_days || 0,
|
data.custom_max_wait_days || 0,
|
||||||
data.custom_description || "",
|
data.custom_description || "",
|
||||||
data.status || "pending",
|
data.status || "pending",
|
||||||
data.priority || "normal"
|
data.priority || "normal",
|
||||||
|
data.created_by || null,
|
||||||
|
data.assigned_to || null
|
||||||
);
|
);
|
||||||
|
|
||||||
taskName = data.custom_task_name;
|
taskName = data.custom_task_name;
|
||||||
@@ -105,14 +127,14 @@ export function createProjectTask(data) {
|
|||||||
const priority = data.priority || "normal";
|
const priority = data.priority || "normal";
|
||||||
const status = data.status || "pending";
|
const status = data.status || "pending";
|
||||||
const logMessage = `Task "${taskName}" created with priority: ${priority}, status: ${status}`;
|
const logMessage = `Task "${taskName}" created with priority: ${priority}, status: ${status}`;
|
||||||
addNoteToTask(result.lastInsertRowid, logMessage, true);
|
addNoteToTask(result.lastInsertRowid, logMessage, true, data.created_by);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update project task status
|
// Update project task status
|
||||||
export function updateProjectTaskStatus(taskId, status) {
|
export function updateProjectTaskStatus(taskId, status, userId = null) {
|
||||||
// First get the current task details for logging
|
// First get the current task details for logging
|
||||||
const getCurrentTask = db.prepare(`
|
const getCurrentTask = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -136,7 +158,7 @@ export function updateProjectTaskStatus(taskId, status) {
|
|||||||
// Starting a task - set date_started
|
// Starting a task - set date_started
|
||||||
stmt = db.prepare(`
|
stmt = db.prepare(`
|
||||||
UPDATE project_tasks
|
UPDATE project_tasks
|
||||||
SET status = ?, date_started = CURRENT_TIMESTAMP
|
SET status = ?, date_started = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
result = stmt.run(status, taskId);
|
result = stmt.run(status, taskId);
|
||||||
@@ -144,7 +166,7 @@ export function updateProjectTaskStatus(taskId, status) {
|
|||||||
// Completing a task - set date_completed
|
// Completing a task - set date_completed
|
||||||
stmt = db.prepare(`
|
stmt = db.prepare(`
|
||||||
UPDATE project_tasks
|
UPDATE project_tasks
|
||||||
SET status = ?, date_completed = CURRENT_TIMESTAMP
|
SET status = ?, date_completed = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
result = stmt.run(status, taskId);
|
result = stmt.run(status, taskId);
|
||||||
@@ -152,7 +174,7 @@ export function updateProjectTaskStatus(taskId, status) {
|
|||||||
// Just updating status without changing timestamps
|
// Just updating status without changing timestamps
|
||||||
stmt = db.prepare(`
|
stmt = db.prepare(`
|
||||||
UPDATE project_tasks
|
UPDATE project_tasks
|
||||||
SET status = ?
|
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
result = stmt.run(status, taskId);
|
result = stmt.run(status, taskId);
|
||||||
@@ -162,7 +184,7 @@ export function updateProjectTaskStatus(taskId, status) {
|
|||||||
if (result.changes > 0 && oldStatus !== status) {
|
if (result.changes > 0 && oldStatus !== status) {
|
||||||
const taskName = currentTask.task_name || "Unknown task";
|
const taskName = currentTask.task_name || "Unknown task";
|
||||||
const logMessage = `Status changed from "${oldStatus}" to "${status}"`;
|
const logMessage = `Status changed from "${oldStatus}" to "${status}"`;
|
||||||
addNoteToTask(taskId, logMessage, true);
|
addNoteToTask(taskId, logMessage, true, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -173,3 +195,99 @@ export function deleteProjectTask(taskId) {
|
|||||||
const stmt = db.prepare("DELETE FROM project_tasks WHERE id = ?");
|
const stmt = db.prepare("DELETE FROM project_tasks WHERE id = ?");
|
||||||
return stmt.run(taskId);
|
return stmt.run(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get project tasks assigned to a specific user
|
||||||
|
export function getProjectTasksByAssignedUser(userId) {
|
||||||
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
pt.*,
|
||||||
|
COALESCE(pt.custom_task_name, t.name) as task_name,
|
||||||
|
COALESCE(pt.custom_max_wait_days, t.max_wait_days) as max_wait_days,
|
||||||
|
COALESCE(pt.custom_description, t.description) as description,
|
||||||
|
CASE
|
||||||
|
WHEN pt.task_template_id IS NOT NULL THEN 'template'
|
||||||
|
ELSE 'custom'
|
||||||
|
END as task_type,
|
||||||
|
p.project_name,
|
||||||
|
p.wp,
|
||||||
|
p.plot,
|
||||||
|
p.city,
|
||||||
|
p.address,
|
||||||
|
p.finish_date,
|
||||||
|
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 project_tasks pt
|
||||||
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
|
LEFT JOIN projects p ON pt.project_id = p.project_id
|
||||||
|
LEFT JOIN users creator ON pt.created_by = creator.id
|
||||||
|
LEFT JOIN users assignee ON pt.assigned_to = assignee.id
|
||||||
|
WHERE pt.assigned_to = ?
|
||||||
|
ORDER BY pt.date_added DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get project tasks created by a specific user
|
||||||
|
export function getProjectTasksByCreator(userId) {
|
||||||
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
pt.*,
|
||||||
|
COALESCE(pt.custom_task_name, t.name) as task_name,
|
||||||
|
COALESCE(pt.custom_max_wait_days, t.max_wait_days) as max_wait_days,
|
||||||
|
COALESCE(pt.custom_description, t.description) as description,
|
||||||
|
CASE
|
||||||
|
WHEN pt.task_template_id IS NOT NULL THEN 'template'
|
||||||
|
ELSE 'custom'
|
||||||
|
END as task_type,
|
||||||
|
p.project_name,
|
||||||
|
p.wp,
|
||||||
|
p.plot,
|
||||||
|
p.city,
|
||||||
|
p.address,
|
||||||
|
p.finish_date,
|
||||||
|
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 project_tasks pt
|
||||||
|
LEFT JOIN tasks t ON pt.task_template_id = t.task_id
|
||||||
|
LEFT JOIN projects p ON pt.project_id = p.project_id
|
||||||
|
LEFT JOIN users creator ON pt.created_by = creator.id
|
||||||
|
LEFT JOIN users assignee ON pt.assigned_to = assignee.id
|
||||||
|
WHERE pt.created_by = ?
|
||||||
|
ORDER BY pt.date_added DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update project task assignment
|
||||||
|
export function updateProjectTaskAssignment(taskId, assignedToUserId) {
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
UPDATE project_tasks
|
||||||
|
SET assigned_to = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`);
|
||||||
|
return stmt.run(assignedToUserId, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active users for task assignment (same as projects)
|
||||||
|
export function getAllUsersForTaskAssignment() {
|
||||||
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT id, name, email, role
|
||||||
|
FROM users
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY name ASC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|||||||
44
test-task-api.mjs
Normal file
44
test-task-api.mjs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Test the project-tasks API endpoints
|
||||||
|
|
||||||
|
async function testAPI() {
|
||||||
|
const baseURL = "http://localhost:3000";
|
||||||
|
|
||||||
|
console.log("Testing project-tasks API endpoints...\n");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test 1: Check if users endpoint exists
|
||||||
|
console.log("1. Testing /api/project-tasks/users:");
|
||||||
|
const usersResponse = await fetch(`${baseURL}/api/project-tasks/users`);
|
||||||
|
console.log("Status:", usersResponse.status);
|
||||||
|
if (usersResponse.ok) {
|
||||||
|
const users = await usersResponse.json();
|
||||||
|
console.log("Users found:", users.length);
|
||||||
|
console.log("First user:", users[0]);
|
||||||
|
} else {
|
||||||
|
const error = await usersResponse.text();
|
||||||
|
console.log("Error:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Try to create a task (this will fail without auth, but let's see the response)
|
||||||
|
console.log("\n2. Testing POST /api/project-tasks:");
|
||||||
|
const taskData = {
|
||||||
|
project_id: 1,
|
||||||
|
task_template_id: 1,
|
||||||
|
priority: "normal",
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResponse = await fetch(`${baseURL}/api/project-tasks`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(taskData),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Status:", createResponse.status);
|
||||||
|
const responseText = await createResponse.text();
|
||||||
|
console.log("Response:", responseText);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error testing API:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testAPI();
|
||||||
Reference in New Issue
Block a user