diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1ed5d321 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,97 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +EMPACT (Environment and Maturity Program Assessment and Control Tool) is a full-stack assessment platform implementing the IP2M METRR evaluation model. It's built with Next.js 15, TypeScript, and supports both web and desktop deployment via Tauri. + +## Development Commands + +All commands should be run from the `web/` directory unless specified otherwise: + +```bash +# Development +yarn dev # Start development server on http://localhost:3000 + +# Building +yarn build # Generate Prisma clients and build Next.js app +yarn start # Start production server + +# Code Quality +yarn lint # Run ESLint +yarn lint:fix # Run ESLint with auto-fix +yarn typecheck # Run TypeScript type checking + +# Testing +yarn test # Build and run Playwright tests +yarn test:ui # Run tests with UI +yarn test:debug # Run tests in debug mode + +# Database +yarn prisma-generate # Generate Prisma clients for all database types + +# Data Import +yarn import:ip2m # Import legacy IP2M METRR data from Excel + # See web/scripts/README.md for detailed documentation +``` + +## Architecture Overview + +The application uses Next.js App Router with a clear separation between frontend routes and API endpoints: + +- **Frontend Routes** (`app/(frontend)/`): + - Protected routes under `(logged-in)/` require authentication via Clerk + - Public routes under `(logged-out)/` for authentication flows + - Key pages: Dashboard, Assessments, Collections, Recommendations, Questionnaire + +- **API Layer** (`app/api/`): + - RESTful endpoints for data operations + - Clerk webhook handling for user synchronization + - Database operations via Prisma ORM + +- **Database Architecture**: + - Multi-database support (MSSQL primary, PostgreSQL/SQLite for flexibility) + - Prisma schemas in `web/prisma/[db-type]/schema.prisma` + - Generated clients in `web/prisma/generated/[db-type]/` + - Use `getDatabaseClient()` from `web/lib/db.ts` for database access + +- **Component Structure**: + - Shared components in `web/components/` + - UI components from Shadcn UI library + - All components use TypeScript with proper type definitions + +## Key Patterns and Conventions + +1. **Authentication**: + - Clerk handles authentication with SSO support + - User sync happens via webhooks to maintain database records + - Check authentication state using Clerk's hooks/utilities + +2. **Database Access**: + - Always use the database client from `lib/db.ts` + - Follow existing query patterns in API routes + - Use Prisma's type-safe query builders + +3. **State Management**: + - Server components by default in App Router + - Client components marked with 'use client' + - Use URL state for filters and pagination + +4. **Error Handling**: + - API routes return appropriate HTTP status codes + - Client-side error boundaries for graceful degradation + - Consistent error response format + +5. **Testing**: + - Playwright for E2E tests + - Tests located in `web/tests/` + - Follow existing test patterns for new features + +## Important Context + +- The project implements the IP2M METRR evaluation model for project maturity assessment +- Three main user roles: System Admin, Collection Manager, Assessment Manager +- Collections group related assessments together +- Recent work includes facilitator-controlled stopwatch functionality for timed assessments +- The application supports both online (web) and offline (Tauri desktop) usage \ No newline at end of file diff --git a/web/package.json b/web/package.json index 45f8a66f..4d83d4ac 100644 --- a/web/package.json +++ b/web/package.json @@ -31,7 +31,8 @@ "test:ui:dev": "cross-env PLAYWRIGHT_SERVER_COMMAND=\"yarn dev\" playwright test --ui", "test:ci": "playwright test --reporter=list", "test:nobuild:ui": "playwright test --ui", - "test:nobuild": "playwright test" + "test:nobuild": "playwright test", + "import:ip2m": "npx tsx scripts/import-ip2m-data-v2.ts" }, "dependencies": { "@clerk/nextjs": "^6.12.6", @@ -101,6 +102,7 @@ "@types/node": "20.14.15", "@types/react": "19.0.12", "@types/react-dom": "19.0.4", + "@types/sanitize-html": "^2.16.0", "@typescript-eslint/eslint-plugin": "^8.27.0", "@typescript-eslint/parser": "^8.27.0", "autoprefixer": "^10.4.21", @@ -117,7 +119,9 @@ "prettier-plugin-organize-imports": "^4.1.0", "prisma-dbml-generator": "0.12.0", "prompt-confirm": "2.0.4", - "tailwindcss": "3.4.17" + "sanitize-html": "^2.17.0", + "tailwindcss": "3.4.17", + "xlsx": "^0.18.5" }, "pkg": { "scripts": ".next/standalone/**/*.*", diff --git a/web/scripts/README.md b/web/scripts/README.md new file mode 100644 index 00000000..5ae6681a --- /dev/null +++ b/web/scripts/README.md @@ -0,0 +1,224 @@ +# IP2M METRR Data Import Utility + +This utility imports legacy assessment data from the IP2M METRR system into EMPACT. It was created to migrate historical assessment data while preserving all responses and maintaining data integrity. + +## Usage + +```bash +# Dry run (preview what will be imported) +yarn import:ip2m --file="path/to/excel-file.xlsx" --dryRun + +# Actual import +yarn import:ip2m --file="path/to/excel-file.xlsx" +``` + +## Excel File Format Specification + +The Excel file must contain exactly two sheets with specific column structures: + +### Sheet 1: `assessment` + +This sheet contains assessment metadata. Expected columns: + +| Column Name | Type | Description | Example | +|------------|------|-------------|---------| +| `id` | number | Legacy assessment ID (not used) | 130 | +| `project_id` | string | Project identifier | "LCCF" | +| `name` | string | Assessment name | "LCCF EVMS Review" | +| `type_id` | number | Legacy type ID (not used) | 16 | +| `type` | string | Legacy type name (not used) | "NSF Project" | +| `location` | string | Assessment location | "Austin, Texas" | +| `date` | number | JavaScript timestamp (milliseconds since epoch) | 1729712460000 | +| `manager` | string | Manager name (not used) | "" | +| `description` | string | Assessment description (can contain HTML) | "" | +| `hide_description` | boolean | (not used) | true | +| `status` | string | Assessment status: "STARTED", "COMPLETED", etc. | "STARTED" | +| `has_maturity` | boolean | (not used) | false | +| `has_environment` | boolean | (not used) | true | +| `maturity_score` | number | Maturity score (if available) | 842 | +| `current_progress` | string | (not used) | "IN_PROGRESS" | +| `percent_completed` | number | (not used) | 0 | +| `maturity_progress` | string | (not used) | "COMPLETED" | +| `env_progress` | string | (not used) | "COMPLETED" | +| `is_env_anonymous` | boolean | (not used) | true | +| `lock_env` | boolean | (not used) | false | +| `internal_assessment_status` | string | (not used) | "ACTIVE" | +| `factor_facilitator_answers` | boolean | (not used) | false | +| `environment_score` | number | Environment score (if available) | 926 | + +### Sheet 2: `assessment_user_responses` + +This sheet contains all user responses. Expected columns: + +| Column Name | Type | Description | Example | +|------------|------|-------------|---------| +| `id` | number | Legacy response ID | 18030 | +| `user_id` | number | Legacy user ID | 381 | +| `attribute_id` | string | EMPACT attribute ID | "1a" | +| `level_id` | number | Legacy level ID (not used directly) | 340 | +| `notes` | string | Response notes (can contain HTML) | "

project saying...

" | +| `section_id` | string | Section ID for validation | "1" | +| `is_facilitator_response` | boolean | (not used) | false | +| `attributes.name` | string | Attribute name (for reference) | "The contractor organization..." | +| `attributes.description` | string | Attribute description (for reference) | "

The contractor's...

" | +| `levels.short_description` | string | Level short description | "Meets Most" | +| `levels.long_description` | string | Level long description | "Rating a factor..." | +| `levels.level_number` | number | Level number (1-5) - CRITICAL for mapping | 4 | +| `levels.weight` | number | Level weight/score | 58 | +| `sections.name` | string | Section name | "Culture" | +| `sections.description` | string | Section description | "The culture category..." | + +## Import Process + +The import utility performs the following steps: + +### 1. Validation Phase +- Verifies IP2M METRR assessment type exists in EMPACT +- Validates all attribute IDs from responses exist in EMPACT +- Maps legacy level numbers to EMPACT level IDs +- Checks for existing data to prevent duplicates + +### 2. Import Phase (in transaction) +For each assessment in the Excel file: + +1. **Create Assessment Collection** + - Name: "Import - [Assessment Name]" + - Type: IP2M METRR + +2. **Create Assessment** + - Project ID, name, location from Excel + - Status mapping: + - "STARTED" → "Active" + - "COMPLETED" → "Final" + - "NOT_STARTED" → "Inactive" + - Completed date set if status is "COMPLETED" + - HTML cleaned from description + +3. **Create Assessment Parts** + - Creates parts for Environment and Maturity + - Status: "Active" + - Date from Excel timestamp + +4. **Add Assessment Attributes** + - Links all 83 IP2M METRR attributes to assessment + +5. **Create User Group** + - Name: "Imported Participants" + - Status: "Active" + +6. **Create/Find Users** + - Email format: `imported_ip2m_user_{legacy_id}@doe.gov` + - Name: "IP2M User {legacy_id}" + - Checks for existing users to avoid duplicates + +7. **Create Assessment Users** + - Role: "Participant" + - Linked to user group + - Connected to all assessment parts + - No permissions (not real users) + +8. **Import Responses** + - Maps attribute IDs directly + - Maps level numbers to EMPACT level IDs + - Cleans HTML from notes + - Creates AssessmentUserResponse records + +### 3. Post-Import Notes +- Score summaries are skipped due to database schema mismatch +- All operations occur in a transaction (all-or-nothing) +- 60-second timeout for large imports + +## Data Mapping Details + +### Status Mapping +``` +Excel Status → EMPACT Status +STARTED → Active +COMPLETED → Final +NOT_STARTED → Inactive +ACTIVE → Active +INACTIVE → Inactive +FINAL → Final +ARCHIVED → Archived +(default) → Active +``` + +### Level Mapping +Levels are mapped using the combination of attribute ID and level number: +- Key: `{attribute_id}-{level_number}` (e.g., "1a-4") +- Maps to EMPACT level ID + +### HTML Cleaning +Basic HTML tags and entities are removed from: +- Assessment descriptions +- Response notes + +Conversions: +- `

`, `

`, etc. → removed +- ` ` → space +- `’` → ' +- `“` → " +- `”` → " +- `&` → & + +## Prerequisites + +1. **IP2M METRR Assessment Type** must exist in EMPACT database +2. **All Attributes** referenced in responses must exist in EMPACT +3. **All Levels** must exist for each attribute (levels 0-5) +4. **Database Access** with write permissions + +## Error Handling + +- **Validation Errors**: Stop before import if data doesn't match +- **Duplicate Users**: Reuses existing imported users +- **Duplicate Assessment Users**: Logs warning but continues +- **Missing Level Mappings**: Logs error and skips that response +- **Transaction Rollback**: All changes reverted if any error occurs + +## Output + +The import provides detailed progress information: +``` +📊 Starting IP2M METRR Data Import +✅ IP2M METRR Assessment type found (ID: 1) +✅ All 83 attributes validated +📁 Processing assessment: LCCF EVMS Review + ✅ Created assessment (ID: 15) + ✅ Created 2 assessment parts + ✅ Added 83 attributes to assessment + ✅ Created user group for imported participants + ✅ Created user: imported_ip2m_user_381@doe.gov + ✅ Processed 16 users + ✅ Imported 466 responses +🎉 Import completed successfully! +``` + +## Troubleshooting + +### "IP2M METRR assessment type not found" +The IP2M METRR assessment type doesn't exist in the database. This is required base data. + +### "Missing attributes in EMPACT database" +Some attributes in the Excel file don't exist in EMPACT. Check that all IP2M METRR attributes are loaded. + +### "Cannot find level mapping" +The level number for an attribute doesn't exist. Verify levels 0-5 exist for all attributes. + +### "The column 'type' does not exist" +Database schema mismatch. The ScoreSummary table structure differs from Prisma schema. + +## Source Code Location + +The import utility is located at: +``` +web/scripts/import-ip2m-data-v2.ts +``` + +## Future Improvements + +1. **Score Calculation**: Calculate scores from responses rather than using Excel values +2. **User Mapping**: Option to map legacy users to real EMPACT users +3. **Multiple Assessments**: Support multiple assessments in one Excel file +4. **Validation Report**: Generate pre-import validation report +5. **Export Utility**: Create matching export functionality \ No newline at end of file diff --git a/web/scripts/import-ip2m-data-v2.ts b/web/scripts/import-ip2m-data-v2.ts new file mode 100644 index 00000000..8cb3fe9a --- /dev/null +++ b/web/scripts/import-ip2m-data-v2.ts @@ -0,0 +1,363 @@ +import { PrismaClient } from '../prisma/mssql/generated/client' +import * as XLSX from 'xlsx' +import { parseArgs } from 'node:util' +import sanitizeHtml from 'sanitize-html' + +const prisma = new PrismaClient({ + log: ['info', 'warn', 'error'], +}) + +interface ExcelAssessment { + id: number + project_id: string + name: string + type_id: number + type: string + location: string + date: number + manager: string + description: string + hide_description: boolean + status: string + has_maturity: boolean + has_environment: boolean + maturity_score: number + current_progress: string + percent_completed: number + maturity_progress: string + env_progress: string + is_env_anonymous: boolean + lock_env: boolean + internal_assessment_status: string + factor_facilitator_answers: boolean + environment_score: number +} + +interface ExcelUserResponse { + id: number + user_id: number + attribute_id: string + level_id: number + notes: string + section_id: string + is_facilitator_response: boolean + 'attributes.name': string + 'attributes.description': string + 'levels.short_description': string + 'levels.long_description': string + 'levels.level_number': number + 'levels.weight': number + 'sections.name': string + 'sections.description': string +} + +const options = { + file: { type: 'string' as const }, + dryRun: { type: 'boolean' as const }, +} + +async function main() { + const { + values: { file, dryRun }, + } = parseArgs({ options }) + + if (!file) { + console.error('Please provide a file path with --file parameter') + process.exit(1) + } + + console.log(`\n📊 Starting IP2M METRR Data Import`) + console.log(`File: ${file}`) + console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE IMPORT'}`) + console.log('─'.repeat(50)) + + try { + // Read Excel file + const workbook = XLSX.readFile(file) + const assessmentSheet = workbook.Sheets['assessment'] + const responseSheet = workbook.Sheets['assessment_user_responses'] + + const assessmentData = XLSX.utils.sheet_to_json(assessmentSheet) + const responseData = XLSX.utils.sheet_to_json(responseSheet) + + console.log(`\n📋 Found ${assessmentData.length} assessment(s)`) + console.log(`📝 Found ${responseData.length} responses`) + + // Pre-validate all data before starting transaction + const assessmentType = await prisma.assessmentType.findFirst({ + where: { name: 'IP2M METRR' }, + include: { parts: true } + }) + + if (!assessmentType) { + throw new Error('IP2M METRR assessment type not found in database.') + } + + console.log(`\n✅ IP2M METRR Assessment type found (ID: ${assessmentType.id})`) + + // Get all attributes for validation + const attributes = await prisma.attribute.findMany() + const attributeMap = new Map(attributes.map(a => [a.id, a])) + + // Validate all attribute IDs exist + const uniqueAttributeIds = [...new Set(responseData.map(r => r.attribute_id))] + const missingAttributes = uniqueAttributeIds.filter(id => !attributeMap.has(id)) + + if (missingAttributes.length > 0) { + console.error(`\n❌ Missing attributes in EMPACT database: ${missingAttributes.join(', ')}`) + throw new Error('Some attributes from Excel do not exist in EMPACT database') + } + + console.log(`✅ All ${uniqueAttributeIds.length} attributes validated`) + + // Map legacy level IDs to EMPACT level IDs + const levels = await prisma.level.findMany() + const levelMap = new Map() + + // Create mapping key: attributeId + levelNumber -> levelId + levels.forEach(level => { + const key = `${level.attributeId}-${level.level}` + levelMap.set(key, level.id) + }) + + + if (dryRun) { + console.log('\n🔍 DRY RUN - No changes will be made to the database') + console.log('\nPlanned actions:') + + for (const excelAssessment of assessmentData) { + console.log(`\n📁 Processing assessment: ${excelAssessment.name}`) + const assessmentDate = new Date(excelAssessment.date) + + console.log(` - Would create assessment collection: "Import - ${excelAssessment.name}"`) + console.log(` - Would create assessment with:`) + console.log(` - Project ID: ${excelAssessment.project_id}`) + console.log(` - Name: ${excelAssessment.name}`) + console.log(` - Location: ${excelAssessment.location}`) + console.log(` - Status: ${mapStatus(excelAssessment.status)}`) + + // Get unique users from ALL responses (no filter) + const uniqueUserIds = [...new Set(responseData.map(r => r.user_id))] + console.log(` - Would create/use ${uniqueUserIds.length} users`) + console.log(` - Would create 1 assessment user group for all imported users`) + console.log(` - Would import ${responseData.length} responses`) + } + + console.log('\n✅ Dry run completed. Run without --dryRun to perform actual import.') + return + } + + // Execute import in transaction + await prisma.$transaction(async (tx) => { + // Process each assessment + for (const excelAssessment of assessmentData) { + console.log(`\n📁 Processing assessment: ${excelAssessment.name}`) + + const assessmentDate = new Date(excelAssessment.date) + const isCompleted = excelAssessment.status === 'COMPLETED' + + // Create assessment collection + const collection = await tx.assessmentCollection.create({ + data: { + name: `Import - ${excelAssessment.name}`, + assessmentTypeId: assessmentType.id, + } + }) + + // Create assessment + const assessment = await tx.assessment.create({ + data: { + projectId: excelAssessment.project_id, + name: excelAssessment.name, + location: excelAssessment.location, + description: cleanHtml(excelAssessment.description || ''), + status: mapStatus(excelAssessment.status), + completedDate: isCompleted ? assessmentDate : null, + assessmentCollectionId: collection.id, + } + }) + + console.log(` ✅ Created assessment (ID: ${assessment.id})`) + + // Create assessment parts + const createdParts = [] + for (const part of assessmentType.parts) { + const assessmentPart = await tx.assessmentPart.create({ + data: { + assessmentId: assessment.id, + partId: part.id, + status: 'Active', + date: assessmentDate, + } + }) + createdParts.push(assessmentPart) + } + + console.log(` ✅ Created ${assessmentType.parts.length} assessment parts`) + + // Add all attributes to assessment + const assessmentAttributes = uniqueAttributeIds.map(attributeId => ({ + assessmentId: assessment.id, + attributeId: attributeId, + })) + + await tx.assessmentAttribute.createMany({ + data: assessmentAttributes, + }) + + console.log(` ✅ Added ${assessmentAttributes.length} attributes to assessment`) + + // Create a single user group for all imported users + const userGroup = await tx.assessmentUserGroup.create({ + data: { + name: 'Imported Participants', + assessmentId: assessment.id, + status: 'Active' + } + }) + + console.log(` ✅ Created user group for imported participants`) + + // Get unique users from ALL responses (removed filter) + const uniqueUserIds = [...new Set(responseData.map(r => r.user_id))] + + // Create or find users + const userMap = new Map() + + for (const legacyUserId of uniqueUserIds) { + const email = `imported_ip2m_user_${legacyUserId}@doe.gov` + + let user = await tx.user.findUnique({ + where: { email } + }) + + if (!user) { + user = await tx.user.create({ + data: { + email, + firstName: 'IP2M', + lastName: `User ${legacyUserId}`, + } + }) + console.log(` ✅ Created user: ${email}`) + } + + userMap.set(legacyUserId, user.id) + + // Add user to assessment - no permissions needed for imported users + try { + await tx.assessmentUser.create({ + data: { + assessmentId: assessment.id, + userId: user.id, + role: 'Participant', + assessmentUserGroupId: userGroup.id, + // Connect to all assessment parts for participants + participantParts: { + connect: createdParts.map(p => ({ id: p.id })) + } + } + }) + } catch (error: any) { + if (error.code === 'P2002') { + // Unique constraint violation - user already exists for this assessment + console.log(` ℹ️ User ${email} already assigned to assessment`) + } else { + console.error(` ⚠️ Error creating assessment user for ${email}:`, error.message) + } + } + } + + console.log(` ✅ Processed ${uniqueUserIds.length} users`) + + // Import ALL responses + let successCount = 0 + let errorCount = 0 + + for (const response of responseData) { + const empactUserId = userMap.get(response.user_id) + if (!empactUserId) { + console.error(` ⚠️ Cannot find user mapping for legacy user ${response.user_id}`) + errorCount++ + continue + } + + // Map level ID + const levelKey = `${response.attribute_id}-${response['levels.level_number']}` + const empactLevelId = levelMap.get(levelKey) + + if (!empactLevelId) { + console.error(` ⚠️ Cannot find level mapping for ${levelKey}`) + errorCount++ + continue + } + + try { + await tx.assessmentUserResponse.create({ + data: { + assessmentId: assessment.id, + userId: empactUserId, + assessmentUserGroupId: userGroup.id, + attributeId: response.attribute_id, + levelId: empactLevelId, + notes: cleanHtml(response.notes || ''), + } + }) + successCount++ + } catch (error: any) { + console.error(` ⚠️ Error importing response: ${error.message}`) + errorCount++ + } + } + + console.log(` ✅ Imported ${successCount} responses`) + if (errorCount > 0) { + console.log(` ⚠️ Failed to import ${errorCount} responses`) + } + + // Skip score summaries - schema mismatch with database + if (excelAssessment.maturity_score || excelAssessment.environment_score) { + console.log(` ℹ️ Skipping score summaries (database schema mismatch)`) + } + } + }, { + timeout: 60000 // 60 second timeout for large imports + }) + + console.log('\n🎉 Import completed successfully!') + + } catch (error) { + console.error('\n❌ Import failed:', error) + process.exit(1) + } finally { + await prisma.$disconnect() + } +} + +function mapStatus(excelStatus: string): string { + // Map Excel statuses to EMPACT statuses + const statusMap: Record = { + 'STARTED': 'Active', + 'COMPLETED': 'Final', + 'NOT_STARTED': 'Inactive', + 'ACTIVE': 'Active', + 'INACTIVE': 'Inactive', + 'FINAL': 'Final', + 'ARCHIVED': 'Archived' + } + return statusMap[excelStatus.toUpperCase()] || 'Active' +} + +function cleanHtml(html: string): string { + // Use sanitize-html to remove all HTML tags and attributes + return sanitizeHtml(html, { + allowedTags: [], + allowedAttributes: {} + }).trim() +} + +main().catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) +}) \ No newline at end of file diff --git a/web/scripts/tsconfig.json b/web/scripts/tsconfig.json new file mode 100644 index 00000000..2ad1dbaa --- /dev/null +++ b/web/scripts/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "noEmit": false, + "esModuleInterop": true + }, + "include": ["./**/*"] +} \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index 67320bd8..3d0fa3eb 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1783,6 +1783,13 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/sanitize-html@^2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.16.0.tgz#860d72c1ba8a5d044946f37559cc359c0a13b24e" + integrity sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw== + dependencies: + htmlparser2 "^8.0.0" + "@typescript-eslint/eslint-plugin@^8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz#fbef10802365832ee1d1bd5d2117dcec82727a72" @@ -2024,6 +2031,11 @@ acorn@^8.14.0, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== +adler-32@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" + integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + ag-charts-types@11.2.3: version "11.2.3" resolved "https://registry.yarnpkg.com/ag-charts-types/-/ag-charts-types-11.2.3.tgz#482a7b56f0a0d775627ffc786d7562ac2da1db5d" @@ -2683,6 +2695,14 @@ caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz#6b33a8857e6c7dcb41a0caa2dd0f0489c823a52d" integrity sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q== +cfb@~1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" + integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + dependencies: + adler-32 "~1.3.0" + crc-32 "~1.2.0" + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -2790,6 +2810,11 @@ cmdk@1.0.0: "@radix-ui/react-dialog" "1.0.5" "@radix-ui/react-primitive" "1.0.3" +codepage@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" + integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== + collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -2881,7 +2906,7 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -crc-32@^1.2.0: +crc-32@^1.2.0, crc-32@~1.2.0, crc-32@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== @@ -3004,6 +3029,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -3104,6 +3134,36 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -3171,6 +3231,11 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -3770,6 +3835,11 @@ fp-ts@2.16.0: resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.16.0.tgz#64e03314dfc1c7ce5e975d3496ac14bc3eb7f92e" integrity sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ== +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -4068,6 +4138,16 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +htmlparser2@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-proxy-agent@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673" @@ -4380,6 +4460,11 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-regex@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" @@ -4902,7 +4987,7 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.6, nanoid@^3.3.7, nanoid@^3.3.8: +nanoid@^3.3.11, nanoid@^3.3.6, nanoid@^3.3.7, nanoid@^3.3.8: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -5241,6 +5326,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -5392,6 +5482,15 @@ postcss@8.4.41: picocolors "^1.0.1" source-map-js "^1.2.0" +postcss@^8.3.11: + version "8.5.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postcss@^8.4.4, postcss@^8.4.47: version "8.5.3" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" @@ -5854,6 +5953,18 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" +sanitize-html@^2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" + integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^8.0.0" + is-plain-object "^5.0.0" + parse-srcset "^1.0.2" + postcss "^8.3.11" + scheduler@^0.25.0: version "0.25.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" @@ -6123,6 +6234,13 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3" integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + dependencies: + frac "~1.1.2" + stable-hash@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269" @@ -6869,11 +6987,21 @@ window-size@^1.1.0: define-property "^1.0.0" is-number "^3.0.0" +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6897,6 +7025,19 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +xlsx@^0.18.5: + version "0.18.5" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0" + integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== + dependencies: + adler-32 "~1.3.0" + cfb "~1.2.1" + codepage "~1.15.0" + crc-32 "~1.2.1" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"