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();