diff --git a/apps/app/src/app/(app)/[orgId]/layout.tsx b/apps/app/src/app/(app)/[orgId]/layout.tsx index 6bc8fb313..3bb85b700 100644 --- a/apps/app/src/app/(app)/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/layout.tsx @@ -63,6 +63,10 @@ export default async function Layout({ return redirect('/auth/unauthorized'); } + if (member.role === 'employee') { + return redirect('/no-access'); + } + // If this org is not accessible on current plan, redirect to upgrade if (!organization.hasAccess) { return redirect(`/upgrade/${organization.id}`); diff --git a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx index 55ff38f56..a2c820684 100644 --- a/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/dashboard/components/EmployeeCompletionChart.tsx @@ -2,7 +2,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@comp/ui/card'; import { Input } from '@comp/ui/input'; -import { Search } from 'lucide-react'; +import { ExternalLink, Search } from 'lucide-react'; +import Link from 'next/link'; +import { useParams } from 'next/navigation'; import type { CSSProperties } from 'react'; import * as React from 'react'; @@ -47,6 +49,8 @@ export function EmployeeCompletionChart({ trainingVideos, showAll = false, }: EmployeeCompletionChartProps) { + const params = useParams(); + const orgId = params.orgId as string; const [searchTerm, setSearchTerm] = React.useState(''); const [displayedItems, setDisplayedItems] = React.useState(showAll ? 20 : 5); const [isLoading, setIsLoading] = React.useState(false); @@ -209,8 +213,19 @@ export function EmployeeCompletionChart({ {sortedStats.map((stat) => (
-
-

{stat.name}

+
+
+

{stat.name}

+ + View Profile + + +

{stat.email}

diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index 61552c2c6..c83d58de7 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -72,10 +72,6 @@ export default async function RootPage({ }, }); - if (member?.role === 'employee') { - return redirect(await buildUrlWithParams('/no-access')); - } - if (!member) { return redirect(await buildUrlWithParams('/setup')); } diff --git a/packages/db/package.json b/packages/db/package.json index 993261b7a..9ed8a996f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -34,7 +34,7 @@ "db:generate": "prisma generate", "db:migrate": "prisma migrate dev", "db:push": "prisma db push", - "db:seed": "prisma db seed", + "db:seed": "bun prisma/seed/seed.ts", "db:studio": "prisma studio", "docker:clean": "docker compose down -v", "docker:down": "docker compose down", diff --git a/packages/db/prisma/seed/frameworkEditorSchemas.js b/packages/db/prisma/seed/frameworkEditorSchemas.js deleted file mode 100644 index 28709b66b..000000000 --- a/packages/db/prisma/seed/frameworkEditorSchemas.js +++ /dev/null @@ -1,136 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.frameworkEditorModelSchemas = exports.FrameworkEditorControlTemplateSchema = exports.FrameworkEditorTaskTemplateSchema = exports.FrameworkEditorPolicyTemplateSchema = exports.FrameworkEditorRequirementSchema = exports.FrameworkEditorFrameworkSchema = exports.FrameworkEditorVideoSchema = void 0; -const zod_1 = require("zod"); -// Assuming Frequency and Departments enums are defined elsewhere and imported -// For now, we'll use z.string() as a placeholder if their definitions aren't available. -// import { Frequency, Departments } from './path-to-shared-enums'; // Example import -const datePreprocess = (arg) => { - if (typeof arg === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/.test(arg)) { - return arg.replace(' ', 'T') + 'Z'; - } - return arg; -}; -exports.FrameworkEditorVideoSchema = zod_1.z.object({ - id: zod_1.z.string().optional(), // @id @default - title: zod_1.z.string(), - description: zod_1.z.string(), - youtubeId: zod_1.z.string(), - url: zod_1.z.string(), - createdAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for createdAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) - updatedAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for updatedAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) @updatedAt -}); -exports.FrameworkEditorFrameworkSchema = zod_1.z.object({ - id: zod_1.z.string().optional(), // @id @default - name: zod_1.z.string(), - version: zod_1.z.string(), - description: zod_1.z.string(), - visible: zod_1.z.boolean().optional(), // @default(true) - // requirements: FrameworkEditorRequirement[] - relational, omitted - // frameworkInstances: FrameworkInstance[] - relational, omitted - createdAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for createdAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) - updatedAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for updatedAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) @updatedAt -}); -exports.FrameworkEditorRequirementSchema = zod_1.z.object({ - id: zod_1.z.string().optional(), // @id @default - frameworkId: zod_1.z.string(), - // framework: FrameworkEditorFramework - relational, omitted - name: zod_1.z.string(), - identifier: zod_1.z.string().optional(), // @default("") - description: zod_1.z.string(), - // controlTemplates: FrameworkEditorControlTemplate[] - relational, omitted - // requirementMaps: RequirementMap[] - relational, omitted - createdAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for createdAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) - updatedAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for updatedAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) @updatedAt -}); -exports.FrameworkEditorPolicyTemplateSchema = zod_1.z.object({ - id: zod_1.z.string().optional(), // @id @default - name: zod_1.z.string(), - description: zod_1.z.string(), - frequency: zod_1.z.string(), // Placeholder for Frequency enum - department: zod_1.z.string(), // Placeholder for Departments enum - content: zod_1.z.any(), // Json - // controlTemplates: FrameworkEditorControlTemplate[] - relational, omitted - createdAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for createdAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) - updatedAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for updatedAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) @updatedAt - // policies: Policy[] - relational, omitted -}); -exports.FrameworkEditorTaskTemplateSchema = zod_1.z.object({ - id: zod_1.z.string().optional(), // @id @default - name: zod_1.z.string(), - description: zod_1.z.string(), - frequency: zod_1.z.string(), // Placeholder for Frequency enum - department: zod_1.z.string(), // Placeholder for Departments enum - // controlTemplates: FrameworkEditorControlTemplate[] - relational, omitted - createdAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for createdAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) - updatedAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for updatedAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) @updatedAt - // tasks: Task[] - relational, omitted -}); -exports.FrameworkEditorControlTemplateSchema = zod_1.z.object({ - id: zod_1.z.string().optional(), // @id @default - name: zod_1.z.string(), - description: zod_1.z.string(), - // policyTemplates: FrameworkEditorPolicyTemplate[] - relational, omitted - // requirements: FrameworkEditorRequirement[] - relational, omitted - // taskTemplates: FrameworkEditorTaskTemplate[] - relational, omitted - createdAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for createdAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) - updatedAt: zod_1.z - .preprocess(datePreprocess, zod_1.z.string().datetime({ - message: 'Invalid datetime string for updatedAt. Expected ISO 8601 format.', - })) - .optional(), // @default(now()) @updatedAt - // controls: Control[] - relational, omitted -}); -// For use in seed script validation -exports.frameworkEditorModelSchemas = { - FrameworkEditorVideo: exports.FrameworkEditorVideoSchema, - FrameworkEditorFramework: exports.FrameworkEditorFrameworkSchema, - FrameworkEditorRequirement: exports.FrameworkEditorRequirementSchema, - FrameworkEditorPolicyTemplate: exports.FrameworkEditorPolicyTemplateSchema, - FrameworkEditorTaskTemplate: exports.FrameworkEditorTaskTemplateSchema, - FrameworkEditorControlTemplate: exports.FrameworkEditorControlTemplateSchema, -}; diff --git a/packages/db/prisma/seed/seed.js b/packages/db/prisma/seed/seed.js deleted file mode 100644 index 4836087ad..000000000 --- a/packages/db/prisma/seed/seed.js +++ /dev/null @@ -1,160 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const client_1 = require("@prisma/client"); -const promises_1 = __importDefault(require("node:fs/promises")); -const node_path_1 = __importDefault(require("node:path")); -const frameworkEditorSchemas_1 = require("./frameworkEditorSchemas"); -const prisma = new client_1.PrismaClient(); -async function seedJsonFiles(subDirectory) { - const directoryPath = node_path_1.default.join(__dirname, subDirectory); - console.log(`Starting to seed files from: ${directoryPath}`); - const files = await promises_1.default.readdir(directoryPath); - const jsonFiles = files.filter((file) => file.endsWith('.json')); - for (const jsonFile of jsonFiles) { - try { - const filePath = node_path_1.default.join(directoryPath, jsonFile); - const jsonContent = await promises_1.default.readFile(filePath, 'utf-8'); - const jsonData = JSON.parse(jsonContent); - if (!Array.isArray(jsonData) || jsonData.length === 0) { - console.log(`Skipping empty or invalid JSON file: ${jsonFile}`); - continue; - } - if (subDirectory === 'primitives') { - const modelNameForPrisma = jsonFile.replace('.json', ''); - const prismaModelKey = modelNameForPrisma.charAt(0).toLowerCase() + modelNameForPrisma.slice(1); - const zodModelKey = modelNameForPrisma; - const prismaAny = prisma; - if (!prismaAny[prismaModelKey] || - typeof prismaAny[prismaModelKey].createMany !== 'function') { - console.warn(`Model ${prismaModelKey} not found on Prisma client or does not support createMany. Skipping ${jsonFile}.`); - continue; - } - const zodSchema = frameworkEditorSchemas_1.frameworkEditorModelSchemas[zodModelKey]; - if (!zodSchema) { - console.warn(`Zod schema not found for model ${String(zodModelKey)}. Skipping validation for ${jsonFile}.`); - } - else { - console.log(`Validating ${jsonData.length} records from ${jsonFile} against ${String(zodModelKey)} schema...`); - for (const item of jsonData) { - try { - zodSchema.parse(item); - } - catch (validationError) { - console.error(`Validation failed for an item in ${jsonFile} for model ${String(zodModelKey)}:`, item); - console.error('Validation errors:', validationError); - throw new Error(`Data validation failed for ${jsonFile}.`); - } - } - console.log(`Validation successful for ${jsonFile}.`); - } - const processedData = jsonData.map((item) => { - const newItem = { ...item }; - if (newItem.createdAt && typeof newItem.createdAt === 'string') { - newItem.createdAt = new Date(newItem.createdAt); - } - if (newItem.updatedAt && typeof newItem.updatedAt === 'string') { - newItem.updatedAt = new Date(newItem.updatedAt); - } - return newItem; - }); - console.log(`Seeding ${processedData.length} records from ${jsonFile} into ${prismaModelKey}...`); - // Use upsert to update existing records instead of skipping them - for (const record of processedData) { - await prismaAny[prismaModelKey].upsert({ - where: { id: record.id }, - create: record, - update: record, - }); - } - console.log(`Finished seeding ${jsonFile} from primitives.`); - } - else if (subDirectory === 'relations') { - // Expected filename format: _ModelAToModelB.json - if (!jsonFile.startsWith('_') || !jsonFile.includes('To')) { - console.warn(`Skipping relation file with unexpected format: ${jsonFile}`); - continue; - } - const modelNamesPart = jsonFile.substring(1, jsonFile.indexOf('.json')); - const [modelANamePascal, modelBNamePascal] = modelNamesPart.split('To'); - if (!modelANamePascal || !modelBNamePascal) { - console.warn(`Could not parse model names from relation file: ${jsonFile}`); - continue; - } - const prismaModelAName = modelANamePascal.charAt(0).toLowerCase() + modelANamePascal.slice(1); - // Infer relation field name on ModelA: pluralized, camelCased ModelB name - // e.g., if ModelB is FrameworkEditorPolicyTemplate, relation field is frameworkEditorPolicyTemplates - // This is a common convention, but might need adjustment based on actual schema - let relationFieldNameOnModelA = modelBNamePascal.charAt(0).toLowerCase() + modelBNamePascal.slice(1); - if (!relationFieldNameOnModelA.endsWith('s')) { - // basic pluralization - relationFieldNameOnModelA += 's'; - } - // Special handling for 'Requirement' -> 'requirements' (already plural) - // and other specific cases if 's' isn't the right pluralization. - // For now, using a direct map for known cases from the user's file names. - if (modelBNamePascal === 'FrameworkEditorPolicyTemplate') { - relationFieldNameOnModelA = 'policyTemplates'; - } - else if (modelBNamePascal === 'FrameworkEditorRequirement') { - relationFieldNameOnModelA = 'requirements'; - } - else if (modelBNamePascal === 'FrameworkEditorTaskTemplate') { - relationFieldNameOnModelA = 'taskTemplates'; - } - const prismaAny = prisma; - if (!prismaAny[prismaModelAName] || - typeof prismaAny[prismaModelAName].update !== 'function') { - console.warn(`Model ${prismaModelAName} not found on Prisma client or does not support update. Skipping ${jsonFile}.`); - continue; - } - console.log(`Processing relations from ${jsonFile} for ${prismaModelAName} to connect via ${relationFieldNameOnModelA}...`); - let connectionsMade = 0; - for (const relationItem of jsonData) { - if (!relationItem.A || !relationItem.B) { - console.warn(`Skipping invalid relation item in ${jsonFile}:`, relationItem); - continue; - } - const idA = relationItem.A; - const idB = relationItem.B; - try { - await prismaAny[prismaModelAName].update({ - where: { id: idA }, - data: { - [relationFieldNameOnModelA]: { - connect: { id: idB }, - }, - }, - }); - connectionsMade++; - } - catch (error) { - console.error(`Failed to connect ${prismaModelAName} (${idA}) with ${modelBNamePascal} (${idB}) from ${jsonFile}:`, error); - // Decide if one error should stop the whole process for this file or continue - } - } - console.log(`Finished processing ${jsonFile}. Made ${connectionsMade} connections.`); - } - } - catch (error) { - console.error(`Error processing ${jsonFile}:`, error); - throw error; - } - } -} -async function main() { - try { - await seedJsonFiles('primitives'); - await seedJsonFiles('relations'); - await prisma.$disconnect(); - console.log('Seeding completed successfully for primitives and relations.'); - } - catch (error) { - console.error('Seeding failed:', error); - await prisma.$disconnect(); - process.exit(1); - } -} -main();