Valid_filters_and_texanomy

This commit is contained in:
kenilkb 2026-07-22 12:33:24 +05:30
parent 61f537057b
commit a127b5ffc4
24 changed files with 1486 additions and 3860 deletions

4
.gitignore vendored
View File

@ -35,4 +35,6 @@ uploads/*
/memory.md
/phases.md
/architecture.md
/Guide.md
/Guide.md
# Dedicated Documentation Folder
/documents/

View File

@ -1,95 +0,0 @@
const fs = require('fs');
async function runTests() {
const BASE_URL = 'http://localhost:5001/api/v1';
let token = '';
try {
console.log('1. Testing Health Endpoint...');
const health = await fetch(`${BASE_URL}/health`);
const healthData = await health.json();
console.log('Health:', healthData);
if (!health.ok) throw new Error('Health check failed');
console.log('\n2. Testing Registration...');
const regRes = await fetch(`${BASE_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'admin@tech4biz.com', password: 'securepassword', role: 'ADMIN' })
});
console.log('Registration Status (Admin):', regRes.status);
if (!regRes.ok && regRes.status !== 400) throw new Error('Registration failed');
const regPartnerRes = await fetch(`${BASE_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'partner@tech4biz.com', password: 'securepassword', role: 'PARTNER_USER' })
});
console.log('Registration Status (Partner):', regPartnerRes.status);
if (!regPartnerRes.ok && regPartnerRes.status !== 400) throw new Error('Partner Registration failed');
console.log('\n3. Testing Login...');
const loginRes = await fetch(`${BASE_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'admin@tech4biz.com', password: 'securepassword' })
});
const loginData = await loginRes.json();
console.log('Login Status:', loginRes.status);
if (!loginRes.ok) throw new Error('Login failed');
token = loginData.accessToken;
console.log('Received Access Token: ', token.substring(0, 15) + '...');
console.log('\n4. Testing Organization Creation...');
const orgRes = await fetch(`${BASE_URL}/organizations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ name: 'Tech4Biz Partners' })
});
const orgData = await orgRes.json();
console.log('Organization Created:', orgData);
if (!orgRes.ok) throw new Error('Org creation failed');
console.log('\n5. Testing Legal Document Creation...');
const legalRes = await fetch(`${BASE_URL}/legal/documents`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ type: 'NDA', version: '1.0', content: 'You must not disclose anything.' })
});
const legalData = await legalRes.json();
console.log('Legal Document Created:', legalData);
if (!legalRes.ok) throw new Error('Legal creation failed');
console.log('\n6. Testing Asset Upload...');
// Create a dummy file
fs.writeFileSync('test-file.txt', 'This is a test file for upload.');
const formData = new FormData();
const fileBlob = new Blob([fs.readFileSync('test-file.txt')], { type: 'text/plain' });
formData.append('file', fileBlob, 'test-file.txt');
formData.append('title', 'My Secret Document');
const uploadRes = await fetch(`${BASE_URL}/assets/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const uploadData = await uploadRes.json();
console.log('Asset Uploaded:', uploadData);
if (!uploadRes.ok) throw new Error('Upload failed');
fs.unlinkSync('test-file.txt');
console.log('\n✅ ALL TESTS PASSED SUCCESSFULLY!');
} catch (error) {
console.error('\n❌ TEST FAILED:', error);
}
}
runTests();

View File

@ -78,13 +78,16 @@ model Asset {
solution String? @db.Text
contentType String?
folderId String?
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
verticals Vertical[] @relation("AssetVerticals")
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
verticals Vertical[] @relation("AssetVerticals")
techStacks TechStack[] @relation("AssetTechStacks")
engagementTypes EngagementType[] @relation("AssetEngagementTypes")
complianceStandards ComplianceStandard[] @relation("AssetComplianceStandards")
sharedWith SharedAsset[]
downloadRequests DownloadRequest[]
assetGroups AssetGroup[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Vertical {
@ -101,6 +104,49 @@ model Vertical {
updatedAt DateTime @updatedAt
}
model TechStack {
id String @id @default(uuid())
name String @unique
slug String @unique
category String // "Languages & Frameworks", "AI & ML", "Data & Backend", "Cloud & Infra"
icon String?
description String?
color String?
orderIndex Int @default(0)
isActive Boolean @default(true)
assets Asset[] @relation("AssetTechStacks")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model EngagementType {
id String @id @default(uuid())
name String @unique
slug String @unique
icon String?
description String?
color String?
orderIndex Int @default(0)
isActive Boolean @default(true)
assets Asset[] @relation("AssetEngagementTypes")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model ComplianceStandard {
id String @id @default(uuid())
name String @unique
slug String @unique
icon String?
description String?
color String?
orderIndex Int @default(0)
isActive Boolean @default(true)
assets Asset[] @relation("AssetComplianceStandards")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AssetNotification {
id String @id @default(uuid())
title String

View File

@ -1,26 +0,0 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function seed() {
await prisma.legalDocument.create({
data: {
type: 'NDA',
version: '1.0',
content: 'This is the standard Non-Disclosure Agreement content...',
isActive: true,
}
});
await prisma.legalDocument.create({
data: {
type: 'MSA',
version: '1.0',
content: 'This is the standard Master Services Agreement content...',
isActive: true,
}
});
console.log('Documents seeded.');
}
seed().catch(console.error).finally(() => prisma.$disconnect());

View File

@ -2,6 +2,7 @@ import dotenv from 'dotenv';
dotenv.config();
import prisma from './src/utils/db';
import bcrypt from 'bcrypt';
import { seedFourGroupTaxonomy } from './src/utils/seed-taxonomy';
async function seed() {
console.log('Starting database seeding...');
@ -197,6 +198,9 @@ async function seed() {
}
console.log('Seeded Ecosystem Offerings.');
// 6. Seed 4-Group Asset Taxonomy
await seedFourGroupTaxonomy();
console.log('Seeding completed successfully.');
}

View File

@ -59,10 +59,18 @@ export class AssetController {
fileUrl = req.body.url;
}
let verticalIds = req.body.verticalIds;
if (typeof verticalIds === 'string' && verticalIds.trim()) {
try { verticalIds = JSON.parse(verticalIds); } catch { verticalIds = verticalIds.split(',').map((id: string) => id.trim()).filter(Boolean); }
}
const parseJsonOrArray = (val: any) => {
if (!val) return undefined;
if (typeof val === 'string' && val.trim()) {
try { return JSON.parse(val); } catch { return val.split(',').map((id: string) => id.trim()).filter(Boolean); }
}
return val;
};
let verticalIds = parseJsonOrArray(req.body.verticalIds);
let techStackIds = parseJsonOrArray(req.body.techStackIds);
let engagementTypeIds = parseJsonOrArray(req.body.engagementTypeIds);
let complianceIds = parseJsonOrArray(req.body.complianceIds);
const assetData = {
title: req.body.title || (req.file ? req.file.originalname : 'URL Asset'),
@ -81,6 +89,9 @@ export class AssetController {
problemStatement: req.body.problemStatement || null,
solution: req.body.solution || null,
verticalIds,
techStackIds,
engagementTypeIds,
complianceIds,
shares,
sharedOrgIds: req.body.sharedOrgIds || null,
};
@ -96,6 +107,9 @@ export class AssetController {
const filters = {
search: req.query.search as string,
verticalIds: req.query.verticalIds ? (req.query.verticalIds as string).split(',') : undefined,
techStackIds: req.query.techStackIds ? (req.query.techStackIds as string).split(',') : undefined,
engagementTypeIds: req.query.engagementTypeIds ? (req.query.engagementTypeIds as string).split(',') : undefined,
complianceIds: req.query.complianceIds ? (req.query.complianceIds as string).split(',') : undefined,
contentTypes: req.query.contentTypes ? (req.query.contentTypes as string).split(',') : undefined,
subcategories: req.query.subcategories ? (req.query.subcategories as string).split(',') : undefined,
tags: req.query.tags ? (req.query.tags as string).split(',') : undefined,

View File

@ -19,15 +19,30 @@ export class TaxonomyController {
} catch (err) { next(err); }
};
// Public/Authenticated: Get taxonomy metadata with real-time asset counts
// Public/Authenticated: Get taxonomy metadata with real-time asset counts across all 4 groups
public getTaxonomyMeta = async (req: Request, res: Response, next: NextFunction) => {
try {
const [verticals, assets] = await Promise.all([
const [verticals, techStacks, engagementTypes, complianceStandards, assets] = await Promise.all([
prisma.vertical.findMany({
where: { isActive: true },
include: { _count: { select: { assets: true } } },
orderBy: { orderIndex: 'asc' },
}),
prisma.techStack.findMany({
where: { isActive: true },
include: { _count: { select: { assets: true } } },
orderBy: { orderIndex: 'asc' },
}),
prisma.engagementType.findMany({
where: { isActive: true },
include: { _count: { select: { assets: true } } },
orderBy: { orderIndex: 'asc' },
}),
prisma.complianceStandard.findMany({
where: { isActive: true },
include: { _count: { select: { assets: true } } },
orderBy: { orderIndex: 'asc' },
}),
prisma.asset.findMany({
select: {
categoryId: true,
@ -62,6 +77,9 @@ export class TaxonomyController {
res.status(200).json({
verticals,
techStacks,
engagementTypes,
complianceStandards,
categories: Object.entries(categoryCounts).map(([name, count]) => ({ name, count })),
subcategories: Object.entries(subcategoryCounts).map(([name, count]) => ({ name, count })),
contentTypes: Object.entries(contentTypeCounts).map(([name, count]) => ({ name, count })),
@ -117,4 +135,73 @@ export class TaxonomyController {
res.status(204).send();
} catch (err) { next(err); }
};
// Admin: Create Tech Stack
public createTechStack = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { name, category, icon, description, color } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '');
const item = await prisma.techStack.create({
data: { name, slug, category: category || 'Languages & Frameworks', icon, description, color: color || '#64748b' }
});
res.status(201).json(item);
} catch (err) { next(err); }
};
// Admin: Delete Tech Stack
public deleteTechStack = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
await prisma.techStack.delete({ where: { id } });
res.status(204).send();
} catch (err) { next(err); }
};
// Admin: Create Engagement Type
public createEngagementType = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { name, icon, description, color } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '');
const item = await prisma.engagementType.create({
data: { name, slug, icon, description, color: color || '#0284c7' }
});
res.status(201).json(item);
} catch (err) { next(err); }
};
// Admin: Delete Engagement Type
public deleteEngagementType = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
await prisma.engagementType.delete({ where: { id } });
res.status(204).send();
} catch (err) { next(err); }
};
// Admin: Create Compliance Standard
public createComplianceStandard = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { name, icon, description, color } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '');
const item = await prisma.complianceStandard.create({
data: { name, slug, icon, description, color: color || '#10b981' }
});
res.status(201).json(item);
} catch (err) { next(err); }
};
// Admin: Delete Compliance Standard
public deleteComplianceStandard = async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
await prisma.complianceStandard.delete({ where: { id } });
res.status(204).send();
} catch (err) { next(err); }
};
}

View File

@ -12,4 +12,13 @@ router.post('/verticals', authenticate, requireRole('ADMIN'), controller.createV
router.put('/verticals/:id', authenticate, requireRole('ADMIN'), controller.updateVertical);
router.delete('/verticals/:id', authenticate, requireRole('ADMIN'), controller.deleteVertical);
router.post('/tech-stacks', authenticate, requireRole('ADMIN'), controller.createTechStack);
router.delete('/tech-stacks/:id', authenticate, requireRole('ADMIN'), controller.deleteTechStack);
router.post('/engagement-types', authenticate, requireRole('ADMIN'), controller.createEngagementType);
router.delete('/engagement-types/:id', authenticate, requireRole('ADMIN'), controller.deleteEngagementType);
router.post('/compliance-standards', authenticate, requireRole('ADMIN'), controller.createComplianceStandard);
router.delete('/compliance-standards/:id', authenticate, requireRole('ADMIN'), controller.deleteComplianceStandard);
export default router;

View File

@ -55,7 +55,7 @@ export class AssetService {
}
public async createAsset(data: any) {
const { sharedOrgIds, shares, tags, verticalIds, ...rest } = data;
const { sharedOrgIds, shares, tags, verticalIds, techStackIds, engagementTypeIds, complianceIds, ...rest } = data;
// Parse tags
let parsedTags: string[] = [];
@ -69,27 +69,36 @@ export class AssetService {
}
}
// Parse verticalIds
let parsedVerticalIds: string[] = [];
if (Array.isArray(verticalIds)) {
parsedVerticalIds = verticalIds;
} else if (typeof verticalIds === 'string' && verticalIds.trim()) {
try {
parsedVerticalIds = JSON.parse(verticalIds);
} catch {
parsedVerticalIds = verticalIds.split(',').map((id: string) => id.trim()).filter(Boolean);
const parseIds = (val: any): string[] => {
if (Array.isArray(val)) return val;
if (typeof val === 'string' && val.trim()) {
try { return JSON.parse(val); }
catch { return val.split(',').map((s: string) => s.trim()).filter(Boolean); }
}
}
return [];
};
const parsedVerticalIds = parseIds(verticalIds);
const parsedTechStackIds = parseIds(techStackIds);
const parsedEngagementTypeIds = parseIds(engagementTypeIds);
const parsedComplianceIds = parseIds(complianceIds);
const asset = await prisma.asset.create({
data: {
...rest,
tags: parsedTags,
...(parsedVerticalIds.length > 0 ? {
verticals: {
connect: parsedVerticalIds.map(id => ({ id }))
}
} : {})
verticals: { connect: parsedVerticalIds.map(id => ({ id })) }
} : {}),
...(parsedTechStackIds.length > 0 ? {
techStacks: { connect: parsedTechStackIds.map(id => ({ id })) }
} : {}),
...(parsedEngagementTypeIds.length > 0 ? {
engagementTypes: { connect: parsedEngagementTypeIds.map(id => ({ id })) }
} : {}),
...(parsedComplianceIds.length > 0 ? {
complianceStandards: { connect: parsedComplianceIds.map(id => ({ id })) }
} : {}),
}
});
@ -160,6 +169,9 @@ export class AssetService {
filters?: {
search?: string;
verticalIds?: string[];
techStackIds?: string[];
engagementTypeIds?: string[];
complianceIds?: string[];
contentTypes?: string[];
subcategories?: string[];
tags?: string[];
@ -235,9 +247,31 @@ export class AssetService {
if (filters?.verticalIds && filters.verticalIds.length > 0) {
andConditions.push({
verticals: {
some: {
id: { in: filters.verticalIds }
}
some: { id: { in: filters.verticalIds } }
}
});
}
if (filters?.techStackIds && filters.techStackIds.length > 0) {
andConditions.push({
techStacks: {
some: { id: { in: filters.techStackIds } }
}
});
}
if (filters?.engagementTypeIds && filters.engagementTypeIds.length > 0) {
andConditions.push({
engagementTypes: {
some: { id: { in: filters.engagementTypeIds } }
}
});
}
if (filters?.complianceIds && filters.complianceIds.length > 0) {
andConditions.push({
complianceStandards: {
some: { id: { in: filters.complianceIds } }
}
});
}
@ -275,6 +309,9 @@ export class AssetService {
where: whereClause,
include: {
verticals: true,
techStacks: true,
engagementTypes: true,
complianceStandards: true,
sharedWith: {
include: {
organization: {
@ -301,6 +338,10 @@ export class AssetService {
return await prisma.asset.findUnique({
where: { id },
include: {
verticals: true,
techStacks: true,
engagementTypes: true,
complianceStandards: true,
sharedWith: {
include: {
organization: {
@ -323,7 +364,7 @@ export class AssetService {
}
public async updateAsset(id: string, data: any) {
const { shares, sharedOrgIds, tags, verticalIds, ...rest } = data;
const { shares, sharedOrgIds, tags, verticalIds, techStackIds, engagementTypeIds, complianceIds, ...rest } = data;
const updateData: any = { ...rest };
@ -341,20 +382,26 @@ export class AssetService {
updateData.tags = parsedTags;
}
if (verticalIds !== undefined) {
let parsedVerticalIds: string[] = [];
if (Array.isArray(verticalIds)) {
parsedVerticalIds = verticalIds;
} else if (typeof verticalIds === 'string') {
try {
parsedVerticalIds = JSON.parse(verticalIds);
} catch {
parsedVerticalIds = verticalIds.split(',').map((v: string) => v.trim()).filter(Boolean);
}
const parseIds = (val: any): string[] => {
if (Array.isArray(val)) return val;
if (typeof val === 'string') {
try { return JSON.parse(val); }
catch { return val.split(',').map((s: string) => s.trim()).filter(Boolean); }
}
updateData.verticals = {
set: parsedVerticalIds.map(vid => ({ id: vid }))
};
return [];
};
if (verticalIds !== undefined) {
updateData.verticals = { set: parseIds(verticalIds).map(vid => ({ id: vid })) };
}
if (techStackIds !== undefined) {
updateData.techStacks = { set: parseIds(techStackIds).map(tid => ({ id: tid })) };
}
if (engagementTypeIds !== undefined) {
updateData.engagementTypes = { set: parseIds(engagementTypeIds).map(eid => ({ id: eid })) };
}
if (complianceIds !== undefined) {
updateData.complianceStandards = { set: parseIds(complianceIds).map(cid => ({ id: cid })) };
}
await prisma.asset.update({

View File

@ -0,0 +1,108 @@
import prisma from './db';
export async function seedFourGroupTaxonomy() {
console.log('[Taxonomy Seeder] Starting 4-Group Taxonomy Seeding...');
// 1. Group 1: Industry Verticals (AI & ML removed to prevent double-counting)
const verticalsData = [
{ name: 'Cybersecurity', slug: 'cybersecurity', color: '#3b82f6', description: 'OT, Infrastructure & Data Defense' },
{ name: 'Healthcare & Pharma', slug: 'healthcare-pharma', color: '#ec4899', description: 'Medical, Diagnostics & BioTech' },
{ name: 'Finance & Banking', slug: 'finance-banking', color: '#10b981', description: 'FinTech, Payments & Risk Analytics' },
{ name: 'Insurance', slug: 'insurance', color: '#6366f1', description: 'InsurTech, Claims & Underwriting' },
{ name: 'Energy & Utilities', slug: 'energy-utilities', color: '#f59e0b', description: 'Smart Grid, Power & Renewables' },
{ name: 'Agriculture', slug: 'agriculture', color: '#84cc16', description: 'AgriTech, Precision Farming & Supply' },
{ name: 'Education', slug: 'education', color: '#8b5cf6', description: 'EdTech, LMS & Digital Classrooms' },
{ name: 'Manufacturing & IoT', slug: 'manufacturing-iot', color: '#06b6d4', description: 'Industry 4.0, Robotics & Sensors' },
{ name: 'Automotive', slug: 'automotive', color: '#ef4444', description: 'EV, Autonomous Systems & Fleet' },
{ name: 'Retail & Supply Chain', slug: 'retail-supply-chain', color: '#f97316', description: 'E-Commerce, Logistics & Inventory' },
{ name: 'Blockchain', slug: 'blockchain', color: '#14b8a6', description: 'Web3, Smart Contracts & Distributed Ledgers' },
];
for (const [idx, v] of verticalsData.entries()) {
await prisma.vertical.upsert({
where: { slug: v.slug },
update: { name: v.name, color: v.color, description: v.description, orderIndex: idx, isActive: true },
create: { name: v.name, slug: v.slug, color: v.color, description: v.description, orderIndex: idx, isActive: true },
});
}
// Deactivate old legacy "AI & ML" entry if present in Verticals table to prevent double counting
await prisma.vertical.updateMany({
where: { slug: 'ai-ml' },
data: { isActive: false }
}).catch(() => {});
// 2. Group 2: Technology Stack
const techStacksData = [
// Languages & Frameworks
{ name: 'Java / Spring Boot', slug: 'java-spring-boot', category: 'Languages & Frameworks', color: '#f97316' },
{ name: 'Node.js', slug: 'nodejs', category: 'Languages & Frameworks', color: '#22c55e' },
{ name: 'Python', slug: 'python', category: 'Languages & Frameworks', color: '#3b82f6' },
{ name: 'React', slug: 'react', category: 'Languages & Frameworks', color: '#06b6d4' },
{ name: 'Go', slug: 'go', category: 'Languages & Frameworks', color: '#00add8' },
{ name: '.NET', slug: 'dotnet', category: 'Languages & Frameworks', color: '#512bd4' },
// AI & ML
{ name: 'LLM / Agentic', slug: 'llm-agentic', category: 'AI & ML', color: '#8b5cf6' },
{ name: 'RAG Systems', slug: 'rag-systems', category: 'AI & ML', color: '#a855f7' },
{ name: 'Computer Vision', slug: 'computer-vision', category: 'AI & ML', color: '#d946ef' },
{ name: 'ML Pipelines', slug: 'ml-pipelines', category: 'AI & ML', color: '#ec4899' },
{ name: 'Deepfake / Detection', slug: 'deepfake-detection', category: 'AI & ML', color: '#f43f5e' },
// Data & Backend
{ name: 'PostgreSQL', slug: 'postgresql', category: 'Data & Backend', color: '#336791' },
{ name: 'Temporal', slug: 'temporal', category: 'Data & Backend', color: '#111827' },
{ name: 'Kafka', slug: 'kafka', category: 'Data & Backend', color: '#231f20' },
{ name: 'Event-driven', slug: 'event-driven', category: 'Data & Backend', color: '#eab308' },
{ name: 'Microservices', slug: 'microservices', category: 'Data & Backend', color: '#10b981' },
// Cloud & Infra
{ name: 'AWS', slug: 'aws', category: 'Cloud & Infra', color: '#ff9900' },
{ name: 'Sovereign / On-prem', slug: 'sovereign-onprem', category: 'Cloud & Infra', color: '#64748b' },
{ name: 'Kubernetes', slug: 'kubernetes', category: 'Cloud & Infra', color: '#326ce5' },
{ name: 'IaaS', slug: 'iaas', category: 'Cloud & Infra', color: '#0284c7' },
];
for (const [idx, t] of techStacksData.entries()) {
await prisma.techStack.upsert({
where: { slug: t.slug },
update: { name: t.name, category: t.category, color: t.color, orderIndex: idx, isActive: true },
create: { name: t.name, slug: t.slug, category: t.category, color: t.color, orderIndex: idx, isActive: true },
});
}
// 3. Group 3: Engagement Type
const engagementData = [
{ name: 'Build', slug: 'build', description: 'Greenfield architecture & end-to-end development', color: '#3b82f6' },
{ name: 'Rescue', slug: 'rescue', description: 'Turnaround, refactoring & distressed project recovery', color: '#ef4444' },
{ name: 'Scale', slug: 'scale', description: 'Performance tuning, cloud expansion & enterprise scaling', color: '#10b981' },
{ name: 'Due Diligence', slug: 'due-diligence', description: 'Tech audit, risk assessment & code quality review', color: '#f59e0b' },
];
for (const [idx, e] of engagementData.entries()) {
await prisma.engagementType.upsert({
where: { slug: e.slug },
update: { name: e.name, description: e.description, color: e.color, orderIndex: idx, isActive: true },
create: { name: e.name, slug: e.slug, description: e.description, color: e.color, orderIndex: idx, isActive: true },
});
}
// 4. Group 4: Compliance & Regulatory
const complianceData = [
{ name: 'HIPAA', slug: 'hipaa', description: 'Health Insurance Portability and Accountability Act', color: '#ec4899' },
{ name: 'GxP', slug: 'gxp', description: 'Good Practice regulations (FDA/EMEA Pharma)', color: '#8b5cf6' },
{ name: 'APRA CPS 230', slug: 'apra-cps-230', description: 'Operational Risk Management (Australian Prudential)', color: '#3b82f6' },
{ name: 'SOC 2', slug: 'soc-2', description: 'Security, Availability, and Confidentiality Trust Criteria', color: '#10b981' },
{ name: 'GDPR / Sovereign', slug: 'gdpr-sovereign', description: 'EU General Data Protection & Data Sovereignty', color: '#06b6d4' },
];
for (const [idx, c] of complianceData.entries()) {
await prisma.complianceStandard.upsert({
where: { slug: c.slug },
update: { name: c.name, description: c.description, color: c.color, orderIndex: idx, isActive: true },
create: { name: c.name, slug: c.slug, description: c.description, color: c.color, orderIndex: idx, isActive: true },
});
}
console.log('[Taxonomy Seeder] 4-Group Taxonomy Seeding completed successfully.');
}

View File

@ -1,7 +1,13 @@
import React, { useState } from 'react';
import { X, Shield, Plus, Trash2, Send, Tag, CheckCircle2, AlertCircle, Bell } from 'lucide-react';
import { X, Shield, Plus, Trash2, Tag, CheckCircle2, AlertCircle, Bell, Layers, Cpu, ShieldCheck, Send } from 'lucide-react';
import type { TaxonomyMeta, Organization, Asset } from '../../../types/assets';
import { createVertical, deleteVertical, sendAssetAnnouncement } from '../../../services/assets-api';
import {
createVertical, deleteVertical,
createTechStack, deleteTechStack,
createEngagementType, deleteEngagementType,
createComplianceStandard, deleteComplianceStandard,
sendAssetAnnouncement
} from '../../../services/assets-api';
interface AssetAdminManagerModalProps {
isOpen: boolean;
@ -20,18 +26,31 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
allAssets = [],
onRefreshMeta,
}) => {
const [activeTab, setActiveTab] = useState<'verticals' | 'taxonomy' | 'announcements'>('verticals');
const [activeTab, setActiveTab] = useState<'verticals' | 'techStacks' | 'engagements' | 'compliance' | 'taxonomy' | 'announcements'>('verticals');
const [loading, setLoading] = useState(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
// Inspect filter state for Taxonomy Stats interactive list
const [inspectFilter, setInspectFilter] = useState<{ type: 'Subcategory' | 'Vertical'; name: string; id?: string } | null>(null);
// New Vertical Form State
// Form State: Verticals
const [newVerticalName, setNewVerticalName] = useState('');
const [newVerticalColor, setNewVerticalColor] = useState('#3b82f6');
const [newVerticalDesc, setNewVerticalDesc] = useState('');
// Form State: Tech Stacks
const [newTechName, setNewTechName] = useState('');
const [newTechCategory, setNewTechCategory] = useState('Languages & Frameworks');
const [newTechColor, setNewTechColor] = useState('#64748b');
// Form State: Engagements
const [newEngagementName, setNewEngagementName] = useState('');
const [newEngagementColor, setNewEngagementColor] = useState('#0284c7');
// Form State: Compliance
const [newComplianceName, setNewComplianceName] = useState('');
const [newComplianceColor, setNewComplianceColor] = useState('#10b981');
// Announcement Form State
const [announcementTitle, setAnnouncementTitle] = useState('');
const [announcementMsg, setAnnouncementMsg] = useState('');
@ -50,6 +69,7 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
return false;
});
// Vertical CRUD
const handleCreateVertical = async (e: React.FormEvent) => {
e.preventDefault();
if (!newVerticalName.trim()) return;
@ -86,6 +106,109 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
}
};
// Tech Stack CRUD
const handleCreateTechStack = async (e: React.FormEvent) => {
e.preventDefault();
if (!newTechName.trim()) return;
setLoading(true);
try {
await createTechStack({
name: newTechName.trim(),
category: newTechCategory,
color: newTechColor,
});
setStatusMsg({ type: 'success', text: `Tech Stack item "${newTechName}" created successfully!` });
setNewTechName('');
onRefreshMeta();
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.response?.data?.error || 'Failed to create tech stack' });
} finally {
setLoading(false);
}
};
const handleDeleteTechStack = async (id: string, name: string) => {
if (!window.confirm(`Are you sure you want to delete tech stack item "${name}"?`)) return;
setLoading(true);
try {
await deleteTechStack(id);
setStatusMsg({ type: 'success', text: `Tech Stack item "${name}" deleted` });
onRefreshMeta();
} catch (err: any) {
setStatusMsg({ type: 'error', text: 'Failed to delete tech stack item' });
} finally {
setLoading(false);
}
};
// Engagement Type CRUD
const handleCreateEngagement = async (e: React.FormEvent) => {
e.preventDefault();
if (!newEngagementName.trim()) return;
setLoading(true);
try {
await createEngagementType({
name: newEngagementName.trim(),
color: newEngagementColor,
});
setStatusMsg({ type: 'success', text: `Engagement Type "${newEngagementName}" created!` });
setNewEngagementName('');
onRefreshMeta();
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.response?.data?.error || 'Failed to create engagement type' });
} finally {
setLoading(false);
}
};
const handleDeleteEngagement = async (id: string, name: string) => {
if (!window.confirm(`Are you sure you want to delete engagement type "${name}"?`)) return;
setLoading(true);
try {
await deleteEngagementType(id);
setStatusMsg({ type: 'success', text: `Engagement Type "${name}" deleted` });
onRefreshMeta();
} catch (err: any) {
setStatusMsg({ type: 'error', text: 'Failed to delete engagement type' });
} finally {
setLoading(false);
}
};
// Compliance Standard CRUD
const handleCreateCompliance = async (e: React.FormEvent) => {
e.preventDefault();
if (!newComplianceName.trim()) return;
setLoading(true);
try {
await createComplianceStandard({
name: newComplianceName.trim(),
color: newComplianceColor,
});
setStatusMsg({ type: 'success', text: `Compliance Standard "${newComplianceName}" created!` });
setNewComplianceName('');
onRefreshMeta();
} catch (err: any) {
setStatusMsg({ type: 'error', text: err.response?.data?.error || 'Failed to create compliance standard' });
} finally {
setLoading(false);
}
};
const handleDeleteCompliance = async (id: string, name: string) => {
if (!window.confirm(`Are you sure you want to delete compliance standard "${name}"?`)) return;
setLoading(true);
try {
await deleteComplianceStandard(id);
setStatusMsg({ type: 'success', text: `Compliance Standard "${name}" deleted` });
onRefreshMeta();
} catch (err: any) {
setStatusMsg({ type: 'error', text: 'Failed to delete compliance standard' });
} finally {
setLoading(false);
}
};
const handleSendAnnouncement = async (e: React.FormEvent) => {
e.preventDefault();
if (!announcementTitle.trim() || !announcementMsg.trim()) return;
@ -108,7 +231,7 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-2xl w-full max-w-3xl overflow-hidden flex flex-col max-h-[85vh]">
<div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-2xl w-full max-w-4xl overflow-hidden flex flex-col max-h-[85vh]">
{/* Modal Header */}
<div className="px-6 py-5 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between bg-slate-50/50 dark:bg-slate-900/50">
@ -118,10 +241,10 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
</div>
<div>
<h2 className="text-lg font-bold text-slate-900 dark:text-slate-100">
Taxonomy & Partner Announcements Control Panel
Taxonomy & Announcements Control Panel
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400">
Manage industry verticals, taxonomy metadata, and partner communications
Manage 4-Group Taxonomy items, metadata stats, and partner notifications
</p>
</div>
</div>
@ -134,39 +257,77 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
</div>
{/* Tab Navigation */}
<div className="flex border-b border-slate-200 dark:border-slate-800 bg-slate-100/50 dark:bg-slate-800/50 px-6">
<div className="flex items-center overflow-x-auto whitespace-nowrap custom-scrollbar border-b border-slate-200 dark:border-slate-800 bg-slate-100/50 dark:bg-slate-800/50 px-4 scroll-smooth shrink-0">
<button
onClick={() => { setActiveTab('verticals'); setStatusMsg(null); }}
className={`px-4 py-3 text-xs font-bold border-b-2 flex items-center gap-2 transition-colors ${
className={`px-3 py-3 text-xs font-bold border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
activeTab === 'verticals'
? 'border-slate-900 text-slate-900 dark:border-slate-100 dark:text-slate-100'
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
>
<Shield className="w-4 h-4" />
Industry Verticals ({meta?.verticals.length || 0})
<Shield className="w-3.5 h-3.5" />
1. Verticals ({meta?.verticals.length || 0})
</button>
<button
onClick={() => { setActiveTab('techStacks'); setStatusMsg(null); }}
className={`px-3 py-3 text-xs font-bold border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
activeTab === 'techStacks'
? 'border-purple-600 text-purple-600 dark:border-purple-400 dark:text-purple-400'
: 'border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
>
<Cpu className="w-3.5 h-3.5" />
2. Tech Stack ({meta?.techStacks?.length || 0})
</button>
<button
onClick={() => { setActiveTab('engagements'); setStatusMsg(null); }}
className={`px-3 py-3 text-xs font-bold border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
activeTab === 'engagements'
? 'border-sky-600 text-sky-600 dark:border-sky-400 dark:text-sky-400'
: 'border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
>
<Layers className="w-3.5 h-3.5" />
3. Engagements ({meta?.engagementTypes?.length || 0})
</button>
<button
onClick={() => { setActiveTab('compliance'); setStatusMsg(null); }}
className={`px-3 py-3 text-xs font-bold border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
activeTab === 'compliance'
? 'border-emerald-600 text-emerald-600 dark:border-emerald-400 dark:text-emerald-400'
: 'border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
>
<ShieldCheck className="w-3.5 h-3.5" />
4. Compliance ({meta?.complianceStandards?.length || 0})
</button>
<button
onClick={() => { setActiveTab('taxonomy'); setStatusMsg(null); }}
className={`px-4 py-3 text-xs font-bold border-b-2 flex items-center gap-2 transition-colors ${
className={`px-3 py-3 text-xs font-bold border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
activeTab === 'taxonomy'
? 'border-slate-900 text-slate-900 dark:border-slate-100 dark:text-slate-100'
: 'border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
>
<Tag className="w-4 h-4" />
Taxonomy Stats ({meta?.totalAssets || 0} Assets)
<Tag className="w-3.5 h-3.5" />
Stats ({meta?.totalAssets || 0})
</button>
<button
onClick={() => { setActiveTab('announcements'); setStatusMsg(null); }}
className={`px-4 py-3 text-xs font-bold border-b-2 flex items-center gap-2 transition-colors ${
className={`px-3 py-3 text-xs font-bold border-b-2 flex items-center gap-1.5 transition-colors shrink-0 ${
activeTab === 'announcements'
? 'border-slate-900 text-slate-900 dark:border-slate-100 dark:text-slate-100'
? 'border-amber-500 text-amber-500 dark:border-amber-400 dark:text-amber-400'
: 'border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
>
<Bell className="w-4 h-4" />
Partner Announcements
<Bell className="w-3.5 h-3.5" />
Announcements
</button>
</div>
@ -284,72 +445,303 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
</div>
)}
{/* TAB 2: Taxonomy Stats */}
{activeTab === 'taxonomy' && (
{/* TAB 2: Tech Stacks */}
{activeTab === 'techStacks' && (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-2xl font-bold text-slate-900 dark:text-slate-100">{meta?.totalAssets || 0}</div>
<div className="text-xs text-slate-500 font-medium">Total Catalog Assets</div>
</div>
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-2xl font-bold text-emerald-600">{meta?.verticals.length || 0}</div>
<div className="text-xs text-slate-500 font-medium font-mono">Industry Verticals</div>
</div>
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-2xl font-bold text-amber-500">{meta?.subcategories.length || 0}</div>
<div className="text-xs text-slate-500 font-medium">Subcategories</div>
</div>
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-2xl font-bold text-cyan-600">{meta?.tags.length || 0}</div>
<div className="text-xs text-slate-500 font-medium">Unique Tags</div>
</div>
</div>
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-3">
Subcategory Breakdown (Click to Inspect Records)
</h4>
<div className="flex flex-wrap gap-2">
{(meta?.subcategories || []).map(s => (
<button
key={s.name}
onClick={() => setInspectFilter({ type: 'Subcategory', name: s.name })}
className={`px-3 py-1.5 rounded-lg border text-xs font-semibold transition-all cursor-pointer flex items-center gap-1.5 ${
inspectFilter?.name === s.name
? 'bg-slate-900 text-white border-slate-900 dark:bg-slate-100 dark:text-slate-900'
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 hover:border-slate-400'
}`}
{/* Create Tech Stack Form */}
<form onSubmit={handleCreateTechStack} className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 space-y-4">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300 flex items-center gap-2">
<Plus className="w-4 h-4 text-slate-700 dark:text-slate-300" />
Add New Tech Stack / Capability Item
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Tech Item Name
</label>
<input
type="text"
placeholder="e.g. Rust, PyTorch, GraphQL"
value={newTechName}
onChange={e => setNewTechName(e.target.value)}
className="w-full px-3 py-2 text-xs rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Tech Category / Group
</label>
<select
value={newTechCategory}
onChange={e => setNewTechCategory(e.target.value)}
className="w-full px-3 py-2 text-xs rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
>
<span>{s.name}</span>
<span className="font-extrabold px-1.5 py-0.2 bg-slate-100 dark:bg-slate-700 rounded text-[10px]">
{s.count}
</span>
</button>
<option value="Languages & Frameworks">Languages & Frameworks</option>
<option value="AI & ML">AI & ML</option>
<option value="Data & Backend">Data & Backend</option>
<option value="Cloud & Infra">Cloud & Infra</option>
</select>
</div>
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Color
</label>
<input
type="color"
value={newTechColor}
onChange={e => setNewTechColor(e.target.value)}
className="w-full h-9 p-0.5 rounded border border-slate-200 dark:border-slate-700 cursor-pointer"
/>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={loading || !newTechName.trim()}
className="px-4 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 dark:bg-slate-100 dark:hover:bg-white text-white dark:text-slate-900 text-xs font-bold disabled:opacity-50 transition-all flex items-center gap-1.5"
>
<Plus className="w-4 h-4" />
Create Tech Item
</button>
</div>
</form>
{/* Tech Stack List */}
<div className="space-y-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400">
Active Tech Stack Items ({meta?.techStacks?.length || 0})
</h3>
<div className="divide-y divide-slate-100 dark:divide-slate-800 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden">
{(meta?.techStacks || []).map(t => (
<div key={t.id} className="p-3 bg-white dark:bg-slate-900 flex items-center justify-between text-xs">
<div className="flex items-center gap-3">
<span className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: t.color || '#64748b' }} />
<div>
<div className="font-semibold text-slate-900 dark:text-slate-100 flex items-center gap-2">
{t.name}
<span className="text-[10px] font-extrabold px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-500">
{t.category}
</span>
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500">
{t._count?.assets ?? 0} assets
</span>
</div>
</div>
</div>
<button
onClick={() => handleDeleteTechStack(t.id, t.name)}
className="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded-lg transition-colors"
title="Delete Tech Item"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
</div>
)}
{/* TAB 3: Engagement Types */}
{activeTab === 'engagements' && (
<div className="space-y-6">
{/* Create Engagement Form */}
<form onSubmit={handleCreateEngagement} className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 space-y-4">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300 flex items-center gap-2">
<Plus className="w-4 h-4 text-slate-700 dark:text-slate-300" />
Add New Engagement Type
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Engagement Name
</label>
<input
type="text"
placeholder="e.g. Audit & Advisory, Advisory"
value={newEngagementName}
onChange={e => setNewEngagementName(e.target.value)}
className="w-full px-3 py-2 text-xs rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Badge Color
</label>
<input
type="color"
value={newEngagementColor}
onChange={e => setNewEngagementColor(e.target.value)}
className="w-full h-9 p-0.5 rounded border border-slate-200 dark:border-slate-700 cursor-pointer"
/>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={loading || !newEngagementName.trim()}
className="px-4 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 dark:bg-slate-100 dark:hover:bg-white text-white dark:text-slate-900 text-xs font-bold disabled:opacity-50 transition-all flex items-center gap-1.5"
>
<Plus className="w-4 h-4" />
Create Engagement Type
</button>
</div>
</form>
{/* Engagements List */}
<div className="space-y-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400">
Active Engagement Types ({meta?.engagementTypes?.length || 0})
</h3>
<div className="divide-y divide-slate-100 dark:divide-slate-800 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden">
{(meta?.engagementTypes || []).map(e => (
<div key={e.id} className="p-3 bg-white dark:bg-slate-900 flex items-center justify-between text-xs">
<div className="flex items-center gap-3">
<span className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: e.color || '#0284c7' }} />
<span className="font-semibold text-slate-900 dark:text-slate-100">{e.name}</span>
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500">
{e._count?.assets ?? 0} assets
</span>
</div>
<button
onClick={() => handleDeleteEngagement(e.id, e.name)}
className="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded-lg transition-colors"
title="Delete Engagement Type"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
</div>
)}
{/* TAB 4: Compliance Standards */}
{activeTab === 'compliance' && (
<div className="space-y-6">
{/* Create Compliance Form */}
<form onSubmit={handleCreateCompliance} className="p-4 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 space-y-4">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300 flex items-center gap-2">
<Plus className="w-4 h-4 text-slate-700 dark:text-slate-300" />
Add New Compliance Standard / Regulatory Certification
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Compliance Name
</label>
<input
type="text"
placeholder="e.g. ISO 27001, PCI-DSS, FedRAMP"
value={newComplianceName}
onChange={e => setNewComplianceName(e.target.value)}
className="w-full px-3 py-2 text-xs rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1">
Badge Color
</label>
<input
type="color"
value={newComplianceColor}
onChange={e => setNewComplianceColor(e.target.value)}
className="w-full h-9 p-0.5 rounded border border-slate-200 dark:border-slate-700 cursor-pointer"
/>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={loading || !newComplianceName.trim()}
className="px-4 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 dark:bg-slate-100 dark:hover:bg-white text-white dark:text-slate-900 text-xs font-bold disabled:opacity-50 transition-all flex items-center gap-1.5"
>
<Plus className="w-4 h-4" />
Create Compliance Standard
</button>
</div>
</form>
{/* Compliance List */}
<div className="space-y-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400">
Active Compliance Standards ({meta?.complianceStandards?.length || 0})
</h3>
<div className="divide-y divide-slate-100 dark:divide-slate-800 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden">
{(meta?.complianceStandards || []).map(c => (
<div key={c.id} className="p-3 bg-white dark:bg-slate-900 flex items-center justify-between text-xs">
<div className="flex items-center gap-3">
<span className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: c.color || '#10b981' }} />
<span className="font-semibold text-slate-900 dark:text-slate-100 flex items-center gap-1.5">
<Shield className="w-3.5 h-3.5 text-emerald-500" />
{c.name}
</span>
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500">
{c._count?.assets ?? 0} assets
</span>
</div>
<button
onClick={() => handleDeleteCompliance(c.id, c.name)}
className="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/50 rounded-lg transition-colors"
title="Delete Compliance Standard"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
</div>
)}
{/* TAB 2: Taxonomy Stats */}
{activeTab === 'taxonomy' && (
<div className="space-y-6">
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2.5">
<div className="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-xl font-bold text-slate-900 dark:text-slate-100">{meta?.totalAssets || 0}</div>
<div className="text-[11px] text-slate-500 font-medium">Total Assets</div>
</div>
<div className="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-xl font-bold text-blue-600">{meta?.verticals.length || 0}</div>
<div className="text-[11px] text-slate-500 font-medium">1. Verticals</div>
</div>
<div className="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-xl font-bold text-purple-600">{meta?.techStacks?.length || 0}</div>
<div className="text-[11px] text-slate-500 font-medium">2. Tech Stacks</div>
</div>
<div className="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-xl font-bold text-sky-600">{meta?.engagementTypes?.length || 0}</div>
<div className="text-[11px] text-slate-500 font-medium">3. Engagements</div>
</div>
<div className="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800">
<div className="text-xl font-bold text-emerald-600">{meta?.complianceStandards?.length || 0}</div>
<div className="text-[11px] text-slate-500 font-medium">4. Compliance</div>
</div>
</div>
{/* 1. Industry Verticals Breakdown */}
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-3">
Vertical Domain Distribution
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2.5">
1. Industry Verticals Domain Distribution
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{(meta?.verticals || []).map(v => (
<button
key={v.id}
onClick={() => setInspectFilter({ type: 'Vertical', name: v.name, id: v.id })}
className={`p-3 rounded-xl border text-left transition-all cursor-pointer flex items-center justify-between ${
className={`p-2.5 rounded-xl border text-left transition-all cursor-pointer flex items-center justify-between ${
inspectFilter?.name === v.name
? 'bg-slate-900 text-white border-slate-900 dark:bg-slate-100 dark:text-slate-900'
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-200 hover:border-slate-400'
}`}
>
<div className="flex items-center gap-2.5">
<span className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: v.color || '#3b82f6' }} />
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: v.color || '#3b82f6' }} />
<span className="font-bold text-xs">{v.name}</span>
</div>
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-700">
<span className="text-[10px] font-mono font-bold px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-700">
{v._count?.assets ?? 0} assets
</span>
</button>
@ -357,6 +749,59 @@ export const AssetAdminManagerModal: React.FC<AssetAdminManagerModalProps> = ({
</div>
</div>
{/* 2. Tech Stack Breakdown */}
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2.5">
2. Technology Stack & Capabilities
</h4>
<div className="flex flex-wrap gap-1.5">
{(meta?.techStacks || []).map(t => (
<div
key={t.id}
className="px-2.5 py-1 rounded-lg border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-xs font-medium flex items-center gap-1.5"
>
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: t.color || '#64748b' }} />
<span className="font-semibold">{t.name}</span>
<span className="text-[10px] font-mono opacity-70">({t._count?.assets ?? 0})</span>
</div>
))}
</div>
</div>
{/* 3 & 4. Engagement & Compliance Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">
3. Engagement Types
</h4>
<div className="space-y-1.5">
{(meta?.engagementTypes || []).map(e => (
<div key={e.id} className="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 flex items-center justify-between text-xs font-semibold">
<span>{e.name}</span>
<span className="font-mono text-[10px] opacity-70">({e._count?.assets ?? 0} assets)</span>
</div>
))}
</div>
</div>
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">
4. Compliance & Regulatory
</h4>
<div className="space-y-1.5">
{(meta?.complianceStandards || []).map(c => (
<div key={c.id} className="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 flex items-center justify-between text-xs font-semibold">
<span className="flex items-center gap-1.5">
<Shield className="w-3.5 h-3.5 text-emerald-500" />
{c.name}
</span>
<span className="font-mono text-[10px] opacity-70">({c._count?.assets ?? 0} assets)</span>
</div>
))}
</div>
</div>
</div>
{/* Inspect Filter Asset List Details */}
{inspectFilter && (
<div className="p-4 rounded-xl bg-slate-100 dark:bg-slate-800/80 border border-slate-300 dark:border-slate-700 space-y-3">

View File

@ -14,7 +14,8 @@ import {
Clock,
AlertCircle,
Globe,
Sparkles
Sparkles,
Shield
} from 'lucide-react';
import type { Asset } from '../../../types/assets';
import type { User } from '../../../types/auth';
@ -572,19 +573,54 @@ export const AssetCard: React.FC<AssetCardProps> = ({
<div className="space-y-1 mb-4 flex-grow flex flex-col">
<div className="flex flex-wrap items-center gap-1.5 mb-2">
{asset.verticals && asset.verticals.length > 0 && (
{/* Group 1: Industry Verticals */}
{asset.verticals && asset.verticals.map(v => (
<span
key={v.id}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold tracking-wider border shrink-0"
style={{
backgroundColor: `${asset.verticals[0].color || '#6366f1'}15`,
borderColor: `${asset.verticals[0].color || '#6366f1'}40`,
color: asset.verticals[0].color || '#6366f1',
backgroundColor: `${v.color || '#3b82f6'}15`,
borderColor: `${v.color || '#3b82f6'}40`,
color: v.color || '#3b82f6',
}}
>
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: asset.verticals[0].color || '#6366f1' }} />
{asset.verticals[0].name}
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: v.color || '#3b82f6' }} />
{v.name}
</span>
)}
))}
{/* Group 2: Tech Stack */}
{asset.techStacks && asset.techStacks.map(t => (
<span
key={t.id}
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold text-slate-700 dark:text-slate-300 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shrink-0"
>
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: t.color || '#64748b' }} />
{t.name}
</span>
))}
{/* Group 3: Engagement Type */}
{asset.engagementTypes && asset.engagementTypes.map(e => (
<span
key={e.id}
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-sky-700 dark:text-sky-300 bg-sky-50 dark:bg-sky-950/60 border border-sky-200 dark:border-sky-800 shrink-0"
>
{e.name}
</span>
))}
{/* Group 4: Compliance Standards */}
{asset.complianceStandards && asset.complianceStandards.map(c => (
<span
key={c.id}
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/60 border border-emerald-200 dark:border-emerald-800 shrink-0"
>
<Shield className="w-2.5 h-2.5 text-emerald-500" />
{c.name}
</span>
))}
<div className="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-500 uppercase tracking-wider bg-ink-50 border border-ink-200">
{asset.subcategory || asset.categoryId || 'General'}
</div>

View File

@ -50,7 +50,6 @@ export const AssetTableView: React.FC<AssetTableViewProps> = ({
{assets.map((asset) => {
const isSelected = selectedIds.includes(asset.id);
const isRecommended = recommendedIds.includes(asset.id);
const primaryVertical = asset.verticals && asset.verticals.length > 0 ? asset.verticals[0] : null;
return (
<tr
@ -117,23 +116,58 @@ export const AssetTableView: React.FC<AssetTableViewProps> = ({
</div>
</td>
{/* Vertical */}
{/* Taxonomy Classification */}
<td className="py-3 px-4">
{primaryVertical ? (
<span
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-semibold border"
style={{
backgroundColor: `${primaryVertical.color}15`,
borderColor: `${primaryVertical.color}40`,
color: primaryVertical.color || '#94a3b8',
}}
>
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: primaryVertical.color || '#94a3b8' }} />
{primaryVertical.name}
</span>
) : (
<span className="text-slate-400 italic text-[11px]">General</span>
)}
<div className="flex flex-wrap gap-1 max-w-xs">
{asset.verticals && asset.verticals.map(v => (
<span
key={v.id}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold border"
style={{
backgroundColor: `${v.color || '#3b82f6'}15`,
borderColor: `${v.color || '#3b82f6'}40`,
color: v.color || '#3b82f6',
}}
>
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: v.color || '#3b82f6' }} />
{v.name}
</span>
))}
{asset.techStacks && asset.techStacks.map(t => (
<span
key={t.id}
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-medium text-slate-700 dark:text-slate-300 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700"
>
{t.name}
</span>
))}
{asset.engagementTypes && asset.engagementTypes.map(e => (
<span
key={e.id}
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold text-sky-700 dark:text-sky-300 bg-sky-50 dark:bg-sky-950/60 border border-sky-200 dark:border-sky-800"
>
{e.name}
</span>
))}
{asset.complianceStandards && asset.complianceStandards.map(c => (
<span
key={c.id}
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/60 border border-emerald-200 dark:border-emerald-800"
>
{c.name}
</span>
))}
{(!asset.verticals || asset.verticals.length === 0) &&
(!asset.techStacks || asset.techStacks.length === 0) &&
(!asset.engagementTypes || asset.engagementTypes.length === 0) &&
(!asset.complianceStandards || asset.complianceStandards.length === 0) && (
<span className="text-slate-400 italic text-[11px]">General</span>
)}
</div>
</td>
{/* Type */}

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { updateAsset, getVerticals } from '../../../services/assets-api';
import type { Asset, Vertical } from '../../../types/assets';
import { updateAsset, getTaxonomyMeta } from '../../../services/assets-api';
import type { Asset, TaxonomyMeta } from '../../../types/assets';
import Modal from '../../../components/ui/Modal';
import Button from '../../../components/ui/Button';
import { useToast } from '../../../hooks/use-toast';
@ -19,8 +19,13 @@ export const EditAssetModal: React.FC<EditAssetModalProps> = ({
onSuccess
}) => {
const { success, error } = useToast();
const [verticals, setVerticals] = useState<Vertical[]>([]);
const [editVerticalId, setEditVerticalId] = useState<string>('');
const [taxonomyMeta, setTaxonomyMeta] = useState<TaxonomyMeta | null>(null);
const [editVerticalIds, setEditVerticalIds] = useState<string[]>([]);
const [editTechStackIds, setEditTechStackIds] = useState<string[]>([]);
const [editEngagementTypeIds, setEditEngagementTypeIds] = useState<string[]>([]);
const [editComplianceIds, setEditComplianceIds] = useState<string[]>([]);
const [editTitle, setEditTitle] = useState('');
const [editDescription, setEditDescription] = useState('');
const [editCategory, setEditCategory] = useState('Marketing');
@ -31,7 +36,7 @@ export const EditAssetModal: React.FC<EditAssetModalProps> = ({
const [isSavingEdit, setIsSavingEdit] = useState(false);
useEffect(() => {
getVerticals().then(setVerticals).catch(console.error);
getTaxonomyMeta().then(setTaxonomyMeta).catch(console.error);
}, []);
useEffect(() => {
@ -43,10 +48,18 @@ export const EditAssetModal: React.FC<EditAssetModalProps> = ({
setEditTags(asset.tags.join(', '));
setEditGithubUrl(asset.githubUrl || '');
setEditIsDownloadable(asset.isDownloadable);
setEditVerticalId(asset.verticals && asset.verticals.length > 0 ? asset.verticals[0].id : '');
setEditVerticalIds(asset.verticals ? asset.verticals.map(v => v.id) : []);
setEditTechStackIds(asset.techStacks ? asset.techStacks.map(t => t.id) : []);
setEditEngagementTypeIds(asset.engagementTypes ? asset.engagementTypes.map(e => e.id) : []);
setEditComplianceIds(asset.complianceStandards ? asset.complianceStandards.map(c => c.id) : []);
}
}, [asset]);
const toggleSelection = (list: string[], item: string) => {
return list.includes(item) ? list.filter(i => i !== item) : [...list, item];
};
const handleEditSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!asset) return;
@ -58,7 +71,10 @@ export const EditAssetModal: React.FC<EditAssetModalProps> = ({
description: editDescription,
categoryId: editCategory,
subcategory: editSubcategory,
verticalIds: editVerticalId ? [editVerticalId] : [],
verticalIds: editVerticalIds,
techStackIds: editTechStackIds,
engagementTypeIds: editEngagementTypeIds,
complianceIds: editComplianceIds,
tags: editTags.split(',').map(t => t.trim()).filter(Boolean),
githubUrl: editGithubUrl,
isDownloadable: editIsDownloadable,
@ -115,43 +131,130 @@ export const EditAssetModal: React.FC<EditAssetModalProps> = ({
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 4-Group Taxonomy Demarcation Selection */}
<div className="space-y-4 pt-3 pb-3 border-t border-b border-slate-200 dark:border-slate-800">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Taxonomy Classification (Strict Admin-Managed)
</h4>
{/* Group 1: Industry Verticals */}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Industry Vertical</label>
<select
value={editVerticalId}
onChange={(e) => setEditVerticalId(e.target.value)}
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900"
>
<option value="">General (No Domain)</option>
{verticals.map(v => (
<option key={v.id} value={v.id}>{v.name}</option>
))}
</select>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
1. Industry Verticals / Domains
</label>
<div className="flex flex-wrap gap-1.5 max-h-32 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{(taxonomyMeta?.verticals || []).map(v => {
const isSelected = editVerticalIds.includes(v.id);
return (
<button
key={v.id}
type="button"
onClick={() => setEditVerticalIds(toggleSelection(editVerticalIds, v.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-blue-600 text-white font-bold border-blue-600 shadow-sm ring-2 ring-blue-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{v.name}
</button>
);
})}
</div>
</div>
{/* Group 2: Tech Stack */}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Category</label>
<select
value={editCategory}
onChange={(e) => setEditCategory(e.target.value)}
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900"
>
<option value="Marketing">Marketing</option>
<option value="Presentations">Presentations</option>
<option value="Branding">Branding</option>
<option value="Resources">Resources</option>
<option value="Technical">Technical</option>
</select>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
2. Technology Stack & Capabilities
</label>
<div className="flex flex-col gap-2 max-h-40 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{['Languages & Frameworks', 'AI & ML', 'Data & Backend', 'Cloud & Infra'].map(catName => {
const items = (taxonomyMeta?.techStacks || []).filter(t => t.category === catName || (!t.category && catName === 'Languages & Frameworks'));
if (items.length === 0) return null;
return (
<div key={catName} className="space-y-1">
<div className="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
{catName}
</div>
<div className="flex flex-wrap gap-1.5">
{items.map(t => {
const isSelected = editTechStackIds.includes(t.id);
return (
<button
key={t.id}
type="button"
onClick={() => setEditTechStackIds(toggleSelection(editTechStackIds, t.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-purple-600 text-white font-bold border-purple-600 shadow-sm ring-2 ring-purple-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{t.name}
</button>
);
})}
</div>
</div>
);
})}
</div>
</div>
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Subcategory</label>
<input
type="text"
value={editSubcategory}
onChange={(e) => setEditSubcategory(e.target.value)}
placeholder="e.g. Slide Deck"
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400"
/>
{/* Group 3 & Group 4 Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Group 3: Engagement Type */}
<div>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
3. Engagement Type
</label>
<div className="flex flex-wrap gap-1.5 p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{(taxonomyMeta?.engagementTypes || []).map(e => {
const isSelected = editEngagementTypeIds.includes(e.id);
return (
<button
key={e.id}
type="button"
onClick={() => setEditEngagementTypeIds(toggleSelection(editEngagementTypeIds, e.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-sky-600 text-white font-bold border-sky-600 shadow-sm ring-2 ring-sky-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{e.name}
</button>
);
})}
</div>
</div>
{/* Group 4: Compliance Standards */}
<div>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
4. Compliance & Governance
</label>
<div className="flex flex-wrap gap-1.5 p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{(taxonomyMeta?.complianceStandards || []).map(c => {
const isSelected = editComplianceIds.includes(c.id);
return (
<button
key={c.id}
type="button"
onClick={() => setEditComplianceIds(toggleSelection(editComplianceIds, c.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-emerald-600 text-white font-bold border-emerald-600 shadow-sm ring-2 ring-emerald-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{c.name}
</button>
);
})}
</div>
</div>
</div>
</div>

View File

@ -129,10 +129,10 @@ export const FilterDrawer: React.FC<FilterDrawerProps> = ({
{/* Body Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-7 custom-scrollbar">
{/* 1. Industry Verticals */}
{/* 1. Industry Verticals (Group 1) */}
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3 flex items-center justify-between">
<span>Industry Verticals</span>
<span>1. Industry Verticals</span>
<span className="text-[10px] lowercase font-normal text-slate-400">
({filteredVerticals.length})
</span>
@ -189,7 +189,130 @@ export const FilterDrawer: React.FC<FilterDrawerProps> = ({
</div>
</div>
{/* 2. Content Types */}
{/* 2. Technology Stack (Group 2) */}
{(meta?.techStacks || []).length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3 flex items-center justify-between">
<span>2. Technology Stack</span>
<span className="text-[10px] lowercase font-normal text-slate-400">
({(meta?.techStacks || []).length})
</span>
</h3>
<div className="space-y-3">
{['Languages & Frameworks', 'AI & ML', 'Data & Backend', 'Cloud & Infra'].map(cat => {
const groupItems = (meta?.techStacks || []).filter(t => t.category === cat);
if (groupItems.length === 0) return null;
return (
<div key={cat} className="space-y-1">
<div className="text-[11px] font-semibold text-slate-500 dark:text-slate-400 px-1">
{cat}
</div>
<div className="flex flex-wrap gap-1.5">
{groupItems.map(tech => {
const isSelected = filters.techStackIds?.includes(tech.id);
return (
<button
key={tech.id}
onClick={() =>
onChangeFilters({
...filters,
techStackIds: toggleArrayItem(filters.techStackIds, tech.id),
})
}
className={`px-2.5 py-1 rounded-lg text-xs font-medium border transition-all flex items-center gap-1.5 ${
isSelected
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900 border-slate-900 dark:border-slate-100 font-bold'
: 'bg-white dark:bg-slate-800/80 text-slate-700 dark:text-slate-200 border-slate-200 dark:border-slate-700 hover:border-slate-400'
}`}
>
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: tech.color || '#64748b' }} />
{tech.name}
<span className="text-[10px] opacity-70">({tech._count?.assets ?? 0})</span>
</button>
);
})}
</div>
</div>
);
})}
</div>
</div>
)}
{/* 3. Engagement Type (Group 3) */}
{(meta?.engagementTypes || []).length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3 flex items-center justify-between">
<span>3. Engagement Type</span>
</h3>
<div className="grid grid-cols-2 gap-2">
{(meta?.engagementTypes || []).map(eng => {
const isSelected = filters.engagementTypeIds?.includes(eng.id);
return (
<button
key={eng.id}
onClick={() =>
onChangeFilters({
...filters,
engagementTypeIds: toggleArrayItem(filters.engagementTypeIds, eng.id),
})
}
className={`p-2.5 rounded-lg border text-left text-xs transition-all ${
isSelected
? 'bg-slate-900 text-white border-slate-900 shadow-sm font-bold dark:bg-slate-100 dark:text-slate-900'
: 'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 border-slate-200 dark:border-slate-700 hover:border-slate-400'
}`}
>
<div className="font-bold flex items-center justify-between">
<span>{eng.name}</span>
<span className="text-[10px] opacity-70 font-mono">({eng._count?.assets ?? 0})</span>
</div>
{eng.description && (
<div className="text-[10px] opacity-75 mt-0.5 line-clamp-1">{eng.description}</div>
)}
</button>
);
})}
</div>
</div>
)}
{/* 4. Compliance & Regulatory (Group 4) */}
{(meta?.complianceStandards || []).length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3 flex items-center justify-between">
<span>4. Compliance & Governance</span>
</h3>
<div className="flex flex-wrap gap-1.5">
{(meta?.complianceStandards || []).map(comp => {
const isSelected = filters.complianceIds?.includes(comp.id);
return (
<button
key={comp.id}
onClick={() =>
onChangeFilters({
...filters,
complianceIds: toggleArrayItem(filters.complianceIds, comp.id),
})
}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all flex items-center gap-2 ${
isSelected
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900 border-slate-900 dark:border-slate-100 shadow-sm'
: 'bg-slate-50 dark:bg-slate-800/60 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-700 hover:bg-slate-100'
}`}
>
<Shield className="w-3.5 h-3.5 text-emerald-500" />
{comp.name}
<span className="text-[10px] opacity-70 font-mono">({comp._count?.assets ?? 0})</span>
</button>
);
})}
</div>
</div>
)}
{/* 5. Content Types */}
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3">
Content Types
@ -237,7 +360,7 @@ export const FilterDrawer: React.FC<FilterDrawerProps> = ({
</div>
</div>
{/* 3. Subcategories */}
{/* 6. Subcategories */}
{filteredSubcategories.length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3">
@ -269,7 +392,7 @@ export const FilterDrawer: React.FC<FilterDrawerProps> = ({
</div>
)}
{/* 4. Tag Cloud */}
{/* 7. Tag Cloud */}
{filteredTags.length > 0 && (
<div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-3">

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { X, UploadCloud, Eye, FileText, File } from 'lucide-react';
import { uploadAsset, scrapeCaseStudy, getVerticals } from '../../../services/assets-api';
import type { Vertical } from '../../../types/assets';
import { uploadAsset, scrapeCaseStudy, getTaxonomyMeta } from '../../../services/assets-api';
import type { TaxonomyMeta } from '../../../types/assets';
import Modal from '../../../components/ui/Modal';
import Button from '../../../components/ui/Button';
import { useToast } from '../../../hooks/use-toast';
@ -18,8 +18,12 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
onSuccess
}) => {
const { success, error } = useToast();
const [verticals, setVerticals] = useState<Vertical[]>([]);
const [selectedVerticalId, setSelectedVerticalId] = useState<string>('');
const [taxonomyMeta, setTaxonomyMeta] = useState<TaxonomyMeta | null>(null);
const [selectedVerticalIds, setSelectedVerticalIds] = useState<string[]>([]);
const [selectedTechStackIds, setSelectedTechStackIds] = useState<string[]>([]);
const [selectedEngagementTypeIds, setSelectedEngagementTypeIds] = useState<string[]>([]);
const [selectedComplianceIds, setSelectedComplianceIds] = useState<string[]>([]);
const [uploadTab, setUploadTab] = useState<'file' | 'url' | 'case_study'>('file');
const [uploadFile, setUploadFile] = useState<File | null>(null);
@ -41,7 +45,7 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
getVerticals().then(setVerticals).catch(console.error);
getTaxonomyMeta().then(setTaxonomyMeta).catch(console.error);
}, []);
useEffect(() => {
@ -158,8 +162,17 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
formData.append('tags', JSON.stringify(uploadTags.split(',').map(t => t.trim()).filter(Boolean)));
formData.append('githubUrl', uploadGithubUrl);
formData.append('isDownloadable', String(uploadIsDownloadable));
if (selectedVerticalId) {
formData.append('verticalIds', JSON.stringify([selectedVerticalId]));
if (selectedVerticalIds.length) {
formData.append('verticalIds', JSON.stringify(selectedVerticalIds));
}
if (selectedTechStackIds.length) {
formData.append('techStackIds', JSON.stringify(selectedTechStackIds));
}
if (selectedEngagementTypeIds.length) {
formData.append('engagementTypeIds', JSON.stringify(selectedEngagementTypeIds));
}
if (selectedComplianceIds.length) {
formData.append('complianceIds', JSON.stringify(selectedComplianceIds));
}
if (thumbnailUrl) {
@ -184,6 +197,10 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
setUploadSubcategory('');
setUploadTags('');
setUploadGithubUrl('');
setSelectedVerticalIds([]);
setSelectedTechStackIds([]);
setSelectedEngagementTypeIds([]);
setSelectedComplianceIds([]);
setUploadIsDownloadable(true);
success('Asset published successfully', 'The asset has been added to the catalog.');
onSuccess();
@ -196,6 +213,10 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
}
};
const toggleSelection = (list: string[], item: string) => {
return list.includes(item) ? list.filter(i => i !== item) : [...list, item];
};
return (
<>
<Modal
@ -466,43 +487,130 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* 4-Group Taxonomy Demarcation Selection */}
<div className="space-y-4 pt-3 pb-3 border-t border-b border-slate-200 dark:border-slate-800">
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Taxonomy Classification (Strict Admin-Managed)
</h4>
{/* Group 1: Industry Verticals */}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Industry Vertical / Domain</label>
<select
value={selectedVerticalId}
onChange={(e) => setSelectedVerticalId(e.target.value)}
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900"
>
<option value="">General (No Domain)</option>
{verticals.map(v => (
<option key={v.id} value={v.id}>{v.name}</option>
))}
</select>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
1. Industry Verticals / Domains
</label>
<div className="flex flex-wrap gap-1.5 max-h-32 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{(taxonomyMeta?.verticals || []).map(v => {
const isSelected = selectedVerticalIds.includes(v.id);
return (
<button
key={v.id}
type="button"
onClick={() => setSelectedVerticalIds(toggleSelection(selectedVerticalIds, v.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-blue-600 text-white font-bold border-blue-600 shadow-sm ring-2 ring-blue-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{v.name}
</button>
);
})}
</div>
</div>
{/* Group 2: Tech Stack */}
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Category</label>
<select
value={uploadCategory}
onChange={(e) => setUploadCategory(e.target.value)}
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900"
>
<option value="Marketing">Marketing</option>
<option value="Presentations">Presentations</option>
<option value="Branding">Branding</option>
<option value="Resources">Resources</option>
<option value="Technical">Technical</option>
</select>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
2. Technology Stack & Capabilities
</label>
<div className="flex flex-col gap-2 max-h-40 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{['Languages & Frameworks', 'AI & ML', 'Data & Backend', 'Cloud & Infra'].map(catName => {
const items = (taxonomyMeta?.techStacks || []).filter(t => t.category === catName || (!t.category && catName === 'Languages & Frameworks'));
if (items.length === 0) return null;
return (
<div key={catName} className="space-y-1">
<div className="text-[10px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
{catName}
</div>
<div className="flex flex-wrap gap-1.5">
{items.map(t => {
const isSelected = selectedTechStackIds.includes(t.id);
return (
<button
key={t.id}
type="button"
onClick={() => setSelectedTechStackIds(toggleSelection(selectedTechStackIds, t.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-purple-600 text-white font-bold border-purple-600 shadow-sm ring-2 ring-purple-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{t.name}
</button>
);
})}
</div>
</div>
);
})}
</div>
</div>
<div>
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Subcategory</label>
<input
type="text"
value={uploadSubcategory}
onChange={(e) => setUploadSubcategory(e.target.value)}
placeholder="e.g. Slide Deck"
className="w-full bg-ink-50 border border-ink-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ink-900/10 text-ink-900 placeholder-ink-400"
/>
{/* Group 3 & Group 4 Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Group 3: Engagement Type */}
<div>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
3. Engagement Type
</label>
<div className="flex flex-wrap gap-1.5 p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{(taxonomyMeta?.engagementTypes || []).map(e => {
const isSelected = selectedEngagementTypeIds.includes(e.id);
return (
<button
key={e.id}
type="button"
onClick={() => setSelectedEngagementTypeIds(toggleSelection(selectedEngagementTypeIds, e.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-sky-600 text-white font-bold border-sky-600 shadow-sm ring-2 ring-sky-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{e.name}
</button>
);
})}
</div>
</div>
{/* Group 4: Compliance Standards */}
<div>
<label className="text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5 block">
4. Compliance & Governance
</label>
<div className="flex flex-wrap gap-1.5 p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
{(taxonomyMeta?.complianceStandards || []).map(c => {
const isSelected = selectedComplianceIds.includes(c.id);
return (
<button
key={c.id}
type="button"
onClick={() => setSelectedComplianceIds(toggleSelection(selectedComplianceIds, c.id))}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer border ${
isSelected
? 'bg-emerald-600 text-white font-bold border-emerald-600 shadow-sm ring-2 ring-emerald-500/30'
: 'bg-white dark:bg-slate-800/90 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
}`}
>
{c.name}
</button>
);
})}
</div>
</div>
</div>
</div>

View File

@ -314,7 +314,7 @@ export const AssetsPage = () => {
const toolbarNode = (
<div className="flex flex-col lg:flex-row gap-3 items-stretch lg:items-center justify-between p-3.5 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm">
<div className="flex flex-wrap items-center gap-3 flex-1 min-w-0">
{/* Search Bar */}
<div className="relative w-full sm:w-[280px] shrink-0">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
@ -347,33 +347,30 @@ export const AssetsPage = () => {
<div className="flex items-center p-0.5 bg-slate-100 dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 shrink-0">
<button
onClick={() => setViewMode('grid')}
className={`p-1.5 rounded-md text-xs transition-all ${
viewMode === 'grid'
className={`p-1.5 rounded-md text-xs transition-all ${viewMode === 'grid'
? 'bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs font-bold'
: 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
}`}
title="Grid Cards"
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('compact')}
className={`p-1.5 rounded-md text-xs transition-all ${
viewMode === 'compact'
className={`p-1.5 rounded-md text-xs transition-all ${viewMode === 'compact'
? 'bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs font-bold'
: 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
}`}
title="Compact Cards"
>
<LayoutGrid className="w-3.5 h-3.5 stroke-[2.5]" />
</button>
<button
onClick={() => setViewMode('table')}
className={`p-1.5 rounded-md text-xs transition-all ${
viewMode === 'table'
className={`p-1.5 rounded-md text-xs transition-all ${viewMode === 'table'
? 'bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs font-bold'
: 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
}`}
}`}
title="Tabular List View (Shift+Click)"
>
<List className="w-4 h-4" />
@ -385,21 +382,19 @@ export const AssetsPage = () => {
<div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-800 p-1 rounded-xl shrink-0">
<button
onClick={() => setSelectedCategory("ALL")}
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all ${
selectedCategory === "ALL"
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all ${selectedCategory === "ALL"
? "bg-slate-900 text-white shadow-xs"
: "text-slate-600 dark:text-slate-300 hover:bg-slate-200"
}`}
}`}
>
All Assets
</button>
<button
onClick={() => setSelectedCategory("RECOMMENDED")}
className={`px-3 py-1 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all ${
selectedCategory === "RECOMMENDED"
className={`px-3 py-1 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all ${selectedCategory === "RECOMMENDED"
? "bg-amber-500 text-slate-950 font-bold shadow-xs"
: "text-amber-600 dark:text-amber-400 hover:bg-amber-500/10"
}`}
}`}
>
<Sparkles className="w-3.5 h-3.5" />
<span>Recommended ({recommendedAssets.length})</span>
@ -498,11 +493,10 @@ export const AssetsPage = () => {
{/* Filter Drawer Trigger Button positioned on the far right end */}
<button
onClick={() => setIsFilterDrawerOpen(true)}
className={`flex items-center gap-2 px-3.5 py-2 rounded-lg text-xs font-bold border transition-all ${
activeFilterCount > 0
className={`flex items-center gap-2 px-3.5 py-2 rounded-lg text-xs font-bold border transition-all ${activeFilterCount > 0
? "bg-slate-900 text-white border-slate-900 shadow-sm dark:bg-slate-100 dark:text-slate-900"
: "bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700"
}`}
}`}
>
<Filter className="w-4 h-4" />
<span>Filters</span>
@ -526,7 +520,7 @@ export const AssetsPage = () => {
<span>Showing <strong className="text-slate-900 dark:text-slate-100">{filteredAssets.length}</strong> of {taxonomyMeta?.totalAssets || assets.length} assets</span>
{selectedAssetIds.length > 0 && (
<span className="px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 font-bold border border-slate-300 dark:border-slate-700">
{selectedAssetIds.length} selected (Hold Shift to range-select)
{selectedAssetIds.length} selected
</span>
)}
</div>
@ -583,11 +577,10 @@ export const AssetsPage = () => {
variants={containerVariants}
initial="hidden"
animate="show"
className={`grid grid-cols-1 ${
viewMode === 'compact'
className={`grid grid-cols-1 ${viewMode === 'compact'
? 'md:grid-cols-3 xl:grid-cols-5 gap-3'
: 'md:grid-cols-2 xl:grid-cols-4 gap-4'
} items-start`}
} items-start`}
>
{filteredAssets.map((asset) => (
<motion.div

View File

@ -1,10 +1,13 @@
import { axiosInstance } from './axios';
import type { Asset, Organization, Vertical, TaxonomyMeta, AssetQueryFilters } from '../types/assets';
import type { Asset, Organization, Vertical, TechStack, EngagementType, ComplianceStandard, TaxonomyMeta, AssetQueryFilters } from '../types/assets';
export const getAssets = async (filters?: AssetQueryFilters): Promise<Asset[]> => {
const params: Record<string, string> = {};
if (filters?.search) params.search = filters.search;
if (filters?.verticalIds?.length) params.verticalIds = filters.verticalIds.join(',');
if (filters?.techStackIds?.length) params.techStackIds = filters.techStackIds.join(',');
if (filters?.engagementTypeIds?.length) params.engagementTypeIds = filters.engagementTypeIds.join(',');
if (filters?.complianceIds?.length) params.complianceIds = filters.complianceIds.join(',');
if (filters?.contentTypes?.length) params.contentTypes = filters.contentTypes.join(',');
if (filters?.subcategories?.length) params.subcategories = filters.subcategories.join(',');
if (filters?.tags?.length) params.tags = filters.tags.join(',');
@ -33,6 +36,9 @@ export const updateAsset = async (
categoryId?: string;
subcategory?: string;
verticalIds?: string[];
techStackIds?: string[];
engagementTypeIds?: string[];
complianceIds?: string[];
tags?: string[];
githubUrl?: string;
isDownloadable?: boolean;
@ -139,6 +145,33 @@ export const deleteVertical = async (id: string): Promise<void> => {
await axiosInstance.delete(`/taxonomy/verticals/${id}`);
};
export const createTechStack = async (payload: { name: string; category?: string; icon?: string; description?: string; color?: string }): Promise<TechStack> => {
const response = await axiosInstance.post<TechStack>('/taxonomy/tech-stacks', payload);
return response.data;
};
export const deleteTechStack = async (id: string): Promise<void> => {
await axiosInstance.delete(`/taxonomy/tech-stacks/${id}`);
};
export const createEngagementType = async (payload: { name: string; icon?: string; description?: string; color?: string }): Promise<EngagementType> => {
const response = await axiosInstance.post<EngagementType>('/taxonomy/engagement-types', payload);
return response.data;
};
export const deleteEngagementType = async (id: string): Promise<void> => {
await axiosInstance.delete(`/taxonomy/engagement-types/${id}`);
};
export const createComplianceStandard = async (payload: { name: string; icon?: string; description?: string; color?: string }): Promise<ComplianceStandard> => {
const response = await axiosInstance.post<ComplianceStandard>('/taxonomy/compliance-standards', payload);
return response.data;
};
export const deleteComplianceStandard = async (id: string): Promise<void> => {
await axiosInstance.delete(`/taxonomy/compliance-standards/${id}`);
};
// Notification API
export const sendAssetAnnouncement = async (payload: { title: string; message: string; targetOrgIds?: string[]; assetIds?: string[] }): Promise<void> => {
await axiosInstance.post('/assets/notify', payload);

View File

@ -40,8 +40,54 @@ export interface Vertical {
};
}
export interface TechStack {
id: string;
name: string;
slug: string;
category: string;
icon?: string | null;
description?: string | null;
color?: string | null;
orderIndex?: number;
isActive?: boolean;
_count?: {
assets: number;
};
}
export interface EngagementType {
id: string;
name: string;
slug: string;
icon?: string | null;
description?: string | null;
color?: string | null;
orderIndex?: number;
isActive?: boolean;
_count?: {
assets: number;
};
}
export interface ComplianceStandard {
id: string;
name: string;
slug: string;
icon?: string | null;
description?: string | null;
color?: string | null;
orderIndex?: number;
isActive?: boolean;
_count?: {
assets: number;
};
}
export interface TaxonomyMeta {
verticals: Vertical[];
techStacks: TechStack[];
engagementTypes: EngagementType[];
complianceStandards: ComplianceStandard[];
categories: { name: string; count: number }[];
subcategories: { name: string; count: number }[];
contentTypes: { name: string; count: number }[];
@ -52,6 +98,9 @@ export interface TaxonomyMeta {
export interface AssetQueryFilters {
search?: string;
verticalIds?: string[];
techStackIds?: string[];
engagementTypeIds?: string[];
complianceIds?: string[];
contentTypes?: string[];
subcategories?: string[];
tags?: string[];
@ -80,6 +129,9 @@ export interface Asset {
solution?: string | null;
createdAt: string;
verticals?: Vertical[];
techStacks?: TechStack[];
engagementTypes?: EngagementType[];
complianceStandards?: ComplianceStandard[];
sharedWith?: SharedWithOrg[];
downloadRequests?: DownloadRequest[];
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff