diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index a6410a423..35b97a098 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -17,6 +17,7 @@ import { TasksModule } from './tasks/tasks.module'; import { VendorsModule } from './vendors/vendors.module'; import { ContextModule } from './context/context.module'; import { TrustPortalModule } from './trust-portal/trust-portal.module'; +import { TaskTemplateModule } from './framework-editor/task-template/task-template.module'; @Module({ imports: [ @@ -43,6 +44,7 @@ import { TrustPortalModule } from './trust-portal/trust-portal.module'; CommentsModule, HealthModule, TrustPortalModule, + TaskTemplateModule, ], controllers: [AppController], providers: [AppService], diff --git a/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts b/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts new file mode 100644 index 000000000..9c1f50bf6 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/dto/create-task-template.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty, IsEnum } from 'class-validator'; +import { Frequency, Departments } from '@trycompai/db'; + +export class CreateTaskTemplateDto { + @ApiProperty({ + description: 'Task template name', + example: 'Monthly Security Review', + }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ + description: 'Detailed description of the task template', + example: 'Review and update security policies on a monthly basis', + }) + @IsString() + @IsNotEmpty() + description: string; + + @ApiProperty({ + description: 'Frequency of the task', + enum: Frequency, + example: Frequency.monthly, + }) + @IsEnum(Frequency) + frequency: Frequency; + + @ApiProperty({ + description: 'Department responsible for the task', + enum: Departments, + example: Departments.it, + }) + @IsEnum(Departments) + department: Departments; +} + diff --git a/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts b/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts new file mode 100644 index 000000000..74fdfcf76 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/dto/task-template-response.dto.ts @@ -0,0 +1,49 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Frequency, Departments } from '@trycompai/db'; + +export class TaskTemplateResponseDto { + @ApiProperty({ + description: 'Task template ID', + example: 'frk_tt_abc123def456', + }) + id: string; + + @ApiProperty({ + description: 'Task template name', + example: 'Monthly Security Review', + }) + name: string; + + @ApiProperty({ + description: 'Detailed description of the task template', + example: 'Review and update security policies on a monthly basis', + }) + description: string; + + @ApiProperty({ + description: 'Frequency of the task', + enum: Frequency, + example: Frequency.monthly, + }) + frequency: Frequency; + + @ApiProperty({ + description: 'Department responsible for the task', + enum: Departments, + example: Departments.it, + }) + department: Departments; + + @ApiProperty({ + description: 'Creation timestamp', + example: '2025-01-01T00:00:00.000Z', + }) + createdAt: Date; + + @ApiProperty({ + description: 'Last update timestamp', + example: '2025-01-01T00:00:00.000Z', + }) + updatedAt: Date; +} + diff --git a/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts b/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts new file mode 100644 index 000000000..8eca6085f --- /dev/null +++ b/apps/api/src/framework-editor/task-template/dto/update-task-template.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTaskTemplateDto } from './create-task-template.dto'; + +export class UpdateTaskTemplateDto extends PartialType(CreateTaskTemplateDto) {} + diff --git a/apps/api/src/framework-editor/task-template/pipes/validate-id.pipe.ts b/apps/api/src/framework-editor/task-template/pipes/validate-id.pipe.ts new file mode 100644 index 000000000..ffde496f1 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/pipes/validate-id.pipe.ts @@ -0,0 +1,26 @@ +import { + PipeTransform, + Injectable, + BadRequestException, +} from '@nestjs/common'; + +@Injectable() +export class ValidateIdPipe implements PipeTransform { + transform(value: string): string { + // Validate that the ID is not empty + if (!value || typeof value !== 'string' || value.trim() === '') { + throw new BadRequestException('ID must be a non-empty string'); + } + + // Validate CUID format with prefix 'frk_tt_' + const cuidRegex = /^frk_tt_[a-z0-9]+$/i; + if (!cuidRegex.test(value)) { + throw new BadRequestException( + 'Invalid ID format. Expected format: frk_tt_[alphanumeric]', + ); + } + + return value; + } +} + diff --git a/apps/api/src/framework-editor/task-template/schemas/delete-task-template.responses.ts b/apps/api/src/framework-editor/task-template/schemas/delete-task-template.responses.ts new file mode 100644 index 000000000..ed320a4f4 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/delete-task-template.responses.ts @@ -0,0 +1,51 @@ +export const DELETE_TASK_TEMPLATE_RESPONSES = { + 200: { + status: 200, + description: 'Successfully deleted framework editor task template', + schema: { + example: { + message: 'Framework editor task template deleted successfully', + deletedTaskTemplate: { + id: 'frk_tt_abc123def456', + name: 'Monthly Security Review', + }, + authType: 'session', + authenticatedUser: { + id: 'user_123', + email: 'user@example.com', + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid or missing authentication', + schema: { + example: { + statusCode: 401, + message: 'Unauthorized', + }, + }, + }, + 404: { + status: 404, + description: 'Framework editor task template not found', + schema: { + example: { + statusCode: 404, + message: 'Framework editor task template with ID frk_tt_abc123def456 not found', + }, + }, + }, + 500: { + status: 500, + description: 'Internal server error', + schema: { + example: { + statusCode: 500, + message: 'Internal server error', + }, + }, + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/schemas/get-all-task-templates.responses.ts b/apps/api/src/framework-editor/task-template/schemas/get-all-task-templates.responses.ts new file mode 100644 index 000000000..f1cac2b04 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/get-all-task-templates.responses.ts @@ -0,0 +1,48 @@ +export const GET_ALL_TASK_TEMPLATES_RESPONSES = { + 200: { + status: 200, + description: 'Successfully retrieved all framework editor task templates', + schema: { + example: { + data: [ + { + id: 'frk_tt_abc123def456', + name: 'Monthly Security Review', + description: 'Review and update security policies on a monthly basis', + frequency: 'monthly', + department: 'it', + createdAt: '2025-01-01T00:00:00.000Z', + updatedAt: '2025-01-01T00:00:00.000Z', + }, + ], + count: 1, + authType: 'session', + authenticatedUser: { + id: 'user_123', + email: 'user@example.com', + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid or missing authentication', + schema: { + example: { + statusCode: 401, + message: 'Unauthorized', + }, + }, + }, + 500: { + status: 500, + description: 'Internal server error', + schema: { + example: { + statusCode: 500, + message: 'Internal server error', + }, + }, + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/schemas/get-task-template-by-id.responses.ts b/apps/api/src/framework-editor/task-template/schemas/get-task-template-by-id.responses.ts new file mode 100644 index 000000000..1c0ed2514 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/get-task-template-by-id.responses.ts @@ -0,0 +1,53 @@ +export const GET_TASK_TEMPLATE_BY_ID_RESPONSES = { + 200: { + status: 200, + description: 'Successfully retrieved framework editor task template', + schema: { + example: { + id: 'frk_tt_abc123def456', + name: 'Monthly Security Review', + description: 'Review and update security policies on a monthly basis', + frequency: 'monthly', + department: 'it', + createdAt: '2025-01-01T00:00:00.000Z', + updatedAt: '2025-01-01T00:00:00.000Z', + authType: 'session', + authenticatedUser: { + id: 'user_123', + email: 'user@example.com', + }, + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid or missing authentication', + schema: { + example: { + statusCode: 401, + message: 'Unauthorized', + }, + }, + }, + 404: { + status: 404, + description: 'Framework editor task template not found', + schema: { + example: { + statusCode: 404, + message: 'Framework editor task template with ID frk_tt_abc123def456 not found', + }, + }, + }, + 500: { + status: 500, + description: 'Internal server error', + schema: { + example: { + statusCode: 500, + message: 'Internal server error', + }, + }, + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/schemas/task-template-bodies.ts b/apps/api/src/framework-editor/task-template/schemas/task-template-bodies.ts new file mode 100644 index 000000000..dd838bdc7 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/task-template-bodies.ts @@ -0,0 +1,9 @@ +import { UpdateTaskTemplateDto } from '../dto/update-task-template.dto'; + +export const TASK_TEMPLATE_BODIES = { + updateTaskTemplate: { + type: UpdateTaskTemplateDto, + description: 'Update framework editor task template data', + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/schemas/task-template-operations.ts b/apps/api/src/framework-editor/task-template/schemas/task-template-operations.ts new file mode 100644 index 000000000..25fdd00d3 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/task-template-operations.ts @@ -0,0 +1,19 @@ +export const TASK_TEMPLATE_OPERATIONS = { + getAllTaskTemplates: { + summary: 'Get all framework editor task templates', + description: 'Retrieve all framework editor task templates', + }, + getTaskTemplateById: { + summary: 'Get framework editor task template by ID', + description: 'Retrieve a specific framework editor task template by its ID', + }, + updateTaskTemplate: { + summary: 'Update framework editor task template', + description: 'Update a framework editor task template by ID', + }, + deleteTaskTemplate: { + summary: 'Delete framework editor task template', + description: 'Delete a framework editor task template by ID', + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/schemas/task-template-params.ts b/apps/api/src/framework-editor/task-template/schemas/task-template-params.ts new file mode 100644 index 000000000..11f2d3f33 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/task-template-params.ts @@ -0,0 +1,8 @@ +export const TASK_TEMPLATE_PARAMS = { + taskTemplateId: { + name: 'id', + description: 'Framework editor task template ID', + example: 'frk_tt_abc123def456', + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/schemas/update-task-template.responses.ts b/apps/api/src/framework-editor/task-template/schemas/update-task-template.responses.ts new file mode 100644 index 000000000..5c7db2acb --- /dev/null +++ b/apps/api/src/framework-editor/task-template/schemas/update-task-template.responses.ts @@ -0,0 +1,63 @@ +export const UPDATE_TASK_TEMPLATE_RESPONSES = { + 200: { + status: 200, + description: 'Successfully updated framework editor task template', + schema: { + example: { + id: 'frk_tt_abc123def456', + name: 'Monthly Security Review (Updated)', + description: 'Review and update security policies on a monthly basis', + frequency: 'monthly', + department: 'it', + createdAt: '2025-01-01T00:00:00.000Z', + updatedAt: '2025-01-02T00:00:00.000Z', + authType: 'session', + authenticatedUser: { + id: 'user_123', + email: 'user@example.com', + }, + }, + }, + }, + 400: { + status: 400, + description: 'Bad request - Invalid data provided', + schema: { + example: { + statusCode: 400, + message: 'Validation failed', + }, + }, + }, + 401: { + status: 401, + description: 'Unauthorized - Invalid or missing authentication', + schema: { + example: { + statusCode: 401, + message: 'Unauthorized', + }, + }, + }, + 404: { + status: 404, + description: 'Framework editor task template not found', + schema: { + example: { + statusCode: 404, + message: 'Framework editor task template with ID frk_tt_abc123def456 not found', + }, + }, + }, + 500: { + status: 500, + description: 'Internal server error', + schema: { + example: { + statusCode: 500, + message: 'Internal server error', + }, + }, + }, +}; + diff --git a/apps/api/src/framework-editor/task-template/task-template.controller.ts b/apps/api/src/framework-editor/task-template/task-template.controller.ts new file mode 100644 index 000000000..4f6128fbe --- /dev/null +++ b/apps/api/src/framework-editor/task-template/task-template.controller.ts @@ -0,0 +1,141 @@ +import { + Controller, + Get, + Patch, + Delete, + Body, + Param, + UseGuards, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiBody, + ApiHeader, + ApiOperation, + ApiParam, + ApiResponse, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; +import { + AuthContext, +} from '../../auth/auth-context.decorator'; +import { HybridAuthGuard } from '../../auth/hybrid-auth.guard'; +import type { AuthContext as AuthContextType } from '../../auth/types'; +import { UpdateTaskTemplateDto } from './dto/update-task-template.dto'; +import { TaskTemplateService } from './task-template.service'; +import { ValidateIdPipe } from './pipes/validate-id.pipe'; +import { TASK_TEMPLATE_OPERATIONS } from './schemas/task-template-operations'; +import { TASK_TEMPLATE_PARAMS } from './schemas/task-template-params'; +import { TASK_TEMPLATE_BODIES } from './schemas/task-template-bodies'; +import { GET_ALL_TASK_TEMPLATES_RESPONSES } from './schemas/get-all-task-templates.responses'; +import { GET_TASK_TEMPLATE_BY_ID_RESPONSES } from './schemas/get-task-template-by-id.responses'; +import { UPDATE_TASK_TEMPLATE_RESPONSES } from './schemas/update-task-template.responses'; +import { DELETE_TASK_TEMPLATE_RESPONSES } from './schemas/delete-task-template.responses'; + +@ApiTags('Framework Editor Task Templates') +@Controller({ path: 'framework-editor/task-template', version: '1' }) +@UseGuards(HybridAuthGuard) +@ApiSecurity('apikey') +@ApiHeader({ + name: 'X-Organization-Id', + description: + 'Organization ID (required for session auth, optional for API key auth)', + required: false, +}) +export class TaskTemplateController { + constructor(private readonly taskTemplateService: TaskTemplateService) {} + + @Get() + @ApiOperation(TASK_TEMPLATE_OPERATIONS.getAllTaskTemplates) + @ApiResponse(GET_ALL_TASK_TEMPLATES_RESPONSES[200]) + @ApiResponse(GET_ALL_TASK_TEMPLATES_RESPONSES[401]) + @ApiResponse(GET_ALL_TASK_TEMPLATES_RESPONSES[500]) + async getAllTaskTemplates() { + return await this.taskTemplateService.findAll(); + } + + @Get(':id') + @ApiOperation(TASK_TEMPLATE_OPERATIONS.getTaskTemplateById) + @ApiParam(TASK_TEMPLATE_PARAMS.taskTemplateId) + @ApiResponse(GET_TASK_TEMPLATE_BY_ID_RESPONSES[200]) + @ApiResponse(GET_TASK_TEMPLATE_BY_ID_RESPONSES[401]) + @ApiResponse(GET_TASK_TEMPLATE_BY_ID_RESPONSES[404]) + @ApiResponse(GET_TASK_TEMPLATE_BY_ID_RESPONSES[500]) + async getTaskTemplateById( + @Param('id', ValidateIdPipe) taskTemplateId: string, + @AuthContext() authContext: AuthContextType, + ) { + const taskTemplate = await this.taskTemplateService.findById(taskTemplateId); + + return { + ...taskTemplate, + authType: authContext.authType, + ...(authContext.userId && authContext.userEmail && { + authenticatedUser: { + id: authContext.userId, + email: authContext.userEmail, + }, + }), + }; + } + + @Patch(':id') + @ApiOperation(TASK_TEMPLATE_OPERATIONS.updateTaskTemplate) + @ApiParam(TASK_TEMPLATE_PARAMS.taskTemplateId) + @ApiBody(TASK_TEMPLATE_BODIES.updateTaskTemplate) + @ApiResponse(UPDATE_TASK_TEMPLATE_RESPONSES[200]) + @ApiResponse(UPDATE_TASK_TEMPLATE_RESPONSES[400]) + @ApiResponse(UPDATE_TASK_TEMPLATE_RESPONSES[401]) + @ApiResponse(UPDATE_TASK_TEMPLATE_RESPONSES[404]) + @ApiResponse(UPDATE_TASK_TEMPLATE_RESPONSES[500]) + @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) + async updateTaskTemplate( + @Param('id', ValidateIdPipe) taskTemplateId: string, + @Body() updateTaskTemplateDto: UpdateTaskTemplateDto, + @AuthContext() authContext: AuthContextType, + ) { + const updatedTaskTemplate = await this.taskTemplateService.updateById( + taskTemplateId, + updateTaskTemplateDto, + ); + + return { + ...updatedTaskTemplate, + authType: authContext.authType, + ...(authContext.userId && authContext.userEmail && { + authenticatedUser: { + id: authContext.userId, + email: authContext.userEmail, + }, + }), + }; + } + + @Delete(':id') + @ApiOperation(TASK_TEMPLATE_OPERATIONS.deleteTaskTemplate) + @ApiParam(TASK_TEMPLATE_PARAMS.taskTemplateId) + @ApiResponse(DELETE_TASK_TEMPLATE_RESPONSES[200]) + @ApiResponse(DELETE_TASK_TEMPLATE_RESPONSES[401]) + @ApiResponse(DELETE_TASK_TEMPLATE_RESPONSES[404]) + @ApiResponse(DELETE_TASK_TEMPLATE_RESPONSES[500]) + async deleteTaskTemplate( + @Param('id', ValidateIdPipe) taskTemplateId: string, + @AuthContext() authContext: AuthContextType, + ) { + const result = await this.taskTemplateService.deleteById(taskTemplateId); + + return { + ...result, + authType: authContext.authType, + ...(authContext.userId && authContext.userEmail && { + authenticatedUser: { + id: authContext.userId, + email: authContext.userEmail, + }, + }), + }; + } +} + diff --git a/apps/api/src/framework-editor/task-template/task-template.module.ts b/apps/api/src/framework-editor/task-template/task-template.module.ts new file mode 100644 index 000000000..fd665ca6a --- /dev/null +++ b/apps/api/src/framework-editor/task-template/task-template.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../../auth/auth.module'; +import { TaskTemplateController } from './task-template.controller'; +import { TaskTemplateService } from './task-template.service'; + +@Module({ + imports: [AuthModule], + controllers: [TaskTemplateController], + providers: [TaskTemplateService], + exports: [TaskTemplateService], +}) +export class TaskTemplateModule {} + diff --git a/apps/api/src/framework-editor/task-template/task-template.service.ts b/apps/api/src/framework-editor/task-template/task-template.service.ts new file mode 100644 index 000000000..ecbe3f404 --- /dev/null +++ b/apps/api/src/framework-editor/task-template/task-template.service.ts @@ -0,0 +1,91 @@ +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { db } from '@trycompai/db'; +import { UpdateTaskTemplateDto } from './dto/update-task-template.dto'; + +@Injectable() +export class TaskTemplateService { + private readonly logger = new Logger(TaskTemplateService.name); + + async findAll() { + try { + const taskTemplates = await db.frameworkEditorTaskTemplate.findMany({ + orderBy: { name: 'asc' }, + }); + + this.logger.log(`Retrieved ${taskTemplates.length} framework editor task templates`); + return taskTemplates; + } catch (error) { + this.logger.error('Failed to retrieve framework editor task templates:', error); + throw error; + } + } + + async findById(id: string) { + try { + const taskTemplate = await db.frameworkEditorTaskTemplate.findUnique({ + where: { id }, + }); + + if (!taskTemplate) { + throw new NotFoundException(`Framework editor task template with ID ${id} not found`); + } + + this.logger.log(`Retrieved framework editor task template: ${taskTemplate.name} (${id})`); + return taskTemplate; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Failed to retrieve framework editor task template ${id}:`, error); + throw error; + } + } + + async updateById(id: string, updateDto: UpdateTaskTemplateDto) { + try { + // First check if the task template exists + await this.findById(id); + + const updatedTaskTemplate = await db.frameworkEditorTaskTemplate.update({ + where: { id }, + data: updateDto, + }); + + this.logger.log(`Updated framework editor task template: ${updatedTaskTemplate.name} (${id})`); + return updatedTaskTemplate; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Failed to update framework editor task template ${id}:`, error); + throw error; + } + } + + async deleteById(id: string) { + try { + // First check if the task template exists + const existingTaskTemplate = await this.findById(id); + + await db.frameworkEditorTaskTemplate.delete({ + where: { id }, + }); + + this.logger.log(`Deleted framework editor task template: ${existingTaskTemplate.name} (${id})`); + return { + message: 'Framework editor task template deleted successfully', + deletedTaskTemplate: { + id: existingTaskTemplate.id, + name: existingTaskTemplate.name, + } + }; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Failed to delete framework editor task template ${id}:`, error); + throw error; + } + } +} + diff --git a/apps/app/src/actions/tasks/create-task-action.ts b/apps/app/src/actions/tasks/create-task-action.ts index b04552813..db77e88e9 100644 --- a/apps/app/src/actions/tasks/create-task-action.ts +++ b/apps/app/src/actions/tasks/create-task-action.ts @@ -17,6 +17,7 @@ const createTaskSchema = z.object({ frequency: z.nativeEnum(TaskFrequency).nullable().optional(), department: z.nativeEnum(Departments).nullable().optional(), controlIds: z.array(z.string()).optional(), + taskTemplateId: z.string().nullable().optional(), }); export const createTaskAction = authActionClient @@ -29,7 +30,8 @@ export const createTaskAction = authActionClient }, }) .action(async ({ parsedInput, ctx }) => { - const { title, description, assigneeId, frequency, department, controlIds } = parsedInput; + const { title, description, assigneeId, frequency, department, controlIds, taskTemplateId } = + parsedInput; const { session: { activeOrganizationId }, user, @@ -50,6 +52,7 @@ export const createTaskAction = authActionClient order: 0, frequency: frequency || null, department: department || null, + taskTemplateId: taskTemplateId || null, ...(controlIds && controlIds.length > 0 && { controls: { diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx index 7d0a1b5fe..ac9d3983f 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx @@ -2,6 +2,7 @@ import { createTaskAction } from '@/actions/tasks/create-task-action'; import { SelectAssignee } from '@/components/SelectAssignee'; +import { useTaskTemplates } from '@/hooks/use-task-template-api'; import { Button } from '@comp/ui/button'; import { Drawer, DrawerContent, DrawerTitle } from '@comp/ui/drawer'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; @@ -16,7 +17,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { ArrowRightIcon, X } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import { useQueryState } from 'nuqs'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; @@ -33,6 +34,7 @@ const createTaskSchema = z.object({ frequency: z.nativeEnum(TaskFrequency).nullable().optional(), department: z.nativeEnum(Departments).nullable().optional(), controlIds: z.array(z.string()).optional(), + taskTemplateId: z.string().nullable().optional(), }); export function CreateTaskSheet({ @@ -46,6 +48,8 @@ export function CreateTaskSheet({ const [createTaskOpen, setCreateTaskOpen] = useQueryState('create-task'); const isOpen = Boolean(createTaskOpen); + const { data: taskTemplates } = useTaskTemplates(); + const handleOpenChange = (open: boolean) => { setCreateTaskOpen(open ? 'true' : null); }; @@ -70,6 +74,7 @@ export function CreateTaskSheet({ frequency: null, department: null, controlIds: [], + taskTemplateId: null, }, }); @@ -90,6 +95,25 @@ export function CreateTaskSheet({ [controls], ); + const frameworkEditorTaskTemplates = useMemo(() => taskTemplates?.data || [], [taskTemplates]); + + // Watch for task template selection + const selectedTaskTemplateId = form.watch('taskTemplateId'); + const selectedTaskTemplate = useMemo( + () => frameworkEditorTaskTemplates.find((template) => template.id === selectedTaskTemplateId), + [selectedTaskTemplateId, frameworkEditorTaskTemplates], + ); + + // Auto-fill form when task template is selected + useEffect(() => { + if (selectedTaskTemplate) { + form.setValue('title', selectedTaskTemplate.name); + form.setValue('description', selectedTaskTemplate.description); + form.setValue('frequency', selectedTaskTemplate.frequency as TaskFrequency); + form.setValue('department', selectedTaskTemplate.department as Departments); + } + }, [selectedTaskTemplate, form]); + // Memoize filter function to prevent re-renders const filterFunction = useCallback( (value: string, search: string) => { @@ -116,9 +140,54 @@ export function CreateTaskSheet({ onChange(options.map((option) => option.value)); }, []); + const handleTaskTemplateChange = useCallback( + (value: string, onChange: (value: any) => void) => { + if (value === 'none') { + onChange(null); + // Clear the fields when "none" is selected + form.setValue('title', ''); + form.setValue('description', ''); + form.setValue('frequency', null); + form.setValue('department', null); + } else { + onChange(value); + } + }, + [form], + ); + const taskForm = (
+ ( + + Task Template (Optional) + + + + )} + /> + ( Frequency (Optional) - handleFrequencyChange(value, field.onChange)} + > @@ -209,7 +281,10 @@ export function CreateTaskSheet({ render={({ field }) => ( Department (Optional) - handleDepartmentChange(value, field.onChange)} + > diff --git a/apps/app/src/app/(app)/onboarding/hooks/usePostPaymentOnboarding.ts b/apps/app/src/app/(app)/onboarding/hooks/usePostPaymentOnboarding.ts index 16b50af31..e9677d8e2 100644 --- a/apps/app/src/app/(app)/onboarding/hooks/usePostPaymentOnboarding.ts +++ b/apps/app/src/app/(app)/onboarding/hooks/usePostPaymentOnboarding.ts @@ -20,8 +20,11 @@ interface UsePostPaymentOnboardingProps { initialData?: Record; } +const showShippingStep = process.env.NEXT_PUBLIC_APP_ENV !== 'staging'; // Use steps 4-12 (post-payment steps) -const postPaymentSteps = steps.slice(3); +const postPaymentSteps = showShippingStep + ? steps.slice(3) + : steps.slice(3).filter((step) => step.key !== 'shipping'); export function usePostPaymentOnboarding({ organizationId, diff --git a/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts b/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts index 7fae96158..c0bffebad 100644 --- a/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts +++ b/apps/app/src/app/(app)/setup/actions/create-organization-minimal.ts @@ -45,7 +45,7 @@ export const createOrganizationMinimal = authActionClientWithoutOrg website: parsedInput.website, onboardingCompleted: false, // Explicitly set to false // Local-only: default access for faster local development - ...(process.env.NODE_ENV !== 'production' && { hasAccess: true }), + ...(process.env.NEXT_PUBLIC_APP_ENV !== 'production' && { hasAccess: true }), members: { create: { userId: session.user.id, diff --git a/apps/app/src/hooks/use-task-template-api.ts b/apps/app/src/hooks/use-task-template-api.ts new file mode 100644 index 000000000..921043a6e --- /dev/null +++ b/apps/app/src/hooks/use-task-template-api.ts @@ -0,0 +1,17 @@ +'use client'; + +import { useApiSWR, UseApiSWROptions } from '@/hooks/use-api-swr'; + +export interface TaskTemplate { + id: string; + name: string; + description: string; + frequency: string; + department: string; + createdAt: string; + updatedAt: string; +} + +export function useTaskTemplates(options: UseApiSWROptions = {}) { + return useApiSWR('/v1/framework-editor/task-template', options); +} diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 610321c9b..8de22c146 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -6899,6 +6899,406 @@ "Trust Portal" ] } + }, + "/v1/framework-editor/task-template": { + "get": { + "description": "Retrieve all framework editor task templates", + "operationId": "TaskTemplateController_getAllTaskTemplates_v1", + "parameters": [ + { + "name": "X-Organization-Id", + "in": "header", + "description": "Organization ID (required for session auth, optional for API key auth)", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved all framework editor task templates", + "content": { + "application/json": { + "schema": { + "example": { + "data": [ + { + "id": "frk_tt_abc123def456", + "name": "Monthly Security Review", + "description": "Review and update security policies on a monthly basis", + "frequency": "monthly", + "department": "it", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "count": 1, + "authType": "session", + "authenticatedUser": { + "id": "user_123", + "email": "user@example.com" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 401, + "message": "Unauthorized" + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 500, + "message": "Internal server error" + } + } + } + } + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Get all framework editor task templates", + "tags": [ + "Framework Editor Task Templates" + ] + } + }, + "/v1/framework-editor/task-template/{id}": { + "get": { + "description": "Retrieve a specific framework editor task template by its ID", + "operationId": "TaskTemplateController_getTaskTemplateById_v1", + "parameters": [ + { + "name": "X-Organization-Id", + "in": "header", + "description": "Organization ID (required for session auth, optional for API key auth)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "Framework editor task template ID", + "schema": { + "example": "frk_tt_abc123def456", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved framework editor task template", + "content": { + "application/json": { + "schema": { + "example": { + "id": "frk_tt_abc123def456", + "name": "Monthly Security Review", + "description": "Review and update security policies on a monthly basis", + "frequency": "monthly", + "department": "it", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z", + "authType": "session", + "authenticatedUser": { + "id": "user_123", + "email": "user@example.com" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 401, + "message": "Unauthorized" + } + } + } + } + }, + "404": { + "description": "Framework editor task template not found", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 404, + "message": "Framework editor task template with ID frk_tt_abc123def456 not found" + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 500, + "message": "Internal server error" + } + } + } + } + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Get framework editor task template by ID", + "tags": [ + "Framework Editor Task Templates" + ] + }, + "patch": { + "description": "Update a framework editor task template by ID", + "operationId": "TaskTemplateController_updateTaskTemplate_v1", + "parameters": [ + { + "name": "X-Organization-Id", + "in": "header", + "description": "Organization ID (required for session auth, optional for API key auth)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "Framework editor task template ID", + "schema": { + "example": "frk_tt_abc123def456", + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "Update framework editor task template data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTaskTemplateDto" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully updated framework editor task template", + "content": { + "application/json": { + "schema": { + "example": { + "id": "frk_tt_abc123def456", + "name": "Monthly Security Review (Updated)", + "description": "Review and update security policies on a monthly basis", + "frequency": "monthly", + "department": "it", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-02T00:00:00.000Z", + "authType": "session", + "authenticatedUser": { + "id": "user_123", + "email": "user@example.com" + } + } + } + } + } + }, + "400": { + "description": "Bad request - Invalid data provided", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 400, + "message": "Validation failed" + } + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 401, + "message": "Unauthorized" + } + } + } + } + }, + "404": { + "description": "Framework editor task template not found", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 404, + "message": "Framework editor task template with ID frk_tt_abc123def456 not found" + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 500, + "message": "Internal server error" + } + } + } + } + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Update framework editor task template", + "tags": [ + "Framework Editor Task Templates" + ] + }, + "delete": { + "description": "Delete a framework editor task template by ID", + "operationId": "TaskTemplateController_deleteTaskTemplate_v1", + "parameters": [ + { + "name": "X-Organization-Id", + "in": "header", + "description": "Organization ID (required for session auth, optional for API key auth)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "Framework editor task template ID", + "schema": { + "example": "frk_tt_abc123def456", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully deleted framework editor task template", + "content": { + "application/json": { + "schema": { + "example": { + "message": "Framework editor task template deleted successfully", + "deletedTaskTemplate": { + "id": "frk_tt_abc123def456", + "name": "Monthly Security Review" + }, + "authType": "session", + "authenticatedUser": { + "id": "user_123", + "email": "user@example.com" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Invalid or missing authentication", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 401, + "message": "Unauthorized" + } + } + } + } + }, + "404": { + "description": "Framework editor task template not found", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 404, + "message": "Framework editor task template with ID frk_tt_abc123def456 not found" + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "example": { + "statusCode": 500, + "message": "Internal server error" + } + } + } + } + } + }, + "security": [ + { + "apikey": [] + } + ], + "summary": "Delete framework editor task template", + "tags": [ + "Framework Editor Task Templates" + ] + } } }, "info": { @@ -9126,6 +9526,45 @@ "domain", "verified" ] + }, + "UpdateTaskTemplateDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Task template name", + "example": "Monthly Security Review" + }, + "description": { + "type": "string", + "description": "Detailed description of the task template", + "example": "Review and update security policies on a monthly basis" + }, + "frequency": { + "type": "string", + "description": "Frequency of the task", + "enum": [ + "monthly", + "quarterly", + "yearly" + ], + "example": "monthly" + }, + "department": { + "type": "string", + "description": "Department responsible for the task", + "enum": [ + "none", + "admin", + "gov", + "hr", + "it", + "itsm", + "qms" + ], + "example": "it" + } + } } } }