diff --git a/apps/pac-shield-api/src/app/ato/ato.controller.ts b/apps/pac-shield-api/src/app/ato/ato.controller.ts index 3326b627..5cf33eba 100644 --- a/apps/pac-shield-api/src/app/ato/ato.controller.ts +++ b/apps/pac-shield-api/src/app/ato/ato.controller.ts @@ -16,6 +16,7 @@ import { CreateATORequestDto } from './dto/create-ato-request.dto'; import { UpdateATORequestDto } from './dto/update-ato-request.dto'; import { ATOLine } from '../generated/aTOLine/aTOLine.entity'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { AtoCreationGuard } from '../auth/ato-creation.guard'; import { AircraftInstance } from '@prisma/client'; import { GetAtoQueryDto } from './dto/get-ato-query.dto'; @@ -73,6 +74,7 @@ export class AtoController { * @example POST /ato */ @Post() + @UseGuards(AtoCreationGuard) async createFlightPlan( @Body() createAtoRequestDto: CreateATORequestDto, @Request() req: any diff --git a/apps/pac-shield-api/src/app/ato/ato.module.ts b/apps/pac-shield-api/src/app/ato/ato.module.ts index cf31e141..78117aaf 100644 --- a/apps/pac-shield-api/src/app/ato/ato.module.ts +++ b/apps/pac-shield-api/src/app/ato/ato.module.ts @@ -2,6 +2,7 @@ import { Module, forwardRef } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { AtoController } from './ato.controller'; import { AtoService } from './ato.service'; +import { AtoCreationGuard } from '../auth/ato-creation.guard'; import { PrismaModule } from '../../prisma/prisma.module'; import { GameModule } from '../../game/game.module'; import { AuthModule } from '../../auth/auth.module'; @@ -20,7 +21,7 @@ import { AuthModule } from '../../auth/auth.module'; }), ], controllers: [AtoController], - providers: [AtoService], + providers: [AtoService, AtoCreationGuard], exports: [AtoService], }) export class AtoModule {} diff --git a/apps/pac-shield-api/src/app/auth/ato-creation.guard.ts b/apps/pac-shield-api/src/app/auth/ato-creation.guard.ts new file mode 100644 index 00000000..e91debb1 --- /dev/null +++ b/apps/pac-shield-api/src/app/auth/ato-creation.guard.ts @@ -0,0 +1,50 @@ +import { Injectable, CanActivate, ExecutionContext, ForbiddenException, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../../prisma/prisma.service'; +import { PlayerRole } from '@prisma/client'; + +/** + * Guard that prevents CAOC and CSpOC team members from creating Air Tasking Orders (ATOs). + * - Allows Game Masters to bypass the restriction + * - Blocks players whose team.type === 'CAOC' or 'CSPOC' + * - Allows all other team types to create ATOs + */ +@Injectable() +export class AtoCreationGuard implements CanActivate { + constructor(private readonly prisma: PrismaService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest(); + const user = req.user; + + const candidate = user?.sub ?? user?.playerId; + if (candidate == null) { + throw new ForbiddenException('Authentication required to create Air Tasking Orders'); + } + + // Resolve player by numeric id (preferred) or by sessionId as a fallback + const candidateStr = String(candidate); + const isNumericId = /^\d+$/.test(candidateStr); + const player = await this.prisma.player.findUnique({ + where: isNumericId ? { id: Number(candidateStr) } : { sessionId: candidateStr }, + include: { team: true } + }); + + if (!player) { + throw new NotFoundException('Player not found'); + } + + // Allow GMs to create ATOs for any team + if (player.role === PlayerRole.GM) { + return true; + } + + // Block CAOC and CSPOC team members + if (player.team?.type === 'CSPOC' || player.team?.type === 'CAOC') { + throw new ForbiddenException( + 'CAOC and CSpOC team members cannot create Air Tasking Orders' + ); + } + + return true; + } +} diff --git a/apps/pac-shield/src/app/features/game/dialogs/aircraft-request/aircraft-request-dialog.component.ts b/apps/pac-shield/src/app/features/game/dialogs/aircraft-request/aircraft-request-dialog.component.ts index d39b2a43..19cbd0f5 100644 --- a/apps/pac-shield/src/app/features/game/dialogs/aircraft-request/aircraft-request-dialog.component.ts +++ b/apps/pac-shield/src/app/features/game/dialogs/aircraft-request/aircraft-request-dialog.component.ts @@ -1,3 +1,8 @@ +/** + * @deprecated This dialog is no longer used in the MOB workflow. + * CAOC now distributes aircraft directly without MOB requests. + * Kept for potential future use or reference. + */ import { Component, OnInit, OnDestroy, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; diff --git a/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.html b/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.html index a9e21758..0e63b5dc 100644 --- a/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.html +++ b/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.html @@ -17,309 +17,53 @@ tooltipPosition="below" > - - - - - - -
-
- -
-
On-Station Personnel
-
- Refueling - Airfield Ops - Security Forces -
-
- - -
-
Commodities
-
- Fuel - Bomb - Missile - Food - Water -
-
- - -
-
Aircraft Inventory
-
- F-16 x16 - F-22 x16 - C-17 x2 -
-
- - -
-
Load Plans
-
- Placeholder for aircraft load planner summaries. -
-
+ +
+
+ +
+
On-Station Personnel
+
+ Refueling + Airfield Ops + Security Forces
- - - - - - - Aircraft Requests - @if (pendingRequests$ | async; as pendingRequests) { - @if (pendingRequests.length > 0) { - - } - } - - - -
- @if (isLoading$ | async) { -
- -
- } @else { - @if (allRequests$ | async; as requests) { - @if (requests.length === 0) { - -
- flight_takeoff -
No Aircraft Requests
-
- Submit your first aircraft allocation request to get started. -
- -
- } @else { - -
- -
-
- Aircraft Requests -
- -
-
- -
-
- flight - {{ request.aircraftType }} -
-
- - -
-
Qty
- {{ request.quantityRequested }} - @if (request.quantityAllocated !== null && request.quantityAllocated !== request.quantityRequested) { -
- ({{ request.quantityAllocated }} alloc.) -
- } -
- - -
-
Priority
- - {{ formatPriority(request.priority) }} - -
- - -
-
Mission
-
- {{ request.missionJustification }} -
-
- - -
-
Status
-
- - {{ getStatusIcon(request.status) }} - - {{ request.status.toLowerCase() }} -
-
- - -
-
Submitted
- - {{ formatDate(request.createdAt.toString() || '') }} - -
-
-
-
-
- - - -
+ +
+
Aircraft Inventory
+
+ F-16 x16 + F-22 x16 + C-17 x2 +
+
- -
-
Request Summary
-
-
-
{{ requests.length }}
-
Total
-
-
-
- {{ (pendingRequests$ | async)?.length || 0 }} -
-
Pending
-
-
-
- {{ countApprovedRequests(requests) }} -
-
Approved
-
-
-
- {{ countDeniedRequests(requests) }} -
-
Denied
-
-
-
- } - } - } + +
+
Load Plans
+
+ Placeholder for aircraft load planner summaries. +
- - +
+
diff --git a/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.ts b/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.ts index d714f2f2..f7aea0ad 100644 --- a/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.ts +++ b/apps/pac-shield/src/app/features/game/game-stats/mob-dashboard/mob-dashboard.component.ts @@ -1,56 +1,39 @@ import { CommonModule } from '@angular/common'; import { Component, inject, OnInit, OnDestroy, Input } from '@angular/core'; -import { ScrollingModule } from '@angular/cdk/scrolling'; -import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatChipsModule } from '@angular/material/chips'; import { MatDialog } from '@angular/material/dialog'; -import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatTableModule } from '@angular/material/table'; -import { MatBadgeModule } from '@angular/material/badge'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { Store } from '@ngrx/store'; -import { Observable, Subject, filter, takeUntil } from 'rxjs'; +import { Subject, filter, takeUntil } from 'rxjs'; -import { AircraftRequestDialogComponent } from '../../dialogs/aircraft-request/aircraft-request-dialog.component'; import { AllocationNotificationBadgeComponent } from '../../notifications/allocation-notification-badge/allocation-notification-badge.component'; import { AllocationNotificationCenterComponent } from '../../notifications/allocation-notification-center/allocation-notification-center.component'; import { AllocationNotificationToastComponent } from '../../notifications/allocation-notification-toast/allocation-notification-toast.component'; import { AllocationWebSocketService } from '../../../../shared/services/allocation-websocket.service'; import * as AllocationActions from '../../../../store/allocation/allocation.actions'; import * as AllocationSelectors from '../../../../store/allocation/allocation.selectors'; -import { AircraftRequest } from '../../../../generated/aircraftRequest/aircraftRequest.entity'; -import { AllocationRequestStatus, TeamType } from '../../../../generated/enums'; +import { TeamType } from '../../../../generated/enums'; import { AllocationNotification } from '../../../../store/allocation/allocation.state'; -import { AircraftRequestDialogData } from '../../dialogs/aircraft-request/aircraft-request-dialog.component'; /** - * MOB dashboard with aircraft allocation workflow integration + * MOB dashboard - displays inventory and receives allocation notifications from CAOC * * Features: * - Aircraft inventory and commodities display - * - Aircraft request submission via dialog - * - Real-time requests tracking with status updates + * - Real-time allocation notifications from CAOC * - Integration with NgRx allocation state management + * + * Note: MOBs no longer request aircraft - CAOC distributes directly */ @Component({ selector: 'app-mob-dashboard', standalone: true, imports: [ CommonModule, - ScrollingModule, MatCardModule, - MatDividerModule, MatIconModule, MatChipsModule, - MatButtonModule, - MatTabsModule, - MatTableModule, - MatBadgeModule, - MatProgressSpinnerModule, AllocationNotificationBadgeComponent, AllocationNotificationToastComponent ], @@ -65,16 +48,9 @@ export class MobDashboardComponent implements OnInit, OnDestroy { private readonly dialog = inject(MatDialog); private readonly store = inject(Store); - private readonly snackBar = inject(MatSnackBar); private readonly webSocketService = inject(AllocationWebSocketService); private readonly destroy$ = new Subject(); - // Observable streams from NgRx store - readonly currentCycle$ = this.store.select(AllocationSelectors.selectCurrentAllocationCycle); - readonly allRequests$: Observable = this.store.select(AllocationSelectors.selectAllRequests); - readonly pendingRequests$: Observable = this.store.select(AllocationSelectors.selectPendingRequests); - readonly isLoading$: Observable = this.store.select(AllocationSelectors.selectIsAnyLoading); - // Notification observables readonly unreadNotificationCount$ = this.store.select(AllocationSelectors.selectUnreadNotificationCount); readonly hasUrgentNotifications$ = this.store.select(AllocationSelectors.selectHasUnreadUrgentNotifications); @@ -84,75 +60,30 @@ export class MobDashboardComponent implements OnInit, OnDestroy { // Current displayed toast notification currentToastNotification: AllocationNotification | null = null; - // Table configuration for requests display - readonly displayedColumns = ['aircraftType', 'quantity', 'priority', 'justification', 'status', 'submittedAt']; - // Expose status constants to template - readonly StatusValues = { - PENDING: 'PENDING' as AllocationRequestStatus, - APPROVED: 'APPROVED' as AllocationRequestStatus, - DENIED: 'DENIED' as AllocationRequestStatus, - MODIFIED: 'MODIFIED' as AllocationRequestStatus, - }; - constructor() { // Component initialization } ngOnInit(): void { - console.warn('MOB Dashboard: Allocation features temporarily disabled. Please restart dev server after app.config.ts changes.'); - - // TEMPORARILY DISABLED: Load allocation data if game ID is available - // TODO: Uncomment after dev server restart - // if (this.currentGameId) { - // this.store.dispatch(AllocationActions.loadLatestAllocationCycle({ gameId: this.currentGameId })); - // } - - // TEMPORARILY DISABLED: Load requests for current team - // TODO: Uncomment after dev server restart - // if (this.teamId) { - // this.store.dispatch(AllocationActions.loadRequestsForTeam({ teamId: this.teamId })); - // } - - // TEMPORARILY DISABLED: Initialize WebSocket connection for real-time notifications - // TODO: Uncomment after dev server restart - // if (this.currentGameId && this.teamId) { - // this.webSocketService.connect({ - // gameId: this.currentGameId, - // teamId: this.teamId, - // reconnect: true - // }); - // } - - // TEMPORARILY DISABLED: Listen for new notifications and show toast - // TODO: Uncomment after dev server restart - // this.recentNotifications$.pipe( - // filter(notifications => notifications.length > 0), - // takeUntil(this.destroy$) - // ).subscribe(notifications => { - // const latestNotification = notifications[0]; - // if (latestNotification && !latestNotification.read) { - // this.showToastNotification(latestNotification); - // } - // }); + // Initialize WebSocket connection for real-time allocation notifications + if (this.currentGameId && this.teamId) { + this.webSocketService.connect({ + gameId: this.currentGameId, + teamId: this.teamId, + reconnect: true + }); + } - // TEMPORARILY DISABLED: Listen for urgent notifications and show snackbar - // TODO: Uncomment after dev server restart - // this.hasUrgentNotifications$.pipe( - // takeUntil(this.destroy$) - // ).subscribe(hasUrgent => { - // if (hasUrgent) { - // this.snackBar.open( - // 'Urgent allocation notification received!', - // 'View', - // { - // duration: 5000, - // panelClass: ['urgent-snackbar'] - // } - // ).onAction().subscribe(() => { - // this.openNotificationCenter(); - // }); - // } - // }); + // Listen for new allocation notifications and show toast + this.recentNotifications$.pipe( + filter(notifications => notifications.length > 0), + takeUntil(this.destroy$) + ).subscribe(notifications => { + const latestNotification = notifications[0]; + if (latestNotification && !latestNotification.read) { + this.showToastNotification(latestNotification); + } + }); } ngOnDestroy(): void { @@ -161,135 +92,6 @@ export class MobDashboardComponent implements OnInit, OnDestroy { this.webSocketService.disconnect(); } - /** - * Opens the aircraft request dialog for MOB teams to submit allocation requests - * TEMPORARILY DISABLED until allocation store is available - */ - openRequestDialog(): void { - this.snackBar.open( - 'Allocation features temporarily disabled. Please restart the dev server.', - 'Close', - { duration: 5000, panelClass: ['error-snackbar'] } - ); - - // TEMPORARILY DISABLED: Get current allocation cycle from store - // TODO: Uncomment after dev server restart - // this.currentCycle$.pipe( - // takeUntil(this.destroy$) - // ).subscribe(cycle => { - // if (!cycle) { - // this.snackBar.open( - // 'No active allocation cycle. Please wait for the next cycle to open.', - // 'Close', - // { duration: 5000, panelClass: ['error-snackbar'] } - // ); - // return; - // } - - // if (!this.teamId) { - // this.snackBar.open( - // 'Team ID not available. Cannot submit request.', - // 'Close', - // { duration: 5000, panelClass: ['error-snackbar'] } - // ); - // return; - // } - - // const dialogData: AircraftRequestDialogData = { - // allocationCycleId: cycle.id, - // teamId: this.teamId, - // currentTurn: this.currentTurn - // }; - - // const dialogRef = this.dialog.open(AircraftRequestDialogComponent, { - // width: '600px', - // maxWidth: '90vw', - // disableClose: false, - // autoFocus: true, - // restoreFocus: true, - // data: dialogData - // }); - - // // Handle successful request submission - // dialogRef.afterClosed().subscribe(result => { - // if (result && this.teamId) { - // // Request was submitted successfully, refresh the requests list - // this.store.dispatch(AllocationActions.loadRequestsForTeam({ teamId: this.teamId })); - // } - // }); - // }); - } - - /** - * Gets the appropriate icon for request status - */ - getStatusIcon(status: AllocationRequestStatus): string { - switch (status) { - case 'PENDING': - return 'schedule'; - case 'APPROVED': - return 'check_circle'; - case 'DENIED': - return 'cancel'; - case 'MODIFIED': - return 'edit'; - default: - return 'help'; - } - } - - /** - * Gets the appropriate color class for request status - */ - getStatusColor(status: AllocationRequestStatus): string { - switch (status) { - case 'PENDING': - return 'md-sys-color-primary'; - case 'APPROVED': - return 'md-sys-color-tertiary'; - case 'DENIED': - return 'md-sys-color-error'; - case 'MODIFIED': - return 'md-sys-color-secondary'; - default: - return 'md-sys-color-on-surface-variant'; - } - } - - /** - * Formats priority level for display - */ - formatPriority(priority: number): string { - const priorities = ['Low', 'Medium', 'High', 'Critical']; - return priorities[priority - 1] || 'Unknown'; - } - - /** - * Formats date for display in requests table - */ - formatDate(date: string): string { - return new Date(date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } - - /** - * Counts approved requests for display - */ - countApprovedRequests(requests: AircraftRequest[]): number { - return requests.filter(r => r.status === 'APPROVED').length; - } - - /** - * Counts denied requests for display - */ - countDeniedRequests(requests: AircraftRequest[]): number { - return requests.filter(r => r.status === 'DENIED').length; - } - /** * Show toast notification for new allocation updates */ @@ -356,11 +158,4 @@ export class MobDashboardComponent implements OnInit, OnDestroy { onNotificationBadgeClick(): void { this.openNotificationCenter(); } - - /** - * Track by function for virtual scrolling performance - */ - trackByRequestId(index: number, item: AircraftRequest): number { - return item.id; - } }