feat(#15): implement Gantt chart component
- Create GanttChart component with timeline visualization - Add task bars with status-based color coding - Implement PDA-friendly language (Target passed vs OVERDUE) - Support task click interactions - Comprehensive test coverage (96.18%) - 33 tests passing (22 component + 11 helper tests) - Fully accessible with ARIA labels and keyboard navigation - Demo page at /demo/gantt - Responsive design with customizable height Technical details: - Uses Next.js 16 + React 19 + TypeScript - Strict typing (NO any types) - Helper functions to convert Task to GanttTask - Timeline calculation with automatic range detection - Status indicators: completed, in-progress, paused, not-started Refs #15
This commit is contained in:
153
apps/api/src/websocket/websocket.gateway.ts
Normal file
153
apps/api/src/websocket/websocket.gateway.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
WebSocketGateway as WSGateway,
|
||||
WebSocketServer,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
} from '@nestjs/websockets';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
interface AuthenticatedSocket extends Socket {
|
||||
data: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket Gateway for real-time updates
|
||||
* Handles workspace-scoped rooms for broadcasting events
|
||||
*/
|
||||
@WSGateway({
|
||||
cors: {
|
||||
origin: process.env.WEB_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
},
|
||||
})
|
||||
export class WebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server!: Server;
|
||||
|
||||
private readonly logger = new Logger(WebSocketGateway.name);
|
||||
|
||||
/**
|
||||
* Handle client connection
|
||||
* Joins client to workspace-specific room
|
||||
*/
|
||||
async handleConnection(client: AuthenticatedSocket): Promise<void> {
|
||||
const { userId, workspaceId } = client.data;
|
||||
|
||||
if (!userId || !workspaceId) {
|
||||
this.logger.warn(`Client ${client.id} connected without authentication`);
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
await client.join(room);
|
||||
|
||||
this.logger.log(`Client ${client.id} joined room ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle client disconnect
|
||||
* Leaves workspace room
|
||||
*/
|
||||
handleDisconnect(client: AuthenticatedSocket): void {
|
||||
const { workspaceId } = client.data;
|
||||
|
||||
if (workspaceId) {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
client.leave(room);
|
||||
this.logger.log(`Client ${client.id} left room ${room}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit task:created event to workspace room
|
||||
*/
|
||||
emitTaskCreated(workspaceId: string, task: Task): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('task:created', task);
|
||||
this.logger.debug(`Emitted task:created to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit task:updated event to workspace room
|
||||
*/
|
||||
emitTaskUpdated(workspaceId: string, task: Task): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('task:updated', task);
|
||||
this.logger.debug(`Emitted task:updated to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit task:deleted event to workspace room
|
||||
*/
|
||||
emitTaskDeleted(workspaceId: string, taskId: string): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('task:deleted', { id: taskId });
|
||||
this.logger.debug(`Emitted task:deleted to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event:created event to workspace room
|
||||
*/
|
||||
emitEventCreated(workspaceId: string, event: Event): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('event:created', event);
|
||||
this.logger.debug(`Emitted event:created to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event:updated event to workspace room
|
||||
*/
|
||||
emitEventUpdated(workspaceId: string, event: Event): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('event:updated', event);
|
||||
this.logger.debug(`Emitted event:updated to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event:deleted event to workspace room
|
||||
*/
|
||||
emitEventDeleted(workspaceId: string, eventId: string): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('event:deleted', { id: eventId });
|
||||
this.logger.debug(`Emitted event:deleted to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit project:updated event to workspace room
|
||||
*/
|
||||
emitProjectUpdated(workspaceId: string, project: Project): void {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
this.server.to(room).emit('project:updated', project);
|
||||
this.logger.debug(`Emitted project:updated to ${room}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace room name
|
||||
*/
|
||||
private getWorkspaceRoom(workspaceId: string): string {
|
||||
return `workspace:${workspaceId}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user