diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..a4eced76 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs` +6931e92c330346f224e44283fde38346afb1d527 # prettier, 199 files diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 48f00ef5..dd93782e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,26 +1,26 @@ version: 2 updates: # JavaScript/TypeScript dependencies (npm) - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" - day: "monday" + interval: 'weekly' + day: 'monday' open-pull-requests-limit: 10 commit-message: - prefix: "deps" + prefix: 'deps' labels: - - "dependencies" - - "security" + - 'dependencies' + - 'security' # GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: 'github-actions' + directory: '/' schedule: - interval: "weekly" - day: "monday" + interval: 'weekly' + day: 'monday' commit-message: - prefix: "ci" + prefix: 'ci' labels: - - "ci" - - "dependencies" + - 'ci' + - 'dependencies' diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..c4a196fc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,26 @@ +# Build output and vendored trees โ€” formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md + +# Generated during the build CI runs before format:check, so it never exists +# locally at check time. Contentlayer output also uses import assertions, +# which prettier cannot parse. +.contentlayer diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..616247af --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index 0d128b4c..f0e18072 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -1,7 +1,14 @@ const prisma = require('../lib/prisma'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); -const { success, error, validationError, unauthorized, notFound, created } = require('../utils/responseHandlers'); +const { + success, + error, + validationError, + unauthorized, + notFound, + created, +} = require('../utils/responseHandlers'); const redis = require('../lib/redis'); function setAuthCookies(res, token) { @@ -26,7 +33,7 @@ exports.register = async (req, res) => { // Check if user already exists const existingUser = await prisma.user.findUnique({ - where: { email } + where: { email }, }); if (existingUser) { @@ -43,15 +50,15 @@ exports.register = async (req, res) => { email, passwordHash, name: name || null, - role: 'USER' + role: 'USER', }, select: { id: true, email: true, name: true, role: true, - createdAt: true - } + createdAt: true, + }, }); // Create and sign a JWT @@ -59,7 +66,7 @@ exports.register = async (req, res) => { user: { id: user.id, email: user.email, - role: user.role + role: user.role, }, }; @@ -74,7 +81,7 @@ exports.register = async (req, res) => { } setAuthCookies(res, token); return created(res, { token, user }, 'User registered successfully'); - } + }, ); } catch (err) { console.error('Registration error:', err); @@ -101,8 +108,8 @@ exports.login = async (req, res) => { role: true, passwordHash: true, isActive: true, - createdAt: true - } + createdAt: true, + }, }); if (!user || !user.isActive) { @@ -120,7 +127,7 @@ exports.login = async (req, res) => { user: { id: user.id, email: user.email, - role: user.role + role: user.role, }, }; @@ -134,17 +141,21 @@ exports.login = async (req, res) => { return error(res, 'Token generation failed'); } setAuthCookies(res, token); - return success(res, { - token, - user: { - id: user.id, - email: user.email, - name: user.name, - role: user.role, - createdAt: user.createdAt - } - }, 'Login successful'); - } + return success( + res, + { + token, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + createdAt: user.createdAt, + }, + }, + 'Login successful', + ); + }, ); } catch (err) { console.error('Login error:', err); @@ -170,10 +181,10 @@ exports.getProfile = async (req, res) => { select: { id: true, name: true, - slug: true - } - } - } + slug: true, + }, + }, + }, }); if (!user) { @@ -191,12 +202,12 @@ exports.getProfile = async (req, res) => { exports.updateProfile = async (req, res) => { try { const { name, avatar } = req.body; - + const user = await prisma.user.update({ where: { id: req.user.id }, data: { ...(name && { name }), - ...(avatar && { avatar }) + ...(avatar && { avatar }), }, select: { id: true, @@ -204,8 +215,8 @@ exports.updateProfile = async (req, res) => { name: true, role: true, avatar: true, - updatedAt: true - } + updatedAt: true, + }, }); return success(res, { user }, 'Profile updated successfully'); @@ -229,8 +240,8 @@ exports.changePassword = async (req, res) => { where: { id: req.user.id }, select: { id: true, - passwordHash: true - } + passwordHash: true, + }, }); if (!user || !user.passwordHash) { @@ -250,7 +261,7 @@ exports.changePassword = async (req, res) => { // Update password await prisma.user.update({ where: { id: req.user.id }, - data: { passwordHash: newPasswordHash } + data: { passwordHash: newPasswordHash }, }); return success(res, {}, 'Password changed successfully'); @@ -269,7 +280,7 @@ exports.verifyToken = async (req, res) => { console.error('Verify token error:', err); return error(res, 'Server error verifying token', 500, err); } -}; +}; // Logout: clear cookie exports.logout = async (req, res) => { @@ -279,4 +290,4 @@ exports.logout = async (req, res) => { } catch (err) { return error(res, 'Server error during logout', 500, err); } -}; \ No newline at end of file +}; diff --git a/backend/controllers/databaseController.js b/backend/controllers/databaseController.js index 10360378..af1ff2cc 100644 --- a/backend/controllers/databaseController.js +++ b/backend/controllers/databaseController.js @@ -16,14 +16,13 @@ exports.getDatabases = async (req, res) => { res.json({ success: true, databases, - total: databases.length + total: databases.length, }); - } catch (error) { console.error('Get databases error:', error); res.status(500).json({ success: false, - message: 'Failed to retrieve databases' + message: 'Failed to retrieve databases', }); } }; @@ -40,21 +39,20 @@ exports.getDatabaseRecords = async (req, res) => { limit: parseInt(limit) || 50, filters: filters ? JSON.parse(filters) : {}, sort: sort || 'submittedAt', - sortOrder: sortOrder || 'desc' + sortOrder: sortOrder || 'desc', }; const result = await databaseService.getDatabaseRecords(databaseId, userId, options); res.json({ success: true, - ...result + ...result, }); - } catch (error) { console.error('Get database records error:', error); res.status(404).json({ success: false, - message: error.message || 'Database not found' + message: error.message || 'Database not found', }); } }; @@ -68,28 +66,27 @@ exports.searchDatabases = async (req, res) => { if (!query) { return res.status(400).json({ success: false, - message: 'Search query is required' + message: 'Search query is required', }); } const options = { databases: databases || [], fields: fields || [], - limit: parseInt(limit) || 20 + limit: parseInt(limit) || 20, }; const results = await databaseService.searchDatabases(userId, query, options); res.json({ success: true, - ...results + ...results, }); - } catch (error) { console.error('Search databases error:', error); res.status(500).json({ success: false, - message: 'Search failed' + message: 'Search failed', }); } }; @@ -104,14 +101,13 @@ exports.getDatabaseAnalytics = async (req, res) => { res.json({ success: true, - analytics + analytics, }); - } catch (error) { console.error('Get database analytics error:', error); res.status(404).json({ success: false, - message: error.message || 'Database not found' + message: error.message || 'Database not found', }); } }; @@ -124,7 +120,7 @@ exports.exportDatabase = async (req, res) => { const { format = 'json', limit } = req.query; const options = { - limit: parseInt(limit) || 10000 + limit: parseInt(limit) || 10000, }; const exportData = await databaseService.exportDatabase(databaseId, userId, format, options); @@ -139,12 +135,11 @@ exports.exportDatabase = async (req, res) => { } else { res.send(exportData); } - } catch (error) { console.error('Export database error:', error); res.status(500).json({ success: false, - message: error.message || 'Export failed' + message: error.message || 'Export failed', }); } }; @@ -161,28 +156,27 @@ exports.analyzeDatabase = async (req, res) => { if (!query) { return res.status(400).json({ success: false, - message: 'Analysis query is required' + message: 'Analysis query is required', }); } const analysisRequest = { query, analysisType: analysisType || 'CUSTOM', - model: model || 'gpt-4' + model: model || 'gpt-4', }; const result = await aiAnalysisService.analyzeDatabase(databaseId, userId, analysisRequest); res.json({ success: true, - analysis: result + analysis: result, }); - } catch (error) { console.error('AI analysis error:', error); res.status(500).json({ success: false, - message: error.message || 'Analysis failed' + message: error.message || 'Analysis failed', }); } }; @@ -198,14 +192,13 @@ exports.generateInsights = async (req, res) => { res.json({ success: true, - insights + insights, }); - } catch (error) { console.error('Generate insights error:', error); res.status(500).json({ success: false, - message: error.message || 'Insight generation failed' + message: error.message || 'Insight generation failed', }); } }; @@ -219,27 +212,26 @@ exports.intelligentSearch = async (req, res) => { if (!query) { return res.status(400).json({ success: false, - message: 'Search query is required' + message: 'Search query is required', }); } const options = { databases: databases || [], - includeInsights: includeInsights !== false + includeInsights: includeInsights !== false, }; const results = await aiAnalysisService.intelligentSearch(userId, query, options); res.json({ success: true, - ...results + ...results, }); - } catch (error) { console.error('Intelligent search error:', error); res.status(500).json({ success: false, - message: 'Intelligent search failed' + message: 'Intelligent search failed', }); } }; @@ -254,14 +246,13 @@ exports.getPredictiveInsights = async (req, res) => { res.json({ success: true, - insights + insights, }); - } catch (error) { console.error('Predictive insights error:', error); res.status(500).json({ success: false, - message: error.message || 'Predictive analysis failed' + message: error.message || 'Predictive analysis failed', }); } }; @@ -283,12 +274,11 @@ exports.exportAnalysis = async (req, res) => { } else { res.send(exportData); } - } catch (error) { console.error('Export analysis error:', error); res.status(404).json({ success: false, - message: error.message || 'Analysis not found' + message: error.message || 'Analysis not found', }); } }; @@ -302,7 +292,7 @@ exports.getAnalysisHistory = async (req, res) => { const whereClause = { userId, ...(databaseId && { formId: databaseId }), - ...(analysisType && { analysisType }) + ...(analysisType && { analysisType }), }; const skip = (parseInt(page) - 1) * parseInt(limit); @@ -321,19 +311,19 @@ exports.getAnalysisHistory = async (req, res) => { form: { select: { id: true, - title: true - } - } + title: true, + }, + }, }, orderBy: { - createdAt: 'desc' + createdAt: 'desc', }, skip, - take: parseInt(limit) + take: parseInt(limit), }), prisma.lLMAnalysis.count({ - where: whereClause - }) + where: whereClause, + }), ]); res.json({ @@ -343,15 +333,14 @@ exports.getAnalysisHistory = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); - } catch (error) { console.error('Get analysis history error:', error); res.status(500).json({ success: false, - message: 'Failed to retrieve analysis history' + message: 'Failed to retrieve analysis history', }); } -}; \ No newline at end of file +}; diff --git a/backend/controllers/formController.js b/backend/controllers/formController.js index 5437093a..f3f3b402 100644 --- a/backend/controllers/formController.js +++ b/backend/controllers/formController.js @@ -20,7 +20,7 @@ exports.saveForm = async (req, res) => { isTemplate: isTemplate || false, templateTags: templateTags || [], userId, - isPublished: false + isPublished: false, }, select: { id: true, @@ -32,8 +32,8 @@ exports.saveForm = async (req, res) => { isTemplate: true, templateTags: true, createdAt: true, - updatedAt: true - } + updatedAt: true, + }, }); return created(res, { form }, 'Form created successfully'); @@ -49,12 +49,12 @@ exports.getForms = async (req, res) => { try { const skip = (parseInt(page) - 1) * parseInt(limit); - + const [forms, total] = await Promise.all([ prisma.form.findMany({ where: { userId, - isTemplate: isTemplate === 'true' + isTemplate: isTemplate === 'true', }, select: { id: true, @@ -67,22 +67,22 @@ exports.getForms = async (req, res) => { updatedAt: true, _count: { select: { - submissions: true - } - } + submissions: true, + }, + }, }, orderBy: { - updatedAt: 'desc' + updatedAt: 'desc', }, skip, - take: parseInt(limit) + take: parseInt(limit), }), prisma.form.count({ where: { userId, - isTemplate: isTemplate === 'true' - } - }) + isTemplate: isTemplate === 'true', + }, + }), ]); return success(res, { @@ -91,8 +91,8 @@ exports.getForms = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); } catch (err) { console.error('Get forms error:', err); @@ -108,7 +108,7 @@ exports.getForm = async (req, res) => { const form = await prisma.form.findFirst({ where: { id, - userId + userId, }, select: { id: true, @@ -123,10 +123,10 @@ exports.getForm = async (req, res) => { updatedAt: true, _count: { select: { - submissions: true - } - } - } + submissions: true, + }, + }, + }, }); if (!form) { @@ -149,17 +149,17 @@ exports.deleteForm = async (req, res) => { const form = await prisma.form.findFirst({ where: { id, - userId + userId, }, select: { id: true, title: true, _count: { select: { - submissions: true - } - } - } + submissions: true, + }, + }, + }, }); if (!form) { @@ -171,11 +171,11 @@ exports.deleteForm = async (req, res) => { // For forms with submissions, we might want to implement soft delete // For now, we'll just delete everything await prisma.form.delete({ - where: { id } + where: { id }, }); } else { await prisma.form.delete({ - where: { id } + where: { id }, }); } @@ -196,12 +196,12 @@ exports.updateForm = async (req, res) => { const existingForm = await prisma.form.findFirst({ where: { id, - userId + userId, }, select: { id: true, - currentVersion: true - } + currentVersion: true, + }, }); if (!existingForm) { @@ -214,13 +214,13 @@ exports.updateForm = async (req, res) => { ...(description !== undefined && { description }), ...(settings && { settings }), ...(isPublished !== undefined && { isPublished }), - updatedAt: new Date() + updatedAt: new Date(), }; if (schema) { updateData.schema = schema; updateData.currentVersion = existingForm.currentVersion + 1; - + // Create version record await prisma.formVersion.create({ data: { @@ -228,8 +228,8 @@ exports.updateForm = async (req, res) => { version: existingForm.currentVersion + 1, schema, settings: settings || {}, - createdBy: userId - } + createdBy: userId, + }, }); } @@ -247,8 +247,8 @@ exports.updateForm = async (req, res) => { templateTags: true, currentVersion: true, createdAt: true, - updatedAt: true - } + updatedAt: true, + }, }); return success(res, { form: updatedForm }, 'Form updated successfully'); @@ -267,12 +267,12 @@ exports.publishForm = async (req, res) => { const form = await prisma.form.findFirst({ where: { id, - userId + userId, }, select: { id: true, - isPublished: true - } + isPublished: true, + }, }); if (!form) { @@ -281,19 +281,23 @@ exports.publishForm = async (req, res) => { const updatedForm = await prisma.form.update({ where: { id }, - data: { + data: { isPublished: !form.isPublished, - updatedAt: new Date() + updatedAt: new Date(), }, select: { id: true, title: true, isPublished: true, - updatedAt: true - } + updatedAt: true, + }, }); - return success(res, { form: updatedForm }, `Form ${updatedForm.isPublished ? 'published' : 'unpublished'} successfully`); + return success( + res, + { form: updatedForm }, + `Form ${updatedForm.isPublished ? 'published' : 'unpublished'} successfully`, + ); } catch (err) { console.error('Publish form error:', err); return error(res, 'Server error publishing form', 500, err); @@ -307,7 +311,7 @@ exports.getPublicForm = async (req, res) => { const form = await prisma.form.findFirst({ where: { id, - isPublished: true + isPublished: true, }, select: { id: true, @@ -318,16 +322,16 @@ exports.getPublicForm = async (req, res) => { createdAt: true, user: { select: { - name: true - } - } - } + name: true, + }, + }, + }, }); if (!form) { - return res.status(404).json({ - success: false, - message: 'Form not found or not published' + return res.status(404).json({ + success: false, + message: 'Form not found or not published', }); } @@ -336,4 +340,4 @@ exports.getPublicForm = async (req, res) => { console.error('Get public form error:', err); return error(res, 'Server error getting form', 500, err); } -}; \ No newline at end of file +}; diff --git a/backend/controllers/submissionController.js b/backend/controllers/submissionController.js index 688d2599..4b923a36 100644 --- a/backend/controllers/submissionController.js +++ b/backend/controllers/submissionController.js @@ -7,9 +7,9 @@ exports.createSubmission = async (req, res) => { try { // Validate input if (!formId || !data) { - return res.status(400).json({ - success: false, - message: 'Form ID and data are required' + return res.status(400).json({ + success: false, + message: 'Form ID and data are required', }); } @@ -17,20 +17,20 @@ exports.createSubmission = async (req, res) => { const form = await prisma.form.findFirst({ where: { id: formId, - isPublished: true + isPublished: true, }, select: { id: true, title: true, userId: true, - settings: true - } + settings: true, + }, }); if (!form) { - return res.status(404).json({ - success: false, - message: 'Form not found or not published' + return res.status(404).json({ + success: false, + message: 'Form not found or not published', }); } @@ -44,27 +44,27 @@ exports.createSubmission = async (req, res) => { status: 'PENDING', source: 'DIRECT', ipAddress: req.ip, - userAgent: req.get('User-Agent') + userAgent: req.get('User-Agent'), }, select: { id: true, formId: true, data: true, status: true, - submittedAt: true - } + submittedAt: true, + }, }); - res.status(201).json({ - success: true, + res.status(201).json({ + success: true, message: 'Form submitted successfully', - submission + submission, }); } catch (err) { console.error('Submit form error:', err); - res.status(500).json({ - success: false, - message: 'Server error submitting form' + res.status(500).json({ + success: false, + message: 'Server error submitting form', }); } }; @@ -79,25 +79,25 @@ exports.getSubmissions = async (req, res) => { const form = await prisma.form.findFirst({ where: { id: formId, - userId + userId, }, select: { id: true, - title: true - } + title: true, + }, }); if (!form) { - return res.status(404).json({ - success: false, - message: 'Form not found or you do not have access' + return res.status(404).json({ + success: false, + message: 'Form not found or you do not have access', }); } const skip = (parseInt(page) - 1) * parseInt(limit); - + const where = { - formId + formId, }; if (status) { @@ -118,20 +118,20 @@ exports.getSubmissions = async (req, res) => { select: { id: true, name: true, - email: true - } - } + email: true, + }, + }, }, orderBy: { - submittedAt: 'desc' + submittedAt: 'desc', }, skip, - take: parseInt(limit) + take: parseInt(limit), }), - prisma.submission.count({ where }) + prisma.submission.count({ where }), ]); - res.json({ + res.json({ success: true, form, submissions, @@ -139,14 +139,14 @@ exports.getSubmissions = async (req, res) => { page: parseInt(page), limit: parseInt(limit), total, - pages: Math.ceil(total / parseInt(limit)) - } + pages: Math.ceil(total / parseInt(limit)), + }, }); } catch (err) { console.error('Get submissions error:', err); - res.status(500).json({ - success: false, - message: 'Server error getting submissions' + res.status(500).json({ + success: false, + message: 'Server error getting submissions', }); } }; @@ -160,8 +160,8 @@ exports.getSubmission = async (req, res) => { where: { id, form: { - userId - } + userId, + }, }, select: { id: true, @@ -176,35 +176,35 @@ exports.getSubmission = async (req, res) => { select: { id: true, name: true, - email: true - } + email: true, + }, }, form: { select: { id: true, title: true, - schema: true - } - } - } + schema: true, + }, + }, + }, }); if (!submission) { - return res.status(404).json({ - success: false, - message: 'Submission not found or you do not have access' + return res.status(404).json({ + success: false, + message: 'Submission not found or you do not have access', }); } - res.json({ - success: true, - submission + res.json({ + success: true, + submission, }); } catch (err) { console.error('Get submission error:', err); - res.status(500).json({ - success: false, - message: 'Server error getting submission' + res.status(500).json({ + success: false, + message: 'Server error getting submission', }); } -}; \ No newline at end of file +}; diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js index d036005a..325d81d2 100644 --- a/backend/controllers/userController.js +++ b/backend/controllers/userController.js @@ -21,16 +21,16 @@ exports.getMe = async (req, res) => { select: { id: true, name: true, - slug: true - } + slug: true, + }, }, _count: { select: { forms: true, - submissions: true - } - } - } + submissions: true, + }, + }, + }, }); if (!user) { @@ -54,15 +54,15 @@ exports.updateUser = async (req, res) => { data: { ...(name && { name }), ...(avatar && { avatar }), - updatedAt: new Date() + updatedAt: new Date(), }, select: { id: true, email: true, name: true, avatar: true, - updatedAt: true - } + updatedAt: true, + }, }); return success(res, { user }, 'User updated successfully'); @@ -80,13 +80,13 @@ exports.deleteUser = async (req, res) => { // 1. Send confirmation email // 2. Soft delete instead of hard delete // 3. Archive user data instead of deleting - + await prisma.user.update({ where: { id: userId }, - data: { + data: { isActive: false, - updatedAt: new Date() - } + updatedAt: new Date(), + }, }); return success(res, {}, 'User account deactivated successfully'); @@ -102,19 +102,19 @@ exports.getUserStats = async (req, res) => { try { const [formsCount, submissionsCount, publishedFormsCount] = await Promise.all([ prisma.form.count({ - where: { userId } + where: { userId }, }), prisma.submission.count({ where: { - form: { userId } - } + form: { userId }, + }, }), prisma.form.count({ - where: { + where: { userId, - isPublished: true - } - }) + isPublished: true, + }, + }), ]); // Get recent activity @@ -124,31 +124,31 @@ exports.getUserStats = async (req, res) => { id: true, title: true, isPublished: true, - updatedAt: true + updatedAt: true, }, orderBy: { - updatedAt: 'desc' + updatedAt: 'desc', }, - take: 5 + take: 5, }); const recentSubmissions = await prisma.submission.findMany({ where: { - form: { userId } + form: { userId }, }, select: { id: true, submittedAt: true, form: { select: { - title: true - } - } + title: true, + }, + }, }, orderBy: { - submittedAt: 'desc' + submittedAt: 'desc', }, - take: 5 + take: 5, }); return success(res, { @@ -157,11 +157,11 @@ exports.getUserStats = async (req, res) => { submissionsCount, publishedFormsCount, recentForms, - recentSubmissions - } + recentSubmissions, + }, }); } catch (err) { console.error('Get user stats error:', err); return error(res, 'Server error getting user stats', 500, err); } -}; \ No newline at end of file +}; diff --git a/backend/db/index.js b/backend/db/index.js index 223155d1..7c1771c3 100644 --- a/backend/db/index.js +++ b/backend/db/index.js @@ -11,4 +11,4 @@ const pool = new Pool({ module.exports = { query: (text, params) => pool.query(text, params), -}; \ No newline at end of file +}; diff --git a/backend/index.js b/backend/index.js index bf7f7bab..4922f2b0 100644 --- a/backend/index.js +++ b/backend/index.js @@ -11,15 +11,19 @@ const pinoHttp = require('pino-http'); const webSocketService = require('./services/websocket'); // Middleware -app.use(cors({ - origin: process.env.FRONTEND_URL || 'http://localhost:3000', - credentials: true, -})); +app.use( + cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true, + }), +); app.use(express.json()); app.use(cookieParser()); -app.use(pinoHttp({ - redact: ['req.headers.authorization', 'req.headers.cookie'], -})); +app.use( + pinoHttp({ + redact: ['req.headers.authorization', 'req.headers.cookie'], + }), +); // Health check app.get('/', (req, res) => { @@ -41,7 +45,7 @@ app.get('/health', (req, res) => { timestamp: new Date().toISOString(), uptime: process.uptime(), version: '2.0.0', - environment: process.env.NODE_ENV || 'development' + environment: process.env.NODE_ENV || 'development', }); }); @@ -98,4 +102,4 @@ server.listen(PORT, '0.0.0.0', () => { console.log(`๐Ÿ“Š API endpoints available at http://localhost:${PORT}/api/trpc`); console.log(`๐Ÿ”Œ WebSocket server ready for real-time connections`); console.log(`๐Ÿ“š Architecture documentation: /backend/ARCHITECTURE.md`); -}); \ No newline at end of file +}); diff --git a/backend/jobs/processors.js b/backend/jobs/processors.js index bfc280e7..9aaec25c 100644 --- a/backend/jobs/processors.js +++ b/backend/jobs/processors.js @@ -17,59 +17,58 @@ const path = require('path'); // LLM Analysis Job Processor analysisQueue.process('analyze-submission', async (job) => { const { submissionId, analysisTypes } = job.data; - + console.log(`Processing LLM analysis for submission ${submissionId}`); - + try { // Update job progress await job.progress(10); - + // Get submission with form info for WebSocket notification const submission = await prisma.submission.findUnique({ where: { id: submissionId }, include: { form: { select: { id: true } } }, }); - + // Update submission status await prisma.submission.update({ where: { id: submissionId }, data: { status: 'PROCESSING' }, }); - + await job.progress(20); - + // Perform LLM analysis - const results = await getLLMService().analyzeSubmission(submissionId, analysisTypes); - + const results = await getLLMService().analyzeSubmission(submissionId, analysisTypes); + await job.progress(80); - + // Update submission status await prisma.submission.update({ where: { id: submissionId }, data: { status: 'PROCESSED' }, }); - + await job.progress(100); - + // Notify connected clients via WebSocket webSocketService.notifyAnalysisComplete(submission.form.id, submissionId, results); - + console.log(`LLM analysis completed for submission ${submissionId}`); return { submissionId, analysisCount: results.length, completedAt: new Date(), }; - } catch (error) { console.error(`LLM analysis failed for submission ${submissionId}:`, error); - + // Update submission status to failed await prisma.submission.update({ where: { id: submissionId }, data: { status: 'PENDING' }, // Reset to pending for retry }); - + throw error; } }); @@ -77,37 +76,36 @@ analysisQueue.process('analyze-submission', async (job) => { // Email Notification Job Processor emailQueue.process('send-notification', async (job) => { const { type, recipient, data } = job.data; - + console.log(`Processing email notification: ${type} to ${recipient}`); - + try { await job.progress(10); - + // Here you would integrate with your email service (SendGrid, AWS SES, etc.) // For now, we'll simulate the email sending - + await job.progress(50); - + // Simulate email sending delay - await new Promise(resolve => setTimeout(resolve, 1000)); - + await new Promise((resolve) => setTimeout(resolve, 1000)); + await job.progress(90); - + // Log the email (in production, replace with actual email service) console.log(`Email sent: ${type} to ${recipient}`, { subject: data.subject, preview: data.content?.substring(0, 100) + '...', }); - + await job.progress(100); - + return { type, recipient, sentAt: new Date(), status: 'sent', }; - } catch (error) { console.error(`Email sending failed for ${type} to ${recipient}:`, error); throw error; @@ -117,22 +115,22 @@ emailQueue.process('send-notification', async (job) => { // Data Export Job Processor exportQueue.process('export-data', async (job) => { const { formId, format, userId, dateFrom, dateTo } = job.data; - + console.log(`Processing data export for form ${formId} in ${format} format`); - + try { await job.progress(10); - + // Fetch submissions const submissions = await prisma.submission.findMany({ where: { formId, - ...(dateFrom || dateTo) && { + ...((dateFrom || dateTo) && { submittedAt: { ...(dateFrom && { gte: new Date(dateFrom) }), ...(dateTo && { lte: new Date(dateTo) }), }, - }, + }), }, include: { form: { @@ -157,35 +155,41 @@ exportQueue.process('export-data', async (job) => { }, orderBy: { submittedAt: 'desc' }, }); - + await job.progress(30); - + // Prepare export data let exportData; let filename; let mimeType; - + if (format === 'json') { - exportData = JSON.stringify({ - form: { - id: formId, - title: submissions[0]?.form.title || 'Unknown Form', - exportedAt: new Date(), - totalSubmissions: submissions.length, + exportData = JSON.stringify( + { + form: { + id: formId, + title: submissions[0]?.form.title || 'Unknown Form', + exportedAt: new Date(), + totalSubmissions: submissions.length, + }, + submissions: submissions.map((sub) => ({ + id: sub.id, + submittedAt: sub.submittedAt, + status: sub.status, + source: sub.source, + data: sub.data, + submitter: sub.submitter + ? { + name: sub.submitter.name, + email: sub.submitter.email, + } + : null, + analyses: sub.analyses, + })), }, - submissions: submissions.map(sub => ({ - id: sub.id, - submittedAt: sub.submittedAt, - status: sub.status, - source: sub.source, - data: sub.data, - submitter: sub.submitter ? { - name: sub.submitter.name, - email: sub.submitter.email, - } : null, - analyses: sub.analyses, - })), - }, null, 2); + null, + 2, + ); filename = `form-${formId}-export-${Date.now()}.json`; mimeType = 'application/json'; } else if (format === 'csv') { @@ -194,18 +198,18 @@ exportQueue.process('export-data', async (job) => { filename = `form-${formId}-export-${Date.now()}.csv`; mimeType = 'text/csv'; } - + await job.progress(70); - + // Save export file const exportDir = path.join(process.cwd(), 'exports'); await fs.mkdir(exportDir, { recursive: true }); - + const filePath = path.join(exportDir, filename); await fs.writeFile(filePath, exportData); - + await job.progress(90); - + // Update background job record await prisma.backgroundJob.updateMany({ where: { @@ -228,9 +232,9 @@ exportQueue.process('export-data', async (job) => { completedAt: new Date(), }, }); - + await job.progress(100); - + console.log(`Data export completed: ${filename}`); return { formId, @@ -238,10 +242,9 @@ exportQueue.process('export-data', async (job) => { recordCount: submissions.length, completedAt: new Date(), }; - } catch (error) { console.error(`Data export failed for form ${formId}:`, error); - + // Update job status await prisma.backgroundJob.updateMany({ where: { @@ -257,7 +260,7 @@ exportQueue.process('export-data', async (job) => { error: error.message, }, }); - + throw error; } }); @@ -270,8 +273,8 @@ async function convertToCSV(submissions) { // Get all unique field keys from submissions const fieldKeys = new Set(); - submissions.forEach(sub => { - Object.keys(sub.data).forEach(key => fieldKeys.add(key)); + submissions.forEach((sub) => { + Object.keys(sub.data).forEach((key) => fieldKeys.add(key)); }); // Create CSV headers @@ -287,7 +290,7 @@ async function convertToCSV(submissions) { ]; // Create CSV rows - const rows = submissions.map(sub => { + const rows = submissions.map((sub) => { const row = [ sub.id, sub.submittedAt.toISOString(), @@ -298,7 +301,7 @@ async function convertToCSV(submissions) { ]; // Add field values - fieldKeys.forEach(key => { + fieldKeys.forEach((key) => { const value = sub.data[key]; if (typeof value === 'object') { row.push(JSON.stringify(value)); @@ -308,7 +311,7 @@ async function convertToCSV(submissions) { }); // Add analysis summary - const analysisTypes = sub.analyses.map(a => a.analysisType).join(', '); + const analysisTypes = sub.analyses.map((a) => a.analysisType).join(', '); row.push(analysisTypes || 'No analysis'); return row; @@ -316,7 +319,7 @@ async function convertToCSV(submissions) { // Combine headers and rows const csvContent = [headers, ...rows] - .map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(',')) + .map((row) => row.map((field) => `"${String(field).replace(/"/g, '""')}"`).join(',')) .join('\n'); return csvContent; @@ -362,4 +365,4 @@ module.exports = { analysisQueue, emailQueue, exportQueue, -}; \ No newline at end of file +}; diff --git a/backend/jobs/queue.js b/backend/jobs/queue.js index f2fbd684..4bcd1e7d 100644 --- a/backend/jobs/queue.js +++ b/backend/jobs/queue.js @@ -13,7 +13,7 @@ const redis = new Redis({ // Create job queues const analysisQueue = new Bull('llm-analysis', { redis: { - host: process.env.REDIS_HOST || 'localhost', + host: process.env.REDIS_HOST || 'localhost', port: process.env.REDIS_PORT || 6379, password: process.env.REDIS_PASSWORD, }, @@ -63,21 +63,25 @@ const exportQueue = new Bull('data-export', { class QueueManager { static async addAnalysisJob(submissionId, options = {}) { const { analysisTypes = ['SENTIMENT', 'CLASSIFICATION'], priority = 1, delay = 0 } = options; - - return analysisQueue.add('analyze-submission', { - submissionId, - analysisTypes, - timestamp: new Date(), - }, { - priority, - delay, - attempts: 3, - }); + + return analysisQueue.add( + 'analyze-submission', + { + submissionId, + analysisTypes, + timestamp: new Date(), + }, + { + priority, + delay, + attempts: 3, + }, + ); } static async addEmailJob(emailData, options = {}) { const { priority = 2, delay = 0 } = options; - + return emailQueue.add('send-notification', emailData, { priority, delay, @@ -87,7 +91,7 @@ class QueueManager { static async addExportJob(exportData, options = {}) { const { priority = 3, delay = 0 } = options; - + return exportQueue.add('export-data', exportData, { priority, delay, @@ -139,7 +143,7 @@ class QueueManager { // Queue cleanup static async cleanupQueues() { const olderThan = 24 * 60 * 60 * 1000; // 24 hours - + await Promise.all([ analysisQueue.clean(olderThan, 'completed'), analysisQueue.clean(olderThan, 'failed'), @@ -185,12 +189,8 @@ class QueueManager { // Graceful shutdown static async shutdown() { console.log('Shutting down queues gracefully...'); - - await Promise.all([ - analysisQueue.close(), - emailQueue.close(), - exportQueue.close(), - ]); + + await Promise.all([analysisQueue.close(), emailQueue.close(), exportQueue.close()]); await redis.disconnect(); console.log('All queues shut down successfully'); @@ -204,4 +204,4 @@ module.exports = { exportQueue, QueueManager, redis, -}; \ No newline at end of file +}; diff --git a/backend/lib/prisma.js b/backend/lib/prisma.js index 13a23dd7..a23f8992 100644 --- a/backend/lib/prisma.js +++ b/backend/lib/prisma.js @@ -7,4 +7,4 @@ if (process.env.NODE_ENV === 'development') { global.prisma = prisma; } -module.exports = prisma; \ No newline at end of file +module.exports = prisma; diff --git a/backend/lib/redis.js b/backend/lib/redis.js index 1bf0854e..e381159a 100644 --- a/backend/lib/redis.js +++ b/backend/lib/redis.js @@ -19,7 +19,3 @@ function getRedis() { } module.exports = getRedis(); - - - - diff --git a/backend/lib/trpc.js b/backend/lib/trpc.js index 6551babf..bae37cf5 100644 --- a/backend/lib/trpc.js +++ b/backend/lib/trpc.js @@ -44,4 +44,4 @@ module.exports = { prisma, z, TRPCError, -}; \ No newline at end of file +}; diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 11e7c02e..16343e1f 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -4,21 +4,22 @@ const prisma = require('../lib/prisma'); const auth = async (req, res, next) => { try { // Get token from header (support multiple formats) - let token = req.header('x-auth-token') || - req.header('Authorization')?.replace('Bearer ', '') || - req.cookies?.token; + let token = + req.header('x-auth-token') || + req.header('Authorization')?.replace('Bearer ', '') || + req.cookies?.token; // Check if no token if (!token) { - return res.status(401).json({ - success: false, - message: 'No token provided, authorization denied' + return res.status(401).json({ + success: false, + message: 'No token provided, authorization denied', }); } // Verify token const decoded = jwt.verify(token, process.env.JWT_SECRET); - + // Verify user still exists and is active const user = await prisma.user.findUnique({ where: { id: decoded.user.id }, @@ -27,14 +28,14 @@ const auth = async (req, res, next) => { email: true, name: true, role: true, - isActive: true - } + isActive: true, + }, }); if (!user || !user.isActive) { - return res.status(401).json({ - success: false, - message: 'User not found or inactive' + return res.status(401).json({ + success: false, + message: 'User not found or inactive', }); } @@ -43,26 +44,26 @@ const auth = async (req, res, next) => { next(); } catch (err) { console.error('Auth middleware error:', err); - + if (err.name === 'JsonWebTokenError') { - return res.status(401).json({ - success: false, - message: 'Invalid token' + return res.status(401).json({ + success: false, + message: 'Invalid token', }); } - + if (err.name === 'TokenExpiredError') { - return res.status(401).json({ - success: false, - message: 'Token expired' + return res.status(401).json({ + success: false, + message: 'Token expired', }); } - res.status(500).json({ - success: false, - message: 'Server error in authentication' + res.status(500).json({ + success: false, + message: 'Server error in authentication', }); } }; -module.exports = auth; \ No newline at end of file +module.exports = auth; diff --git a/backend/middleware/rateLimit.js b/backend/middleware/rateLimit.js index 570cce28..6a9a98a4 100644 --- a/backend/middleware/rateLimit.js +++ b/backend/middleware/rateLimit.js @@ -8,7 +8,3 @@ const authLimiter = rateLimit({ }); module.exports = { authLimiter }; - - - - diff --git a/backend/middleware/trpc.js b/backend/middleware/trpc.js index eae1574e..318cf3de 100644 --- a/backend/middleware/trpc.js +++ b/backend/middleware/trpc.js @@ -48,7 +48,7 @@ const trpcMiddleware = createExpressMiddleware({ createContext, onError: ({ error, type, path, input }) => { console.error(`tRPC Error [${type}] at ${path}:`, error); - + // Log input for debugging (be careful with sensitive data) if (process.env.NODE_ENV === 'development') { console.error('Input:', input); @@ -60,4 +60,4 @@ module.exports = { trpcMiddleware, createContext, appRouter, -}; \ No newline at end of file +}; diff --git a/backend/middleware/validate.js b/backend/middleware/validate.js index c823b53d..8be2a302 100644 --- a/backend/middleware/validate.js +++ b/backend/middleware/validate.js @@ -9,7 +9,9 @@ function validate(schema) { next(); } catch (err) { if (err instanceof ZodError) { - return res.status(400).json({ success: false, message: 'Validation failed', errors: err.errors }); + return res + .status(400) + .json({ success: false, message: 'Validation failed', errors: err.errors }); } return res.status(400).json({ success: false, message: 'Invalid request' }); } @@ -17,7 +19,3 @@ function validate(schema) { } module.exports = { validate }; - - - - diff --git a/backend/routes/audio.js b/backend/routes/audio.js index 0c59b854..b7e8c9ba 100644 --- a/backend/routes/audio.js +++ b/backend/routes/audio.js @@ -12,28 +12,35 @@ const storage = multer.diskStorage({ cb(null, uploadDir); }, filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); cb(null, `audio-${uniqueSuffix}${path.extname(file.originalname)}`); - } + }, }); const upload = multer({ storage, limits: { - fileSize: 25 * 1024 * 1024 // 25MB limit (Whisper API max) + fileSize: 25 * 1024 * 1024, // 25MB limit (Whisper API max) }, fileFilter: (req, file, cb) => { const allowedTypes = [ - 'audio/mpeg', 'audio/mp3', 'audio/mp4', 'audio/m4a', - 'audio/wav', 'audio/webm', 'audio/ogg', 'audio/flac', - 'audio/x-m4a', 'audio/x-wav' + 'audio/mpeg', + 'audio/mp3', + 'audio/mp4', + 'audio/m4a', + 'audio/wav', + 'audio/webm', + 'audio/ogg', + 'audio/flac', + 'audio/x-m4a', + 'audio/x-wav', ]; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error(`Invalid file type: ${file.mimetype}. Only audio files are allowed.`), false); } - } + }, }); /** @@ -48,7 +55,7 @@ router.post('/upload', auth, upload.single('audio'), async (req, res) => { if (!req.file) { return res.status(400).json({ success: false, - error: 'No audio file provided' + error: 'No audio file provided', }); } @@ -58,25 +65,20 @@ router.post('/upload', auth, upload.single('audio'), async (req, res) => { language: req.body.language, prompt: req.body.prompt, formId: req.body.formId, - organizationId: req.body.organizationId + organizationId: req.body.organizationId, }; - const result = await audioIngestionService.processAudioFile( - req.file, - userId, - options - ); + const result = await audioIngestionService.processAudioFile(req.file, userId, options); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Audio upload error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -94,26 +96,27 @@ router.post('/record', auth, async (req, res) => { if (!audioData) { return res.status(400).json({ success: false, - error: 'No audio data provided' + error: 'No audio data provided', }); } - const result = await audioIngestionService.processBase64Audio( - audioData, - userId, - { name, description, language, prompt, formId } - ); + const result = await audioIngestionService.processBase64Audio(audioData, userId, { + name, + description, + language, + prompt, + formId, + }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Audio record error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -132,19 +135,18 @@ router.get('/', auth, async (req, res) => { limit: parseInt(limit) || 50, offset: parseInt(offset) || 0, status, - formId + formId, }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get audio sources error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -163,14 +165,13 @@ router.get('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get audio source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -189,14 +190,13 @@ router.delete('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Delete audio source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -215,7 +215,7 @@ router.post('/:id/analyze', auth, async (req, res) => { if (!prompt) { return res.status(400).json({ success: false, - error: 'Analysis prompt is required' + error: 'Analysis prompt is required', }); } @@ -223,14 +223,13 @@ router.post('/:id/analyze', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Analyze transcription error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index d5ad8b38..21c6dfdd 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -9,12 +9,28 @@ const { authLimiter } = require('../middleware/rateLimit'); // @route POST api/auth/register // @desc Register a new user // @access Public -router.post('/register', authLimiter, validate({ body: z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().optional() }) }), authController.register); +router.post( + '/register', + authLimiter, + validate({ + body: z.object({ + email: z.string().email(), + password: z.string().min(8), + name: z.string().optional(), + }), + }), + authController.register, +); // @route POST api/auth/login // @desc Authenticate user & get token // @access Public -router.post('/login', authLimiter, validate({ body: z.object({ email: z.string().email(), password: z.string().min(1) }) }), authController.login); +router.post( + '/login', + authLimiter, + validate({ body: z.object({ email: z.string().email(), password: z.string().min(1) }) }), + authController.login, +); // @route POST api/auth/logout // @desc Logout user (clear cookies) @@ -41,4 +57,4 @@ router.post('/change-password', auth, authController.changePassword); // @access Private router.get('/verify', auth, authController.verifyToken); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/database.js b/backend/routes/database.js index 617ba694..e8e771a7 100644 --- a/backend/routes/database.js +++ b/backend/routes/database.js @@ -23,4 +23,4 @@ router.get('/:databaseId/predictive-insights', databaseController.getPredictiveI router.get('/analyses/history', databaseController.getAnalysisHistory); router.get('/analyses/:analysisId/export', databaseController.exportAnalysis); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/dbIngestion.js b/backend/routes/dbIngestion.js index ead80f06..29da8e29 100644 --- a/backend/routes/dbIngestion.js +++ b/backend/routes/dbIngestion.js @@ -20,7 +20,7 @@ router.post('/test-connection', auth, async (req, res) => { if (!type || !host || !database || !username) { return res.status(400).json({ success: false, - error: 'Missing required connection parameters: type, host, database, username' + error: 'Missing required connection parameters: type, host, database, username', }); } @@ -31,16 +31,15 @@ router.post('/test-connection', auth, async (req, res) => { database, username, password, - ssl + ssl, }); res.json(result); - } catch (error) { console.error('Test connection error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -57,7 +56,7 @@ router.post('/tables', auth, async (req, res) => { if (!type || !host || !database || !username) { return res.status(400).json({ success: false, - error: 'Missing required connection parameters' + error: 'Missing required connection parameters', }); } @@ -68,16 +67,15 @@ router.post('/tables', auth, async (req, res) => { database, username, password, - ssl + ssl, }); res.json(result); - } catch (error) { console.error('Get tables error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -94,19 +92,18 @@ router.post('/schema', auth, async (req, res) => { if (!connection || !tableName) { return res.status(400).json({ success: false, - error: 'Connection config and tableName are required' + error: 'Connection config and tableName are required', }); } const result = await externalDatabaseService.getTableSchema(connection, tableName); res.json(result); - } catch (error) { console.error('Get schema error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -124,24 +121,18 @@ router.post('/import', auth, async (req, res) => { if (!connection || !tableName) { return res.status(400).json({ success: false, - error: 'Connection config and tableName are required' + error: 'Connection config and tableName are required', }); } - const result = await externalDatabaseService.importData( - connection, - tableName, - options, - userId - ); + const result = await externalDatabaseService.importData(connection, tableName, options, userId); res.json(result); - } catch (error) { console.error('Import data error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -159,19 +150,18 @@ router.post('/query', auth, async (req, res) => { if (!connection || !query) { return res.status(400).json({ success: false, - error: 'Connection config and query are required' + error: 'Connection config and query are required', }); } const result = await externalDatabaseService.executeQuery(connection, query, userId); res.json(result); - } catch (error) { console.error('Execute query error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -188,16 +178,15 @@ router.get('/', auth, async (req, res) => { const result = await externalDatabaseService.getRecentImports(userId, { limit: parseInt(limit) || 20, - offset: parseInt(offset) || 0 + offset: parseInt(offset) || 0, }); res.json(result); - } catch (error) { console.error('Get recent imports error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -215,30 +204,30 @@ router.get('/supported', (req, res) => { type: 'postgresql', name: 'PostgreSQL', defaultPort: 5432, - supported: true + supported: true, }, { type: 'mysql', name: 'MySQL', defaultPort: 3306, supported: false, - comingSoon: true + comingSoon: true, }, { type: 'sqlite', name: 'SQLite', defaultPort: null, supported: false, - comingSoon: true + comingSoon: true, }, { type: 'mssql', name: 'Microsoft SQL Server', defaultPort: 1433, supported: false, - comingSoon: true - } - ] + comingSoon: true, + }, + ], }); }); diff --git a/backend/routes/forms.js b/backend/routes/forms.js index 21d66017..085355e2 100644 --- a/backend/routes/forms.js +++ b/backend/routes/forms.js @@ -33,4 +33,4 @@ router.put('/:id', auth, formController.updateForm); // @access Private router.patch('/:id/publish', auth, formController.publishForm); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/images.js b/backend/routes/images.js index 2ef28425..876537e4 100644 --- a/backend/routes/images.js +++ b/backend/routes/images.js @@ -12,28 +12,36 @@ const storage = multer.diskStorage({ cb(null, uploadDir); }, filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); cb(null, `image-${uniqueSuffix}${path.extname(file.originalname)}`); - } + }, }); const upload = multer({ storage, limits: { - fileSize: 20 * 1024 * 1024 // 20MB limit + fileSize: 20 * 1024 * 1024, // 20MB limit }, fileFilter: (req, file, cb) => { const allowedTypes = [ - 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', - 'image/webp', 'image/bmp', 'image/tiff', - 'application/pdf' + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/bmp', + 'image/tiff', + 'application/pdf', ]; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { - cb(new Error(`Invalid file type: ${file.mimetype}. Only images and PDFs are allowed.`), false); + cb( + new Error(`Invalid file type: ${file.mimetype}. Only images and PDFs are allowed.`), + false, + ); } - } + }, }); /** @@ -48,7 +56,7 @@ router.post('/upload', auth, upload.single('image'), async (req, res) => { if (!req.file) { return res.status(400).json({ success: false, - error: 'No image file provided' + error: 'No image file provided', }); } @@ -58,25 +66,20 @@ router.post('/upload', auth, upload.single('image'), async (req, res) => { documentType: req.body.documentType || 'auto', extractionPrompt: req.body.extractionPrompt, formId: req.body.formId, - organizationId: req.body.organizationId + organizationId: req.body.organizationId, }; - const result = await imageIngestionService.processImageFile( - req.file, - userId, - options - ); + const result = await imageIngestionService.processImageFile(req.file, userId, options); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Image upload error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -94,26 +97,27 @@ router.post('/capture', auth, async (req, res) => { if (!imageData) { return res.status(400).json({ success: false, - error: 'No image data provided' + error: 'No image data provided', }); } - const result = await imageIngestionService.processBase64Image( - imageData, - userId, - { name, description, documentType, extractionPrompt, formId } - ); + const result = await imageIngestionService.processBase64Image(imageData, userId, { + name, + description, + documentType, + extractionPrompt, + formId, + }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Image capture error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -133,19 +137,18 @@ router.get('/', auth, async (req, res) => { offset: parseInt(offset) || 0, status, formId, - documentType + documentType, }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get image sources error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -164,14 +167,13 @@ router.get('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get image source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -190,14 +192,13 @@ router.delete('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Delete image source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -216,7 +217,7 @@ router.post('/:id/reanalyze', auth, async (req, res) => { if (!prompt) { return res.status(400).json({ success: false, - error: 'Analysis prompt is required' + error: 'Analysis prompt is required', }); } @@ -224,14 +225,13 @@ router.post('/:id/reanalyze', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Reanalyze image error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -253,11 +253,11 @@ router.get('/meta/types', (req, res) => { { id: 'id_card', name: 'ID Card', description: 'Identity documents' }, { id: 'business_card', name: 'Business Card', description: 'Contact cards' }, { id: 'document', name: 'Document', description: 'General documents' }, - { id: 'photo', name: 'Photo', description: 'Photographs and images' } + { id: 'photo', name: 'Photo', description: 'Photographs and images' }, ], supportedFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'pdf'], - maxFileSize: '20MB' - } + maxFileSize: '20MB', + }, }); }); diff --git a/backend/routes/submissions.js b/backend/routes/submissions.js index 3f872ea6..30ae8435 100644 --- a/backend/routes/submissions.js +++ b/backend/routes/submissions.js @@ -18,4 +18,4 @@ router.get('/form/:formId', auth, submissionController.getSubmissions); // @access Private router.get('/:id', auth, submissionController.getSubmission); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/user.js b/backend/routes/user.js index a6c78a60..d0a94e0b 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -23,4 +23,4 @@ router.delete('/me', auth, userController.deleteUser); // @access Private router.get('/stats', auth, userController.getUserStats); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/routes/videos.js b/backend/routes/videos.js index 2a81a17f..e1890b66 100644 --- a/backend/routes/videos.js +++ b/backend/routes/videos.js @@ -12,27 +12,33 @@ const storage = multer.diskStorage({ cb(null, uploadDir); }, filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); cb(null, `video-${uniqueSuffix}${path.extname(file.originalname)}`); - } + }, }); const upload = multer({ storage, limits: { - fileSize: 500 * 1024 * 1024 // 500MB limit + fileSize: 500 * 1024 * 1024, // 500MB limit }, fileFilter: (req, file, cb) => { const allowedTypes = [ - 'video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska', - 'video/webm', 'video/x-flv', 'video/x-m4v', 'video/x-ms-wmv' + 'video/mp4', + 'video/quicktime', + 'video/x-msvideo', + 'video/x-matroska', + 'video/webm', + 'video/x-flv', + 'video/x-m4v', + 'video/x-ms-wmv', ]; if (allowedTypes.includes(file.mimetype) || file.mimetype.startsWith('video/')) { cb(null, true); } else { cb(new Error(`Invalid file type: ${file.mimetype}. Only video files are allowed.`), false); } - } + }, }); /** @@ -47,7 +53,7 @@ router.post('/upload', auth, upload.single('video'), async (req, res) => { if (!req.file) { return res.status(400).json({ success: false, - error: 'No video file provided' + error: 'No video file provided', }); } @@ -59,25 +65,20 @@ router.post('/upload', auth, upload.single('video'), async (req, res) => { transcribeAudio: req.body.transcribeAudio !== 'false', analyzeFrames: req.body.analyzeFrames !== 'false', formId: req.body.formId, - organizationId: req.body.organizationId + organizationId: req.body.organizationId, }; - const result = await videoIngestionService.processVideoFile( - req.file, - userId, - options - ); + const result = await videoIngestionService.processVideoFile(req.file, userId, options); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Video upload error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -95,19 +96,18 @@ router.get('/', auth, async (req, res) => { const result = await videoIngestionService.getUserVideoSources(userId, { limit: parseInt(limit) || 50, offset: parseInt(offset) || 0, - status + status, }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get video sources error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -126,14 +126,13 @@ router.get('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get video source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -152,14 +151,13 @@ router.delete('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Delete video source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -181,15 +179,15 @@ router.get('/meta/info', async (req, res) => { features: { frameExtraction: hasFFmpeg, audioTranscription: true, - visualAnalysis: true + visualAnalysis: true, }, defaultSettings: { frameInterval: 5, maxFrames: 10, transcribeAudio: true, - analyzeFrames: true - } - } + analyzeFrames: true, + }, + }, }); }); diff --git a/backend/routes/websites.js b/backend/routes/websites.js index d3c31244..fe24712f 100644 --- a/backend/routes/websites.js +++ b/backend/routes/websites.js @@ -21,13 +21,13 @@ router.post('/scrape', auth, async (req, res) => { screenshot = false, customPrompt, formId, - organizationId + organizationId, } = req.body; if (!url) { return res.status(400).json({ success: false, - error: 'URL is required' + error: 'URL is required', }); } @@ -39,20 +39,23 @@ router.post('/scrape', auth, async (req, res) => { screenshot, customPrompt, formId, - organizationId + organizationId, }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Website scrape error:', error); - res.status(error.message.includes('Invalid URL') || error.message.includes('not allowed') ? 400 : 500).json({ - success: false, - error: error.message - }); + res + .status( + error.message.includes('Invalid URL') || error.message.includes('not allowed') ? 400 : 500, + ) + .json({ + success: false, + error: error.message, + }); } }); @@ -70,14 +73,14 @@ router.post('/batch', auth, async (req, res) => { if (!urls || !Array.isArray(urls) || urls.length === 0) { return res.status(400).json({ success: false, - error: 'URLs array is required' + error: 'URLs array is required', }); } if (urls.length > 10) { return res.status(400).json({ success: false, - error: 'Maximum 10 URLs per batch' + error: 'Maximum 10 URLs per batch', }); } @@ -87,17 +90,16 @@ router.post('/batch', auth, async (req, res) => { success: true, data: { total: urls.length, - successful: results.filter(r => r.success).length, - failed: results.filter(r => !r.success).length, - results - } + successful: results.filter((r) => r.success).length, + failed: results.filter((r) => !r.success).length, + results, + }, }); - } catch (error) { console.error('Batch scrape error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -115,19 +117,18 @@ router.get('/', auth, async (req, res) => { const result = await websiteIngestionService.getUserWebsiteSources(userId, { limit: parseInt(limit) || 50, offset: parseInt(offset) || 0, - status + status, }); res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get website sources error:', error); res.status(500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -146,14 +147,13 @@ router.get('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Get website source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -172,14 +172,13 @@ router.delete('/:id', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Delete website source error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -199,14 +198,13 @@ router.post('/:id/rescrape', auth, async (req, res) => { res.json({ success: true, - data: result + data: result, }); - } catch (error) { console.error('Rescrape URL error:', error); res.status(error.message.includes('not found') ? 404 : 500).json({ success: false, - error: error.message + error: error.message, }); } }); @@ -226,15 +224,15 @@ router.get('/meta/info', async (req, res) => { customSelectors: true, batchScraping: true, structuredExtraction: true, - aiAnalysis: true + aiAnalysis: true, }, limits: { maxBatchSize: 10, maxContentLength: '100KB', - timeout: '30s' + timeout: '30s', }, - blockedDomains: ['localhost', '127.0.0.1', 'private networks'] - } + blockedDomains: ['localhost', '127.0.0.1', 'private networks'], + }, }); }); diff --git a/backend/services/aiAnalysisService.js b/backend/services/aiAnalysisService.js index 9c4d48b7..5cf174bb 100644 --- a/backend/services/aiAnalysisService.js +++ b/backend/services/aiAnalysisService.js @@ -8,7 +8,7 @@ const OpenAI = require('openai'); class AIAnalysisService { constructor() { this.openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY + apiKey: process.env.OPENAI_API_KEY, }); } @@ -20,7 +20,7 @@ class AIAnalysisService { // Get form and submissions data const formData = await this.getFormDataForAnalysis(formId, userId); - + if (!formData) { throw new Error('Database not found or access denied'); } @@ -33,23 +33,23 @@ class AIAnalysisService { prompt: query, model, userId, - status: 'PROCESSING' - } + status: 'PROCESSING', + }, }); try { const startTime = Date.now(); - + // Prepare data context for AI const dataContext = this.prepareDataContext(formData); - + // Generate AI prompt based on analysis type const systemPrompt = this.generateSystemPrompt(analysisType, formData.schema); const userPrompt = this.generateUserPrompt(query, dataContext); // Call AI model const aiResponse = await this.callAIModel(model, systemPrompt, userPrompt); - + const processingTime = Date.now() - startTime; // Update analysis with results @@ -58,8 +58,8 @@ class AIAnalysisService { data: { result: aiResponse, processingTime, - status: 'COMPLETED' - } + status: 'COMPLETED', + }, }); return { @@ -70,18 +70,17 @@ class AIAnalysisService { databaseInfo: { name: formData.title, recordCount: formData.submissions.length, - fields: Object.keys(formData.schema) - } + fields: Object.keys(formData.schema), + }, }; - } catch (error) { // Update analysis with error await prisma.lLMAnalysis.update({ where: { id: analysis.id }, data: { status: 'FAILED', - error: error.message - } + error: error.message, + }, }); throw error; @@ -93,7 +92,7 @@ class AIAnalysisService { */ async generateInsights(formId, userId, insightType = 'comprehensive') { const formData = await this.getFormDataForAnalysis(formId, userId); - + if (!formData || formData.submissions.length === 0) { throw new Error('Insufficient data for analysis'); } @@ -101,7 +100,7 @@ class AIAnalysisService { const insights = await this.analyzeDatabase(formId, userId, { query: this.getInsightPrompt(insightType, formData), analysisType: 'SUMMARY', - model: 'gpt-4' + model: 'gpt-4', }); return insights; @@ -118,7 +117,7 @@ class AIAnalysisService { where: { userId, isPublished: true, - ...(databases.length > 0 && { id: { in: databases } }) + ...(databases.length > 0 && { id: { in: databases } }), }, select: { id: true, @@ -126,15 +125,15 @@ class AIAnalysisService { description: true, schema: true, _count: { - select: { submissions: true } - } - } + select: { submissions: true }, + }, + }, }); const searchResults = { query: searchQuery, databases: [], - overallInsights: null + overallInsights: null, }; // Search each database @@ -145,7 +144,7 @@ class AIAnalysisService { const analysis = await this.analyzeDatabase(form.id, userId, { query: `Search and analyze this database for: "${searchQuery}". Provide relevant matches and insights.`, analysisType: 'EXTRACTION', - model: 'gpt-4' + model: 'gpt-4', }); searchResults.databases.push({ @@ -153,9 +152,8 @@ class AIAnalysisService { databaseName: form.title, recordCount: form._count.submissions, analysis: analysis.result, - relevanceScore: this.calculateDatabaseRelevance(searchQuery, form, analysis.result) + relevanceScore: this.calculateDatabaseRelevance(searchQuery, form, analysis.result), }); - } catch (error) { console.error(`Search failed for database ${form.id}:`, error.message); } @@ -164,8 +162,8 @@ class AIAnalysisService { // Generate overall insights if requested if (includeInsights && searchResults.databases.length > 0) { searchResults.overallInsights = await this.generateCrossDatabaseInsights( - searchQuery, - searchResults.databases + searchQuery, + searchResults.databases, ); } @@ -180,7 +178,7 @@ class AIAnalysisService { */ async analyzePredictiveInsights(formId, userId) { const formData = await this.getFormDataForAnalysis(formId, userId); - + if (!formData || formData.submissions.length < 10) { throw new Error('Need at least 10 submissions for predictive analysis'); } @@ -193,13 +191,13 @@ class AIAnalysisService { 4. Data quality issues 5. Recommendations for optimization`, analysisType: 'SUMMARY', - model: 'gpt-4' + model: 'gpt-4', }); return { ...analysis, type: 'predictive_insights', - recommendations: await this.generateFormOptimizationSuggestions(formData) + recommendations: await this.generateFormOptimizationSuggestions(formData), }; } @@ -210,16 +208,16 @@ class AIAnalysisService { const analysis = await prisma.lLMAnalysis.findFirst({ where: { id: analysisId, - userId + userId, }, include: { form: { select: { title: true, - description: true - } - } - } + description: true, + }, + }, + }, }); if (!analysis) { @@ -235,7 +233,7 @@ class AIAnalysisService { model: analysis.model, processingTime: analysis.processingTime, confidence: analysis.confidence, - createdAt: analysis.createdAt + createdAt: analysis.createdAt, }; switch (format.toLowerCase()) { @@ -254,7 +252,7 @@ class AIAnalysisService { const form = await prisma.form.findFirst({ where: { id: formId, - userId + userId, }, select: { id: true, @@ -266,28 +264,29 @@ class AIAnalysisService { id: true, data: true, submittedAt: true, - status: true + status: true, }, orderBy: { - submittedAt: 'desc' - } - } - } + submittedAt: 'desc', + }, + }, + }, }); if (!form) return null; return { ...form, - schema: form.schema.fields ? - form.schema.fields.reduce((acc, field) => { - acc[field.name] = { - type: field.type, - label: field.label, - required: field.required - }; - return acc; - }, {}) : {} + schema: form.schema.fields + ? form.schema.fields.reduce((acc, field) => { + acc[field.name] = { + type: field.type, + label: field.label, + required: field.required, + }; + return acc; + }, {}) + : {}, }; } @@ -297,8 +296,8 @@ class AIAnalysisService { description: formData.description, schema: formData.schema, recordCount: formData.submissions.length, - sampleRecords: formData.submissions.slice(0, 10).map(s => s.data), - submissionDates: formData.submissions.map(s => s.submittedAt) + sampleRecords: formData.submissions.slice(0, 10).map((s) => s.data), + submissionDates: formData.submissions.map((s) => s.submittedAt), }; } @@ -315,11 +314,11 @@ Instructions: - Be concise but thorough`; const typeSpecificPrompts = { - 'SENTIMENT': 'Focus on emotional tone and sentiment analysis of text responses.', - 'CLASSIFICATION': 'Categorize and classify the data into meaningful groups.', - 'EXTRACTION': 'Extract specific information and data points as requested.', - 'SUMMARY': 'Provide comprehensive summaries and key insights.', - 'CUSTOM': 'Follow the specific analysis request provided by the user.' + SENTIMENT: 'Focus on emotional tone and sentiment analysis of text responses.', + CLASSIFICATION: 'Categorize and classify the data into meaningful groups.', + EXTRACTION: 'Extract specific information and data points as requested.', + SUMMARY: 'Provide comprehensive summaries and key insights.', + CUSTOM: 'Follow the specific analysis request provided by the user.', }; return `${basePrompt}\n\nSpecific Focus: ${typeSpecificPrompts[analysisType] || typeSpecificPrompts.CUSTOM}`; @@ -346,18 +345,17 @@ Please analyze this data and respond to the query above.`; model: model === 'gpt-4' ? 'gpt-4-turbo-preview' : 'gpt-3.5-turbo', messages: [ { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt } + { role: 'user', content: userPrompt }, ], temperature: 0.3, - max_tokens: 2000 + max_tokens: 2000, }); return { content: response.choices[0].message.content, usage: response.usage, - model: response.model + model: response.model, }; - } catch (error) { console.error('AI model call failed:', error); throw new Error(`AI analysis failed: ${error.message}`); @@ -366,22 +364,22 @@ Please analyze this data and respond to the query above.`; getInsightPrompt(insightType, formData) { const prompts = { - 'comprehensive': `Analyze this database comprehensively and provide: + comprehensive: `Analyze this database comprehensively and provide: 1. Key trends and patterns in the data 2. Notable correlations between fields 3. Data quality assessment 4. Completion rate analysis 5. Actionable recommendations for improvement`, - - 'completion_analysis': `Focus on analyzing form completion patterns: + + completion_analysis: `Focus on analyzing form completion patterns: 1. Which fields have the highest/lowest completion rates? 2. Where do users typically drop off? 3. How can we improve completion rates?`, - - 'trend_analysis': `Analyze temporal trends in the submissions: + + trend_analysis: `Analyze temporal trends in the submissions: 1. How has submission volume changed over time? 2. Are there patterns by day/time/season? - 3. What trends can we predict for the future?` + 3. What trends can we predict for the future?`, }; return prompts[insightType] || prompts.comprehensive; @@ -390,21 +388,21 @@ Please analyze this data and respond to the query above.`; calculateDatabaseRelevance(query, form, analysisResult) { let score = 0; const queryLower = query.toLowerCase(); - + // Title relevance if (form.title.toLowerCase().includes(queryLower)) score += 10; - + // Description relevance if (form.description?.toLowerCase().includes(queryLower)) score += 5; - + // Schema field relevance if (form.schema.fields) { - form.schema.fields.forEach(field => { + form.schema.fields.forEach((field) => { if (field.label.toLowerCase().includes(queryLower)) score += 3; if (field.name.toLowerCase().includes(queryLower)) score += 2; }); } - + // Analysis result relevance (simple keyword matching) if (analysisResult.content && analysisResult.content.toLowerCase().includes(queryLower)) { score += 15; @@ -414,21 +412,22 @@ Please analyze this data and respond to the query above.`; } async generateCrossDatabaseInsights(query, databaseResults) { - const combinedContext = databaseResults.map(db => ({ + const combinedContext = databaseResults.map((db) => ({ name: db.databaseName, records: db.recordCount, - insights: db.analysis.content + insights: db.analysis.content, })); try { - const response = await this.callAIModel('gpt-4', + const response = await this.callAIModel( + 'gpt-4', 'You are analyzing insights across multiple databases. Identify patterns, correlations, and insights that span across the different databases.', `Original Query: ${query} Database Results: ${JSON.stringify(combinedContext, null, 2)} - Provide cross-database insights, patterns, and recommendations.` + Provide cross-database insights, patterns, and recommendations.`, ); return response.content; @@ -441,14 +440,14 @@ Please analyze this data and respond to the query above.`; async generateFormOptimizationSuggestions(formData) { // Analyze completion rates, field types, and patterns const fieldAnalysis = {}; - - formData.submissions.forEach(submission => { + + formData.submissions.forEach((submission) => { Object.entries(formData.schema).forEach(([fieldName, fieldInfo]) => { if (!fieldAnalysis[fieldName]) { fieldAnalysis[fieldName] = { total: 0, completed: 0, - fieldInfo + fieldInfo, }; } fieldAnalysis[fieldName].total++; @@ -459,16 +458,16 @@ Please analyze this data and respond to the query above.`; }); const suggestions = []; - + Object.entries(fieldAnalysis).forEach(([fieldName, analysis]) => { const completionRate = analysis.completed / analysis.total; - + if (completionRate < 0.7) { suggestions.push({ field: fieldName, issue: 'Low completion rate', recommendation: `Consider making ${fieldName} optional or simplifying the input`, - priority: 'high' + priority: 'high', }); } }); @@ -481,7 +480,7 @@ Please analyze this data and respond to the query above.`; return { data, mimeType: 'application/pdf', - filename: `analysis_${data.analysisId}.pdf` + filename: `analysis_${data.analysisId}.pdf`, }; } @@ -490,9 +489,9 @@ Please analyze this data and respond to the query above.`; return { data, mimeType: 'text/csv', - filename: `analysis_${data.analysisId}.csv` + filename: `analysis_${data.analysisId}.csv`, }; } } -module.exports = new AIAnalysisService(); \ No newline at end of file +module.exports = new AIAnalysisService(); diff --git a/backend/services/audioIngestionService.js b/backend/services/audioIngestionService.js index faffdca8..a32ed326 100644 --- a/backend/services/audioIngestionService.js +++ b/backend/services/audioIngestionService.js @@ -14,7 +14,7 @@ const mkdir = promisify(fs.mkdir); class AudioIngestionService { constructor() { this.openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY + apiKey: process.env.OPENAI_API_KEY, }); this.uploadDir = process.env.UPLOAD_DIR || path.join(__dirname, '../uploads/audio'); this.supportedFormats = ['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm', 'ogg', 'flac']; @@ -46,7 +46,7 @@ class AudioIngestionService { prompt = null, // Context prompt to improve transcription formId = null, organizationId = null, - responseFormat = 'verbose_json' + responseFormat = 'verbose_json', } = options; // Validate file @@ -67,19 +67,19 @@ class AudioIngestionService { language, prompt, responseFormat, - uploadedAt: new Date().toISOString() + uploadedAt: new Date().toISOString(), }, userId, organizationId, - formId - } + formId, + }, }); try { // Update status to processing await prisma.dataSource.update({ where: { id: dataSource.id }, - data: { status: 'PROCESSING' } + data: { status: 'PROCESSING' }, }); const startTime = Date.now(); @@ -88,7 +88,7 @@ class AudioIngestionService { const transcription = await this.transcribeAudio(file.path, { language, prompt, - responseFormat + responseFormat, }); const processingTime = Date.now() - startTime; @@ -107,13 +107,13 @@ class AudioIngestionService { segments: transcription.segments || [], words: transcription.words || [], language: transcription.language, - duration: transcription.duration + duration: transcription.duration, }, processingTime, aiModel: 'whisper-1', confidence: this.calculateConfidence(transcription), - processedAt: new Date() - } + processedAt: new Date(), + }, }); return { @@ -125,17 +125,16 @@ class AudioIngestionService { segments: transcription.segments, extractedData, processingTime, - confidence: updatedDataSource.confidence + confidence: updatedDataSource.confidence, }; - } catch (error) { // Update status to failed await prisma.dataSource.update({ where: { id: dataSource.id }, data: { status: 'FAILED', - error: error.message - } + error: error.message, + }, }); throw error; @@ -151,7 +150,7 @@ class AudioIngestionService { const transcriptionParams = { file: fs.createReadStream(filePath), model: 'whisper-1', - response_format: responseFormat + response_format: responseFormat, }; if (language) { @@ -186,15 +185,15 @@ class AudioIngestionService { - actionItems: Array of action items or tasks mentioned - questions: Array of questions asked - decisions: Array of decisions made - - type: Type of audio (meeting, interview, voice_note, lecture, conversation, other)` + - type: Type of audio (meeting, interview, voice_note, lecture, conversation, other)`, }, { role: 'user', - content: transcription.text - } + content: transcription.text, + }, ], temperature: 0.3, - response_format: { type: 'json_object' } + response_format: { type: 'json_object' }, }); return JSON.parse(response.choices[0].message.content); @@ -203,7 +202,7 @@ class AudioIngestionService { return { summary: null, topics: [], - error: 'Failed to extract structured data' + error: 'Failed to extract structured data', }; } } @@ -217,9 +216,10 @@ class AudioIngestionService { // Adjust based on available data if (transcription.segments && transcription.segments.length > 0) { // Calculate average segment confidence if available - const avgSegmentConfidence = transcription.segments.reduce((sum, seg) => { - return sum + (seg.avg_logprob ? Math.exp(seg.avg_logprob) : 0.8); - }, 0) / transcription.segments.length; + const avgSegmentConfidence = + transcription.segments.reduce((sum, seg) => { + return sum + (seg.avg_logprob ? Math.exp(seg.avg_logprob) : 0.8); + }, 0) / transcription.segments.length; confidence = Math.min(avgSegmentConfidence, 0.99); } @@ -248,7 +248,9 @@ class AudioIngestionService { // Check file format const ext = path.extname(file.originalname).toLowerCase().slice(1); if (!this.supportedFormats.includes(ext)) { - throw new Error(`Unsupported audio format: ${ext}. Supported formats: ${this.supportedFormats.join(', ')}`); + throw new Error( + `Unsupported audio format: ${ext}. Supported formats: ${this.supportedFormats.join(', ')}`, + ); } } @@ -276,7 +278,7 @@ class AudioIngestionService { 'audio/ogg': 'ogg', 'audio/mp4': 'm4a', 'audio/m4a': 'm4a', - 'audio/flac': 'flac' + 'audio/flac': 'flac', }; const ext = mimeToExt[mimeType] || 'webm'; @@ -291,7 +293,7 @@ class AudioIngestionService { originalname: options.name || filename, mimetype: mimeType, size: buffer.length, - path: filePath + path: filePath, }; try { @@ -314,7 +316,7 @@ class AudioIngestionService { const where = { userId, - type: 'AUDIO' + type: 'AUDIO', }; if (status) { @@ -343,17 +345,17 @@ class AudioIngestionService { processingTime: true, confidence: true, createdAt: true, - processedAt: true - } + processedAt: true, + }, }), - prisma.dataSource.count({ where }) + prisma.dataSource.count({ where }), ]); return { dataSources, total, limit, - offset + offset, }; } @@ -365,11 +367,11 @@ class AudioIngestionService { where: { id, userId, - type: 'AUDIO' + type: 'AUDIO', }, include: { - analyses: true - } + analyses: true, + }, }); if (!dataSource) { @@ -387,8 +389,8 @@ class AudioIngestionService { where: { id, userId, - type: 'AUDIO' - } + type: 'AUDIO', + }, }); if (!dataSource) { @@ -405,7 +407,7 @@ class AudioIngestionService { } await prisma.dataSource.delete({ - where: { id } + where: { id }, }); return { success: true }; @@ -428,15 +430,16 @@ class AudioIngestionService { messages: [ { role: 'system', - content: 'You are an AI analyst. Analyze the following audio transcription based on the user\'s request. Provide detailed, actionable insights.' + content: + "You are an AI analyst. Analyze the following audio transcription based on the user's request. Provide detailed, actionable insights.", }, { role: 'user', - content: `Transcription:\n${dataSource.extractedText}\n\nAnalysis Request:\n${analysisPrompt}` - } + content: `Transcription:\n${dataSource.extractedText}\n\nAnalysis Request:\n${analysisPrompt}`, + }, ], temperature: 0.4, - max_tokens: 2000 + max_tokens: 2000, }); const processingTime = Date.now() - startTime; @@ -449,18 +452,18 @@ class AudioIngestionService { prompt: analysisPrompt, result: { content: response.choices[0].message.content, - usage: response.usage + usage: response.usage, }, model: 'gpt-4-turbo-preview', processingTime, - status: 'COMPLETED' - } + status: 'COMPLETED', + }, }); return { analysisId: analysis.id, result: response.choices[0].message.content, - processingTime + processingTime, }; } } diff --git a/backend/services/databaseService.js b/backend/services/databaseService.js index 4364c2b2..e71f986b 100644 --- a/backend/services/databaseService.js +++ b/backend/services/databaseService.js @@ -5,7 +5,6 @@ const prisma = require('../lib/prisma'); * Treats each form as a database table, submissions as records */ class DatabaseService { - /** * Get all "databases" (forms) with their record counts and schema info */ @@ -13,7 +12,7 @@ class DatabaseService { const whereClause = { userId, isPublished: true, - ...(organizationId && { organizationId }) + ...(organizationId && { organizationId }), }; const databases = await prisma.form.findMany({ @@ -27,24 +26,24 @@ class DatabaseService { updatedAt: true, _count: { select: { - submissions: true - } - } + submissions: true, + }, + }, }, orderBy: { - updatedAt: 'desc' - } + updatedAt: 'desc', + }, }); // Transform schema to provide database-like metadata - return databases.map(db => ({ + return databases.map((db) => ({ id: db.id, name: db.title, description: db.description, tableSchema: this.extractTableSchema(db.schema), recordCount: db._count.submissions, createdAt: db.createdAt, - updatedAt: db.updatedAt + updatedAt: db.updatedAt, })); } @@ -52,19 +51,25 @@ class DatabaseService { * Get all records (submissions) from a specific database (form) */ async getDatabaseRecords(formId, userId, options = {}) { - const { page = 1, limit = 50, filters = {}, sort = 'submittedAt', sortOrder = 'desc' } = options; - + const { + page = 1, + limit = 50, + filters = {}, + sort = 'submittedAt', + sortOrder = 'desc', + } = options; + // Verify user has access to this form const form = await prisma.form.findFirst({ where: { id: formId, - userId + userId, }, select: { id: true, title: true, - schema: true - } + schema: true, + }, }); if (!form) { @@ -72,11 +77,11 @@ class DatabaseService { } const skip = (page - 1) * limit; - + // Build filter conditions based on form data const whereClause = { formId, - ...this.buildFilterConditions(filters) + ...this.buildFilterConditions(filters), }; const [records, total] = await Promise.all([ @@ -91,40 +96,40 @@ class DatabaseService { submitter: { select: { name: true, - email: true - } - } + email: true, + }, + }, }, orderBy: { - [sort]: sortOrder + [sort]: sortOrder, }, skip, - take: limit + take: limit, }), prisma.submission.count({ - where: whereClause - }) + where: whereClause, + }), ]); return { databaseName: form.title, schema: this.extractTableSchema(form.schema), - records: records.map(record => ({ + records: records.map((record) => ({ id: record.id, ...record.data, _metadata: { submittedAt: record.submittedAt, updatedAt: record.updatedAt, status: record.status, - submittedBy: record.submitter - } + submittedBy: record.submitter, + }, })), pagination: { page, limit, total, - pages: Math.ceil(total / limit) - } + pages: Math.ceil(total / limit), + }, }; } @@ -134,19 +139,19 @@ class DatabaseService { */ async searchDatabases(userId, query, options = {}) { const { databases = [], fields = [], limit = 20 } = options; - + // Get user's forms to search within const userForms = await prisma.form.findMany({ where: { userId, isPublished: true, - ...(databases.length > 0 && { id: { in: databases } }) + ...(databases.length > 0 && { id: { in: databases } }), }, select: { id: true, title: true, - schema: true - } + schema: true, + }, }); const searchResults = []; @@ -154,31 +159,31 @@ class DatabaseService { for (const form of userForms) { // Build search conditions based on query const searchConditions = this.buildSearchConditions(query, form.schema, fields); - + if (searchConditions.length > 0) { const matches = await prisma.submission.findMany({ where: { formId: form.id, - OR: searchConditions + OR: searchConditions, }, select: { id: true, data: true, - submittedAt: true + submittedAt: true, }, - take: limit + take: limit, }); if (matches.length > 0) { searchResults.push({ databaseId: form.id, databaseName: form.title, - matches: matches.map(match => ({ + matches: matches.map((match) => ({ id: match.id, data: match.data, submittedAt: match.submittedAt, - relevanceScore: this.calculateRelevanceScore(query, match.data) - })) + relevanceScore: this.calculateRelevanceScore(query, match.data), + })), }); } } @@ -188,7 +193,7 @@ class DatabaseService { query, totalDatabases: searchResults.length, totalMatches: searchResults.reduce((sum, db) => sum + db.matches.length, 0), - results: searchResults + results: searchResults, }; } @@ -199,14 +204,14 @@ class DatabaseService { const form = await prisma.form.findFirst({ where: { id: formId, - userId + userId, }, select: { id: true, title: true, schema: true, - createdAt: true - } + createdAt: true, + }, }); if (!form) { @@ -218,20 +223,20 @@ class DatabaseService { prisma.submission.findMany({ where: { formId }, select: { - submittedAt: true + submittedAt: true, }, orderBy: { - submittedAt: 'desc' + submittedAt: 'desc', }, - take: 30 + take: 30, }), prisma.submission.groupBy({ by: ['status'], where: { formId }, _count: { - status: true - } - }) + status: true, + }, + }), ]); // Analyze field completion rates @@ -245,7 +250,7 @@ class DatabaseService { recentActivity: this.calculateSubmissionTrends(recentSubmissions), statusDistribution, fieldAnalytics, - dataQuality: this.calculateDataQuality(fieldAnalytics) + dataQuality: this.calculateDataQuality(fieldAnalytics), }; } @@ -253,8 +258,8 @@ class DatabaseService { * Export database to various formats */ async exportDatabase(formId, userId, format = 'json', options = {}) { - const records = await this.getDatabaseRecords(formId, userId, { - limit: options.limit || 10000 + const records = await this.getDatabaseRecords(formId, userId, { + limit: options.limit || 10000, }); switch (format.toLowerCase()) { @@ -273,13 +278,13 @@ class DatabaseService { extractTableSchema(formSchema) { if (!formSchema.fields) return {}; - + return formSchema.fields.reduce((schema, field) => { schema[field.name] = { type: field.type, label: field.label, required: field.required || false, - options: field.options || null + options: field.options || null, }; return schema; }, {}); @@ -287,12 +292,12 @@ class DatabaseService { buildFilterConditions(filters) { const conditions = {}; - + Object.entries(filters).forEach(([field, value]) => { if (value !== undefined && value !== null && value !== '') { conditions[`data.${field}`] = { contains: value, - mode: 'insensitive' + mode: 'insensitive', }; } }); @@ -302,15 +307,15 @@ class DatabaseService { buildSearchConditions(query, formSchema, fields) { const conditions = []; - const searchableFields = fields.length > 0 ? fields : - formSchema.fields?.map(f => f.name) || []; + const searchableFields = + fields.length > 0 ? fields : formSchema.fields?.map((f) => f.name) || []; - searchableFields.forEach(fieldName => { + searchableFields.forEach((fieldName) => { conditions.push({ [`data.${fieldName}`]: { contains: query, - mode: 'insensitive' - } + mode: 'insensitive', + }, }); }); @@ -321,8 +326,8 @@ class DatabaseService { // Simple relevance scoring - can be enhanced with fuzzy matching let score = 0; const queryLower = query.toLowerCase(); - - Object.values(data).forEach(value => { + + Object.values(data).forEach((value) => { if (typeof value === 'string' && value.toLowerCase().includes(queryLower)) { score += 1; } @@ -334,8 +339,8 @@ class DatabaseService { calculateSubmissionTrends(submissions) { const trends = {}; const now = new Date(); - - submissions.forEach(sub => { + + submissions.forEach((sub) => { const dayDiff = Math.floor((now - new Date(sub.submittedAt)) / (1000 * 60 * 60 * 24)); const period = dayDiff <= 7 ? 'week' : dayDiff <= 30 ? 'month' : 'older'; trends[period] = (trends[period] || 0) + 1; @@ -349,21 +354,21 @@ class DatabaseService { const submissions = await prisma.submission.findMany({ where: { formId }, - select: { data: true } + select: { data: true }, }); const fieldStats = {}; - - schema.fields.forEach(field => { - const completionCount = submissions.filter(sub => - sub.data[field.name] && sub.data[field.name] !== '' + + schema.fields.forEach((field) => { + const completionCount = submissions.filter( + (sub) => sub.data[field.name] && sub.data[field.name] !== '', ).length; - + fieldStats[field.name] = { label: field.label, completionRate: submissions.length > 0 ? completionCount / submissions.length : 0, totalResponses: completionCount, - missedResponses: submissions.length - completionCount + missedResponses: submissions.length - completionCount, }; }); @@ -373,22 +378,21 @@ class DatabaseService { calculateDataQuality(fieldAnalytics) { const fields = Object.values(fieldAnalytics); if (fields.length === 0) return 0; - - const avgCompletion = fields.reduce((sum, field) => sum + field.completionRate, 0) / fields.length; + + const avgCompletion = + fields.reduce((sum, field) => sum + field.completionRate, 0) / fields.length; return Math.round(avgCompletion * 100); } exportToCSV(records) { // Implementation for CSV export const headers = Object.keys(records.schema); - const rows = records.records.map(record => - headers.map(header => record[header] || '') - ); - + const rows = records.records.map((record) => headers.map((header) => record[header] || '')); + return { headers, rows, - mimeType: 'text/csv' + mimeType: 'text/csv', }; } @@ -397,7 +401,7 @@ class DatabaseService { database: records.databaseName, schema: records.schema, records: records.records, - mimeType: 'application/json' + mimeType: 'application/json', }; } @@ -405,9 +409,9 @@ class DatabaseService { // Implementation for Excel export would go here return { data: records, - mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }; } } -module.exports = new DatabaseService(); \ No newline at end of file +module.exports = new DatabaseService(); diff --git a/backend/services/externalDatabaseService.js b/backend/services/externalDatabaseService.js index 5b2e5257..60fd4514 100644 --- a/backend/services/externalDatabaseService.js +++ b/backend/services/externalDatabaseService.js @@ -14,7 +14,7 @@ const SUPPORTED_DATABASES = { POSTGRESQL: 'postgresql', MYSQL: 'mysql', SQLITE: 'sqlite', - MSSQL: 'mssql' + MSSQL: 'mssql', }; /** @@ -32,7 +32,7 @@ async function testConnection(connectionConfig) { user: username, password, ssl: ssl ? { rejectUnauthorized: false } : false, - connectionTimeoutMillis: 10000 + connectionTimeoutMillis: 10000, }); const client = await pool.connect(); @@ -44,19 +44,18 @@ async function testConnection(connectionConfig) { success: true, message: 'Connection successful', version: result.rows[0].version, - type: 'postgresql' + type: 'postgresql', }; } // Add support for other database types as needed throw new Error(`Database type '${type}' is not yet supported`); - } catch (error) { console.error('Database connection test failed:', error); return { success: false, message: error.message || 'Connection failed', - error: error.code + error: error.code, }; } } @@ -75,7 +74,7 @@ async function getTables(connectionConfig) { database, user: username, password, - ssl: ssl ? { rejectUnauthorized: false } : false + ssl: ssl ? { rejectUnauthorized: false } : false, }); const client = await pool.connect(); @@ -95,23 +94,22 @@ async function getTables(connectionConfig) { return { success: true, - tables: result.rows.map(row => ({ + tables: result.rows.map((row) => ({ schema: row.table_schema, name: row.table_name, fullName: `${row.table_schema}.${row.table_name}`, - columnCount: parseInt(row.column_count) - })) + columnCount: parseInt(row.column_count), + })), }; } throw new Error(`Database type '${type}' is not yet supported`); - } catch (error) { console.error('Get tables failed:', error); return { success: false, message: error.message, - tables: [] + tables: [], }; } } @@ -130,7 +128,7 @@ async function getTableSchema(connectionConfig, tableName) { database, user: username, password, - ssl: ssl ? { rejectUnauthorized: false } : false + ssl: ssl ? { rejectUnauthorized: false } : false, }); // Parse schema and table name @@ -141,7 +139,8 @@ async function getTableSchema(connectionConfig, tableName) { const client = await pool.connect(); // Get columns - const columnsResult = await client.query(` + const columnsResult = await client.query( + ` SELECT column_name, data_type, @@ -151,14 +150,19 @@ async function getTableSchema(connectionConfig, tableName) { FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position - `, [schema, table]); + `, + [schema, table], + ); // Get row count estimate - const countResult = await client.query(` + const countResult = await client.query( + ` SELECT reltuples::bigint as estimate FROM pg_class WHERE oid = $1::regclass - `, [`${schema}.${table}`]); + `, + [`${schema}.${table}`], + ); client.release(); await pool.end(); @@ -168,25 +172,24 @@ async function getTableSchema(connectionConfig, tableName) { tableName, schema, table, - columns: columnsResult.rows.map(col => ({ + columns: columnsResult.rows.map((col) => ({ name: col.column_name, type: col.data_type, nullable: col.is_nullable === 'YES', default: col.column_default, - maxLength: col.character_maximum_length + maxLength: col.character_maximum_length, })), - estimatedRows: countResult.rows[0]?.estimate || 0 + estimatedRows: countResult.rows[0]?.estimate || 0, }; } throw new Error(`Database type '${type}' is not yet supported`); - } catch (error) { console.error('Get table schema failed:', error); return { success: false, message: error.message, - columns: [] + columns: [], }; } } @@ -208,7 +211,7 @@ async function importData(connectionConfig, tableName, options, userId) { database, user: username, password, - ssl: ssl ? { rejectUnauthorized: false } : false + ssl: ssl ? { rejectUnauthorized: false } : false, }); // Parse schema and table name @@ -254,7 +257,7 @@ async function importData(connectionConfig, tableName, options, userId) { type: 'postgresql', host, database, - table: tableName + table: tableName, }, columns: columns.length > 0 ? columns : Object.keys(result.rows[0] || {}), rowCount: result.rows.length, @@ -262,18 +265,18 @@ async function importData(connectionConfig, tableName, options, userId) { query: { limit, offset, - where: where || null + where: where || null, }, - sampleData: result.rows.slice(0, 10) + sampleData: result.rows.slice(0, 10), }, metadata: { importedAt: new Date().toISOString(), - processingTime + processingTime, }, processingTime, processedAt: new Date(), - userId - } + userId, + }, }); return { @@ -283,12 +286,11 @@ async function importData(connectionConfig, tableName, options, userId) { rowCount: result.rows.length, totalRows: parseInt(countResult.rows[0].total), processingTime, - columns: Object.keys(result.rows[0] || {}) + columns: Object.keys(result.rows[0] || {}), }; } throw new Error(`Database type '${type}' is not yet supported`); - } catch (error) { console.error('Import data failed:', error); @@ -301,16 +303,16 @@ async function importData(connectionConfig, tableName, options, userId) { error: error.message, metadata: { connectionConfig: { type, host, port, database }, - tableName + tableName, }, - userId - } + userId, + }, }); return { success: false, message: error.message, - data: [] + data: [], }; } } @@ -337,7 +339,7 @@ async function executeQuery(connectionConfig, query, userId) { database, user: username, password, - ssl: ssl ? { rejectUnauthorized: false } : false + ssl: ssl ? { rejectUnauthorized: false } : false, }); const client = await pool.connect(); @@ -360,21 +362,21 @@ async function executeQuery(connectionConfig, query, userId) { source: { type: 'postgresql', host, - database + database, }, query, - columns: result.fields.map(f => f.name), + columns: result.fields.map((f) => f.name), rowCount: result.rows.length, - sampleData: result.rows.slice(0, 10) + sampleData: result.rows.slice(0, 10), }, metadata: { executedAt: new Date().toISOString(), - processingTime + processingTime, }, processingTime, processedAt: new Date(), - userId - } + userId, + }, }); return { @@ -382,19 +384,18 @@ async function executeQuery(connectionConfig, query, userId) { dataSourceId: dataSource.id, data: result.rows, rowCount: result.rows.length, - columns: result.fields.map(f => f.name), - processingTime + columns: result.fields.map((f) => f.name), + processingTime, }; } throw new Error(`Database type '${type}' is not yet supported`); - } catch (error) { console.error('Execute query failed:', error); return { success: false, message: error.message, - data: [] + data: [], }; } } @@ -409,20 +410,20 @@ async function getRecentImports(userId, options = {}) { const dataSources = await prisma.dataSource.findMany({ where: { userId, - type: 'DATABASE' + type: 'DATABASE', }, orderBy: { - createdAt: 'desc' + createdAt: 'desc', }, take: limit, - skip: offset + skip: offset, }); const total = await prisma.dataSource.count({ where: { userId, - type: 'DATABASE' - } + type: 'DATABASE', + }, }); return { @@ -432,16 +433,15 @@ async function getRecentImports(userId, options = {}) { pagination: { limit, offset, - hasMore: offset + limit < total - } + hasMore: offset + limit < total, + }, }; - } catch (error) { console.error('Get recent imports failed:', error); return { success: false, dataSources: [], - total: 0 + total: 0, }; } } @@ -453,5 +453,5 @@ module.exports = { getTableSchema, importData, executeQuery, - getRecentImports + getRecentImports, }; diff --git a/backend/services/imageIngestionService.js b/backend/services/imageIngestionService.js index 7886492d..e4008351 100644 --- a/backend/services/imageIngestionService.js +++ b/backend/services/imageIngestionService.js @@ -15,7 +15,7 @@ const readFile = promisify(fs.readFile); class ImageIngestionService { constructor() { this.openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY + apiKey: process.env.OPENAI_API_KEY, }); this.uploadDir = process.env.UPLOAD_DIR || path.join(__dirname, '../uploads/images'); this.supportedFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'pdf']; @@ -46,7 +46,7 @@ class ImageIngestionService { extractionPrompt = null, // Custom prompt for what to extract formId = null, organizationId = null, - documentType = 'auto' // auto, receipt, invoice, form, id_card, business_card, document, photo + documentType = 'auto', // auto, receipt, invoice, form, id_card, business_card, document, photo } = options; // Validate file @@ -66,19 +66,19 @@ class ImageIngestionService { metadata: { documentType, extractionPrompt, - uploadedAt: new Date().toISOString() + uploadedAt: new Date().toISOString(), }, userId, organizationId, - formId - } + formId, + }, }); try { // Update status to processing await prisma.dataSource.update({ where: { id: dataSource.id }, - data: { status: 'PROCESSING' } + data: { status: 'PROCESSING' }, }); const startTime = Date.now(); @@ -86,7 +86,7 @@ class ImageIngestionService { // Analyze image with Vision API const visionResult = await this.analyzeImageWithVision(file.path, { documentType, - extractionPrompt + extractionPrompt, }); const processingTime = Date.now() - startTime; @@ -103,13 +103,13 @@ class ImageIngestionService { extractedData: { ...extractedData, rawAnalysis: visionResult.analysis, - documentType: visionResult.detectedType || documentType + documentType: visionResult.detectedType || documentType, }, processingTime, aiModel: 'gpt-4-vision-preview', confidence: this.calculateConfidence(visionResult, extractedData), - processedAt: new Date() - } + processedAt: new Date(), + }, }); return { @@ -119,17 +119,16 @@ class ImageIngestionService { documentType: visionResult.detectedType || documentType, extractedData, processingTime, - confidence: updatedDataSource.confidence + confidence: updatedDataSource.confidence, }; - } catch (error) { // Update status to failed await prisma.dataSource.update({ where: { id: dataSource.id }, data: { status: 'FAILED', - error: error.message - } + error: error.message, + }, }); throw error; @@ -158,20 +157,20 @@ class ImageIngestionService { content: [ { type: 'text', - text: systemPrompt + text: systemPrompt, }, { type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64Image}`, - detail: 'high' - } - } - ] - } + detail: 'high', + }, + }, + ], + }, ], max_tokens: 4096, - temperature: 0.2 + temperature: 0.2, }); const analysisText = response.choices[0].message.content; @@ -180,8 +179,8 @@ class ImageIngestionService { let parsedResult; try { // Try to parse as JSON if the response contains JSON - const jsonMatch = analysisText.match(/```json\n?([\s\S]*?)\n?```/) || - analysisText.match(/\{[\s\S]*\}/); + const jsonMatch = + analysisText.match(/```json\n?([\s\S]*?)\n?```/) || analysisText.match(/\{[\s\S]*\}/); if (jsonMatch) { parsedResult = JSON.parse(jsonMatch[1] || jsonMatch[0]); } else { @@ -196,7 +195,7 @@ class ImageIngestionService { analysis: analysisText, structured: parsedResult, detectedType: parsedResult.documentType || null, - usage: response.usage + usage: response.usage, }; } @@ -219,7 +218,7 @@ Please provide your response in the following JSON format: } const prompts = { - 'auto': `Analyze this image thoroughly. Identify the document type and extract all relevant information. + auto: `Analyze this image thoroughly. Identify the document type and extract all relevant information. Your response should be in JSON format: \`\`\`json @@ -242,7 +241,7 @@ Your response should be in JSON format: } \`\`\``, - 'receipt': `This is a receipt or bill. Extract all information including: + receipt: `This is a receipt or bill. Extract all information including: - Store/vendor name - Date and time - All line items with prices @@ -268,7 +267,7 @@ Response format: } \`\`\``, - 'invoice': `This is an invoice. Extract all business and transaction information: + invoice: `This is an invoice. Extract all business and transaction information: - Company details (name, address, contact) - Invoice number and date - Bill to / Ship to information @@ -296,7 +295,7 @@ Response format: } \`\`\``, - 'form': `This is a form or document. Extract all field labels and their values: + form: `This is a form or document. Extract all field labels and their values: Response format: \`\`\`json @@ -312,7 +311,7 @@ Response format: } \`\`\``, - 'id_card': `This appears to be an ID card or official document. Extract identifying information: + id_card: `This appears to be an ID card or official document. Extract identifying information: Response format: \`\`\`json @@ -331,7 +330,7 @@ Response format: } \`\`\``, - 'business_card': `This is a business card. Extract contact and professional information: + business_card: `This is a business card. Extract contact and professional information: Response format: \`\`\`json @@ -350,7 +349,7 @@ Response format: } \`\`\``, - 'document': `Analyze this document and extract all text and key information: + document: `Analyze this document and extract all text and key information: Response format: \`\`\`json @@ -367,7 +366,7 @@ Response format: } \`\`\``, - 'photo': `Describe this photo in detail: + photo: `Describe this photo in detail: Response format: \`\`\`json @@ -382,7 +381,7 @@ Response format: "mood": "overall mood/atmosphere", "confidence": 0.0-1.0 } -\`\`\`` +\`\`\``, }; return prompts[documentType] || prompts['auto']; @@ -404,15 +403,15 @@ Response format: messages: [ { role: 'system', - content: `You are a data extraction assistant. Convert the following image analysis into structured JSON data. Focus on extracting key information relevant to a ${documentType} document type.` + content: `You are a data extraction assistant. Convert the following image analysis into structured JSON data. Focus on extracting key information relevant to a ${documentType} document type.`, }, { role: 'user', - content: visionResult.analysis - } + content: visionResult.analysis, + }, ], temperature: 0.2, - response_format: { type: 'json_object' } + response_format: { type: 'json_object' }, }); return JSON.parse(response.choices[0].message.content); @@ -420,7 +419,7 @@ Response format: console.error('Failed to extract structured data:', error); return { text: visionResult.text, - rawAnalysis: visionResult.analysis + rawAnalysis: visionResult.analysis, }; } } @@ -477,14 +476,14 @@ Response format: getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase().slice(1); const mimeTypes = { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'gif': 'image/gif', - 'webp': 'image/webp', - 'bmp': 'image/bmp', - 'tiff': 'image/tiff', - 'pdf': 'application/pdf' + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + bmp: 'image/bmp', + tiff: 'image/tiff', + pdf: 'application/pdf', }; return mimeTypes[ext] || 'image/jpeg'; } @@ -503,7 +502,9 @@ Response format: const ext = path.extname(file.originalname).toLowerCase().slice(1); if (!this.supportedFormats.includes(ext)) { - throw new Error(`Unsupported format: ${ext}. Supported formats: ${this.supportedFormats.join(', ')}`); + throw new Error( + `Unsupported format: ${ext}. Supported formats: ${this.supportedFormats.join(', ')}`, + ); } } @@ -528,7 +529,7 @@ Response format: 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', - 'image/bmp': 'bmp' + 'image/bmp': 'bmp', }; const ext = mimeToExt[mimeType] || 'jpg'; @@ -543,7 +544,7 @@ Response format: originalname: options.name || filename, mimetype: mimeType, size: buffer.length, - path: filePath + path: filePath, }; try { @@ -566,7 +567,7 @@ Response format: const where = { userId, - type: 'IMAGE' + type: 'IMAGE', }; if (status) { @@ -598,18 +599,19 @@ Response format: confidence: true, createdAt: true, processedAt: true, - metadata: true - } + metadata: true, + }, }), - prisma.dataSource.count({ where }) + prisma.dataSource.count({ where }), ]); // Filter by document type if specified let filteredSources = dataSources; if (documentType) { - filteredSources = dataSources.filter(ds => - ds.extractedData?.documentType === documentType || - ds.metadata?.documentType === documentType + filteredSources = dataSources.filter( + (ds) => + ds.extractedData?.documentType === documentType || + ds.metadata?.documentType === documentType, ); } @@ -617,7 +619,7 @@ Response format: dataSources: filteredSources, total: documentType ? filteredSources.length : total, limit, - offset + offset, }; } @@ -629,11 +631,11 @@ Response format: where: { id, userId, - type: 'IMAGE' + type: 'IMAGE', }, include: { - analyses: true - } + analyses: true, + }, }); if (!dataSource) { @@ -651,8 +653,8 @@ Response format: where: { id, userId, - type: 'IMAGE' - } + type: 'IMAGE', + }, }); if (!dataSource) { @@ -669,7 +671,7 @@ Response format: } await prisma.dataSource.delete({ - where: { id } + where: { id }, }); return { success: true }; @@ -689,7 +691,7 @@ Response format: const visionResult = await this.analyzeImageWithVision(dataSource.filePath, { documentType: dataSource.metadata?.documentType || 'auto', - extractionPrompt: customPrompt + extractionPrompt: customPrompt, }); const processingTime = Date.now() - startTime; @@ -703,19 +705,19 @@ Response format: result: { text: visionResult.text, structured: visionResult.structured, - analysis: visionResult.analysis + analysis: visionResult.analysis, }, model: 'gpt-4-vision-preview', processingTime, - status: 'COMPLETED' - } + status: 'COMPLETED', + }, }); return { analysisId: analysis.id, text: visionResult.text, result: visionResult.structured, - processingTime + processingTime, }; } } diff --git a/backend/services/llm-analysis.js b/backend/services/llm-analysis.js index eb1ce30a..6d10c55d 100644 --- a/backend/services/llm-analysis.js +++ b/backend/services/llm-analysis.js @@ -11,7 +11,9 @@ class LLMAnalysisService { getOpenAIClient() { if (!this.openai) { if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY environment variable is required for LLM analysis. Add it to your .env file or disable LLM features.'); + throw new Error( + 'OPENAI_API_KEY environment variable is required for LLM analysis. Add it to your .env file or disable LLM features.', + ); } this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, @@ -52,12 +54,8 @@ class LLMAnalysisService { for (const analysisType of analysisTypes) { try { const startTime = Date.now(); - - const result = await this.performAnalysis( - analysisType, - submission.data, - submission.form - ); + + const result = await this.performAnalysis(analysisType, submission.data, submission.form); const processingTime = Date.now() - startTime; @@ -80,7 +78,7 @@ class LLMAnalysisService { results.push(analysis); } catch (error) { console.error(`LLM Analysis failed for ${analysisType}:`, error); - + // Save failed analysis await prisma.lLMAnalysis.create({ data: { @@ -141,7 +139,8 @@ Provide a JSON response with: messages: [ { role: 'system', - content: 'You are an expert sentiment analysis AI. Provide accurate sentiment analysis in the requested JSON format.', + content: + 'You are an expert sentiment analysis AI. Provide accurate sentiment analysis in the requested JSON format.', }, { role: 'user', @@ -189,7 +188,8 @@ Provide a JSON response with: messages: [ { role: 'system', - content: 'You are an expert content classifier. Analyze and categorize content accurately.', + content: + 'You are an expert content classifier. Analyze and categorize content accurately.', }, { role: 'user', @@ -232,7 +232,8 @@ Provide a JSON response with: messages: [ { role: 'system', - content: 'You are an expert at extracting key information from structured data. Focus on actionable insights.', + content: + 'You are an expert at extracting key information from structured data. Focus on actionable insights.', }, { role: 'user', @@ -320,7 +321,8 @@ Please provide your analysis in JSON format with at least these fields: messages: [ { role: 'system', - content: 'You are an expert analyst. Follow the custom prompt instructions carefully and provide results in JSON format.', + content: + 'You are an expert analyst. Follow the custom prompt instructions carefully and provide results in JSON format.', }, { role: 'user', @@ -344,9 +346,9 @@ Please provide your analysis in JSON format with at least these fields: // Helper: Extract text fields from submission data extractTextFields(submissionData, formSchema) { const textFields = []; - + if (formSchema && formSchema.fields) { - formSchema.fields.forEach(field => { + formSchema.fields.forEach((field) => { const value = submissionData[field.id]; if (value && typeof value === 'string' && value.trim()) { textFields.push(`${field.label || field.id}: ${value}`); @@ -421,12 +423,12 @@ Please provide your analysis in JSON format with at least these fields: where: { formId, ...(analysisType && { analysisType }), - ...(dateFrom || dateTo) && { + ...((dateFrom || dateTo) && { createdAt: { ...(dateFrom && { gte: dateFrom }), ...(dateTo && { lte: dateTo }), }, - }, + }), }, include: { submission: { @@ -470,17 +472,19 @@ Please provide your analysis in JSON format with at least these fields: topCategories: {}, performanceMetrics: { successRate: 100, // All fetched analyses are completed - fastestAnalysis: Math.min(...analyses.map(a => a.processingTime || 0)), - slowestAnalysis: Math.max(...analyses.map(a => a.processingTime || 0)), + fastestAnalysis: Math.min(...analyses.map((a) => a.processingTime || 0)), + slowestAnalysis: Math.max(...analyses.map((a) => a.processingTime || 0)), }, }; if (analyses.length > 0) { - insights.averageConfidence = analyses.reduce((sum, a) => sum + (a.confidence || 0), 0) / analyses.length; - insights.averageProcessingTime = analyses.reduce((sum, a) => sum + (a.processingTime || 0), 0) / analyses.length; + insights.averageConfidence = + analyses.reduce((sum, a) => sum + (a.confidence || 0), 0) / analyses.length; + insights.averageProcessingTime = + analyses.reduce((sum, a) => sum + (a.processingTime || 0), 0) / analyses.length; // Process sentiment distribution - analyses.forEach(analysis => { + analyses.forEach((analysis) => { if (analysis.analysisType === 'SENTIMENT' && analysis.result?.sentiment) { insights.sentimentDistribution[analysis.result.sentiment]++; } @@ -496,4 +500,4 @@ Please provide your analysis in JSON format with at least these fields: } } -module.exports = LLMAnalysisService; \ No newline at end of file +module.exports = LLMAnalysisService; diff --git a/backend/services/videoIngestionService.js b/backend/services/videoIngestionService.js index 54e44deb..e0da3277 100644 --- a/backend/services/videoIngestionService.js +++ b/backend/services/videoIngestionService.js @@ -18,7 +18,7 @@ const readdir = promisify(fs.readdir); class VideoIngestionService { constructor() { this.openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY + apiKey: process.env.OPENAI_API_KEY, }); this.uploadDir = process.env.UPLOAD_DIR || path.join(__dirname, '../uploads/videos'); this.tempDir = path.join(__dirname, '../uploads/temp'); @@ -62,7 +62,7 @@ class VideoIngestionService { transcribeAudio = true, analyzeFrames = true, formId = null, - organizationId = null + organizationId = null, } = options; // Validate file @@ -90,19 +90,19 @@ class VideoIngestionService { maxFrames, transcribeAudio, analyzeFrames, - uploadedAt: new Date().toISOString() + uploadedAt: new Date().toISOString(), }, userId, organizationId, - formId - } + formId, + }, }); try { // Update status to processing await prisma.dataSource.update({ where: { id: dataSource.id }, - data: { status: 'PROCESSING' } + data: { status: 'PROCESSING' }, }); const startTime = Date.now(); @@ -118,7 +118,7 @@ class VideoIngestionService { const frames = await this.extractFrames(file.path, tempDir, { interval: frameInterval, maxFrames, - duration: videoInfo.duration + duration: videoInfo.duration, }); frameAnalyses = await this.analyzeFrames(frames); } @@ -147,27 +147,29 @@ class VideoIngestionService { extractedText: transcription?.text || '', extractedData: { videoInfo, - frames: frameAnalyses.map(f => ({ + frames: frameAnalyses.map((f) => ({ timestamp: f.timestamp, description: f.description, objects: f.objects, - text: f.text + text: f.text, })), - transcription: transcription ? { - text: transcription.text, - language: transcription.language, - duration: transcription.duration, - segments: transcription.segments?.slice(0, 20) // Limit segments - } : null, + transcription: transcription + ? { + text: transcription.text, + language: transcription.language, + duration: transcription.duration, + segments: transcription.segments?.slice(0, 20), // Limit segments + } + : null, summary, keywords: summary.keywords || [], - topics: summary.topics || [] + topics: summary.topics || [], }, processingTime, aiModel: 'gpt-4o + whisper-1', confidence: this.calculateConfidence(frameAnalyses, transcription), - processedAt: new Date() - } + processedAt: new Date(), + }, }); return { @@ -178,17 +180,16 @@ class VideoIngestionService { transcription: transcription?.text || null, summary, processingTime, - confidence: updatedDataSource.confidence + confidence: updatedDataSource.confidence, }; - } catch (error) { // Update status to failed await prisma.dataSource.update({ where: { id: dataSource.id }, data: { status: 'FAILED', - error: error.message - } + error: error.message, + }, }); throw error; @@ -201,12 +202,12 @@ class VideoIngestionService { async getVideoInfo(videoPath) { try { const { stdout } = await execPromise( - `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"` + `ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`, ); const info = JSON.parse(stdout); - const videoStream = info.streams?.find(s => s.codec_type === 'video'); - const audioStream = info.streams?.find(s => s.codec_type === 'audio'); + const videoStream = info.streams?.find((s) => s.codec_type === 'video'); + const audioStream = info.streams?.find((s) => s.codec_type === 'audio'); return { duration: parseFloat(info.format?.duration) || 0, @@ -217,7 +218,7 @@ class VideoIngestionService { bitrate: parseInt(info.format?.bit_rate) || 0, hasAudio: !!audioStream, audioCodec: audioStream?.codec_name || null, - format: info.format?.format_name || 'unknown' + format: info.format?.format_name || 'unknown', }; } catch (error) { console.error('Failed to get video info:', error); @@ -230,7 +231,7 @@ class VideoIngestionService { bitrate: 0, hasAudio: true, // Assume audio exists audioCodec: null, - format: 'unknown' + format: 'unknown', }; } } @@ -242,7 +243,7 @@ class VideoIngestionService { if (!fpsString) return 0; if (fpsString.includes('/')) { const [num, den] = fpsString.split('/').map(Number); - return den ? Math.round(num / den * 100) / 100 : 0; + return den ? Math.round((num / den) * 100) / 100 : 0; } return parseFloat(fpsString) || 0; } @@ -275,14 +276,14 @@ class VideoIngestionService { const outputPath = path.join(outputDir, `frame_${timestamp.toFixed(1)}.jpg`); try { await execPromise( - `ffmpeg -ss ${timestamp} -i "${videoPath}" -vframes 1 -q:v 2 "${outputPath}" -y` + `ffmpeg -ss ${timestamp} -i "${videoPath}" -vframes 1 -q:v 2 "${outputPath}" -y`, ); // Check if file was created if (fs.existsSync(outputPath)) { frames.push({ path: outputPath, - timestamp + timestamp, }); } } catch (error) { @@ -328,44 +329,45 @@ Response in JSON: "sceneType": "indoor/outdoor/etc", "mood": "mood/atmosphere" } -\`\`\`` +\`\`\``, }, { type: 'image_url', image_url: { url: `data:image/jpeg;base64,${base64Image}`, - detail: 'low' - } - } - ] - } + detail: 'low', + }, + }, + ], + }, ], max_tokens: 500, - temperature: 0.3 + temperature: 0.3, }); const analysisText = response.choices[0].message.content; let analysis; try { - const jsonMatch = analysisText.match(/```json\n?([\s\S]*?)\n?```/) || - analysisText.match(/\{[\s\S]*\}/); - analysis = jsonMatch ? JSON.parse(jsonMatch[1] || jsonMatch[0]) : { description: analysisText }; + const jsonMatch = + analysisText.match(/```json\n?([\s\S]*?)\n?```/) || analysisText.match(/\{[\s\S]*\}/); + analysis = jsonMatch + ? JSON.parse(jsonMatch[1] || jsonMatch[0]) + : { description: analysisText }; } catch { analysis = { description: analysisText }; } analyses.push({ timestamp: frame.timestamp, - ...analysis + ...analysis, }); - } catch (error) { console.error(`Failed to analyze frame at ${frame.timestamp}s:`, error.message); analyses.push({ timestamp: frame.timestamp, description: 'Analysis failed', - error: error.message + error: error.message, }); } } @@ -377,9 +379,7 @@ Response in JSON: * Extract audio from video */ async extractAudio(videoPath, audioPath) { - await execPromise( - `ffmpeg -i "${videoPath}" -vn -acodec libmp3lame -q:a 4 "${audioPath}" -y` - ); + await execPromise(`ffmpeg -i "${videoPath}" -vn -acodec libmp3lame -q:a 4 "${audioPath}" -y`); return audioPath; } @@ -391,7 +391,7 @@ Response in JSON: const response = await this.openai.audio.transcriptions.create({ file: fs.createReadStream(audioPath), model: 'whisper-1', - response_format: 'verbose_json' + response_format: 'verbose_json', }); return response; @@ -408,8 +408,8 @@ Response in JSON: const context = { duration: videoInfo.duration, resolution: `${videoInfo.width}x${videoInfo.height}`, - frames: frameAnalyses.map(f => `[${f.timestamp}s]: ${f.description}`).join('\n'), - transcript: transcription?.text?.slice(0, 2000) || 'No audio transcript' + frames: frameAnalyses.map((f) => `[${f.timestamp}s]: ${f.description}`).join('\n'), + transcript: transcription?.text?.slice(0, 2000) || 'No audio transcript', }; try { @@ -418,7 +418,8 @@ Response in JSON: messages: [ { role: 'system', - content: 'You analyze video content based on frame descriptions and audio transcripts. Provide comprehensive summaries.' + content: + 'You analyze video content based on frame descriptions and audio transcripts. Provide comprehensive summaries.', }, { role: 'user', @@ -442,11 +443,11 @@ Provide a JSON response: "keyMoments": [{ "timestamp": 0, "description": "key moment" }], "sentiment": "positive/negative/neutral" } -\`\`\`` - } +\`\`\``, + }, ], temperature: 0.3, - response_format: { type: 'json_object' } + response_format: { type: 'json_object' }, }); return JSON.parse(response.choices[0].message.content); @@ -456,7 +457,7 @@ Provide a JSON response: summary: 'Video analysis completed', topics: [], keywords: [], - contentType: 'unknown' + contentType: 'unknown', }; } } @@ -483,7 +484,7 @@ Provide a JSON response: let confidence = 0.6; // Increase confidence based on successful frame analyses - const successfulFrames = frameAnalyses.filter(f => !f.error).length; + const successfulFrames = frameAnalyses.filter((f) => !f.error).length; if (successfulFrames > 0) { confidence += (successfulFrames / frameAnalyses.length) * 0.2; } @@ -522,7 +523,7 @@ Provide a JSON response: const where = { userId, - type: 'VIDEO' + type: 'VIDEO', }; if (status) { @@ -548,10 +549,10 @@ Provide a JSON response: processingTime: true, confidence: true, createdAt: true, - processedAt: true - } + processedAt: true, + }, }), - prisma.dataSource.count({ where }) + prisma.dataSource.count({ where }), ]); return { dataSources, total, limit, offset }; @@ -563,7 +564,7 @@ Provide a JSON response: async getVideoSource(id, userId) { const dataSource = await prisma.dataSource.findFirst({ where: { id, userId, type: 'VIDEO' }, - include: { analyses: true } + include: { analyses: true }, }); if (!dataSource) { @@ -578,7 +579,7 @@ Provide a JSON response: */ async deleteVideoSource(id, userId) { const dataSource = await prisma.dataSource.findFirst({ - where: { id, userId, type: 'VIDEO' } + where: { id, userId, type: 'VIDEO' }, }); if (!dataSource) { diff --git a/backend/services/websiteIngestionService.js b/backend/services/websiteIngestionService.js index 671e6ad0..8b25b063 100644 --- a/backend/services/websiteIngestionService.js +++ b/backend/services/websiteIngestionService.js @@ -11,7 +11,7 @@ const { URL } = require('url'); class WebsiteIngestionService { constructor() { this.openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY + apiKey: process.env.OPENAI_API_KEY, }); this.maxContentLength = 100000; // 100KB text limit this.timeout = 30000; // 30 second timeout @@ -61,7 +61,7 @@ class WebsiteIngestionService { waitForSelector = null, screenshot = false, fullPage = false, - extractType = 'auto' + extractType = 'auto', } = options; let browser; @@ -73,8 +73,8 @@ class WebsiteIngestionService { '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', - '--disable-gpu' - ] + '--disable-gpu', + ], }); const page = await browser.newPage(); @@ -84,13 +84,13 @@ class WebsiteIngestionService { // Set user agent await page.setUserAgent( - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ); // Navigate to URL await page.goto(url, { waitUntil: 'networkidle2', - timeout: this.timeout + timeout: this.timeout, }); // Wait for specific selector if provided @@ -109,7 +109,7 @@ class WebsiteIngestionService { const screenshotBuffer = await page.screenshot({ fullPage, type: 'jpeg', - quality: 80 + quality: 80, }); screenshotBase64 = screenshotBuffer.toString('base64'); } @@ -129,7 +129,7 @@ class WebsiteIngestionService { ogImage: getMeta('og:image'), ogType: getMeta('og:type'), canonical: document.querySelector('link[rel="canonical"]')?.href, - language: document.documentElement.lang + language: document.documentElement.lang, }; }); @@ -140,9 +140,8 @@ class WebsiteIngestionService { title: pageTitle, url: pageUrl, metadata, - screenshot: screenshotBase64 + screenshot: screenshotBase64, }; - } catch (error) { if (browser) await browser.close(); throw error; @@ -183,7 +182,7 @@ class WebsiteIngestionService { links: [], images: [], tables: [], - lists: [] + lists: [], }; // Extract headings @@ -191,7 +190,7 @@ class WebsiteIngestionService { const $el = $(el); structuredData.headings.push({ level: parseInt(el.tagName.slice(1)), - text: $el.text().trim().slice(0, 200) + text: $el.text().trim().slice(0, 200), }); }); @@ -202,7 +201,7 @@ class WebsiteIngestionService { if (href && !href.startsWith('#') && !href.startsWith('javascript:')) { structuredData.links.push({ text: $el.text().trim().slice(0, 100), - href: href.slice(0, 500) + href: href.slice(0, 500), }); } }); @@ -217,14 +216,14 @@ class WebsiteIngestionService { structuredData.images.push({ src: $el.attr('src'), alt: $el.attr('alt') || '', - title: $el.attr('title') || '' + title: $el.attr('title') || '', }); }); structuredData.images = structuredData.images.slice(0, 50); return { text: mainContent, - structured: structuredData + structured: structuredData, }; } @@ -236,8 +235,10 @@ class WebsiteIngestionService { const typePrompts = { auto: 'Analyze this webpage content and extract key information.', - article: 'Extract the main article content, including title, author, publication date, and key points.', - product: 'Extract product information including name, price, description, specifications, and reviews.', + article: + 'Extract the main article content, including title, author, publication date, and key points.', + product: + 'Extract product information including name, price, description, specifications, and reviews.', contact: 'Extract contact information including names, emails, phone numbers, and addresses.', listing: 'Extract all items from this listing or catalog page.', form: 'Identify all form fields and their purposes on this page.', @@ -251,7 +252,7 @@ class WebsiteIngestionService { messages: [ { role: 'system', - content: `You are a web content analyzer. ${systemPrompt} Return structured JSON.` + content: `You are a web content analyzer. ${systemPrompt} Return structured JSON.`, }, { role: 'user', @@ -265,7 +266,10 @@ Content: ${content.text.slice(0, 8000)} Headings: -${content.structured.headings.slice(0, 20).map(h => `${'#'.repeat(h.level)} ${h.text}`).join('\n')} +${content.structured.headings + .slice(0, 20) + .map((h) => `${'#'.repeat(h.level)} ${h.text}`) + .join('\n')} Extract and return JSON: \`\`\`json @@ -285,11 +289,11 @@ Extract and return JSON: "language": "detected language", "extractedData": {} } -\`\`\`` - } +\`\`\``, + }, ], temperature: 0.3, - response_format: { type: 'json_object' } + response_format: { type: 'json_object' }, }); return JSON.parse(response.choices[0].message.content); @@ -301,7 +305,7 @@ Extract and return JSON: mainTopic: 'unknown', keyPoints: [], entities: {}, - error: error.message + error: error.message, }; } } @@ -318,7 +322,7 @@ Extract and return JSON: screenshot = false, customPrompt = null, formId = null, - organizationId = null + organizationId = null, } = options; // Validate URL @@ -336,19 +340,19 @@ Extract and return JSON: extractType, waitForSelector, screenshot, - scrapedAt: new Date().toISOString() + scrapedAt: new Date().toISOString(), }, userId, organizationId, - formId - } + formId, + }, }); try { // Update status to processing await prisma.dataSource.update({ where: { id: dataSource.id }, - data: { status: 'PROCESSING' } + data: { status: 'PROCESSING' }, }); const startTime = Date.now(); @@ -357,18 +361,22 @@ Extract and return JSON: const scrapeResult = await this.scrapeUrl(validatedUrl, { waitForSelector, screenshot, - extractType + extractType, }); // Extract content const content = this.extractContent(scrapeResult.html, extractType); // Analyze with AI - const analysis = await this.analyzeContent(content, { - title: scrapeResult.title, - url: scrapeResult.url, - description: scrapeResult.metadata?.description - }, { extractType, customPrompt }); + const analysis = await this.analyzeContent( + content, + { + title: scrapeResult.title, + url: scrapeResult.url, + description: scrapeResult.metadata?.description, + }, + { extractType, customPrompt }, + ); const processingTime = Date.now() - startTime; @@ -386,13 +394,15 @@ Extract and return JSON: links: content.structured.links.slice(0, 50), images: content.structured.images.slice(0, 20), analysis, - screenshot: scrapeResult.screenshot ? `data:image/jpeg;base64,${scrapeResult.screenshot}` : null + screenshot: scrapeResult.screenshot + ? `data:image/jpeg;base64,${scrapeResult.screenshot}` + : null, }, processingTime, aiModel: 'gpt-4-turbo-preview', confidence: this.calculateConfidence(content, analysis), - processedAt: new Date() - } + processedAt: new Date(), + }, }); return { @@ -403,17 +413,16 @@ Extract and return JSON: contentLength: content.text.length, analysis, processingTime, - confidence: updatedDataSource.confidence + confidence: updatedDataSource.confidence, }; - } catch (error) { // Update status to failed await prisma.dataSource.update({ where: { id: dataSource.id }, data: { status: 'FAILED', - error: error.message - } + error: error.message, + }, }); throw error; @@ -453,7 +462,10 @@ Extract and return JSON: if (analysis && !analysis.error) { confidence += 0.1; if (analysis.keyPoints?.length > 0) confidence += 0.05; - if (analysis.entities && Object.keys(analysis.entities).some(k => analysis.entities[k]?.length > 0)) { + if ( + analysis.entities && + Object.keys(analysis.entities).some((k) => analysis.entities[k]?.length > 0) + ) { confidence += 0.05; } } @@ -469,7 +481,7 @@ Extract and return JSON: const where = { userId, - type: 'WEBSITE' + type: 'WEBSITE', }; if (status) { @@ -494,10 +506,10 @@ Extract and return JSON: confidence: true, createdAt: true, processedAt: true, - error: true - } + error: true, + }, }), - prisma.dataSource.count({ where }) + prisma.dataSource.count({ where }), ]); return { dataSources, total, limit, offset }; @@ -509,7 +521,7 @@ Extract and return JSON: async getWebsiteSource(id, userId) { const dataSource = await prisma.dataSource.findFirst({ where: { id, userId, type: 'WEBSITE' }, - include: { analyses: true } + include: { analyses: true }, }); if (!dataSource) { @@ -524,7 +536,7 @@ Extract and return JSON: */ async deleteWebsiteSource(id, userId) { const dataSource = await prisma.dataSource.findFirst({ - where: { id, userId, type: 'WEBSITE' } + where: { id, userId, type: 'WEBSITE' }, }); if (!dataSource) { @@ -541,7 +553,7 @@ Extract and return JSON: */ async rescrapeUrl(id, userId, options = {}) { const dataSource = await prisma.dataSource.findFirst({ - where: { id, userId, type: 'WEBSITE' } + where: { id, userId, type: 'WEBSITE' }, }); if (!dataSource) { @@ -552,7 +564,7 @@ Extract and return JSON: const result = await this.processUrl(dataSource.fileUrl, userId, { name: dataSource.name, description: dataSource.description, - ...options + ...options, }); // Delete the old record after successful rescrape diff --git a/backend/services/websocket.js b/backend/services/websocket.js index a64439c3..c25006a1 100644 --- a/backend/services/websocket.js +++ b/backend/services/websocket.js @@ -31,7 +31,7 @@ class WebSocketService { socket.on('authenticate', async (data) => { try { const { token } = data; - + if (!token) { socket.emit('auth-error', { message: 'No token provided' }); return; @@ -58,7 +58,7 @@ class WebSocketService { // Store authenticated user this.authenticatedSockets.set(socket.id, user); - + if (!this.userSockets.has(user.id)) { this.userSockets.set(user.id, new Set()); } @@ -66,18 +66,18 @@ class WebSocketService { // Join user-specific room socket.join(`user:${user.id}`); - + // Join organization room if applicable if (user.organizationId) { socket.join(`org:${user.organizationId}`); } - socket.emit('authenticated', { - user: { - id: user.id, - name: user.name, - email: user.email - } + socket.emit('authenticated', { + user: { + id: user.id, + name: user.name, + email: user.email, + }, }); console.log(`User ${user.email} authenticated on socket ${socket.id}`); @@ -97,15 +97,12 @@ class WebSocketService { try { const { formId } = data; - + // Check if user has access to the form const form = await prisma.form.findFirst({ where: { id: formId, - OR: [ - { userId: user.id }, - { collaborators: { some: { userId: user.id } } }, - ], + OR: [{ userId: user.id }, { collaborators: { some: { userId: user.id } } }], }, }); @@ -117,7 +114,7 @@ class WebSocketService { // Join form-specific room socket.join(`form:${formId}`); socket.emit('subscribed', { formId }); - + console.log(`User ${user.email} subscribed to form ${formId}`); } catch (error) { console.error('Form subscription error:', error); @@ -144,7 +141,7 @@ class WebSocketService { this.userSockets.delete(user.id); } } - + this.authenticatedSockets.delete(socket.id); console.log(`User ${user.email} disconnected from socket ${socket.id}`); } else { @@ -177,7 +174,7 @@ class WebSocketService { type: 'analysis-complete', formId, submissionId, - analysisResults: analysisResults.map(result => ({ + analysisResults: analysisResults.map((result) => ({ id: result.id, analysisType: result.analysisType, confidence: result.confidence, @@ -253,7 +250,7 @@ class WebSocketService { if (!room) return []; const onlineUsers = []; - room.forEach(socketId => { + room.forEach((socketId) => { const user = this.authenticatedSockets.get(socketId); if (user) { onlineUsers.push({ @@ -293,4 +290,4 @@ class WebSocketService { // Create singleton instance const webSocketService = new WebSocketService(); -module.exports = webSocketService; \ No newline at end of file +module.exports = webSocketService; diff --git a/backend/trpc/index.js b/backend/trpc/index.js index 4cedc285..dc5d9a89 100644 --- a/backend/trpc/index.js +++ b/backend/trpc/index.js @@ -13,4 +13,4 @@ const appRouter = router({ }); // Export the app router type for client-side usage -module.exports = appRouter; \ No newline at end of file +module.exports = appRouter; diff --git a/backend/trpc/routers/forms.js b/backend/trpc/routers/forms.js index 4142419d..d24b31dd 100644 --- a/backend/trpc/routers/forms.js +++ b/backend/trpc/routers/forms.js @@ -1,4 +1,11 @@ -const { router, publicProcedure, authedProcedure, prisma, z, TRPCError } = require('../../lib/trpc'); +const { + router, + publicProcedure, + authedProcedure, + prisma, + z, + TRPCError, +} = require('../../lib/trpc'); const { nanoid } = require('nanoid'); const FormCreateInput = z.object({ @@ -39,13 +46,15 @@ const SharedLinkInput = z.object({ const formsRouter = router({ // Get all forms for current user list: authedProcedure - .input(z.object({ - page: z.number().default(1), - limit: z.number().min(1).max(50).default(20), - search: z.string().optional(), - isTemplate: z.boolean().optional(), - isPublished: z.boolean().optional(), - })) + .input( + z.object({ + page: z.number().default(1), + limit: z.number().min(1).max(50).default(20), + search: z.string().optional(), + isTemplate: z.boolean().optional(), + isPublished: z.boolean().optional(), + }), + ) .query(async ({ ctx, input }) => { const { page, limit, search, isTemplate, isPublished } = input; const skip = (page - 1) * limit; @@ -94,80 +103,75 @@ const formsRouter = router({ }), // Get single form by ID - get: authedProcedure - .input(z.object({ id: z.string() })) - .query(async ({ ctx, input }) => { - const form = await prisma.form.findFirst({ - where: { - id: input.id, - OR: [ - { userId: ctx.user.id }, - { collaborators: { some: { userId: ctx.user.id } } }, - ], + get: authedProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => { + const form = await prisma.form.findFirst({ + where: { + id: input.id, + OR: [{ userId: ctx.user.id }, { collaborators: { some: { userId: ctx.user.id } } }], + }, + include: { + user: { + select: { id: true, name: true, email: true }, }, - include: { - user: { - select: { id: true, name: true, email: true }, - }, - shareSettings: true, - sharedLinks: true, - collaborators: { - include: { - user: { - select: { id: true, name: true, email: true, avatar: true }, - }, + shareSettings: true, + sharedLinks: true, + collaborators: { + include: { + user: { + select: { id: true, name: true, email: true, avatar: true }, }, }, - _count: { - select: { - submissions: true, - analysisResults: true, - }, + }, + _count: { + select: { + submissions: true, + analysisResults: true, }, }, - }); + }, + }); - if (!form) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Form not found', - }); - } + if (!form) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Form not found', + }); + } - return form; - }), + return form; + }), // Create new form - create: authedProcedure - .input(FormCreateInput) - .mutation(async ({ ctx, input }) => { - const form = await prisma.form.create({ - data: { - ...input, - userId: ctx.user.id, - organizationId: ctx.user.organizationId, - settings: input.settings || {}, - templateTags: input.templateTags || [], - }, - include: { - _count: { - select: { - submissions: true, - sharedLinks: true, - }, + create: authedProcedure.input(FormCreateInput).mutation(async ({ ctx, input }) => { + const form = await prisma.form.create({ + data: { + ...input, + userId: ctx.user.id, + organizationId: ctx.user.organizationId, + settings: input.settings || {}, + templateTags: input.templateTags || [], + }, + include: { + _count: { + select: { + submissions: true, + sharedLinks: true, }, }, - }); + }, + }); - return form; - }), + return form; + }), // Update form update: authedProcedure - .input(z.object({ - id: z.string(), - data: FormUpdateInput, - })) + .input( + z.object({ + id: z.string(), + data: FormUpdateInput, + }), + ) .mutation(async ({ ctx, input }) => { // Check ownership or collaboration access const existingForm = await prisma.form.findFirst({ @@ -175,13 +179,13 @@ const formsRouter = router({ id: input.id, OR: [ { userId: ctx.user.id }, - { - collaborators: { - some: { + { + collaborators: { + some: { userId: ctx.user.id, - role: { in: ['EDITOR', 'ADMIN'] } - } - } + role: { in: ['EDITOR', 'ADMIN'] }, + }, + }, }, ], }, @@ -195,7 +199,10 @@ const formsRouter = router({ } // Create version if schema changed - if (input.data.schema && JSON.stringify(input.data.schema) !== JSON.stringify(existingForm.schema)) { + if ( + input.data.schema && + JSON.stringify(input.data.schema) !== JSON.stringify(existingForm.schema) + ) { await prisma.formVersion.create({ data: { formId: input.id, @@ -228,36 +235,36 @@ const formsRouter = router({ }), // Delete form - delete: authedProcedure - .input(z.object({ id: z.string() })) - .mutation(async ({ ctx, input }) => { - const form = await prisma.form.findFirst({ - where: { - id: input.id, - userId: ctx.user.id, - }, - }); - - if (!form) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Form not found', - }); - } + delete: authedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { + const form = await prisma.form.findFirst({ + where: { + id: input.id, + userId: ctx.user.id, + }, + }); - await prisma.form.delete({ - where: { id: input.id }, + if (!form) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Form not found', }); + } - return { success: true }; - }), + await prisma.form.delete({ + where: { id: input.id }, + }); + + return { success: true }; + }), // Update share settings updateShareSettings: authedProcedure - .input(z.object({ - formId: z.string(), - settings: ShareSettingsInput, - })) + .input( + z.object({ + formId: z.string(), + settings: ShareSettingsInput, + }), + ) .mutation(async ({ ctx, input }) => { // Check ownership const form = await prisma.form.findFirst({ @@ -292,10 +299,12 @@ const formsRouter = router({ // Create shared link createSharedLink: authedProcedure - .input(z.object({ - formId: z.string(), - settings: SharedLinkInput, - })) + .input( + z.object({ + formId: z.string(), + settings: SharedLinkInput, + }), + ) .mutation(async ({ ctx, input }) => { // Check ownership const form = await prisma.form.findFirst({ @@ -353,10 +362,12 @@ const formsRouter = router({ // Update shared link updateSharedLink: authedProcedure - .input(z.object({ - id: z.string(), - settings: SharedLinkInput.partial(), - })) + .input( + z.object({ + id: z.string(), + settings: SharedLinkInput.partial(), + }), + ) .mutation(async ({ ctx, input }) => { // Check ownership through form const sharedLink = await prisma.sharedLink.findFirst({ @@ -465,12 +476,14 @@ const formsRouter = router({ // Get form templates getTemplates: publicProcedure - .input(z.object({ - page: z.number().default(1), - limit: z.number().min(1).max(50).default(20), - tags: z.array(z.string()).optional(), - search: z.string().optional(), - })) + .input( + z.object({ + page: z.number().default(1), + limit: z.number().min(1).max(50).default(20), + tags: z.array(z.string()).optional(), + search: z.string().optional(), + }), + ) .query(async ({ input }) => { const { page, limit, tags, search } = input; const skip = (page - 1) * limit; @@ -484,9 +497,10 @@ const formsRouter = router({ { description: { contains: search, mode: 'insensitive' } }, ], }), - ...(tags && tags.length > 0 && { - templateTags: { hasSome: tags }, - }), + ...(tags && + tags.length > 0 && { + templateTags: { hasSome: tags }, + }), }; const [templates, total] = await Promise.all([ @@ -525,4 +539,4 @@ const formsRouter = router({ }), }); -module.exports = formsRouter; \ No newline at end of file +module.exports = formsRouter; diff --git a/backend/trpc/routers/llm-analysis.js b/backend/trpc/routers/llm-analysis.js index f322c755..aad48b10 100644 --- a/backend/trpc/routers/llm-analysis.js +++ b/backend/trpc/routers/llm-analysis.js @@ -40,11 +40,15 @@ const llmAnalysisRouter = router({ // Trigger analysis for a submission analyzeSubmission: authedProcedure - .input(z.object({ - submissionId: z.string(), - analysisTypes: z.array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])).default(['SENTIMENT', 'CLASSIFICATION']), - priority: z.number().min(1).max(10).default(1), - })) + .input( + z.object({ + submissionId: z.string(), + analysisTypes: z + .array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])) + .default(['SENTIMENT', 'CLASSIFICATION']), + priority: z.number().min(1).max(10).default(1), + }), + ) .mutation(async ({ ctx, input }) => { // Check if submission belongs to user const submission = await prisma.submission.findFirst({ @@ -84,13 +88,17 @@ const llmAnalysisRouter = router({ // Bulk analyze multiple submissions bulkAnalyze: authedProcedure - .input(z.object({ - formId: z.string(), - analysisTypes: z.array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])).default(['SENTIMENT', 'CLASSIFICATION']), - batchSize: z.number().min(1).max(100).default(10), - dateFrom: z.date().optional(), - dateTo: z.date().optional(), - })) + .input( + z.object({ + formId: z.string(), + analysisTypes: z + .array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])) + .default(['SENTIMENT', 'CLASSIFICATION']), + batchSize: z.number().min(1).max(100).default(10), + dateFrom: z.date().optional(), + dateTo: z.date().optional(), + }), + ) .mutation(async ({ ctx, input }) => { // Check form ownership const form = await prisma.form.findFirst({ @@ -112,12 +120,12 @@ const llmAnalysisRouter = router({ where: { formId: input.formId, status: 'PENDING', - ...(input.dateFrom || input.dateTo) && { + ...((input.dateFrom || input.dateTo) && { submittedAt: { ...(input.dateFrom && { gte: input.dateFrom }), ...(input.dateTo && { lte: input.dateTo }), }, - }, + }), }, take: input.batchSize, orderBy: { submittedAt: 'desc' }, @@ -146,16 +154,18 @@ const llmAnalysisRouter = router({ message: `Bulk analysis queued for ${submissions.length} submissions`, queuedJobs: jobs.length, jobIds: jobs, - estimatedCompletion: new Date(Date.now() + (submissions.length * 2000) + 60000), + estimatedCompletion: new Date(Date.now() + submissions.length * 2000 + 60000), }; }), // Get analysis insights for a form getFormInsights: authedProcedure - .input(z.object({ - formId: z.string(), - dateRange: z.number().min(1).max(365).default(30), - })) + .input( + z.object({ + formId: z.string(), + dateRange: z.number().min(1).max(365).default(30), + }), + ) .query(async ({ ctx, input }) => { // Check form ownership const form = await prisma.form.findFirst({ @@ -178,13 +188,17 @@ const llmAnalysisRouter = router({ // Get analysis history getAnalysisHistory: authedProcedure - .input(z.object({ - formId: z.string(), - limit: z.number().min(1).max(100).default(50), - analysisType: z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM']).optional(), - dateFrom: z.date().optional(), - dateTo: z.date().optional(), - })) + .input( + z.object({ + formId: z.string(), + limit: z.number().min(1).max(100).default(50), + analysisType: z + .enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM']) + .optional(), + dateFrom: z.date().optional(), + dateTo: z.date().optional(), + }), + ) .query(async ({ ctx, input }) => { // Check form ownership const form = await prisma.form.findFirst({ @@ -213,16 +227,20 @@ const llmAnalysisRouter = router({ // Update form LLM settings updateFormLLMSettings: authedProcedure - .input(z.object({ - formId: z.string(), - settings: z.object({ - enableLLMAnalysis: z.boolean(), - llmAnalysisTypes: z.array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])).optional(), - customAnalysisPrompt: z.string().optional(), - analysisCategories: z.array(z.string()).optional(), - autoAnalyzeSubmissions: z.boolean().default(false), + .input( + z.object({ + formId: z.string(), + settings: z.object({ + enableLLMAnalysis: z.boolean(), + llmAnalysisTypes: z + .array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])) + .optional(), + customAnalysisPrompt: z.string().optional(), + analysisCategories: z.array(z.string()).optional(), + autoAnalyzeSubmissions: z.boolean().default(false), + }), }), - })) + ) .mutation(async ({ ctx, input }) => { // Check form ownership const form = await prisma.form.findFirst({ @@ -257,18 +275,19 @@ const llmAnalysisRouter = router({ }), // Get queue statistics (admin only) - getQueueStats: adminProcedure - .query(async () => { - const stats = await QueueManager.getQueueStats(); - return stats; - }), + getQueueStats: adminProcedure.query(async () => { + const stats = await QueueManager.getQueueStats(); + return stats; + }), // Manage queues (admin only) manageQueue: adminProcedure - .input(z.object({ - action: z.enum(['pause', 'resume', 'cleanup']), - queueName: z.enum(['analysis', 'email', 'export']).optional(), - })) + .input( + z.object({ + action: z.enum(['pause', 'resume', 'cleanup']), + queueName: z.enum(['analysis', 'email', 'export']).optional(), + }), + ) .mutation(async ({ input }) => { const { action, queueName } = input; @@ -307,10 +326,14 @@ const llmAnalysisRouter = router({ // Get analysis cost estimates getCostEstimate: authedProcedure - .input(z.object({ - submissionCount: z.number().min(1).max(10000), - analysisTypes: z.array(z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM'])), - })) + .input( + z.object({ + submissionCount: z.number().min(1).max(10000), + analysisTypes: z.array( + z.enum(['SENTIMENT', 'CLASSIFICATION', 'EXTRACTION', 'SUMMARY', 'CUSTOM']), + ), + }), + ) .query(async ({ input }) => { // Simple cost estimation (adjust based on your actual costs) const costPerAnalysis = { @@ -322,7 +345,7 @@ const llmAnalysisRouter = router({ }; const totalCost = input.analysisTypes.reduce((sum, type) => { - return sum + (costPerAnalysis[type] * input.submissionCount); + return sum + costPerAnalysis[type] * input.submissionCount; }, 0); const estimatedTime = input.submissionCount * 3; // 3 seconds per analysis average @@ -332,7 +355,7 @@ const llmAnalysisRouter = router({ analysisTypes: input.analysisTypes, estimatedCost: totalCost, estimatedTimeSeconds: estimatedTime, - costBreakdown: input.analysisTypes.map(type => ({ + costBreakdown: input.analysisTypes.map((type) => ({ type, costPerUnit: costPerAnalysis[type], totalCost: costPerAnalysis[type] * input.submissionCount, @@ -341,4 +364,4 @@ const llmAnalysisRouter = router({ }), }); -module.exports = llmAnalysisRouter; \ No newline at end of file +module.exports = llmAnalysisRouter; diff --git a/backend/trpc/routers/submissions.js b/backend/trpc/routers/submissions.js index 8346d461..d0c44506 100644 --- a/backend/trpc/routers/submissions.js +++ b/backend/trpc/routers/submissions.js @@ -1,4 +1,11 @@ -const { router, publicProcedure, authedProcedure, prisma, z, TRPCError } = require('../../lib/trpc'); +const { + router, + publicProcedure, + authedProcedure, + prisma, + z, + TRPCError, +} = require('../../lib/trpc'); const SubmissionCreateInput = z.object({ formId: z.string().optional(), // For direct submissions @@ -10,14 +17,16 @@ const SubmissionCreateInput = z.object({ const submissionsRouter = router({ // Get all submissions for user's forms list: authedProcedure - .input(z.object({ - page: z.number().default(1), - limit: z.number().min(1).max(100).default(20), - formId: z.string().optional(), - status: z.enum(['PENDING', 'PROCESSED', 'ARCHIVED', 'DELETED']).optional(), - dateFrom: z.date().optional(), - dateTo: z.date().optional(), - })) + .input( + z.object({ + page: z.number().default(1), + limit: z.number().min(1).max(100).default(20), + formId: z.string().optional(), + status: z.enum(['PENDING', 'PROCESSED', 'ARCHIVED', 'DELETED']).optional(), + dateFrom: z.date().optional(), + dateTo: z.date().optional(), + }), + ) .query(async ({ ctx, input }) => { const { page, limit, formId, status, dateFrom, dateTo } = input; const skip = (page - 1) * limit; @@ -26,12 +35,12 @@ const submissionsRouter = router({ form: { userId: ctx.user.id }, ...(formId && { formId }), ...(status && { status }), - ...(dateFrom || dateTo) && { + ...((dateFrom || dateTo) && { submittedAt: { ...(dateFrom && { gte: dateFrom }), ...(dateTo && { lte: dateTo }), }, - }, + }), }; const [submissions, total] = await Promise.all([ @@ -76,165 +85,160 @@ const submissionsRouter = router({ }), // Get single submission - get: authedProcedure - .input(z.object({ id: z.string() })) - .query(async ({ ctx, input }) => { - const submission = await prisma.submission.findFirst({ - where: { - id: input.id, - form: { userId: ctx.user.id }, - }, - include: { - form: { - select: { - id: true, - title: true, - schema: true, - }, - }, - submitter: { - select: { - id: true, - name: true, - email: true, - avatar: true, - }, + get: authedProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => { + const submission = await prisma.submission.findFirst({ + where: { + id: input.id, + form: { userId: ctx.user.id }, + }, + include: { + form: { + select: { + id: true, + title: true, + schema: true, }, - analyses: { - orderBy: { createdAt: 'desc' }, + }, + submitter: { + select: { + id: true, + name: true, + email: true, + avatar: true, }, }, + analyses: { + orderBy: { createdAt: 'desc' }, + }, + }, + }); + + if (!submission) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Submission not found', }); + } - if (!submission) { + return submission; + }), + + // Create submission (public endpoint for shared forms) + create: publicProcedure.input(SubmissionCreateInput).mutation(async ({ input, ctx }) => { + let form; + let sharedLink; + + // Handle shared link submission + if (input.sharedLinkSlug) { + sharedLink = await prisma.sharedLink.findUnique({ + where: { slug: input.sharedLinkSlug }, + include: { form: true }, + }); + + if (!sharedLink || !sharedLink.isActive) { throw new TRPCError({ code: 'NOT_FOUND', - message: 'Submission not found', + message: 'Form link not found or expired', }); } - return submission; - }), - - // Create submission (public endpoint for shared forms) - create: publicProcedure - .input(SubmissionCreateInput) - .mutation(async ({ input, ctx }) => { - let form; - let sharedLink; - - // Handle shared link submission - if (input.sharedLinkSlug) { - sharedLink = await prisma.sharedLink.findUnique({ - where: { slug: input.sharedLinkSlug }, - include: { form: true }, + // Check expiration + if (sharedLink.expiresAt && sharedLink.expiresAt < new Date()) { + throw new TRPCError({ + code: 'GONE', + message: 'This form link has expired', }); + } - if (!sharedLink || !sharedLink.isActive) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Form link not found or expired', - }); - } - - // Check expiration - if (sharedLink.expiresAt && sharedLink.expiresAt < new Date()) { - throw new TRPCError({ - code: 'GONE', - message: 'This form link has expired', - }); - } - - // Check submission limit - if (sharedLink.maxSubmissions && sharedLink.currentSubmissions >= sharedLink.maxSubmissions) { - throw new TRPCError({ - code: 'FORBIDDEN', - message: 'This form has reached its submission limit', - }); - } - - form = sharedLink.form; - } else if (input.formId) { - // Direct form submission (requires auth) - if (!ctx.user) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'Authentication required for direct form submission', - }); - } - - form = await prisma.form.findFirst({ - where: { - id: input.formId, - OR: [ - { userId: ctx.user.id }, - { collaborators: { some: { userId: ctx.user.id } } }, - ], - }, + // Check submission limit + if (sharedLink.maxSubmissions && sharedLink.currentSubmissions >= sharedLink.maxSubmissions) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'This form has reached its submission limit', }); + } - if (!form) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Form not found', - }); - } - } else { + form = sharedLink.form; + } else if (input.formId) { + // Direct form submission (requires auth) + if (!ctx.user) { throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Either formId or sharedLinkSlug must be provided', + code: 'UNAUTHORIZED', + message: 'Authentication required for direct form submission', }); } - // Create submission - const submission = await prisma.submission.create({ - data: { - formId: form.id, - submitterId: ctx.user?.id, - data: input.data, - metadata: { - ...input.metadata, - source: input.sharedLinkSlug ? 'SHARED_LINK' : 'DIRECT', - userAgent: ctx.req?.headers['user-agent'], - ipAddress: ctx.req?.ip, - }, - source: input.sharedLinkSlug ? 'SHARED_LINK' : 'DIRECT', + form = await prisma.form.findFirst({ + where: { + id: input.formId, + OR: [{ userId: ctx.user.id }, { collaborators: { some: { userId: ctx.user.id } } }], }, }); - // Update shared link submission count - if (sharedLink) { - await prisma.sharedLink.update({ - where: { id: sharedLink.id }, - data: { currentSubmissions: { increment: 1 } }, + if (!form) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Form not found', }); } + } else { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Either formId or sharedLinkSlug must be provided', + }); + } + + // Create submission + const submission = await prisma.submission.create({ + data: { + formId: form.id, + submitterId: ctx.user?.id, + data: input.data, + metadata: { + ...input.metadata, + source: input.sharedLinkSlug ? 'SHARED_LINK' : 'DIRECT', + userAgent: ctx.req?.headers['user-agent'], + ipAddress: ctx.req?.ip, + }, + source: input.sharedLinkSlug ? 'SHARED_LINK' : 'DIRECT', + }, + }); + + // Update shared link submission count + if (sharedLink) { + await prisma.sharedLink.update({ + where: { id: sharedLink.id }, + data: { currentSubmissions: { increment: 1 } }, + }); + } - // Queue LLM analysis if enabled - const hasLLMAnalysis = form.settings?.enableLLMAnalysis; - if (hasLLMAnalysis) { - await prisma.backgroundJob.create({ + // Queue LLM analysis if enabled + const hasLLMAnalysis = form.settings?.enableLLMAnalysis; + if (hasLLMAnalysis) { + await prisma.backgroundJob.create({ + data: { + type: 'LLM_ANALYSIS', data: { - type: 'LLM_ANALYSIS', - data: { - submissionId: submission.id, - formId: form.id, - analysisTypes: form.settings.llmAnalysisTypes || ['SENTIMENT', 'CLASSIFICATION'], - }, - priority: 1, + submissionId: submission.id, + formId: form.id, + analysisTypes: form.settings.llmAnalysisTypes || ['SENTIMENT', 'CLASSIFICATION'], }, - }); - } + priority: 1, + }, + }); + } - return submission; - }), + return submission; + }), // Update submission status updateStatus: authedProcedure - .input(z.object({ - id: z.string(), - status: z.enum(['PENDING', 'PROCESSED', 'ARCHIVED', 'DELETED']), - })) + .input( + z.object({ + id: z.string(), + status: z.enum(['PENDING', 'PROCESSED', 'ARCHIVED', 'DELETED']), + }), + ) .mutation(async ({ ctx, input }) => { const submission = await prisma.submission.findFirst({ where: { @@ -260,29 +264,31 @@ const submissionsRouter = router({ // Get submission statistics getStats: authedProcedure - .input(z.object({ - formId: z.string().optional(), - dateFrom: z.date().optional(), - dateTo: z.date().optional(), - })) + .input( + z.object({ + formId: z.string().optional(), + dateFrom: z.date().optional(), + dateTo: z.date().optional(), + }), + ) .query(async ({ ctx, input }) => { const { formId, dateFrom, dateTo } = input; const where = { form: { userId: ctx.user.id }, ...(formId && { formId }), - ...(dateFrom || dateTo) && { + ...((dateFrom || dateTo) && { submittedAt: { ...(dateFrom && { gte: dateFrom }), ...(dateTo && { lte: dateTo }), }, - }, + }), }; const [total, today, thisWeek, byStatus, bySource] = await Promise.all([ // Total submissions prisma.submission.count({ where }), - + // Today's submissions prisma.submission.count({ where: { @@ -292,7 +298,7 @@ const submissionsRouter = router({ }, }, }), - + // This week's submissions prisma.submission.count({ where: { @@ -302,14 +308,14 @@ const submissionsRouter = router({ }, }, }), - + // By status prisma.submission.groupBy({ by: ['status'], where, _count: { status: true }, }), - + // By source prisma.submission.groupBy({ by: ['source'], @@ -335,12 +341,14 @@ const submissionsRouter = router({ // Export submissions export: authedProcedure - .input(z.object({ - formId: z.string(), - format: z.enum(['json', 'csv']).default('json'), - dateFrom: z.date().optional(), - dateTo: z.date().optional(), - })) + .input( + z.object({ + formId: z.string(), + format: z.enum(['json', 'csv']).default('json'), + dateFrom: z.date().optional(), + dateTo: z.date().optional(), + }), + ) .mutation(async ({ ctx, input }) => { const { formId, format, dateFrom, dateTo } = input; @@ -378,29 +386,27 @@ const submissionsRouter = router({ }), // Delete submission - delete: authedProcedure - .input(z.object({ id: z.string() })) - .mutation(async ({ ctx, input }) => { - const submission = await prisma.submission.findFirst({ - where: { - id: input.id, - form: { userId: ctx.user.id }, - }, - }); - - if (!submission) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Submission not found', - }); - } + delete: authedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { + const submission = await prisma.submission.findFirst({ + where: { + id: input.id, + form: { userId: ctx.user.id }, + }, + }); - await prisma.submission.delete({ - where: { id: input.id }, + if (!submission) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Submission not found', }); + } - return { success: true }; - }), + await prisma.submission.delete({ + where: { id: input.id }, + }); + + return { success: true }; + }), }); -module.exports = submissionsRouter; \ No newline at end of file +module.exports = submissionsRouter; diff --git a/backend/trpc/routers/user.js b/backend/trpc/routers/user.js index 3f0c317f..d6ecd412 100644 --- a/backend/trpc/routers/user.js +++ b/backend/trpc/routers/user.js @@ -1,4 +1,12 @@ -const { router, publicProcedure, authedProcedure, adminProcedure, prisma, z, TRPCError } = require('../../lib/trpc'); +const { + router, + publicProcedure, + authedProcedure, + adminProcedure, + prisma, + z, + TRPCError, +} = require('../../lib/trpc'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); @@ -15,83 +23,80 @@ const UserUpdateInput = z.object({ const userRouter = router({ // Get current user - me: authedProcedure - .query(async ({ ctx }) => { - const user = await prisma.user.findUnique({ - where: { id: ctx.user.id }, - include: { - organization: true, - _count: { - select: { - forms: true, - submissions: true, - }, + me: authedProcedure.query(async ({ ctx }) => { + const user = await prisma.user.findUnique({ + where: { id: ctx.user.id }, + include: { + organization: true, + _count: { + select: { + forms: true, + submissions: true, }, }, - }); - - if (!user) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'User not found', - }); - } - - // Remove sensitive data - const { passwordHash, ...userWithoutPassword } = user; - return userWithoutPassword; - }), + }, + }); - // Register new user - register: publicProcedure - .input(UserCreateInput) - .mutation(async ({ input }) => { - // Check if user already exists - const existingUser = await prisma.user.findUnique({ - where: { email: input.email }, + if (!user) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'User not found', }); + } - if (existingUser) { - throw new TRPCError({ - code: 'CONFLICT', - message: 'User with this email already exists', - }); - } + // Remove sensitive data + const { passwordHash, ...userWithoutPassword } = user; + return userWithoutPassword; + }), - // Hash password - const passwordHash = await bcrypt.hash(input.password, 12); - - // Create user - const user = await prisma.user.create({ - data: { - email: input.email, - passwordHash, - name: input.name, - }, + // Register new user + register: publicProcedure.input(UserCreateInput).mutation(async ({ input }) => { + // Check if user already exists + const existingUser = await prisma.user.findUnique({ + where: { email: input.email }, + }); + + if (existingUser) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'User with this email already exists', }); - - // Generate JWT token - const token = jwt.sign( - { userId: user.id, email: user.email }, - process.env.JWT_SECRET, - { expiresIn: '7d' } - ); - - // Remove sensitive data - const { passwordHash: _, ...userWithoutPassword } = user; - - return { - user: userWithoutPassword, - token, - }; - }), + } + + // Hash password + const passwordHash = await bcrypt.hash(input.password, 12); + + // Create user + const user = await prisma.user.create({ + data: { + email: input.email, + passwordHash, + name: input.name, + }, + }); + + // Generate JWT token + const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { + expiresIn: '7d', + }); + + // Remove sensitive data + const { passwordHash: _, ...userWithoutPassword } = user; + + return { + user: userWithoutPassword, + token, + }; + }), // Login user login: publicProcedure - .input(z.object({ - email: z.string().email(), - password: z.string(), - })) + .input( + z.object({ + email: z.string().email(), + password: z.string(), + }), + ) .mutation(async ({ input }) => { // Find user const user = await prisma.user.findUnique({ @@ -107,7 +112,7 @@ const userRouter = router({ // Verify password const isValidPassword = await bcrypt.compare(input.password, user.passwordHash); - + if (!isValidPassword) { throw new TRPCError({ code: 'UNAUTHORIZED', @@ -124,15 +129,13 @@ const userRouter = router({ } // Generate JWT token - const token = jwt.sign( - { userId: user.id, email: user.email }, - process.env.JWT_SECRET, - { expiresIn: '7d' } - ); + const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { + expiresIn: '7d', + }); // Remove sensitive data const { passwordHash: _, ...userWithoutPassword } = user; - + return { user: userWithoutPassword, token, @@ -140,49 +143,48 @@ const userRouter = router({ }), // Update user profile - updateProfile: authedProcedure - .input(UserUpdateInput) - .mutation(async ({ ctx, input }) => { - const user = await prisma.user.update({ - where: { id: ctx.user.id }, - data: input, - }); + updateProfile: authedProcedure.input(UserUpdateInput).mutation(async ({ ctx, input }) => { + const user = await prisma.user.update({ + where: { id: ctx.user.id }, + data: input, + }); - // Remove sensitive data - const { passwordHash, ...userWithoutPassword } = user; - return userWithoutPassword; - }), + // Remove sensitive data + const { passwordHash, ...userWithoutPassword } = user; + return userWithoutPassword; + }), // Get user stats (admin only) - getStats: adminProcedure - .query(async () => { - const [totalUsers, activeUsers, todaysSignups] = await Promise.all([ - prisma.user.count(), - prisma.user.count({ where: { isActive: true } }), - prisma.user.count({ - where: { - createdAt: { - gte: new Date(new Date().setHours(0, 0, 0, 0)), - }, + getStats: adminProcedure.query(async () => { + const [totalUsers, activeUsers, todaysSignups] = await Promise.all([ + prisma.user.count(), + prisma.user.count({ where: { isActive: true } }), + prisma.user.count({ + where: { + createdAt: { + gte: new Date(new Date().setHours(0, 0, 0, 0)), }, - }), - ]); + }, + }), + ]); - return { - totalUsers, - activeUsers, - todaysSignups, - }; - }), + return { + totalUsers, + activeUsers, + todaysSignups, + }; + }), // List users (admin only) list: adminProcedure - .input(z.object({ - page: z.number().default(1), - limit: z.number().min(1).max(100).default(20), - search: z.string().optional(), - role: z.enum(['USER', 'ADMIN', 'SUPER_ADMIN']).optional(), - })) + .input( + z.object({ + page: z.number().default(1), + limit: z.number().min(1).max(100).default(20), + search: z.string().optional(), + role: z.enum(['USER', 'ADMIN', 'SUPER_ADMIN']).optional(), + }), + ) .query(async ({ input }) => { const { page, limit, search, role } = input; const skip = (page - 1) * limit; @@ -240,4 +242,4 @@ const userRouter = router({ }), }); -module.exports = userRouter; \ No newline at end of file +module.exports = userRouter; diff --git a/backend/utils/responseHandlers.js b/backend/utils/responseHandlers.js index 8364760b..abdb9173 100644 --- a/backend/utils/responseHandlers.js +++ b/backend/utils/responseHandlers.js @@ -6,20 +6,20 @@ const success = (res, data = {}, message = 'Success', status = 200) => { return res.status(status).json({ success: true, message, - ...data + ...data, }); }; const error = (res, message = 'Server error', status = 500, details = null) => { const response = { success: false, - message + message, }; - + if (details && process.env.NODE_ENV === 'development') { response.details = details; } - + return res.status(status).json(response); }; @@ -27,28 +27,28 @@ const validationError = (res, message = 'Validation failed', errors = []) => { return res.status(400).json({ success: false, message, - errors + errors, }); }; const unauthorized = (res, message = 'Unauthorized') => { return res.status(401).json({ success: false, - message + message, }); }; const forbidden = (res, message = 'Forbidden') => { return res.status(403).json({ success: false, - message + message, }); }; const notFound = (res, message = 'Not found') => { return res.status(404).json({ success: false, - message + message, }); }; @@ -63,5 +63,5 @@ module.exports = { unauthorized, forbidden, notFound, - created -}; \ No newline at end of file + created, +}; diff --git a/backend/utils/responseHandlers.test.js b/backend/utils/responseHandlers.test.js index d58d6a91..574740da 100644 --- a/backend/utils/responseHandlers.test.js +++ b/backend/utils/responseHandlers.test.js @@ -15,8 +15,8 @@ */ // ESM imports even though the module under test is CommonJS: vitest cannot be // `require`d, and Vite handles the CJS interop on the default import. -import { afterEach, describe, expect, it } from 'vitest' -import handlers from './responseHandlers.js' +import { afterEach, describe, expect, it } from 'vitest'; +import handlers from './responseHandlers.js'; /** Minimal Express `res` double that records what was sent. */ function fakeRes() { @@ -24,91 +24,91 @@ function fakeRes() { statusCode: undefined, body: undefined, status(code) { - this.statusCode = code - return this + this.statusCode = code; + return this; }, json(payload) { - this.body = payload - return this + this.body = payload; + return this; }, - } + }; } -const originalEnv = process.env.NODE_ENV +const originalEnv = process.env.NODE_ENV; afterEach(() => { - process.env.NODE_ENV = originalEnv -}) + process.env.NODE_ENV = originalEnv; +}); describe('success', () => { it('sends 200 with success:true by default', () => { - const res = handlers.success(fakeRes(), { id: 1 }) - expect(res.statusCode).toBe(200) - expect(res.body.success).toBe(true) - expect(res.body.id).toBe(1) - }) + const res = handlers.success(fakeRes(), { id: 1 }); + expect(res.statusCode).toBe(200); + expect(res.body.success).toBe(true); + expect(res.body.id).toBe(1); + }); it('honours an explicit status', () => { - expect(handlers.success(fakeRes(), {}, 'Created', 201).statusCode).toBe(201) - }) + expect(handlers.success(fakeRes(), {}, 'Created', 201).statusCode).toBe(201); + }); it('carries the message through', () => { - expect(handlers.success(fakeRes(), {}, 'Saved').body.message).toBe('Saved') - }) -}) + expect(handlers.success(fakeRes(), {}, 'Saved').body.message).toBe('Saved'); + }); +}); describe('error', () => { it('sends 500 with success:false by default', () => { - const res = handlers.error(fakeRes()) - expect(res.statusCode).toBe(500) - expect(res.body.success).toBe(false) - }) + const res = handlers.error(fakeRes()); + expect(res.statusCode).toBe(500); + expect(res.body.success).toBe(false); + }); it('honours an explicit status', () => { - expect(handlers.error(fakeRes(), 'Not found', 404).statusCode).toBe(404) - }) + expect(handlers.error(fakeRes(), 'Not found', 404).statusCode).toBe(404); + }); it('includes details in development', () => { - process.env.NODE_ENV = 'development' - const res = handlers.error(fakeRes(), 'Boom', 500, { stack: 'at db.query' }) - expect(res.body.details).toEqual({ stack: 'at db.query' }) - }) + process.env.NODE_ENV = 'development'; + const res = handlers.error(fakeRes(), 'Boom', 500, { stack: 'at db.query' }); + expect(res.body.details).toEqual({ stack: 'at db.query' }); + }); it('NEVER includes details in production', () => { // The leak guard. `details` is where stack traces and driver errors go; // shipping them to a client is how internals reach an attacker. Asserted // directly rather than trusted, because the check is one `if` away from // being refactored into always-on. - process.env.NODE_ENV = 'production' - const res = handlers.error(fakeRes(), 'Boom', 500, { stack: 'at db.query' }) - expect(res.body.details).toBeUndefined() - }) + process.env.NODE_ENV = 'production'; + const res = handlers.error(fakeRes(), 'Boom', 500, { stack: 'at db.query' }); + expect(res.body.details).toBeUndefined(); + }); it('omits details when none are given, in any environment', () => { for (const env of ['development', 'production', 'test']) { - process.env.NODE_ENV = env - expect(handlers.error(fakeRes(), 'Boom').body.details).toBeUndefined() + process.env.NODE_ENV = env; + expect(handlers.error(fakeRes(), 'Boom').body.details).toBeUndefined(); } - }) -}) + }); +}); describe('validationError', () => { it('sends 400 with success:false and the errors array', () => { - const res = handlers.validationError(fakeRes(), 'Invalid', [{ field: 'email' }]) - expect(res.statusCode).toBe(400) - expect(res.body.success).toBe(false) - expect(res.body.errors).toEqual([{ field: 'email' }]) - }) -}) + const res = handlers.validationError(fakeRes(), 'Invalid', [{ field: 'email' }]); + expect(res.statusCode).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.errors).toEqual([{ field: 'email' }]); + }); +}); describe('the envelope is uniform', () => { it('every exported handler sets a boolean `success`', () => { // A client that branches on `success` must never meet a response without // it โ€” that reads as neither a win nor a failure. for (const [name, handler] of Object.entries(handlers)) { - if (typeof handler !== 'function') continue - const res = handler(fakeRes()) - expect(typeof res.body?.success, `${name} did not set a boolean success`).toBe('boolean') + if (typeof handler !== 'function') continue; + const res = handler(fakeRes()); + expect(typeof res.body?.success, `${name} did not set a boolean success`).toBe('boolean'); } - }) -}) + }); +}); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0902a3f3..f897e684 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,15 +9,15 @@ services: POSTGRES_DB: ${DB_DATABASE:-formbuilder} POSTGRES_USER: ${DB_USER:-formbuilder} POSTGRES_PASSWORD: ${DB_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C' ports: - - "5432:5432" + - '5432:5432' volumes: - postgres_prod_data:/var/lib/postgresql/data networks: - formular-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-formbuilder} -d ${DB_DATABASE:-formbuilder}"] + test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-formbuilder} -d ${DB_DATABASE:-formbuilder}'] interval: 10s timeout: 5s retries: 5 @@ -28,13 +28,13 @@ services: image: redis:7-alpine container_name: formular-redis-prod ports: - - "6379:6379" + - '6379:6379' volumes: - redis_prod_data:/data networks: - formular-network healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 5s retries: 5 @@ -61,8 +61,8 @@ services: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:5001} NEXT_PUBLIC_BRAND_PRESET: ${NEXT_PUBLIC_BRAND_PRESET:-generic} ports: - - "3000:3000" - - "5001:5001" + - '3000:3000' + - '5001:5001' depends_on: postgres: condition: service_healthy @@ -72,7 +72,7 @@ services: - formular-network restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + test: ['CMD', 'curl', '-f', 'http://localhost:5001/health'] interval: 30s timeout: 10s retries: 3 @@ -83,8 +83,8 @@ services: image: nginx:alpine container_name: formular-nginx-prod ports: - - "80:80" - - "443:443" + - '80:80' + - '443:443' volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./ssl:/etc/nginx/ssl:ro @@ -104,4 +104,4 @@ volumes: networks: formular-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index a3f146db..e2b6c1f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,15 +9,15 @@ services: POSTGRES_DB: formbuilder POSTGRES_USER: formbuilder POSTGRES_PASSWORD: devpassword - POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C' ports: - - "5433:5432" + - '5433:5432' volumes: - postgres_data:/var/lib/postgresql/data networks: - formular-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U formbuilder -d formbuilder"] + test: ['CMD-SHELL', 'pg_isready -U formbuilder -d formbuilder'] interval: 10s timeout: 5s retries: 5 @@ -27,13 +27,13 @@ services: image: redis:7-alpine container_name: formular-redis ports: - - "6380:6379" + - '6380:6379' volumes: - redis_data:/data networks: - formular-network healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ['CMD', 'redis-cli', 'ping'] interval: 10s timeout: 5s retries: 5 @@ -56,7 +56,7 @@ services: JWT_SECRET: your-super-secret-jwt-key-change-in-production DATABASE_URL: postgresql://formbuilder:devpassword@postgres:5432/formbuilder ports: - - "5001:5001" + - '5001:5001' depends_on: postgres: condition: service_healthy @@ -67,9 +67,9 @@ services: volumes: - ./backend:/app/backend:ro - backend_node_modules:/app/backend/node_modules - command: ["npm", "run", "dev:backend"] + command: ['npm', 'run', 'dev:backend'] healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + test: ['CMD', 'curl', '-f', 'http://localhost:5001/health'] interval: 30s timeout: 10s retries: 3 @@ -88,7 +88,7 @@ services: # session token that the real Express backend verifies on data-ingestion routes. JWT_SECRET: your-super-secret-jwt-key-change-in-production ports: - - "3000:3000" + - '3000:3000' depends_on: - backend networks: @@ -97,7 +97,7 @@ services: - ./frontend:/app/frontend:ro - frontend_node_modules:/app/frontend/node_modules - ./frontend/.next:/app/frontend/.next - command: ["npm", "run", "dev:frontend"] + command: ['npm', 'run', 'dev:frontend'] # Database Migration Service (runs once) migrate: @@ -112,7 +112,7 @@ services: condition: service_healthy networks: - formular-network - command: ["npx", "prisma", "migrate", "deploy"] + command: ['npx', 'prisma', 'migrate', 'deploy'] profiles: - migrate @@ -128,4 +128,4 @@ volumes: networks: formular-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..3b525d38 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,30 @@ +# Build output and vendored trees โ€” formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md + +# Playwright output, written during the run CI performs before format:check. +test-results +playwright-report + +# Generated during the build CI runs before format:check, so it never exists +# locally at check time. Contentlayer output also uses import assertions, +# which prettier cannot parse. +.contentlayer diff --git a/frontend/content/blog/intake-engine.mdx b/frontend/content/blog/intake-engine.mdx index 340b2288..dd788d48 100644 --- a/frontend/content/blog/intake-engine.mdx +++ b/frontend/content/blog/intake-engine.mdx @@ -1,16 +1,16 @@ --- created_date: 2025-07-10 last_modified_date: 2025-07-10 -last_modified_summary: "Initial migration of intake-engine article to MDX with richer content and infographic." +last_modified_summary: 'Initial migration of intake-engine article to MDX with richer content and infographic.' -title: "Vom Rohdatenfluss zur smarten Entscheidung โ€“ unsere Data-Intake-Journey" -date: "2025-07-10" -summary: "Warum Datenaufnahme der Grundstein jeder Analyse ist โ€“ und wie wir sie mit Formularen, Streams und AI meistern." -coverImage: "/images/blog/intake-engine/cover.jpg" -tags: ["data-ingestion", "form-builder", "llm"] +title: 'Vom Rohdatenfluss zur smarten Entscheidung โ€“ unsere Data-Intake-Journey' +date: '2025-07-10' +summary: 'Warum Datenaufnahme der Grundstein jeder Analyse ist โ€“ und wie wir sie mit Formularen, Streams und AI meistern.' +coverImage: '/images/blog/intake-engine/cover.jpg' +tags: ['data-ingestion', 'form-builder', 'llm'] --- -import { Callout, Infographic } from '@/components/MDXComponents' +import { Callout, Infographic } from '@/components/MDXComponents'; ## Einleitung @@ -18,29 +18,36 @@ Daten sind das ร–l des 21. Jahrhunderts โ€“ ein abgedroschener Satz, doch o Rohdaten _alleine_ bringen jedoch keinen Motor zum Laufen. Erst wenn Informationen **bequem erfasst**, **intelligent analysiert** und schliesslich in **wirkungsvollen Aktionen** mรผnden, entsteht echter Mehrwert. -Wussten Sie, dass bereits **37 %** aller Datenprojekte am fehlenden Input scheitern? _Garbage in, garbage out_ gilt heute mehr denn je. + Wussten Sie, dass bereits **37 %** aller Datenprojekte am fehlenden Input scheitern? _Garbage in, + garbage out_ gilt heute mehr denn je. ## Unser Intake-Stack auf einen Blick - + -1. **Formulare** โ€“ schnell per Drag-and-Drop gebaut, perfekt fรผr menschlichen Input. -2. **Sensor- & API-Streams** โ€“ alles, was sich sekรผndlich รคndert, landet in unserem Event-Bus. -3. **Contextual Prompts** โ€“ jede Datenspur erhรคlt Zusatzwissen, bevor sie zum LLM geht. -4. **AI-Insights** โ€“ konkrete Empfehlungen, automatisch generiert. +1. **Formulare** โ€“ schnell per Drag-and-Drop gebaut, perfekt fรผr menschlichen Input. +2. **Sensor- & API-Streams** โ€“ alles, was sich sekรผndlich รคndert, landet in unserem Event-Bus. +3. **Contextual Prompts** โ€“ jede Datenspur erhรคlt Zusatzwissen, bevor sie zum LLM geht. +4. **AI-Insights** โ€“ konkrete Empfehlungen, automatisch generiert. --- ## Warum ist Datenerfassung so wichtig? ### 1. Qualitรคt schlรคgt Quantitรคt + Ein kleiner, sauberer Datensatz bringt mehr als eine Big-Data-Mรผllhalde. Durch Validierung _am_ Intake-Punkt verhindern wir Fehler, bevor sie teuer werden. ### 2. Geschwindigkeit entscheidet + Echtzeit-Ingestion ermรถglicht es, Trends sofort zu erkennen und zu reagieren. Unser Stream-Layer verarbeitet > 200 k Events โ„ Sekunde. ### 3. Kontext macht Daten wertvoll + Ohne Metadaten weiss auch das beste Modell nichts anzufangen. Deshalb reichern wir jeden Datensatz bereits beim Intake an โ€“ wer, wo, in welchem Schritt. --- @@ -48,32 +55,35 @@ Ohne Metadaten weiss auch das beste Modell nichts anzufangen. Deshalb reichern w ## Technischer Deep-Dive ### Formular-Ingestion -* **Schema-First**: Jedes Feld besitzt Typ, Constraints und Hilfetext. -* **Edge-Validation**: Client- & Server-Checks verhindern Inkonsistenzen. -* **Zustand Store**: Ein zentrales `useFormBuilderStore` hรคlt Felder & Steps synchron (Drag-and-Drop inklusive!). + +- **Schema-First**: Jedes Feld besitzt Typ, Constraints und Hilfetext. +- **Edge-Validation**: Client- & Server-Checks verhindern Inkonsistenzen. +- **Zustand Store**: Ein zentrales `useFormBuilderStore` hรคlt Felder & Steps synchron (Drag-and-Drop inklusive!). ### Stream-Ingestion -* **Kafka-Bus** fรผr hohe Durchsรคtze, Back-Pressure Handling via Quotas. -* **Protobuf Schemas** fรผr versionierte Payloads. -* **Exactly-Once writes** ins Lakehouse (Iceberg). + +- **Kafka-Bus** fรผr hohe Durchsรคtze, Back-Pressure Handling via Quotas. +- **Protobuf Schemas** fรผr versionierte Payloads. +- **Exactly-Once writes** ins Lakehouse (Iceberg). ### Prompt Engineering Layer -* Dynamische System-Prompts, basierend auf Datenquelle & Benutzerrolle. -* **Few-Shot Examples** aus historischen Entscheidungen. -* Feedback-Loop speichert Modell-Antworten zur weiteren Optimierung. + +- Dynamische System-Prompts, basierend auf Datenquelle & Benutzerrolle. +- **Few-Shot Examples** aus historischen Entscheidungen. +- Feedback-Loop speichert Modell-Antworten zur weiteren Optimierung. --- ## Roadmap -| Phase | Ziel | Status | -|-------|------|--------| -| **MVP** | Formular-Builder mit statischem Intake | โœ… live | -| **v1.1** | Sensor-Streams + Auto-Mapping | ๐Ÿ”„ in Arbeit | -| **v1.2** | AI-Insights Dashboard | โณ geplant | +| Phase | Ziel | Status | +| -------- | -------------------------------------- | ------------ | +| **MVP** | Formular-Builder mit statischem Intake | โœ… live | +| **v1.1** | Sensor-Streams + Auto-Mapping | ๐Ÿ”„ in Arbeit | +| **v1.2** | AI-Insights Dashboard | โณ geplant | --- ## Neugierig geworden? -Am Ende dieser Seite erwartet dich ein Call-to-Action โ€“ teste den **Universal Form Builder** noch heute und bring deine eigene Data-Intake-Journey ins Rollen! ๐Ÿš€ \ No newline at end of file +Am Ende dieser Seite erwartet dich ein Call-to-Action โ€“ teste den **Universal Form Builder** noch heute und bring deine eigene Data-Intake-Journey ins Rollen! ๐Ÿš€ diff --git a/frontend/contentlayer.config.ts b/frontend/contentlayer.config.ts index 3614dc7f..c7c263c6 100644 --- a/frontend/contentlayer.config.ts +++ b/frontend/contentlayer.config.ts @@ -40,4 +40,4 @@ export default makeSource({ remarkPlugins: [remarkGfm], rehypePlugins: [[rehypePrettyCode, { theme: 'github-dark' }]], }, -}); \ No newline at end of file +}); diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 9b6ab435..8c306ed4 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,6 +1,6 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -15,19 +15,19 @@ const eslintConfig = [ // them spelled out or it lints generated/build output (.next, // .contentlayer) that was never meant to be linted. { - ignores: [".next/**", ".contentlayer/**", "next-env.d.ts", "node_modules/**"], + ignores: ['.next/**', '.contentlayer/**', 'next-env.d.ts', 'node_modules/**'], }, - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...compat.extends('next/core-web-vitals', 'next/typescript'), { rules: { // Temporarily disable unused vars for build success - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/ban-ts-comment": "warn", - "react-hooks/exhaustive-deps": "warn", - "react/no-unescaped-entities": "warn", - "@next/next/no-img-element": "warn", - "react/jsx-no-undef": "error", + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/ban-ts-comment': 'warn', + 'react-hooks/exhaustive-deps': 'warn', + 'react/no-unescaped-entities': 'warn', + '@next/next/no-img-element': 'warn', + 'react/jsx-no-undef': 'error', }, }, ]; diff --git a/frontend/next.config.ts b/frontend/next.config.ts index f9934a02..273df616 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,17 +1,17 @@ -import type { NextConfig } from "next"; -import { withContentlayer } from "next-contentlayer"; +import type { NextConfig } from 'next'; +import { withContentlayer } from 'next-contentlayer'; const nextConfig: NextConfig = { - output: "standalone", + output: 'standalone', async rewrites() { // Proxy API requests to the backend only when BACKEND_URL is provided. const backendBase = process.env.BACKEND_URL; // Ensure we don't accidentally double-add `/api` if provided. if (backendBase) { - const sanitizedBase = backendBase.replace(/\/$/, ""); + const sanitizedBase = backendBase.replace(/\/$/, ''); return [ { - source: "/api/:path*", + source: '/api/:path*', destination: `${sanitizedBase}/api/:path*`, }, ]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e57ea4c4..ab1d6b80 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -45,6 +45,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "^15.5.12", + "prettier": "3.9.6", "prisma": "6.19.3", "tailwindcss": "^4", "typescript": "5.8.2", @@ -10724,6 +10725,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9e6ded0b..49ccf08c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,9 @@ "prisma:generate": "prisma generate", "build": "npm run contentlayer:build && npm run prisma:generate && next build --webpack", "start": "next start", - "lint": "eslint ." + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -48,6 +50,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "^15.5.12", + "prettier": "3.9.6", "prisma": "6.19.3", "tailwindcss": "^4", "typescript": "5.8.2", diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx index a5144f80..c9da336e 100644 --- a/frontend/src/app/about/page.tsx +++ b/frontend/src/app/about/page.tsx @@ -1,17 +1,24 @@ -'use client' +'use client'; import React, { useState } from 'react'; -import { - EyeIcon, SpeakerWaveIcon, CpuChipIcon, - DocumentTextIcon, CameraIcon, SparklesIcon, - ChevronDownIcon, ChevronRightIcon, - RocketLaunchIcon, LightBulbIcon, CheckCircleIcon +import { + EyeIcon, + SpeakerWaveIcon, + CpuChipIcon, + DocumentTextIcon, + CameraIcon, + SparklesIcon, + ChevronDownIcon, + ChevronRightIcon, + RocketLaunchIcon, + LightBulbIcon, + CheckCircleIcon, } from '@heroicons/react/24/outline'; import Link from 'next/link'; const AboutPage = () => { const [expandedSections, setExpandedSections] = useState>(new Set()); - + const toggleSection = (sectionId: string) => { const newExpanded = new Set(expandedSections); if (newExpanded.has(sectionId)) { @@ -33,10 +40,11 @@ const AboutPage = () => { Bringen Sie alles ins System

- Jeden Tag verbringen Menschen bei der Arbeit unzรคhlige Stunden damit, Informationen in Systeme einzugeben. - Wir automatisieren diesen gesamten Prozess โ€“ intelligent und mรผhelos. + Jeden Tag verbringen Menschen bei der Arbeit unzรคhlige Stunden damit, Informationen in + Systeme einzugeben. Wir automatisieren diesen gesamten Prozess โ€“{' '} + intelligent und mรผhelos.

- +
@@ -62,39 +70,75 @@ const AboutPage = () => {

Das Problem, das wir lรถsen

- +
- - + +
-

Zeitverschwendung

+

+ Zeitverschwendung +

Millionen von Arbeitsstunden werden tรคglich mit manueller Dateneingabe verschwendet

- +
- - + +
-

Fehleranfรคllig

+

+ Fehleranfรคllig +

Manuelle Eingaben fรผhren zu Inkonsistenzen und kostspieligen Fehlern

- +
- - + +
-

Verpasste Chancen

+

+ Verpasste Chancen +

Wertvolle Insights bleiben in den Daten verborgen, ohne intelligente Analyse

@@ -111,8 +155,9 @@ const AboutPage = () => { Unsere Vision: Das intelligente System

- Stellen Sie sich ein System vor, das wie ein menschliches Gehirn funktioniert โ€“ mit Augen und Ohren, die alles erfassen, - und einem Gehirn, das alles versteht und intelligent handelt. + Stellen Sie sich ein System vor, das wie ein menschliches Gehirn funktioniert โ€“ mit + Augen und Ohren, die alles erfassen, und einem Gehirn, das alles versteht und + intelligent handelt.

AIโ€‘getriebene FormSchemas

@@ -153,13 +198,15 @@ const AboutPage = () => { )}
- + {isExpanded('forms') && (
-

Intelligente Formulare

+

+ Intelligente Formulare +

  • @@ -176,16 +223,24 @@ const AboutPage = () => {
-

Anwendungsbereiche

+

+ Anwendungsbereiche +

- HR & Onboarding + + HR & Onboarding +
- Kundenfeedback + + Kundenfeedback +
- Datenerfassung + + Datenerfassung +
@@ -222,13 +277,15 @@ const AboutPage = () => { )}
- + {isExpanded('sensors') && (
-

Sensor-Technologien

+

+ Sensor-Technologien +

  • @@ -245,16 +302,24 @@ const AboutPage = () => {
-

Erfassung von

+

+ Erfassung von +

- Produktdaten per Foto + + Produktdaten per Foto +
- Umgebungssensoren + + Umgebungssensoren +
- Bewegung & Verhalten + + Bewegung & Verhalten +
@@ -291,13 +356,15 @@ const AboutPage = () => { )}
- + {isExpanded('ai') && (
-

KI-Fรคhigkeiten

+

+ KI-Fรคhigkeiten +

  • @@ -314,16 +381,24 @@ const AboutPage = () => {
-

Intelligente Ausgaben

+

+ Intelligente Ausgaben +

- Dashboards & Reports + + Dashboards & Reports +
- API-Integration + + API-Integration +
- Automatisierte Aktionen + + Automatisierte Aktionen +
@@ -344,13 +419,13 @@ const AboutPage = () => { So funktioniert es zusammen
- +
{/* Connection Lines */}
- +
@@ -358,27 +433,30 @@ const AboutPage = () => {

Erfassen

- Formulare und Sensoren sammeln Daten aus der realen Welt โ€“ automatisch und kontinuierlich + Formulare und Sensoren sammeln Daten aus der realen Welt โ€“ automatisch und + kontinuierlich

- +
2

Verstehen

- KI analysiert, strukturiert und extrahiert wertvollen Kontext aus allen eingehenden Informationen + KI analysiert, strukturiert und extrahiert wertvollen Kontext aus allen + eingehenden Informationen

- +
3

Handeln

- Intelligente Insights werden zu konkreten Aktionen und Entscheidungen fรผr Ihr Business + Intelligente Insights werden zu konkreten Aktionen und Entscheidungen fรผr Ihr + Business

@@ -389,14 +467,12 @@ const AboutPage = () => { {/* Call to Action */}
-

- Die Zukunft beginnt heute -

+

Die Zukunft beginnt heute

- Werden Sie Teil der Revolution, die manuelle Dateneingabe fรผr immer verรคndert. - Schaffen Sie ein System, das sieht, hรถrt und intelligent handelt. + Werden Sie Teil der Revolution, die manuelle Dateneingabe fรผr immer verรคndert. Schaffen + Sie ein System, das sieht, hรถrt und intelligent handelt.

- +
{ ); }; -export default AboutPage; \ No newline at end of file +export default AboutPage; diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts index 0ed68abb..05fb6b9d 100644 --- a/frontend/src/app/api/auth/[...nextauth]/route.ts +++ b/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -3,5 +3,3 @@ import { authOptions } from '@/server/auth/options'; const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; - - diff --git a/frontend/src/app/api/trpc/[trpc]/route.ts b/frontend/src/app/api/trpc/[trpc]/route.ts index 71436121..fb146f06 100644 --- a/frontend/src/app/api/trpc/[trpc]/route.ts +++ b/frontend/src/app/api/trpc/[trpc]/route.ts @@ -13,5 +13,3 @@ const handler = (req: Request) => }); export { handler as GET, handler as POST }; - - diff --git a/frontend/src/app/api/v1/auth/login/route.ts b/frontend/src/app/api/v1/auth/login/route.ts index 8b94d9dc..8dc512c1 100644 --- a/frontend/src/app/api/v1/auth/login/route.ts +++ b/frontend/src/app/api/v1/auth/login/route.ts @@ -22,7 +22,9 @@ export async function POST(req: Request) { .setIssuedAt() .setExpirationTime('7d') .sign(secret); - return Response.json({ success: true, token, user: { id: user.id, email: user.email, name: user.name } }); + return Response.json({ + success: true, + token, + user: { id: user.id, email: user.email, name: user.name }, + }); } - - diff --git a/frontend/src/app/api/v1/auth/register/route.ts b/frontend/src/app/api/v1/auth/register/route.ts index 1e14880e..086fa83b 100644 --- a/frontend/src/app/api/v1/auth/register/route.ts +++ b/frontend/src/app/api/v1/auth/register/route.ts @@ -10,7 +10,8 @@ export async function POST(req: Request) { if (exists) return Response.json({ success: false, message: 'User exists' }, { status: 409 }); const passwordHash = await hash(password, 10); const user = await prisma.user.create({ data: { email, passwordHash, name } }); - return Response.json({ success: true, user: { id: user.id, email: user.email, name: user.name } }); + return Response.json({ + success: true, + user: { id: user.id, email: user.email, name: user.name }, + }); } - - diff --git a/frontend/src/app/api/v1/forms/[id]/route.ts b/frontend/src/app/api/v1/forms/[id]/route.ts index dbd1e7cc..5155948f 100644 --- a/frontend/src/app/api/v1/forms/[id]/route.ts +++ b/frontend/src/app/api/v1/forms/[id]/route.ts @@ -44,5 +44,3 @@ export async function DELETE(req: Request, ctx: any) { await prisma.form.delete({ where: { id } }); return Response.json({ success: true }); } - - diff --git a/frontend/src/app/api/v1/forms/[id]/status/route.ts b/frontend/src/app/api/v1/forms/[id]/status/route.ts index 6a243dab..6a372c83 100644 --- a/frontend/src/app/api/v1/forms/[id]/status/route.ts +++ b/frontend/src/app/api/v1/forms/[id]/status/route.ts @@ -12,5 +12,3 @@ export async function PUT(req: Request, ctx: any) { const updated = await prisma.form.update({ where: { id }, data: { isPublished } }); return Response.json({ success: true, status: updated.isPublished ? 'published' : 'draft' }); } - - diff --git a/frontend/src/app/api/v1/forms/public/[id]/route.ts b/frontend/src/app/api/v1/forms/public/[id]/route.ts index 2cac59b1..26a592fa 100644 --- a/frontend/src/app/api/v1/forms/public/[id]/route.ts +++ b/frontend/src/app/api/v1/forms/public/[id]/route.ts @@ -11,5 +11,3 @@ export async function GET(_req: Request, ctx: any) { structure: form.schema, }); } - - diff --git a/frontend/src/app/api/v1/forms/route.ts b/frontend/src/app/api/v1/forms/route.ts index 7c0d32a2..d11c0dca 100644 --- a/frontend/src/app/api/v1/forms/route.ts +++ b/frontend/src/app/api/v1/forms/route.ts @@ -39,7 +39,10 @@ export async function POST(req: Request) { const body = await req.json(); const { title, description, structure, status, isTemplate } = body || {}; if (!title || !structure) { - return Response.json({ success: false, message: 'Missing title or structure' }, { status: 400 }); + return Response.json( + { success: false, message: 'Missing title or structure' }, + { status: 400 }, + ); } const isPublished = status === 'published'; const created = await prisma.form.create({ @@ -64,5 +67,3 @@ export async function POST(req: Request) { updated_at: created.updatedAt, }); } - - diff --git a/frontend/src/app/api/v1/submissions/route.ts b/frontend/src/app/api/v1/submissions/route.ts index 0219f840..a437e6dc 100644 --- a/frontend/src/app/api/v1/submissions/route.ts +++ b/frontend/src/app/api/v1/submissions/route.ts @@ -2,11 +2,13 @@ import { prisma } from '@/lib/db'; export async function POST(req: Request) { const { form_id, data } = await req.json(); - if (!form_id || !data) return Response.json({ message: 'Missing form_id or data' }, { status: 400 }); + if (!form_id || !data) + return Response.json({ message: 'Missing form_id or data' }, { status: 400 }); const form = await prisma.form.findUnique({ where: { id: form_id } }); - if (!form || !form.isPublished) return Response.json({ message: 'Form not found or not published' }, { status: 404 }); - const created = await prisma.submission.create({ data: { formId: form_id, data, status: 'PENDING' } }); + if (!form || !form.isPublished) + return Response.json({ message: 'Form not found or not published' }, { status: 404 }); + const created = await prisma.submission.create({ + data: { formId: form_id, data, status: 'PENDING' }, + }); return Response.json({ success: true, id: created.id }); } - - diff --git a/frontend/src/app/api/v1/user/me/route.ts b/frontend/src/app/api/v1/user/me/route.ts index 0a248d8d..53486536 100644 --- a/frontend/src/app/api/v1/user/me/route.ts +++ b/frontend/src/app/api/v1/user/me/route.ts @@ -13,7 +13,10 @@ export async function GET(req: Request) { const { payload } = await jwtVerify(token, secret); const user = await prisma.user.findUnique({ where: { id: payload.sub as string } }); if (!user) return Response.json({ success: false, message: 'Not found' }, { status: 404 }); - return Response.json({ success: true, user: { id: user.id, email: user.email, name: user.name } }); + return Response.json({ + success: true, + user: { id: user.id, email: user.email, name: user.name }, + }); } catch { return Response.json({ success: false, message: 'Invalid token' }, { status: 401 }); } @@ -32,10 +35,11 @@ export async function PUT(req: Request) { where: { id: payload.sub as string }, data: { ...(name !== undefined && { name }) }, }); - return Response.json({ success: true, user: { id: user.id, email: user.email, name: user.name } }); + return Response.json({ + success: true, + user: { id: user.id, email: user.email, name: user.name }, + }); } catch { return Response.json({ success: false, message: 'Invalid token' }, { status: 401 }); } } - - diff --git a/frontend/src/app/api/v1/vision/route.ts b/frontend/src/app/api/v1/vision/route.ts index 11ff31cf..5dbdabaf 100644 --- a/frontend/src/app/api/v1/vision/route.ts +++ b/frontend/src/app/api/v1/vision/route.ts @@ -40,29 +40,32 @@ export async function POST(req: NextRequest) { return NextResponse.json({ content }); } catch (error: any) { console.error('OpenAI Vision API Error:', error); - + // Handle specific OpenAI errors if (error.status === 429) { - return NextResponse.json({ - error: 'OpenAI quota exceeded. Please check your billing at platform.openai.com/account/billing and add payment method or wait for quota reset.' - }, { status: 429 }); + return NextResponse.json( + { + error: + 'OpenAI quota exceeded. Please check your billing at platform.openai.com/account/billing and add payment method or wait for quota reset.', + }, + { status: 429 }, + ); } - + if (error.status === 401) { - return NextResponse.json({ - error: 'Invalid OpenAI API key. Please check your configuration.' - }, { status: 401 }); + return NextResponse.json( + { + error: 'Invalid OpenAI API key. Please check your configuration.', + }, + { status: 401 }, + ); } - - return NextResponse.json({ - error: error?.message || 'OpenAI Vision API error' - }, { status: 500 }); + + return NextResponse.json( + { + error: error?.message || 'OpenAI Vision API error', + }, + { status: 500 }, + ); } } - - - - - - - diff --git a/frontend/src/app/blog/[slug]/MDXContent.tsx b/frontend/src/app/blog/[slug]/MDXContent.tsx index c1952be9..bd7dfc60 100644 --- a/frontend/src/app/blog/[slug]/MDXContent.tsx +++ b/frontend/src/app/blog/[slug]/MDXContent.tsx @@ -10,4 +10,4 @@ interface MDXContentProps { export function MDXContent({ code }: MDXContentProps) { const MDXComponent = useMDXComponent(code); return ; -} \ No newline at end of file +} diff --git a/frontend/src/app/blog/[slug]/MDXContentClient.tsx b/frontend/src/app/blog/[slug]/MDXContentClient.tsx index 5362b2a8..7ee3a7b7 100644 --- a/frontend/src/app/blog/[slug]/MDXContentClient.tsx +++ b/frontend/src/app/blog/[slug]/MDXContentClient.tsx @@ -8,7 +8,7 @@ import dynamic from 'next/dynamic'; // static export AND per-request SSR) crashes; client-only rendering doesn't, // since the browser's React copy resolves the same call differently. Loading // via next/dynamic with ssr:false skips server execution altogether. -export const MDXContentClient = dynamic( - () => import('./MDXContent').then((m) => m.MDXContent), - { ssr: false, loading: () =>
Lade Inhaltโ€ฆ
} -); +export const MDXContentClient = dynamic(() => import('./MDXContent').then((m) => m.MDXContent), { + ssr: false, + loading: () =>
Lade Inhaltโ€ฆ
, +}); diff --git a/frontend/src/app/blog/[slug]/page.tsx b/frontend/src/app/blog/[slug]/page.tsx index ab1f3c8d..27e77e01 100644 --- a/frontend/src/app/blog/[slug]/page.tsx +++ b/frontend/src/app/blog/[slug]/page.tsx @@ -1,6 +1,6 @@ // created_date: 2025-07-10 // last_modified_date: 2025-07-10 -// last_modified_summary: "Dynamic MDX blog post page with CTA." +// last_modified_summary: "Dynamic MDX blog post page with CTA." import { allPosts } from '../../../../.contentlayer/generated'; import { notFound } from 'next/navigation'; @@ -8,14 +8,16 @@ import { MDXContentClient } from './MDXContentClient'; import Image from 'next/image'; import Link from 'next/link'; -export const generateStaticParams = async () => allPosts.map(p => ({ slug: p.slug })); +export const generateStaticParams = async () => allPosts.map((p) => ({ slug: p.slug })); -interface PageProps { params: Promise<{ slug: string }> } +interface PageProps { + params: Promise<{ slug: string }>; +} export default async function BlogPostPage({ params }: PageProps) { const { slug } = await params; - const post = allPosts.find(p => p.slug === slug); - + const post = allPosts.find((p) => p.slug === slug); + if (!post) { return notFound(); } @@ -24,7 +26,13 @@ export default async function BlogPostPage({ params }: PageProps) {
{post.coverImage && (
- Cover + Cover
)}

{post.title}

@@ -36,10 +44,13 @@ export default async function BlogPostPage({ params }: PageProps) { {/* CTA */}

Teste den Universal Form Builder selbst

- + Zum Builder โ†’
); -} \ No newline at end of file +} diff --git a/frontend/src/app/blog/page.tsx b/frontend/src/app/blog/page.tsx index 7f0c119d..c8c62d37 100644 --- a/frontend/src/app/blog/page.tsx +++ b/frontend/src/app/blog/page.tsx @@ -14,11 +14,18 @@ export default function BlogPage() { return (
-

Insights & Gedanken

+

+ Insights & Gedanken +

- Lass dich von unseren Ideen rund um intelligente Datenerfassung, KI-Gestรผtzte HR-Prozesse und moderne UX inspirieren. + Lass dich von unseren Ideen rund um intelligente Datenerfassung, KI-Gestรผtzte HR-Prozesse + und moderne UX inspirieren.

-
-
@@ -54,4 +76,4 @@ export default function BlogPage() {
); -} \ No newline at end of file +} diff --git a/frontend/src/app/builder/page.tsx b/frontend/src/app/builder/page.tsx index ff3b6f3c..4fdfc594 100644 --- a/frontend/src/app/builder/page.tsx +++ b/frontend/src/app/builder/page.tsx @@ -38,11 +38,7 @@ export default function FormBuilderPage() { }; if (!showFormBuilder) { - return ( - - ); + return ; } return ( @@ -52,4 +48,4 @@ export default function FormBuilderPage() { onFieldsChange={handleFieldsChange} /> ); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/AddFieldWizard.tsx b/frontend/src/app/components/AddFieldWizard.tsx index aa61a72e..674cd7fc 100644 --- a/frontend/src/app/components/AddFieldWizard.tsx +++ b/frontend/src/app/components/AddFieldWizard.tsx @@ -14,12 +14,12 @@ interface AddFieldWizardProps { className?: string; } -export function AddFieldWizard({ - isOpen, - onClose, - onAddField, +export function AddFieldWizard({ + isOpen, + onClose, + onAddField, availableGroups, - className = "" + className = '', }: AddFieldWizardProps) { const [step, setStep] = useState<'type' | 'details'>('type'); const [fieldType, setFieldType] = useState(''); @@ -27,7 +27,7 @@ export function AddFieldWizard({ label: '', placeholder: '', required: false, - group: '' + group: '', }); const handleTypeSelect = (type: string) => { @@ -50,12 +50,15 @@ export function AddFieldWizard({ required: fieldData.required, placeholder: fieldData.placeholder.trim() || undefined, group: fieldData.group.trim() || undefined, - options: fieldType === 'select' ? [ - { value: '', label: `${fieldData.label} auswรคhlen` }, - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' } - ] : undefined, - rows: fieldType === 'textarea' ? 3 : undefined + options: + fieldType === 'select' + ? [ + { value: '', label: `${fieldData.label} auswรคhlen` }, + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ] + : undefined, + rows: fieldType === 'textarea' ? 3 : undefined, }; onAddField(newField); @@ -69,7 +72,7 @@ export function AddFieldWizard({ label: '', placeholder: '', required: false, - group: '' + group: '', }); onClose(); }; @@ -83,21 +86,37 @@ export function AddFieldWizard({ tel: 'Telefon', date: 'Datum', select: 'Auswahl', - textarea: 'Textbereich' + textarea: 'Textbereich', }; return labels[type] || type; }; return ( -
-
e.stopPropagation()}> +
+
e.stopPropagation()} + > {/* Header */}
- - + +
@@ -105,7 +124,9 @@ export function AddFieldWizard({ Neues Feld hinzufรผgen

- {step === 'type' ? 'Wรคhlen Sie einen Feldtyp' : `${getFieldTypeLabel(fieldType)}-Feld konfigurieren`} + {step === 'type' + ? 'Wรคhlen Sie einen Feldtyp' + : `${getFieldTypeLabel(fieldType)}-Feld konfigurieren`}

@@ -114,24 +135,37 @@ export function AddFieldWizard({ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg" > - +
{/* Progress Indicator */}
-
+
{step === 'type' ? '1' : 'โœ“'}
-
-
+
+
2
@@ -149,11 +183,8 @@ export function AddFieldWizard({ Wรคhlen Sie den passenden Feldtyp fรผr Ihre Daten aus.

- - + +
)} @@ -162,8 +193,18 @@ export function AddFieldWizard({
- - + +
@@ -184,7 +225,7 @@ export function AddFieldWizard({ name="fieldLabel" label="Feldbezeichnung" value={fieldData.label} - onChange={(e) => setFieldData(prev => ({ ...prev, label: e.target.value }))} + onChange={(e) => setFieldData((prev) => ({ ...prev, label: e.target.value }))} placeholder="z.B. Vollstรคndiger Name" required /> @@ -195,7 +236,9 @@ export function AddFieldWizard({ name="fieldPlaceholder" label="Platzhaltertext (optional)" value={fieldData.placeholder} - onChange={(e) => setFieldData(prev => ({ ...prev, placeholder: e.target.value }))} + onChange={(e) => + setFieldData((prev) => ({ ...prev, placeholder: e.target.value })) + } placeholder="z.B. Max Mustermann" /> @@ -206,10 +249,10 @@ export function AddFieldWizard({ name="fieldGroup" label="Gruppe (optional)" value={fieldData.group} - onChange={(e) => setFieldData(prev => ({ ...prev, group: e.target.value }))} + onChange={(e) => setFieldData((prev) => ({ ...prev, group: e.target.value }))} options={[ { value: '', label: 'Keine Gruppe' }, - ...availableGroups.map(group => ({ value: group, label: group })) + ...availableGroups.map((group) => ({ value: group, label: group })), ]} /> )} @@ -220,15 +263,16 @@ export function AddFieldWizard({ type="checkbox" id="fieldRequired" checked={fieldData.required} - onChange={(e) => setFieldData(prev => ({ ...prev, required: e.target.checked }))} + onChange={(e) => + setFieldData((prev) => ({ ...prev, required: e.target.checked })) + } className="h-5 w-5 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
@@ -248,11 +292,15 @@ export function AddFieldWizard({ onChange={() => {}} placeholder={fieldData.placeholder || 'Platzhaltertext...'} required={fieldData.required} - options={fieldType === 'select' ? [ - { value: '', label: `${fieldData.label || 'Feld'} auswรคhlen` }, - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' } - ] : undefined} + options={ + fieldType === 'select' + ? [ + { value: '', label: `${fieldData.label || 'Feld'} auswรคhlen` }, + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ] + : undefined + } rows={fieldType === 'textarea' ? 3 : undefined} />
@@ -265,29 +313,17 @@ export function AddFieldWizard({
{step === 'details' && ( - )}
- {step === 'details' && ( - )} @@ -296,4 +332,4 @@ export function AddFieldWizard({
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/BreadcrumbNavigation.tsx b/frontend/src/app/components/BreadcrumbNavigation.tsx index ec9c6cf2..c223e153 100644 --- a/frontend/src/app/components/BreadcrumbNavigation.tsx +++ b/frontend/src/app/components/BreadcrumbNavigation.tsx @@ -18,7 +18,12 @@ export function BreadcrumbNavigation({ items }: BreadcrumbNavigationProps) { {items.map((item, index) => ( {index > 0 && ( - + )} @@ -26,8 +31,8 @@ export function BreadcrumbNavigation({ items }: BreadcrumbNavigationProps) { ) : ( - + {item.label} )} ))} - + ); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/BuilderBottomBar.tsx b/frontend/src/app/components/BuilderBottomBar.tsx index e6454c84..c3783e8c 100644 --- a/frontend/src/app/components/BuilderBottomBar.tsx +++ b/frontend/src/app/components/BuilderBottomBar.tsx @@ -66,4 +66,4 @@ export function BuilderBottomBar({
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/BuilderToolbar.tsx b/frontend/src/app/components/BuilderToolbar.tsx index c95ad6a8..d7e7abe3 100644 --- a/frontend/src/app/components/BuilderToolbar.tsx +++ b/frontend/src/app/components/BuilderToolbar.tsx @@ -83,4 +83,4 @@ export function BuilderToolbar({
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/Button.tsx b/frontend/src/app/components/Button.tsx index 33b19cdd..5b3afc72 100644 --- a/frontend/src/app/components/Button.tsx +++ b/frontend/src/app/components/Button.tsx @@ -7,29 +7,33 @@ interface ButtonProps extends React.ButtonHTMLAttributes { className?: string; } -export function Button({ - variant = 'primary', - size = 'md', - children, - className = "", - ...props +export function Button({ + variant = 'primary', + size = 'md', + children, + className = '', + ...props }: ButtonProps) { - const baseClasses = "font-semibold rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all duration-200 transform hover:scale-105 active:scale-95"; - + const baseClasses = + 'font-semibold rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all duration-200 transform hover:scale-105 active:scale-95'; + const sizeClasses = { - sm: "px-4 py-2 text-sm", - md: "px-6 py-3 text-sm", - lg: "px-8 py-4 text-base" + sm: 'px-4 py-2 text-sm', + md: 'px-6 py-3 text-sm', + lg: 'px-8 py-4 text-base', }; - + const variantClasses = { - primary: "bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 shadow-lg hover:shadow-xl", - secondary: "bg-gradient-to-r from-gray-600 to-gray-700 text-white hover:from-gray-700 hover:to-gray-800 shadow-lg hover:shadow-xl", - outline: "border-2 border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-500 backdrop-blur-sm" + primary: + 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700 shadow-lg hover:shadow-xl', + secondary: + 'bg-gradient-to-r from-gray-600 to-gray-700 text-white hover:from-gray-700 hover:to-gray-800 shadow-lg hover:shadow-xl', + outline: + 'border-2 border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:border-gray-300 dark:hover:border-gray-500 backdrop-blur-sm', }; - - const disabledClasses = props.disabled ? "opacity-50 cursor-not-allowed hover:bg-current" : ""; - + + const disabledClasses = props.disabled ? 'opacity-50 cursor-not-allowed hover:bg-current' : ''; + return (
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/DynamicForm.tsx b/frontend/src/app/components/DynamicForm.tsx index af778ee7..0a76bf2c 100644 --- a/frontend/src/app/components/DynamicForm.tsx +++ b/frontend/src/app/components/DynamicForm.tsx @@ -23,14 +23,18 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic const [fields, setFields] = useState(initialFields); const [formData, setFormData] = useState(() => { const data: FormData = {}; - initialFields.forEach(field => { + initialFields.forEach((field) => { data[field.name] = ''; }); return data; }); - - const { validateForm, validateSingleField, getFieldError, hasErrors, clearErrors } = useFormValidation(fields); - const { savedData, saveNow, clearSavedData, getLastSaveTime, hasSavedData } = useAutoSave(formData, fields); + + const { validateForm, validateSingleField, getFieldError, hasErrors, clearErrors } = + useFormValidation(fields); + const { savedData, saveNow, clearSavedData, getLastSaveTime, hasSavedData } = useAutoSave( + formData, + fields, + ); const { saveTemplate } = useTemplateManager(); const [showSaveTemplateModal, setShowSaveTemplateModal] = useState(false); const [showGroupManager, setShowGroupManager] = useState(false); @@ -38,7 +42,7 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic // Load saved data on component mount useEffect(() => { - if (savedData && Object.keys(formData).every(key => !formData[key])) { + if (savedData && Object.keys(formData).every((key) => !formData[key])) { // Only load if current form is empty setFormData(savedData.formData); if (savedData.fields.length !== fields.length) { @@ -48,11 +52,13 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic } }, [savedData]); // Only run when savedData changes, not on every render - const handleInputChange = (e: React.ChangeEvent) => { + const handleInputChange = ( + e: React.ChangeEvent, + ) => { const { name, value } = e.target; - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - [name]: value + [name]: value, })); }; @@ -64,7 +70,7 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const errors = validateForm(formData); - + if (errors.length === 0) { clearSavedData(); // Clear auto-save data on successful submit onSubmit(formData); @@ -85,10 +91,8 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic const handleDeleteGroup = (groupName: string) => { // Move all fields from this group to 'Allgemeine Felder' - const updatedFields = fields.map(field => - field.group === groupName - ? { ...field, group: undefined } - : field + const updatedFields = fields.map((field) => + field.group === groupName ? { ...field, group: undefined } : field, ); setFields(updatedFields); onFieldsChange?.(updatedFields); @@ -96,7 +100,7 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic const getAvailableGroups = () => { const groups = new Set(); - fields.forEach(field => { + fields.forEach((field) => { if (field.group) { groups.add(field.group); } @@ -107,9 +111,9 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic const addField = (fieldConfig: FieldConfig) => { const newFields = [...fields, fieldConfig]; setFields(newFields); - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - [fieldConfig.name]: '' + [fieldConfig.name]: '', })); onFieldsChange?.(newFields); }; @@ -128,7 +132,7 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic range: 'Bereich', file: 'Datei', url: 'URL', - password: 'Passwort' + password: 'Passwort', }; const fieldConfig: FieldConfig = { @@ -138,23 +142,26 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic label: fieldNames[type] || type, required: false, placeholder: type === 'select' ? undefined : `${fieldNames[type]} eingeben...`, - options: type === 'select' ? [ - { value: '', label: 'Auswahl treffen' }, - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' } - ] : undefined, - rows: type === 'textarea' ? 3 : undefined + options: + type === 'select' + ? [ + { value: '', label: 'Auswahl treffen' }, + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ] + : undefined, + rows: type === 'textarea' ? 3 : undefined, }; addField(fieldConfig); }; const removeField = (fieldId: string) => { - const newFields = fields.filter(field => field.id !== fieldId); + const newFields = fields.filter((field) => field.id !== fieldId); setFields(newFields); - const fieldToRemove = fields.find(field => field.id === fieldId); + const fieldToRemove = fields.find((field) => field.id === fieldId); if (fieldToRemove) { - setFormData(prev => { + setFormData((prev) => { const newData = { ...prev }; delete newData[fieldToRemove.name]; return newData; @@ -164,8 +171,8 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic }; const updateField = (fieldId: string, updates: Partial) => { - const newFields = fields.map(field => - field.id === fieldId ? { ...field, ...updates } : field + const newFields = fields.map((field) => + field.id === fieldId ? { ...field, ...updates } : field, ); setFields(newFields); onFieldsChange?.(newFields); @@ -203,7 +210,11 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic
- +
@@ -225,8 +236,18 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic {fields.length === 0 && (
- - + +

@@ -242,19 +263,25 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic
{/* Group fields by their group property */} {(() => { - const groupedFields = fields.reduce((groups, field) => { - const groupName = field.group || 'Allgemeine Felder'; - if (!groups[groupName]) { - groups[groupName] = []; - } - groups[groupName].push(field); - return groups; - }, {} as Record); + const groupedFields = fields.reduce( + (groups, field) => { + const groupName = field.group || 'Allgemeine Felder'; + if (!groups[groupName]) { + groups[groupName] = []; + } + groups[groupName].push(field); + return groups; + }, + {} as Record, + ); return (
{Object.entries(groupedFields).map(([groupName, groupFields]) => { - if (groupName === 'Allgemeine Felder' && Object.keys(groupedFields).length === 1) { + if ( + groupName === 'Allgemeine Felder' && + Object.keys(groupedFields).length === 1 + ) { // If there's only one group and it's the default, render fields directly return (
@@ -277,7 +304,10 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic // Render as field groups return ( -
+

{groupName} @@ -315,7 +345,11 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic
- +
@@ -338,7 +372,12 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 px-8" > - + Formular absenden @@ -347,24 +386,32 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic {/* Modals */} - {editingField && ( - - )} + {editingField && } {showGroupManager && ( -
setShowGroupManager(false)}> -
e.stopPropagation()}> +
setShowGroupManager(false)} + > +
e.stopPropagation()} + >
-

Gruppen verwalten

+

+ Gruppen verwalten +

@@ -387,4 +434,3 @@ export function DynamicForm({ initialFields, onSubmit, onFieldsChange }: Dynamic ); } - diff --git a/frontend/src/app/components/EmptyStep.tsx b/frontend/src/app/components/EmptyStep.tsx index 3b5c2ab6..b98b616f 100644 --- a/frontend/src/app/components/EmptyStep.tsx +++ b/frontend/src/app/components/EmptyStep.tsx @@ -12,13 +12,13 @@ interface EmptyStepProps { onAddTemplate: (template: FieldTemplate) => void; } -const quickFieldTypes: Array<{type: FieldConfig['type'], label: string, icon: string}> = [ +const quickFieldTypes: Array<{ type: FieldConfig['type']; label: string; icon: string }> = [ { type: 'text', label: 'Text', icon: '๐Ÿ“' }, { type: 'email', label: 'E-Mail', icon: '๐Ÿ“ง' }, { type: 'tel', label: 'Telefon', icon: '๐Ÿ“ž' }, { type: 'date', label: 'Datum', icon: '๐Ÿ“…' }, { type: 'select', label: 'Auswahl', icon: '๐Ÿ“‹' }, - { type: 'textarea', label: 'Textbereich', icon: '๐Ÿ“„' } + { type: 'textarea', label: 'Textbereich', icon: '๐Ÿ“„' }, ]; export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { @@ -35,7 +35,8 @@ export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { Fรผgen Sie Ihr erstes Feld hinzu

- Beginnen Sie mit einem einzelnen Feld oder fรผgen Sie eine vordefinierte Sektion hinzu, um Ihr perfektes Formular zu erstellen. + Beginnen Sie mit einem einzelnen Feld oder fรผgen Sie eine vordefinierte Sektion hinzu, um + Ihr perfektes Formular zu erstellen.

@@ -49,7 +50,9 @@ export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { onClick={() => onAddField(fieldType.type)} className="flex flex-col items-center justify-center p-6 bg-white dark:bg-gray-700 rounded-xl border border-gray-200 dark:border-gray-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:border-blue-400 dark:hover:border-blue-500 hover:shadow-md transition-all duration-200 text-center group" > -
{fieldType.icon}
+
+ {fieldType.icon} +
{fieldType.label}
@@ -68,7 +71,9 @@ export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { onClick={() => onAddTemplate(template)} >
-
{template.icon}
+
+ {template.icon} +

{template.name} @@ -93,7 +98,9 @@ export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { onClick={() => setPendingTemplateId(template.id)} >

-
๐Ÿ“„
+
+ ๐Ÿ“„ +

{template.name} @@ -114,7 +121,7 @@ export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { message="Das aktuelle Formular wird รผberschrieben. Mรถchten Sie fortfahren?" confirmLabel="รœbernehmen" onConfirm={() => { - const tmpl = formTemplates.find(t => t.id === pendingTemplateId); + const tmpl = formTemplates.find((t) => t.id === pendingTemplateId); if (tmpl) loadTemplate(tmpl); setPendingTemplateId(null); }} @@ -122,4 +129,4 @@ export function EmptyStep({ onAddField, onAddTemplate }: EmptyStepProps) { />

); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FieldEditor.tsx b/frontend/src/app/components/FieldEditor.tsx index c858582f..bcd09688 100644 --- a/frontend/src/app/components/FieldEditor.tsx +++ b/frontend/src/app/components/FieldEditor.tsx @@ -16,7 +16,9 @@ export function FieldEditor({ field, onUpdate }: FieldEditorProps) { const [rows, setRows] = useState(field.rows || 3); const { fields, steps, isMultiStep } = useFormBuilderStore(); - const allOtherFields = (isMultiStep ? steps.flatMap(s => s.fields) : fields).filter(f => f.id !== field.id); + const allOtherFields = (isMultiStep ? steps.flatMap((s) => s.fields) : fields).filter( + (f) => f.id !== field.id, + ); // Reset state when the selected field changes useEffect(() => { @@ -29,18 +31,18 @@ export function FieldEditor({ field, onUpdate }: FieldEditorProps) { const handleUpdate = (updates: Partial) => { onUpdate(updates); }; - + const handleLabelChange = (e: React.ChangeEvent) => { setLabel(e.target.value); }; const handleLabelBlur = () => { - if(label.trim() === '') { + if (label.trim() === '') { setLabel(field.label); // revert if empty } else if (label.trim() !== field.label) { handleUpdate({ label: label.trim(), - name: label.trim().toLowerCase().replace(/\s+/g, '_') + name: label.trim().toLowerCase().replace(/\s+/g, '_'), }); } }; @@ -48,36 +50,43 @@ export function FieldEditor({ field, onUpdate }: FieldEditorProps) { const handlePlaceholderChange = (e: React.ChangeEvent) => { setPlaceholder(e.target.value); }; - + const handlePlaceholderBlur = () => { if (placeholder !== field.placeholder) { handleUpdate({ placeholder }); } }; - + const handleOptionChange = (index: number, value: string) => { const newOptions = [...options]; - newOptions[index] = { ...newOptions[index], label: value, value: value.toLowerCase().replace(/\s+/g, '_') }; + newOptions[index] = { + ...newOptions[index], + label: value, + value: value.toLowerCase().replace(/\s+/g, '_'), + }; setOptions(newOptions); }; const handleOptionBlur = () => { handleUpdate({ options }); }; - + const addOption = () => { - const newOption = { label: `Option ${options.length + 1}`, value: `option_${options.length + 1}` }; + const newOption = { + label: `Option ${options.length + 1}`, + value: `option_${options.length + 1}`, + }; const newOptions = [...options, newOption]; setOptions(newOptions); handleUpdate({ options: newOptions }); }; - + const removeOption = (index: number) => { const newOptions = options.filter((_, i) => i !== index); setOptions(newOptions); handleUpdate({ options: newOptions }); }; - + const handleRowsChange = (e: React.ChangeEvent) => { const newRows = parseInt(e.target.value, 10); setRows(newRows); @@ -86,7 +95,7 @@ export function FieldEditor({ field, onUpdate }: FieldEditorProps) { const handleLogicChange = ( key: keyof NonNullable, - value: string + value: string, ) => { const newLogic = { ...(field.conditionalLogic || { fieldId: '', condition: 'isEqualTo', value: '' }), @@ -99,21 +108,26 @@ export function FieldEditor({ field, onUpdate }: FieldEditorProps) { onUpdate({ conditionalLogic: newLogic }); }; - + const clearLogic = () => { onUpdate({ conditionalLogic: undefined }); }; - + return (
-

Feld bearbeiten: {field.label}

+

+ Feld bearbeiten: {field.label} +

{/* General Settings */}
-
- + {/* Type-specific Settings */} {field.type === 'select' && (
-
Optionen
- {options.map((option, index) => ( -
- handleOptionChange(index, e.target.value)} - onBlur={handleOptionBlur} - className="flex-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - /> - -
- ))} - +
Optionen
+ {options.map((option, index) => ( +
+ handleOptionChange(index, e.target.value)} + onBlur={handleOptionBlur} + className="flex-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + /> + +
+ ))} +
)} {field.type === 'textarea' && ( -
- - +
+ +
)} {/* Conditional Logic Section */}
-

Conditional Logic

- +

+ Conditional Logic +

+ {!field.conditionalLogic ? ( - ) : (
-

- Show this field only if... -

- +

Show this field only if...

+
- +
- +
- +
- +
- +
- -
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FieldGroup.tsx b/frontend/src/app/components/FieldGroup.tsx index ae351dac..1dc0e5b3 100644 --- a/frontend/src/app/components/FieldGroup.tsx +++ b/frontend/src/app/components/FieldGroup.tsx @@ -7,7 +7,9 @@ interface FieldGroupProps { description?: string; fields: FieldConfig[]; formData: FormData; - onFieldChange: (e: React.ChangeEvent) => void; + onFieldChange: ( + e: React.ChangeEvent, + ) => void; onFieldBlur: (fieldName: string) => void; getFieldError: (fieldName: string) => string | null; isCollapsible?: boolean; @@ -15,24 +17,24 @@ interface FieldGroupProps { className?: string; } -export function FieldGroup({ - title, - description, - fields, - formData, - onFieldChange, - onFieldBlur, +export function FieldGroup({ + title, + description, + fields, + formData, + onFieldChange, + onFieldBlur, getFieldError, isCollapsible = false, defaultExpanded = true, - className = "" + className = '', }: FieldGroupProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); - const hasErrors = fields.some(field => getFieldError(field.name)); - const hasValues = fields.some(field => formData[field.name]?.trim()); - const requiredFields = fields.filter(field => field.required); - const completedRequiredFields = requiredFields.filter(field => formData[field.name]?.trim()); + const hasErrors = fields.some((field) => getFieldError(field.name)); + const hasValues = fields.some((field) => formData[field.name]?.trim()); + const requiredFields = fields.filter((field) => field.required); + const completedRequiredFields = requiredFields.filter((field) => formData[field.name]?.trim()); const getGroupStatus = () => { if (hasErrors) return 'error'; @@ -50,25 +52,47 @@ export function FieldGroup({ case 'complete': return ( - + ); case 'error': return ( - + ); case 'partial': return ( - + ); default: return ( - - + + ); } @@ -89,8 +113,10 @@ export function FieldGroup({ }; return ( -
-
+
setIsExpanded(!isExpanded) : undefined} > @@ -98,17 +124,13 @@ export function FieldGroup({
{getGroupIcon()}
-

- {title} -

+

{title}

{description && ( -

- {description} -

+

{description}

)}
- +
{/* Progress indicator */} {requiredFields.length > 0 && ( @@ -117,32 +139,40 @@ export function FieldGroup({ {completedRequiredFields.length}/{requiredFields.length}
-
0 ? (completedRequiredFields.length / requiredFields.length) * 100 : 0}%` + style={{ + width: `${requiredFields.length > 0 ? (completedRequiredFields.length / requiredFields.length) * 100 : 0}%`, }} />
)} - + {/* Collapse/Expand button */} {isCollapsible && ( )} @@ -176,4 +206,4 @@ export function FieldGroup({ )}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FieldTypeSelector.tsx b/frontend/src/app/components/FieldTypeSelector.tsx index 9ff6d08d..aab16248 100644 --- a/frontend/src/app/components/FieldTypeSelector.tsx +++ b/frontend/src/app/components/FieldTypeSelector.tsx @@ -16,7 +16,11 @@ interface FieldTypeSelectorProps { className?: string; } -export function FieldTypeSelector({ onSelect, selectedType, className = "" }: FieldTypeSelectorProps) { +export function FieldTypeSelector({ + onSelect, + selectedType, + className = '', +}: FieldTypeSelectorProps) { const fieldTypes: FieldType[] = [ { type: 'text', @@ -25,9 +29,14 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi example: 'Max Mustermann', icon: ( - + - ) + ), }, { type: 'email', @@ -36,9 +45,14 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi example: 'max@example.com', icon: ( - + - ) + ), }, { type: 'tel', @@ -47,9 +61,14 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi example: '+49 123 456789', icon: ( - + - ) + ), }, { type: 'date', @@ -58,9 +77,14 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi example: '15.12.2024', icon: ( - + - ) + ), }, { type: 'select', @@ -69,9 +93,14 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi example: 'Option wรคhlen...', icon: ( - + - ) + ), }, { type: 'textarea', @@ -80,10 +109,15 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi example: 'Lรคngere Beschreibung...', icon: ( - + - ) - } + ), + }, ]; return ( @@ -100,42 +134,54 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi }`} >
-
+
{fieldType.icon}
-

+

{fieldType.label}

-

+

{fieldType.description}

-

+

{fieldType.example}

- + {selectedType === fieldType.type && (
- +
)} @@ -143,4 +189,4 @@ export function FieldTypeSelector({ onSelect, selectedType, className = "" }: Fi ))}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FilterControls.tsx b/frontend/src/app/components/FilterControls.tsx index 65a3e01e..b4af5cad 100644 --- a/frontend/src/app/components/FilterControls.tsx +++ b/frontend/src/app/components/FilterControls.tsx @@ -19,15 +19,15 @@ export interface Filter { interface FilterControlsProps { searchTerm: string; onSearchChange: (value: string) => void; - + filters: Filter[]; selectedFilters: Record; onFilterChange: (filterId: string, value: string | string[]) => void; - + sortByOptions: FilterOption[]; currentSortBy: string; onSortByChange: (value: string) => void; - + onClearAll?: () => void; isFiltered?: boolean; @@ -45,7 +45,7 @@ export function FilterControls({ onSortByChange, onClearAll, isFiltered, - children + children, }: FilterControlsProps) { const [pillSearchTerms, setPillSearchTerms] = useState>({}); @@ -57,9 +57,19 @@ export function FilterControls({
- - - + + +
- {/* Dynamic Filters */} - {filters.filter(f => f.type !== 'pills').map(filter => ( -
- - -
- ))} - - {/* Sort By */} -
- + {/* Dynamic Filters */} + {filters + .filter((f) => f.type !== 'pills') + .map((filter) => ( +
+ -
+
+ ))} - {isFiltered && ( - - )} + {/* Sort By */} +
+ + +
+ + {isFiltered && ( + + )} - {/* Children for additional controls */} - {children} + {/* Children for additional controls */} + {children}
{/* Pills Filters */} - {filters.filter(f => f.type === 'pills').map(filter => { + {filters + .filter((f) => f.type === 'pills') + .map((filter) => { const selectedPills = (selectedFilters[filter.id] as string[]) || []; const pillSearchTerm = pillSearchTerms[filter.id] || ''; return ( -
-
- - {filter.options.length > 7 && ( -
-
- -
- setPillSearchTerms(prev => ({ ...prev, [filter.id]: e.target.value }))} - className="block w-full pl-9 pr-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg text-sm leading-5 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" +
+
+ + {filter.options.length > 7 && ( +
+
+ + -
- )} + +
+ + setPillSearchTerms((prev) => ({ ...prev, [filter.id]: e.target.value })) + } + className="block w-full pl-9 pr-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg text-sm leading-5 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" + />
+ )} +
-
- {filter.options - .filter(option => option.label.toLowerCase().includes(pillSearchTerm.toLowerCase())) - .map(option => { - const isSelected = selectedPills.includes(option.value); - return ( - - ) - })} -
+
+ {filter.options + .filter((option) => + option.label.toLowerCase().includes(pillSearchTerm.toLowerCase()), + ) + .map((option) => { + const isSelected = selectedPills.includes(option.value); + return ( + + ); + })}
- ) - })} +
+ ); + })}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FormBuilderLayout.tsx b/frontend/src/app/components/FormBuilderLayout.tsx index 1323a2d8..54ca8c73 100644 --- a/frontend/src/app/components/FormBuilderLayout.tsx +++ b/frontend/src/app/components/FormBuilderLayout.tsx @@ -19,14 +19,14 @@ export function FormBuilderLayout({ onSaveTemplate, onSaveDraft, onManageGroups, - canSubmit + canSubmit, }: FormBuilderLayoutProps) { const [sidebarOpen, setSidebarOpen] = useState(false); const handleDrop = (e: React.DragEvent) => { e.preventDefault(); const fieldType = e.dataTransfer.getData('text/plain'); - + if (fieldType) { const fieldNames = { text: 'Textfeld', @@ -34,7 +34,7 @@ export function FormBuilderLayout({ tel: 'Telefon', date: 'Datum', select: 'Auswahl', - textarea: 'Textbereich' + textarea: 'Textbereich', }; const fieldConfig: FieldConfig = { @@ -43,13 +43,19 @@ export function FormBuilderLayout({ name: `${fieldNames[fieldType as keyof typeof fieldNames]?.toLowerCase().replace(/[^a-z]/g, '_') || fieldType}_${Date.now()}`, label: fieldNames[fieldType as keyof typeof fieldNames] || fieldType, required: false, - placeholder: fieldType === 'select' ? undefined : `${fieldNames[fieldType as keyof typeof fieldNames]} eingeben...`, - options: fieldType === 'select' ? [ - { value: '', label: 'Auswahl treffen' }, - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' } - ] : undefined, - rows: fieldType === 'textarea' ? 3 : undefined + placeholder: + fieldType === 'select' + ? undefined + : `${fieldNames[fieldType as keyof typeof fieldNames]} eingeben...`, + options: + fieldType === 'select' + ? [ + { value: '', label: 'Auswahl treffen' }, + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ] + : undefined, + rows: fieldType === 'textarea' ? 3 : undefined, }; onAddField(fieldConfig); @@ -71,15 +77,15 @@ export function FormBuilderLayout({ isOpen={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} /> - -
+ +
{/* Header */}
-

- Formular-Editor -

+

Formular-Editor

Erstellen Sie Ihr individuelles Formular
@@ -93,16 +99,10 @@ export function FormBuilderLayout({
{/* Main Content */} -
-
- {children} -
+
+
{children}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FormBuilderQuickStart.tsx b/frontend/src/app/components/FormBuilderQuickStart.tsx index ddd72104..c0265622 100644 --- a/frontend/src/app/components/FormBuilderQuickStart.tsx +++ b/frontend/src/app/components/FormBuilderQuickStart.tsx @@ -1,7 +1,13 @@ 'use client'; import React, { useState } from 'react'; -import { CameraIcon, DocumentArrowUpIcon, SquaresPlusIcon, RocketLaunchIcon, SparklesIcon } from '@heroicons/react/24/outline'; +import { + CameraIcon, + DocumentArrowUpIcon, + SquaresPlusIcon, + RocketLaunchIcon, + SparklesIcon, +} from '@heroicons/react/24/outline'; import { fieldBlocks } from '../data/fieldBlocks'; import { fieldTemplates } from '../data/fieldTemplates'; import { visionService, VisionAnalysisProgress } from '../services/visionService'; @@ -27,15 +33,16 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP setAnalysisProgress(null); try { - const result = file.type === 'application/pdf' - ? await visionService.analyzePDF(file, setAnalysisProgress) - : await visionService.analyzeImage(file, setAnalysisProgress); + const result = + file.type === 'application/pdf' + ? await visionService.analyzePDF(file, setAnalysisProgress) + : await visionService.analyzeImage(file, setAnalysisProgress); if (result.success) { const stepId = isMultiStep ? steps[currentStep]?.id : undefined; - + // Add analyzed fields to the form - result.fields.forEach(fieldConfig => { + result.fields.forEach((fieldConfig) => { addField(fieldConfig.type, stepId); // Note: Field configuration would be updated here in a real implementation }); @@ -77,7 +84,7 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP {analysisProgress.message}

-
@@ -106,7 +113,7 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP {/* Creation Methods Grid */}
{/* Manual Creation + Upload */} -
onMethodSelect('manual')} className="group cursor-pointer bg-white dark:bg-gray-800 rounded-xl border-2 border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 p-6 transition-all duration-200 hover:shadow-lg" > @@ -118,13 +125,12 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP

Felder hinzufรผgen

-

- Manuell oder per Upload -

+

Manuell oder per Upload

- Einzelne Felder, Bausteine oder Upload von Screenshots/PDFs. KI erstellt automatisch Felder aus Ihren Bildern. + Einzelne Felder, Bausteine oder Upload von Screenshots/PDFs. KI erstellt automatisch + Felder aus Ihren Bildern.

@@ -137,13 +143,18 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP
Jetzt starten - +
{/* Field Blocks */} -
onMethodSelect('blocks')} className="group cursor-pointer bg-white dark:bg-gray-800 rounded-xl border-2 border-gray-200 dark:border-gray-700 hover:border-green-500 dark:hover:border-green-500 p-6 transition-all duration-200 hover:shadow-lg" > @@ -155,17 +166,19 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP

Mit Bausteinen

-

- Fertige Feldgruppen -

+

Fertige Feldgruppen

- Effizient: Kombinieren Sie vorgefertigte Bausteine wie Kontaktdaten, Adresse oder Firmendaten. + Effizient: Kombinieren Sie vorgefertigte Bausteine wie Kontaktdaten, Adresse oder + Firmendaten.

- {fieldBlocks.slice(0, 3).map(block => ( - + {fieldBlocks.slice(0, 3).map((block) => ( + {block.name} ))} @@ -173,13 +186,18 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP
Bausteine ansehen - +
{/* Templates */} -
onMethodSelect('templates')} className="group cursor-pointer bg-white dark:bg-gray-800 rounded-xl border-2 border-gray-200 dark:border-gray-700 hover:border-amber-500 dark:hover:border-amber-500 p-6 transition-all duration-200 hover:shadow-lg" > @@ -188,20 +206,20 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP
-

- Aus Vorlage -

-

- Bewรคhrte Formulare -

+

Aus Vorlage

+

Bewรคhrte Formulare

- Schnellstart: Verwenden Sie professionelle Vorlagen und passen Sie diese an Ihre Bedรผrfnisse an. + Schnellstart: Verwenden Sie professionelle Vorlagen und passen Sie diese an Ihre + Bedรผrfnisse an.

- {fieldTemplates.slice(0, 3).map(template => ( - + {fieldTemplates.slice(0, 3).map((template) => ( + {template.name} ))} @@ -209,7 +227,12 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP
Vorlagen durchsuchen - +
@@ -227,10 +250,11 @@ export function FormBuilderQuickStart({ onMethodSelect }: FormBuilderQuickStartP {/* Bottom help text */}

- ๐Ÿ’ก Sie kรถnnen jederzeit zwischen den Methoden wechseln und verschiedene Ansรคtze kombinieren + ๐Ÿ’ก Sie kรถnnen jederzeit zwischen den Methoden wechseln und verschiedene Ansรคtze + kombinieren

); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FormBuilderSidebar.tsx b/frontend/src/app/components/FormBuilderSidebar.tsx index f936a47d..3ffe7e51 100644 --- a/frontend/src/app/components/FormBuilderSidebar.tsx +++ b/frontend/src/app/components/FormBuilderSidebar.tsx @@ -21,7 +21,7 @@ export function FormBuilderSidebar({ onManageGroups, canSubmit, isOpen, - onToggle + onToggle, }: FormBuilderSidebarProps) { const [activeSection, setActiveSection] = useState<'fields' | 'actions' | 'templates'>('fields'); @@ -32,9 +32,14 @@ export function FormBuilderSidebar({ description: 'Einfache Texteingabe', icon: ( - + - ) + ), }, { type: 'email', @@ -42,9 +47,14 @@ export function FormBuilderSidebar({ description: 'E-Mail-Adresse', icon: ( - + - ) + ), }, { type: 'tel', @@ -52,9 +62,14 @@ export function FormBuilderSidebar({ description: 'Telefonnummer', icon: ( - + - ) + ), }, { type: 'date', @@ -62,9 +77,14 @@ export function FormBuilderSidebar({ description: 'Datumsauswahl', icon: ( - + - ) + ), }, { type: 'select', @@ -72,9 +92,14 @@ export function FormBuilderSidebar({ description: 'Dropdown-Liste', icon: ( - + - ) + ), }, { type: 'textarea', @@ -82,10 +107,15 @@ export function FormBuilderSidebar({ description: 'Mehrzeiliger Text', icon: ( - + - ) - } + ), + }, ]; const handleDragStart = (e: React.DragEvent, fieldType: string) => { @@ -99,7 +129,7 @@ export function FormBuilderSidebar({ tel: 'Telefon', date: 'Datum', select: 'Auswahl', - textarea: 'Textbereich' + textarea: 'Textbereich', }; const fieldConfig: FieldConfig = { @@ -108,13 +138,19 @@ export function FormBuilderSidebar({ name: `${fieldNames[type as keyof typeof fieldNames]?.toLowerCase().replace(/[^a-z]/g, '_') || type}_${Date.now()}`, label: fieldNames[type as keyof typeof fieldNames] || type, required: false, - placeholder: type === 'select' ? undefined : `${fieldNames[type as keyof typeof fieldNames]} eingeben...`, - options: type === 'select' ? [ - { value: '', label: 'Auswahl treffen' }, - { value: 'option1', label: 'Option 1' }, - { value: 'option2', label: 'Option 2' } - ] : undefined, - rows: type === 'textarea' ? 3 : undefined + placeholder: + type === 'select' + ? undefined + : `${fieldNames[type as keyof typeof fieldNames]} eingeben...`, + options: + type === 'select' + ? [ + { value: '', label: 'Auswahl treffen' }, + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + ] + : undefined, + rows: type === 'textarea' ? 3 : undefined, }; onAddField(fieldConfig); @@ -129,28 +165,49 @@ export function FormBuilderSidebar({ isOpen ? 'translate-x-80' : 'translate-x-0' }`} > - - + + {/* Sidebar */} -
+
{/* Header */}
-

- Formular-Builder -

+

Formular-Builder

@@ -187,7 +244,7 @@ export function FormBuilderSidebar({
Ziehen Sie Felder in das Formular oder klicken Sie zum Hinzufรผgen
- +
{fieldTypes.map((field) => (
- - + +
@@ -227,8 +294,18 @@ export function FormBuilderSidebar({ variant="outline" className="w-full justify-start" > - - + + Gruppen verwalten @@ -238,19 +315,35 @@ export function FormBuilderSidebar({ variant="outline" className="w-full justify-start" > - - + + Als Vorlage speichern - @@ -275,12 +378,7 @@ export function FormBuilderSidebar({
{/* Overlay */} - {isOpen && ( -
- )} + {isOpen &&
} ); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FormCaptureLanding.tsx b/frontend/src/app/components/FormCaptureLanding.tsx index 572a7087..8f6616ed 100644 --- a/frontend/src/app/components/FormCaptureLanding.tsx +++ b/frontend/src/app/components/FormCaptureLanding.tsx @@ -9,7 +9,7 @@ import { ArrowRightIcon, CheckCircleIcon, ClockIcon, - ServerIcon + ServerIcon, } from '@heroicons/react/24/outline'; import Link from 'next/link'; @@ -19,9 +19,7 @@ interface FormCaptureLandingProps { onFieldsChange?: () => void; } -export function FormCaptureLanding({ - onStartBuilding, -}: FormCaptureLandingProps) { +export function FormCaptureLanding({ onStartBuilding }: FormCaptureLandingProps) { return (
{/* Hero Section */} @@ -38,7 +36,11 @@ export function FormCaptureLanding({

Erstellen Sie intelligente Formulare, die mehr als nur Daten sammeln. - Jede Antwort wird automatisch strukturiert, kategorisiert und fรผr KI-Analyse vorbereitet. + + {' '} + Jede Antwort wird automatisch strukturiert, kategorisiert und fรผr KI-Analyse + vorbereitet. +

@@ -68,7 +70,8 @@ export function FormCaptureLanding({ Was ist Formular-Erfassung?

- Mehr als nur ein Formular-Baukasten. Ein intelligentes System, das Ihre Daten fรผr die moderne Welt strukturiert. + Mehr als nur ein Formular-Baukasten. Ein intelligentes System, das Ihre Daten fรผr die + moderne Welt strukturiert.

@@ -80,7 +83,8 @@ export function FormCaptureLanding({ Menschen erfassen

- Sammeln Sie strukturierte Daten von Ihren Zielgruppen - Kunden, Patienten, Bewerber, oder Forschungsobjekte. + Sammeln Sie strukturierte Daten von Ihren Zielgruppen - Kunden, Patienten, + Bewerber, oder Forschungsobjekte.

@@ -104,7 +108,8 @@ export function FormCaptureLanding({ KI-gestรผtzte Analyse

- Jede Antwort wird automatisch verarbeitet und fรผr fortschrittliche Analysen vorbereitet. + Jede Antwort wird automatisch verarbeitet und fรผr fortschrittliche Analysen + vorbereitet.

@@ -129,12 +134,15 @@ export function FormCaptureLanding({ Strukturierte Daten fรผr die Zukunft

- Ihre Daten werden in einem standardisierten Format gespeichert, das fรผr alle modernen Analysetools bereit ist. + Ihre Daten werden in einem standardisierten Format gespeichert, das fรผr alle + modernen Analysetools bereit ist.

๐Ÿ“Š
-
Business Intelligence
+
+ Business Intelligence +
๐Ÿค–
@@ -162,7 +170,8 @@ export function FormCaptureLanding({ Der komplette Workflow

- Von der Formular-Erstellung bis zur intelligenten Datenanalyse - alles in einem System. + Von der Formular-Erstellung bis zur intelligenten Datenanalyse - alles in einem + System.

@@ -173,59 +182,68 @@ export function FormCaptureLanding({ title: 'Formular erstellen', description: 'Designen Sie Ihr Formular mit unserem Drag-and-Drop Builder', icon: 'โœ๏ธ', - color: 'indigo' + color: 'indigo', }, { step: 2, title: 'Teilen & Sammeln', description: 'Verteilen Sie Ihr Formular รผber Link, E-Mail oder Einbettung', icon: '๐Ÿ“ค', - color: 'blue' + color: 'blue', }, { step: 3, title: 'Antworten erhalten', - description: 'Menschen fรผllen Ihr Formular aus - Daten werden automatisch strukturiert', + description: + 'Menschen fรผllen Ihr Formular aus - Daten werden automatisch strukturiert', icon: '๐Ÿ“', - color: 'green' + color: 'green', }, { step: 4, title: 'KI-Analyse & Export', description: 'Automatische Analyse und Export fรผr BI, LLM oder bestehende Systeme', icon: '๐Ÿค–', - color: 'purple' - } + color: 'purple', + }, ].map((item, index) => (
-
+
{item.icon}
{item.step} - + Schritt {item.step}

{item.title}

-

- {item.description} -

+

{item.description}

{index < 3 && (
@@ -244,7 +262,8 @@ export function FormCaptureLanding({ Was passiert mit Ihren Daten?

- Jede Antwort wird automatisch in strukturierte Daten umgewandelt und fรผr moderne Analysen vorbereitet. + Jede Antwort wird automatisch in strukturierte Daten umgewandelt und fรผr moderne + Analysen vorbereitet.

@@ -257,7 +276,8 @@ export function FormCaptureLanding({ Strukturierte Speicherung

- Alle Antworten werden in einer standardisierten Datenbank gespeichert, bereit fรผr Export und Integration. + Alle Antworten werden in einer standardisierten Datenbank gespeichert, bereit fรผr + Export und Integration.

@@ -269,7 +289,8 @@ export function FormCaptureLanding({ KI-Analyse

- Automatische Sentiment-Analyse, Kategorisierung und Pattern-Erkennung fรผr tiefere Insights. + Automatische Sentiment-Analyse, Kategorisierung und Pattern-Erkennung fรผr tiefere + Insights.

@@ -297,7 +318,8 @@ export function FormCaptureLanding({ Fรผr wen ist Formular-Erfassung?

- Von der Kundenbefragung bis zur medizinischen Datenerfassung - fรผr jeden Anwendungsfall das richtige Tool. + Von der Kundenbefragung bis zur medizinischen Datenerfassung - fรผr jeden + Anwendungsfall das richtige Tool.

@@ -305,49 +327,56 @@ export function FormCaptureLanding({ {[ { title: 'Kundenfeedback', - description: 'Sammeln Sie strukturierte Kundenmeinungen fรผr Produktentwicklung und Service-Verbesserung.', + description: + 'Sammeln Sie strukturierte Kundenmeinungen fรผr Produktentwicklung und Service-Verbesserung.', icon: '๐Ÿ’ฌ', - examples: ['NPS-Befragungen', 'Produkt-Feedback', 'Service-Bewertung'] + examples: ['NPS-Befragungen', 'Produkt-Feedback', 'Service-Bewertung'], }, { title: 'Bewerbungsprozesse', - description: 'Digitalisieren Sie Bewerbungsformulare mit automatischer Qualifikations-Analyse.', + description: + 'Digitalisieren Sie Bewerbungsformulare mit automatischer Qualifikations-Analyse.', icon: '๐Ÿ‘ฅ', - examples: ['Stellenbewerbungen', 'Talent-Pools', 'Mitarbeiter-Feedback'] + examples: ['Stellenbewerbungen', 'Talent-Pools', 'Mitarbeiter-Feedback'], }, { title: 'Marktforschung', - description: 'Fรผhren Sie strukturierte Umfragen durch mit automatischer Trend-Erkennung.', + description: + 'Fรผhren Sie strukturierte Umfragen durch mit automatischer Trend-Erkennung.', icon: '๐Ÿ“Š', - examples: ['Marktanalysen', 'Konsumenten-Studien', 'Trend-Monitoring'] + examples: ['Marktanalysen', 'Konsumenten-Studien', 'Trend-Monitoring'], }, { title: 'Medizin & Gesundheit', - description: 'Patientenaufnahme und Symptom-Erfassung mit KI-gestรผtzter Vor-Diagnose.', + description: + 'Patientenaufnahme und Symptom-Erfassung mit KI-gestรผtzter Vor-Diagnose.', icon: '๐Ÿฅ', - examples: ['Patienten-Intake', 'Symptom-Tracking', 'Behandlungs-Evaluation'] + examples: ['Patienten-Intake', 'Symptom-Tracking', 'Behandlungs-Evaluation'], }, { title: 'Bildung & Training', - description: 'Kurs-Evaluationen und Lernfortschritt-Tracking mit automatischer Analyse.', + description: + 'Kurs-Evaluationen und Lernfortschritt-Tracking mit automatischer Analyse.', icon: '๐ŸŽ“', - examples: ['Kurs-Bewertung', 'Lernfortschritt', 'Zertifizierungs-Tests'] + examples: ['Kurs-Bewertung', 'Lernfortschritt', 'Zertifizierungs-Tests'], }, { title: 'Event-Management', - description: 'Teilnehmer-Registrierung und Event-Feedback mit automatischer Auswertung.', + description: + 'Teilnehmer-Registrierung und Event-Feedback mit automatischer Auswertung.', icon: '๐ŸŽ‰', - examples: ['Event-Anmeldung', 'Teilnehmer-Feedback', 'Follow-up-Umfragen'] - } + examples: ['Event-Anmeldung', 'Teilnehmer-Feedback', 'Follow-up-Umfragen'], + }, ].map((useCase, index) => ( -
+
{useCase.icon}

{useCase.title}

-

- {useCase.description} -

+

{useCase.description}

{useCase.examples.map((example, exampleIndex) => (
@@ -368,8 +397,8 @@ export function FormCaptureLanding({ Bereit fรผr intelligente Datenerfassung?

- Beginnen Sie mit der Erstellung Ihres ersten intelligenten Formulars. - Die KI-Analyse ist bereits integriert. + Beginnen Sie mit der Erstellung Ihres ersten intelligenten Formulars. Die KI-Analyse ist + bereits integriert.

diff --git a/frontend/src/app/components/FormCard.tsx b/frontend/src/app/components/FormCard.tsx index 000192b0..c58853d8 100644 --- a/frontend/src/app/components/FormCard.tsx +++ b/frontend/src/app/components/FormCard.tsx @@ -7,14 +7,26 @@ interface FormCardProps { className?: string; } -export function FormCard({ title, description, children, className = "" }: FormCardProps) { +export function FormCard({ title, description, children, className = '' }: FormCardProps) { return ( -
+
- - + +

@@ -22,14 +34,10 @@ export function FormCard({ title, description, children, className = "" }: FormC

{description && ( -

- {description} -

+

{description}

)}
-
- {children} -
+
{children}
); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FormCreationHub.tsx b/frontend/src/app/components/FormCreationHub.tsx index f5c43215..c07163de 100644 --- a/frontend/src/app/components/FormCreationHub.tsx +++ b/frontend/src/app/components/FormCreationHub.tsx @@ -45,7 +45,7 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC setSavedTemplates( data .filter((form: { is_template?: boolean }) => form.is_template) - .map((form: { id: string; title: string }) => ({ id: form.id, title: form.title })) + .map((form: { id: string; title: string }) => ({ id: form.id, title: form.title })), ); } catch (error) { console.error('Error fetching saved templates:', error); @@ -68,15 +68,16 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC setAnalysisProgress(null); try { - const result = file.type === 'application/pdf' - ? await visionService.analyzePDF(file, setAnalysisProgress) - : await visionService.analyzeImage(file, setAnalysisProgress); + const result = + file.type === 'application/pdf' + ? await visionService.analyzePDF(file, setAnalysisProgress) + : await visionService.analyzeImage(file, setAnalysisProgress); if (result.success) { const stepId = isMultiStep ? steps[currentStep]?.id : undefined; - + // Add analyzed fields to the form - result.fields.forEach(fieldConfig => { + result.fields.forEach((fieldConfig) => { addField(fieldConfig.type, stepId); // Update the newly added field with the analyzed configuration const currentFields = isMultiStep ? steps[currentStep]?.fields || [] : fields; @@ -87,7 +88,7 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC name: fieldConfig.name, required: fieldConfig.required, placeholder: fieldConfig.placeholder, - options: fieldConfig.options + options: fieldConfig.options, }); } }); @@ -128,7 +129,9 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC return (
-

Vorlagen-Bibliothek

+

+ Vorlagen-Bibliothek +

- { // Handle template selection setShowTemplateLibrary(false); @@ -168,7 +171,7 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC {analysisProgress.message}

-
@@ -192,14 +195,15 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC Fรผgen Sie Ihr erstes Feld hinzu

- Beginnen Sie mit einem einzelnen Feld oder fรผgen Sie eine vordefinierte Sektion hinzu, um Ihr perfektes Formular zu erstellen. + Beginnen Sie mit einem einzelnen Feld oder fรผgen Sie eine vordefinierte Sektion hinzu, + um Ihr perfektes Formular zu erstellen.

{/* Creation Methods Grid */}
{/* Individual Fields */} -
onMethodSelect('fields')} className="group cursor-pointer bg-white dark:bg-gray-800 rounded-xl border-2 border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 p-6 transition-all duration-200 hover:shadow-lg" > @@ -216,14 +220,19 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC
Felder hinzufรผgen - +
{/* Field Sections */} -
onMethodSelect('templates')} className="group cursor-pointer bg-white dark:bg-gray-800 rounded-xl border-2 border-gray-200 dark:border-gray-700 hover:border-green-500 dark:hover:border-green-500 p-6 transition-all duration-200 hover:shadow-lg" > @@ -238,8 +247,11 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC Persรถnliche Daten, Kontaktdaten, Adresse, Berufsinformationen

- {microTemplates.slice(0, 3).map(template => ( - + {microTemplates.slice(0, 3).map((template) => ( + {template.name} ))} @@ -247,14 +259,19 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC
Sektionen ansehen - +
{/* Complete Templates */} -
@@ -269,8 +286,11 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC HR Mitarbeiter-Erfassung, HR Mitarbeiter-Onboarding (Mehrstufig)

- {formTemplates.slice(0, 2).map(template => ( - + {formTemplates.slice(0, 2).map((template) => ( + {template.name} ))} @@ -278,14 +298,19 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC
Bibliothek durchsuchen - +
{/* File Upload */} -
document.getElementById('hub-file-input')?.click()} className="group cursor-pointer bg-gradient-to-br from-indigo-50 to-purple-50 dark:from-indigo-900/20 dark:to-purple-900/20 rounded-xl border-2 border-indigo-200 dark:border-indigo-700 hover:border-indigo-500 dark:hover:border-indigo-500 p-6 transition-all duration-200 hover:shadow-lg" > @@ -295,7 +320,9 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC

Datei hochladen - KI + + KI +

Screenshot oder PDF hochladen und automatisch ein Formular generieren lassen @@ -303,7 +330,12 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC

Datei auswรคhlen - +
@@ -313,8 +345,10 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC {/* Saved Templates Row - Only when logged in */} {isLoggedIn && savedTemplates.length > 0 && (
-

Ihre gespeicherten Vorlagen

-
+ Ihre gespeicherten Vorlagen + +
@@ -323,8 +357,12 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC
โญ
-

Gespeicherte Vorlagen

-

Ihre eigenen, gespeicherten Formular-Vorlagen

+

+ Gespeicherte Vorlagen +

+

+ Ihre eigenen, gespeicherten Formular-Vorlagen +

@@ -343,10 +381,11 @@ export function FormCreationHub({ onMethodSelect, onShowTemplateLibrary }: FormC {/* Bottom help text */}

- ๐Ÿ’ก Sie kรถnnen jederzeit zwischen den Methoden wechseln und verschiedene Ansรคtze kombinieren + ๐Ÿ’ก Sie kรถnnen jederzeit zwischen den Methoden wechseln und verschiedene Ansรคtze + kombinieren

); -} \ No newline at end of file +} diff --git a/frontend/src/app/components/FormField.tsx b/frontend/src/app/components/FormField.tsx index cf3e7cd8..6bf011ea 100644 --- a/frontend/src/app/components/FormField.tsx +++ b/frontend/src/app/components/FormField.tsx @@ -14,10 +14,23 @@ interface BaseFieldProps { } type FormFieldProps = BaseFieldProps & { - type: 'text' | 'email' | 'tel' | 'date' | 'select' | 'textarea' | 'checkbox' | 'radio' | 'number' | 'range' | 'file' | 'url' | 'password'; + type: + | 'text' + | 'email' + | 'tel' + | 'date' + | 'select' + | 'textarea' + | 'checkbox' + | 'radio' + | 'number' + | 'range' + | 'file' + | 'url' + | 'password'; value: string; onChange: ( - e: React.ChangeEvent + e: React.ChangeEvent, ) => void; placeholder?: string; rows?: number; @@ -29,8 +42,9 @@ type FormFieldProps = BaseFieldProps & { multiple?: boolean; }; -const baseInputClasses = "w-full px-4 py-3 border rounded-xl shadow-sm focus:outline-none focus:ring-2 dark:bg-gray-700/50 dark:text-white transition-all duration-200 backdrop-blur-sm"; -const baseLabelClasses = "block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"; +const baseInputClasses = + 'w-full px-4 py-3 border rounded-xl shadow-sm focus:outline-none focus:ring-2 dark:bg-gray-700/50 dark:text-white transition-all duration-200 backdrop-blur-sm'; +const baseLabelClasses = 'block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2'; const getInputClasses = (hasError: boolean) => { if (hasError) { @@ -40,7 +54,7 @@ const getInputClasses = (hasError: boolean) => { }; export function FormField(props: FormFieldProps) { - const { id, name, label, required, className = "", error, onBlur } = props; + const { id, name, label, required, className = '', error, onBlur } = props; const isFullWidth = props.type === 'textarea'; const hasError = !!error; @@ -63,7 +77,7 @@ export function FormField(props: FormFieldProps) { className={`${getInputClasses(hasError)} ${className}`} /> ); - + case 'select': { const options = props.options || []; return ( @@ -84,7 +98,7 @@ export function FormField(props: FormFieldProps) { ); } - + case 'textarea': return ( -
- - +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + - + diff --git a/frontend/tests/example.spec.ts b/frontend/tests/example.spec.ts index bd71a388..c21cbcc6 100644 --- a/frontend/tests/example.spec.ts +++ b/frontend/tests/example.spec.ts @@ -1,6 +1,6 @@ -import { test, expect } from "@playwright/test"; +import { test, expect } from '@playwright/test'; -test("basic test", async ({ page }) => { - await page.goto("https://playwright.dev/"); +test('basic test', async ({ page }) => { + await page.goto('https://playwright.dev/'); await expect(page).toHaveTitle(/Playwright/); }); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index b575f7da..19c51c83 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2017", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] } }, "include": [ @@ -35,7 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/frontend/vitest.config.mts b/frontend/vitest.config.mts index 169989c0..76affeeb 100644 --- a/frontend/vitest.config.mts +++ b/frontend/vitest.config.mts @@ -1,12 +1,12 @@ -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; // The config lives in frontend/ because that is the ONLY place CI installs // dependencies (.github/workflows/ci.yml installs with `working-directory: // frontend`), so it is the only place `vitest/config` resolves from. The suite // itself spans both packages, so `root` points back up at the repo. -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); export default defineConfig({ root: repoRoot, @@ -17,4 +17,4 @@ export default defineConfig({ include: ['frontend/src/**/*.test.ts', 'backend/**/*.test.js'], exclude: ['**/node_modules/**', '**/.next/**', 'tests/**'], }, -}) +}); diff --git a/package.json b/package.json index 3014c385..f60f7115 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "test:e2e:headed": "npx playwright test --headed", "test:e2e:report": "npx playwright show-report", "typecheck": "cd frontend && npx tsc --noEmit", - "verify": "npm run lint && npm run typecheck && npm run test && npm run build" + "verify": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run build", + "format": "cd frontend && npm run format", + "format:check": "cd frontend && npm run format:check" }, "keywords": [ "data-capture", diff --git a/playwright.config.ts b/playwright.config.ts index b670890e..8a7ced5d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -69,4 +69,4 @@ export default defineConfig({ reuseExistingServer: !process.env.CI, timeout: 120000, }, -}); \ No newline at end of file +}); diff --git a/scripts/start-dev.js b/scripts/start-dev.js index d7ca7c42..07f980d7 100755 --- a/scripts/start-dev.js +++ b/scripts/start-dev.js @@ -5,18 +5,24 @@ const { spawn } = require('child_process'); console.log('\n๐Ÿš€ Starting all development servers...\n'); // Start concurrently with both servers -const dev = spawn('npx', [ - 'concurrently', - '-k', - '-n', 'FRONTEND,BACKEND', - '-c', 'blue,green', - '"cd frontend && npm run dev"', - '"cd backend && npm start"' -], { - stdio: 'inherit', - shell: true, - cwd: process.cwd() -}); +const dev = spawn( + 'npx', + [ + 'concurrently', + '-k', + '-n', + 'FRONTEND,BACKEND', + '-c', + 'blue,green', + '"cd frontend && npm run dev"', + '"cd backend && npm start"', + ], + { + stdio: 'inherit', + shell: true, + cwd: process.cwd(), + }, +); // Wait a bit for servers to start, then show links setTimeout(() => { @@ -39,4 +45,4 @@ dev.on('close', (code) => { process.on('SIGINT', () => { console.log('\n๐Ÿ›‘ Stopping all development servers...'); dev.kill('SIGINT'); -}); \ No newline at end of file +}); diff --git a/test-auth.js b/test-auth.js index f6a01cc8..a8063866 100644 --- a/test-auth.js +++ b/test-auth.js @@ -13,7 +13,7 @@ function makeRequest(options, data = null) { return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let body = ''; - res.on('data', chunk => body += chunk); + res.on('data', (chunk) => (body += chunk)); res.on('end', () => { try { const jsonData = JSON.parse(body); @@ -25,22 +25,22 @@ function makeRequest(options, data = null) { }); req.on('error', reject); - + if (data) { req.write(JSON.stringify(data)); } - + req.end(); }); } async function runAuthTests() { console.log('๐Ÿงช Starting Authentication Integration Tests\n'); - + const testUser = { email: `test-${Date.now()}@example.com`, password: 'testpass123', - name: 'Integration Test User' + name: 'Integration Test User', }; let userToken = ''; @@ -53,14 +53,14 @@ async function runAuthTests() { path: '/api/v1/auth/register', method: 'POST', headers: { - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }; - + const registerResponse = await makeRequest(registerOptions, testUser); console.log(` Status: ${registerResponse.status}`); console.log(` Message: ${registerResponse.data.message}`); - + if (registerResponse.status === 201 && registerResponse.data.success) { console.log(' โœ… Registration successful'); console.log(` โœ… User ID: ${registerResponse.data.user.id.substring(0, 10)}...`); @@ -79,18 +79,18 @@ async function runAuthTests() { path: '/api/v1/auth/login', method: 'POST', headers: { - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }; - + const loginResponse = await makeRequest(loginOptions, { email: testUser.email, - password: testUser.password + password: testUser.password, }); - + console.log(` Status: ${loginResponse.status}`); console.log(` Message: ${loginResponse.data.message}`); - + if (loginResponse.status === 200 && loginResponse.data.success) { console.log(' โœ… Login successful'); userToken = loginResponse.data.token; @@ -107,13 +107,13 @@ async function runAuthTests() { path: '/api/v1/auth/profile', method: 'GET', headers: { - 'Authorization': `Bearer ${userToken}` - } + Authorization: `Bearer ${userToken}`, + }, }; - + const profileResponse = await makeRequest(profileOptions); console.log(` Status: ${profileResponse.status}`); - + if (profileResponse.status === 200 && profileResponse.data.success) { console.log(' โœ… Protected endpoint access successful'); console.log(` โœ… User profile retrieved: ${profileResponse.data.user.email}`); @@ -129,13 +129,13 @@ async function runAuthTests() { path: '/api/v1/auth/profile', method: 'GET', headers: { - 'Authorization': 'Bearer invalid-token-here' - } + Authorization: 'Bearer invalid-token-here', + }, }; - + const invalidResponse = await makeRequest(invalidTokenOptions); console.log(` Status: ${invalidResponse.status}`); - + if (invalidResponse.status === 401 && !invalidResponse.data.success) { console.log(' โœ… Invalid token correctly rejected'); } else { @@ -150,13 +150,13 @@ async function runAuthTests() { path: '/api/v1/auth/verify', method: 'GET', headers: { - 'Authorization': `Bearer ${userToken}` - } + Authorization: `Bearer ${userToken}`, + }, }; - + const verifyResponse = await makeRequest(verifyOptions); console.log(` Status: ${verifyResponse.status}`); - + if (verifyResponse.status === 200 && verifyResponse.data.success) { console.log(' โœ… Token verification successful'); } else { @@ -167,11 +167,11 @@ async function runAuthTests() { console.log('\n6๏ธโƒฃ Testing Invalid Login Credentials...'); const invalidLoginResponse = await makeRequest(loginOptions, { email: testUser.email, - password: 'wrongpassword' + password: 'wrongpassword', }); - + console.log(` Status: ${invalidLoginResponse.status}`); - + if (invalidLoginResponse.status === 401 && !invalidLoginResponse.data.success) { console.log(' โœ… Invalid credentials correctly rejected'); } else { @@ -188,11 +188,10 @@ async function runAuthTests() { console.log(' ๐Ÿ”„ CRUD Operations: Create users, read profiles'); console.log('\nโœ… All Authentication Tests Completed Successfully! ๐ŸŽ‰'); - } catch (error) { console.error('\nโŒ Test Suite Failed:', error.message); } } // Run the tests -runAuthTests(); \ No newline at end of file +runAuthTests(); diff --git a/tests/auth-integration.spec.ts b/tests/auth-integration.spec.ts index b7865566..52c8e5f1 100644 --- a/tests/auth-integration.spec.ts +++ b/tests/auth-integration.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'; /** * Authentication Integration Tests - * + * * Tests the complete authentication flow including: * 1. User registration via API * 2. User login and token generation @@ -16,68 +16,68 @@ test.describe('Backend Authentication System', () => { const testUser = { email: `test-${Date.now()}@example.com`, password: 'testpass123', - name: 'E2E Test User' + name: 'E2E Test User', }; let userToken: string; test('should register a new user via API', async ({ request }) => { console.log('๐Ÿงช Testing user registration...'); - + const response = await request.post(`${API_BASE}/auth/register`, { - data: testUser + data: testUser, }); - + expect(response.status()).toBe(201); - + const responseData = await response.json(); console.log('โœ… Registration response:', responseData.message); - + expect(responseData.success).toBe(true); expect(responseData.token).toBeTruthy(); expect(responseData.user.email).toBe(testUser.email); expect(responseData.user.name).toBe(testUser.name); expect(responseData.user.role).toBe('USER'); - + userToken = responseData.token; console.log('โœ… JWT Token generated successfully'); }); test('should login with registered credentials', async ({ request }) => { console.log('๐Ÿงช Testing user login...'); - + const response = await request.post(`${API_BASE}/auth/login`, { data: { email: testUser.email, - password: testUser.password - } + password: testUser.password, + }, }); - + expect(response.status()).toBe(200); - + const responseData = await response.json(); console.log('โœ… Login response:', responseData.message); - + expect(responseData.success).toBe(true); expect(responseData.token).toBeTruthy(); expect(responseData.user.email).toBe(testUser.email); - + userToken = responseData.token; }); test('should access protected profile endpoint with valid token', async ({ request }) => { console.log('๐Ÿงช Testing protected endpoint access...'); - + const response = await request.get(`${API_BASE}/auth/profile`, { headers: { - 'Authorization': `Bearer ${userToken}` - } + Authorization: `Bearer ${userToken}`, + }, }); - + expect(response.status()).toBe(200); - + const responseData = await response.json(); console.log('โœ… Profile access successful'); - + expect(responseData.success).toBe(true); expect(responseData.user.email).toBe(testUser.email); expect(responseData.user.id).toBeTruthy(); @@ -85,90 +85,90 @@ test.describe('Backend Authentication System', () => { test('should reject access to protected endpoint with invalid token', async ({ request }) => { console.log('๐Ÿงช Testing invalid token rejection...'); - + const response = await request.get(`${API_BASE}/auth/profile`, { headers: { - 'Authorization': 'Bearer invalid-token-here' - } + Authorization: 'Bearer invalid-token-here', + }, }); - + expect(response.status()).toBe(401); - + const responseData = await response.json(); console.log('โœ… Invalid token correctly rejected'); - + expect(responseData.success).toBe(false); expect(responseData.message).toContain('Invalid token'); }); test('should reject login with invalid credentials', async ({ request }) => { console.log('๐Ÿงช Testing invalid login credentials...'); - + const response = await request.post(`${API_BASE}/auth/login`, { data: { email: testUser.email, - password: 'wrongpassword' - } + password: 'wrongpassword', + }, }); - + expect(response.status()).toBe(401); - + const responseData = await response.json(); console.log('โœ… Invalid credentials correctly rejected'); - + expect(responseData.success).toBe(false); expect(responseData.message).toBe('Invalid credentials'); }); test('should verify token validity', async ({ request }) => { console.log('๐Ÿงช Testing token verification...'); - + const response = await request.get(`${API_BASE}/auth/verify`, { headers: { - 'Authorization': `Bearer ${userToken}` - } + Authorization: `Bearer ${userToken}`, + }, }); - + expect(response.status()).toBe(200); - + const responseData = await response.json(); console.log('โœ… Token verification successful'); - + expect(responseData.success).toBe(true); expect(responseData.user.email).toBe(testUser.email); }); test('should prevent duplicate user registration', async ({ request }) => { console.log('๐Ÿงช Testing duplicate registration prevention...'); - + const response = await request.post(`${API_BASE}/auth/register`, { - data: testUser // Same user data as before + data: testUser, // Same user data as before }); - + expect(response.status()).toBe(400); - + const responseData = await response.json(); console.log('โœ… Duplicate registration correctly prevented'); - + expect(responseData.success).toBe(false); expect(responseData.message).toBe('User already exists'); }); test('should require email and password for registration', async ({ request }) => { console.log('๐Ÿงช Testing registration validation...'); - + const response = await request.post(`${API_BASE}/auth/register`, { data: { - name: 'Test User' + name: 'Test User', // Missing email and password - } + }, }); - + expect(response.status()).toBe(400); - + const responseData = await response.json(); console.log('โœ… Validation working correctly'); - + expect(responseData.success).toBe(false); expect(responseData.message).toBe('Email and password are required'); }); @@ -177,26 +177,26 @@ test.describe('Backend Authentication System', () => { test.describe('Backend Engineering Concepts - Authentication', () => { test('should demonstrate JWT token structure', async ({ request }) => { console.log('๐Ÿงช Demonstrating JWT token concepts...'); - + const response = await request.post(`${API_BASE}/auth/login`, { data: { email: 'test@example.com', - password: 'testpass123' - } + password: 'testpass123', + }, }); - + const responseData = await response.json(); const token = responseData.token; - + // JWT tokens have 3 parts separated by dots const tokenParts = token.split('.'); expect(tokenParts).toHaveLength(3); - + console.log('โœ… JWT Structure:'); console.log(' - Header (algorithm & type):', tokenParts[0].substring(0, 20) + '...'); console.log(' - Payload (user data):', tokenParts[1].substring(0, 20) + '...'); console.log(' - Signature (verification):', tokenParts[2].substring(0, 20) + '...'); - + // Decode payload (base64) to show user data try { const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString()); @@ -212,29 +212,29 @@ test.describe('Backend Engineering Concepts - Authentication', () => { test('should demonstrate password hashing security', async ({ request }) => { console.log('๐Ÿงช Demonstrating password security concepts...'); - + // Create two users with same password const user1Email = `security-test-1-${Date.now()}@example.com`; const user2Email = `security-test-2-${Date.now()}@example.com`; const samePassword = 'samepassword123'; - + const response1 = await request.post(`${API_BASE}/auth/register`, { - data: { email: user1Email, password: samePassword, name: 'User 1' } + data: { email: user1Email, password: samePassword, name: 'User 1' }, }); - + const response2 = await request.post(`${API_BASE}/auth/register`, { - data: { email: user2Email, password: samePassword, name: 'User 2' } + data: { email: user2Email, password: samePassword, name: 'User 2' }, }); - + expect(response1.status()).toBe(201); expect(response2.status()).toBe(201); - + const user1Data = await response1.json(); const user2Data = await response2.json(); - + // Tokens should be different even with same password expect(user1Data.token).not.toBe(user2Data.token); - + console.log('โœ… Security Concepts Demonstrated:'); console.log(' - Same password โ†’ Different hashed values'); console.log(' - Same password โ†’ Different JWT tokens'); @@ -244,35 +244,35 @@ test.describe('Backend Engineering Concepts - Authentication', () => { test('should demonstrate API rate limiting concepts', async ({ request }) => { console.log('๐Ÿงช Testing API behavior under load...'); - + const requests = []; const testEmail = `rate-test-${Date.now()}@example.com`; - + // Make multiple concurrent requests for (let i = 0; i < 5; i++) { requests.push( request.post(`${API_BASE}/auth/login`, { data: { email: testEmail, - password: 'wrongpassword' - } - }) + password: 'wrongpassword', + }, + }), ); } - + const responses = await Promise.all(requests); - + console.log('โœ… Concurrent Request Results:'); responses.forEach((response, index) => { console.log(` - Request ${index + 1}: ${response.status()}`); }); - + // All should return 401 (unauthorized) consistently - responses.forEach(response => { + responses.forEach((response) => { expect(response.status()).toBe(401); }); - + console.log('โœ… API handled concurrent requests correctly'); console.log('โ„น๏ธ Rate limiting can be added for production security'); }); -}); \ No newline at end of file +}); diff --git a/tests/erfassung-platform.spec.ts b/tests/erfassung-platform.spec.ts index 260b991b..23bf4f4c 100644 --- a/tests/erfassung-platform.spec.ts +++ b/tests/erfassung-platform.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test'; /** * Erfassung Platform End-to-End Tests - * + * * These tests demonstrate key backend engineering concepts: * 1. API testing through user interactions * 2. State management and persistence @@ -14,19 +14,19 @@ import { test, expect } from '@playwright/test'; test.describe('Erfassung Platform - Form Builder', () => { test.beforeEach(async ({ page }) => { // Start the frontend development server - await page.goto('http://localhost:3000', { + await page.goto('http://localhost:3000', { waitUntil: 'networkidle', - timeout: 30000 + timeout: 30000, }); }); test('should load the homepage with form builder elements', async ({ page }) => { // Test: Basic connectivity and server response console.log('๐Ÿงช Testing homepage load and form builder presence...'); - + // Verify page loads successfully await expect(page).toHaveTitle(/Erfassung|Formular|Form/i); - + // Look for form builder components const formBuilderSelectors = [ '[class*="form"]', @@ -35,20 +35,20 @@ test.describe('Erfassung Platform - Form Builder', () => { 'button:has-text("form")', 'button:has-text("create")', 'text="Form Builder"', - 'text="Create Form"' + 'text="Create Form"', ]; let builderElementFound = false; for (const selector of formBuilderSelectors) { const elements = page.locator(selector); - if (await elements.count() > 0) { + if ((await elements.count()) > 0) { console.log(`โœ… Found form builder element: ${selector}`); builderElementFound = true; await expect(elements.first()).toBeVisible(); break; } } - + if (!builderElementFound) { console.log('โ„น๏ธ Form builder UI not yet implemented - testing basic page structure'); await expect(page.locator('body')).toBeVisible(); @@ -58,34 +58,34 @@ test.describe('Erfassung Platform - Form Builder', () => { test('should handle form field creation', async ({ page }) => { // Test: Dynamic UI creation and state management console.log('๐Ÿงช Testing form field creation workflow...'); - + // Look for field addition controls const addFieldControls = [ 'button:has-text("Add Field")', 'button:has-text("New Field")', 'button:has-text("+")', '[data-testid*="add"]', - '[class*="add-field"]' + '[class*="add-field"]', ]; let fieldControlFound = false; for (const selector of addFieldControls) { const control = page.locator(selector); - if (await control.count() > 0) { + if ((await control.count()) > 0) { console.log(`โœ… Found field addition control: ${selector}`); await control.click(); fieldControlFound = true; - + // Wait for field to be added await page.waitForTimeout(500); break; } } - + if (fieldControlFound) { // Test field configuration const fieldInputs = page.locator('input[placeholder*="name"], input[placeholder*="label"]'); - if (await fieldInputs.count() > 0) { + if ((await fieldInputs.count()) > 0) { await fieldInputs.first().fill('Test Field Name'); console.log('โœ… Field configuration working'); } @@ -97,28 +97,26 @@ test.describe('Erfassung Platform - Form Builder', () => { test('should persist form data using localStorage', async ({ page }) => { // Test: Client-side data persistence (preparation for backend APIs) console.log('๐Ÿงช Testing data persistence mechanisms...'); - + // Check if localStorage is being used for form state const localStorageData = await page.evaluate(() => { const keys = Object.keys(localStorage); - return keys.filter(key => - key.includes('form') || - key.includes('builder') || - key.includes('erfassung') + return keys.filter( + (key) => key.includes('form') || key.includes('builder') || key.includes('erfassung'), ); }); - + console.log('๐Ÿ“Š Found localStorage keys:', localStorageData); - + // Fill any available form inputs const inputs = page.locator('input[type="text"], textarea'); - if (await inputs.count() > 0) { + if ((await inputs.count()) > 0) { const testData = 'Persistence Test Data'; await inputs.first().fill(testData); - + // Reload page and check if data persists await page.reload(); - + const persistedValue = await inputs.first().inputValue(); if (persistedValue === testData) { console.log('โœ… Data persistence working correctly'); @@ -131,64 +129,66 @@ test.describe('Erfassung Platform - Form Builder', () => { test('should handle responsive design across devices', async ({ page }) => { // Test: Mobile-first design and responsive APIs console.log('๐Ÿงช Testing responsive design...'); - + // Test mobile viewport await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE await expect(page.locator('body')).toBeVisible(); - + // Check for mobile navigation const mobileElements = page.locator('[class*="mobile"], [data-mobile], [class*="hamburger"]'); - if (await mobileElements.count() > 0) { + if ((await mobileElements.count()) > 0) { console.log('โœ… Mobile-specific elements found'); } - + // Test tablet viewport await page.setViewportSize({ width: 768, height: 1024 }); await page.waitForTimeout(300); - + // Test desktop viewport await page.setViewportSize({ width: 1920, height: 1080 }); await expect(page.locator('body')).toBeVisible(); - + console.log('โœ… Responsive design testing completed'); }); test('should simulate API error scenarios', async ({ page }) => { // Test: Error handling and resilience console.log('๐Ÿงช Testing error handling capabilities...'); - + // Mock network failures - await page.route('**/api/**', route => route.abort('failed')); - + await page.route('**/api/**', (route) => route.abort('failed')); + // Navigate and see how the app handles API failures - await page.goto('http://localhost:3000', { - waitUntil: 'networkidle', - timeout: 10000 - }).catch(() => { - console.log('Expected: Navigation with API failures handled'); - }); - + await page + .goto('http://localhost:3000', { + waitUntil: 'networkidle', + timeout: 10000, + }) + .catch(() => { + console.log('Expected: Navigation with API failures handled'); + }); + // App should still render even with API failures await expect(page.locator('body')).toBeVisible(); - + // Look for error messages or fallback states const errorStates = [ '[role="alert"]', '[class*="error"]', 'text*="error"', 'text*="failed"', - 'text*="try again"' + 'text*="try again"', ]; - + let errorHandlingFound = false; for (const selector of errorStates) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log(`โœ… Found error handling: ${selector}`); errorHandlingFound = true; break; } } - + if (!errorHandlingFound) { console.log('โ„น๏ธ Error handling UI not yet implemented'); } @@ -199,31 +199,33 @@ test.describe('Backend Engineering Concepts Demo', () => { test('should demonstrate database-like operations through UI', async ({ page }) => { // Test: CRUD operations simulation console.log('๐Ÿงช Testing CRUD operation patterns...'); - + await page.goto('http://localhost:3000'); - + // CREATE: Add new form/field - const createButtons = page.locator('button:has-text("Create"), button:has-text("Add"), button:has-text("New")'); - if (await createButtons.count() > 0) { + const createButtons = page.locator( + 'button:has-text("Create"), button:has-text("Add"), button:has-text("New")', + ); + if ((await createButtons.count()) > 0) { console.log('โœ… CREATE operation UI found'); await createButtons.first().click(); } - + // READ: Display existing data const dataDisplays = page.locator('[class*="list"], [class*="table"], [class*="grid"]'); - if (await dataDisplays.count() > 0) { + if ((await dataDisplays.count()) > 0) { console.log('โœ… READ operation UI found'); } - + // UPDATE: Edit existing items const editButtons = page.locator('button:has-text("Edit"), [class*="edit"]'); - if (await editButtons.count() > 0) { + if ((await editButtons.count()) > 0) { console.log('โœ… UPDATE operation UI found'); } - + // DELETE: Remove items const deleteButtons = page.locator('button:has-text("Delete"), button:has-text("Remove")'); - if (await deleteButtons.count() > 0) { + if ((await deleteButtons.count()) > 0) { console.log('โœ… DELETE operation UI found'); } }); @@ -231,34 +233,34 @@ test.describe('Backend Engineering Concepts Demo', () => { test('should test pagination and data loading patterns', async ({ page }) => { // Test: Database query optimization concepts console.log('๐Ÿงช Testing pagination and lazy loading...'); - + await page.goto('http://localhost:3000'); - + // Look for pagination controls const paginationElements = [ '[class*="pagination"]', 'button:has-text("Next")', 'button:has-text("Previous")', - '[aria-label*="page"]' + '[aria-label*="page"]', ]; - + for (const selector of paginationElements) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log(`โœ… Found pagination pattern: ${selector}`); break; } } - + // Look for loading states const loadingStates = [ '[class*="loading"]', '[class*="spinner"]', '[class*="skeleton"]', - '[role="progressbar"]' + '[role="progressbar"]', ]; - + for (const selector of loadingStates) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log(`โœ… Found loading state: ${selector}`); break; } @@ -268,28 +270,28 @@ test.describe('Backend Engineering Concepts Demo', () => { test('should measure performance metrics', async ({ page }) => { // Test: Performance monitoring (backend concept) console.log('๐Ÿงช Testing performance characteristics...'); - + const startTime = Date.now(); - + await page.goto('http://localhost:3000', { - waitUntil: 'networkidle' + waitUntil: 'networkidle', }); - + const loadTime = Date.now() - startTime; console.log(`๐Ÿ“Š Page load time: ${loadTime}ms`); - + // Performance should be reasonable for development expect(loadTime).toBeLessThan(5000); - + // Check for performance optimization patterns const performanceFeatures = [ - '[loading="lazy"]', // Lazy loading - '[class*="virtual"]', // Virtualization - '[class*="skeleton"]' // Skeleton screens + '[loading="lazy"]', // Lazy loading + '[class*="virtual"]', // Virtualization + '[class*="skeleton"]', // Skeleton screens ]; - + for (const selector of performanceFeatures) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log(`โœ… Performance optimization found: ${selector}`); } } @@ -300,30 +302,30 @@ test.describe('Security and Validation Testing', () => { test('should test input validation patterns', async ({ page }) => { // Test: Security through validation console.log('๐Ÿงช Testing input validation and security...'); - + await page.goto('http://localhost:3000'); - + const inputs = page.locator('input, textarea'); const inputCount = await inputs.count(); - + if (inputCount > 0) { // Test XSS prevention const maliciousScript = ''; await inputs.first().fill(maliciousScript); - + // Check if script is properly escaped const inputValue = await inputs.first().inputValue(); if (inputValue !== maliciousScript) { console.log('โœ… Input sanitization working'); } - + // Test validation patterns const emailInput = page.locator('input[type="email"]'); - if (await emailInput.count() > 0) { + if ((await emailInput.count()) > 0) { await emailInput.fill('invalid-email'); // Look for validation messages const validationMsg = page.locator('[class*="error"], [role="alert"]'); - if (await validationMsg.count() > 0) { + if ((await validationMsg.count()) > 0) { console.log('โœ… Email validation working'); } } @@ -333,16 +335,19 @@ test.describe('Security and Validation Testing', () => { test('should test CSRF protection patterns', async ({ page }) => { // Test: Cross-Site Request Forgery protection concepts console.log('๐Ÿงช Testing CSRF protection patterns...'); - + await page.goto('http://localhost:3000'); - + // Look for CSRF tokens in forms - const csrfTokens = page.locator('input[name*="csrf"], input[name*="token"], meta[name="csrf-token"]'); - - if (await csrfTokens.count() > 0) { + const csrfTokens = page.locator( + 'input[name*="csrf"], input[name*="token"], meta[name="csrf-token"]', + ); + + if ((await csrfTokens.count()) > 0) { console.log('โœ… CSRF token found'); - const tokenValue = await csrfTokens.first().getAttribute('value') || - await csrfTokens.first().getAttribute('content'); + const tokenValue = + (await csrfTokens.first().getAttribute('value')) || + (await csrfTokens.first().getAttribute('content')); if (tokenValue && tokenValue.length > 10) { console.log('โœ… CSRF token appears valid'); } @@ -350,4 +355,4 @@ test.describe('Security and Validation Testing', () => { console.log('โ„น๏ธ CSRF tokens not yet implemented (normal for development)'); } }); -}); \ No newline at end of file +}); diff --git a/tests/example.spec.ts b/tests/example.spec.ts index a301c515..22b8d099 100644 --- a/tests/example.spec.ts +++ b/tests/example.spec.ts @@ -17,4 +17,4 @@ test('basic test', async ({ page }) => { // Expects page to have a heading with the name of Installation. await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); \ No newline at end of file +}); diff --git a/tests/form-builder-comprehensive.spec.ts b/tests/form-builder-comprehensive.spec.ts index 5d4f22ab..692ef3bb 100644 --- a/tests/form-builder-comprehensive.spec.ts +++ b/tests/form-builder-comprehensive.spec.ts @@ -16,18 +16,18 @@ test.describe('Erfassung Platform Tests', () => { test('should handle form interactions', async ({ page }) => { await page.goto('http://localhost:3000'); - + const buttons = page.locator('button'); const buttonCount = await buttons.count(); console.log(`Found ${buttonCount} buttons on the page`); - + if (buttonCount > 0) { const buttonText = await buttons.first().textContent(); console.log(`First button text: ${buttonText}`); } - + const inputs = page.locator('input'); const inputCount = await inputs.count(); console.log(`Found ${inputCount} input fields on the page`); }); -}); \ No newline at end of file +}); diff --git a/tests/form-builder.spec.ts b/tests/form-builder.spec.ts index 6bcb8b38..9f44f58c 100644 --- a/tests/form-builder.spec.ts +++ b/tests/form-builder.spec.ts @@ -18,7 +18,7 @@ test.describe('Form Builder', () => { test('should load the homepage successfully', async ({ page }) => { // Test basic page loading - this tests your backend health await expect(page).toHaveTitle(/Erfassung Platform/i); - + // Look for key UI elements await expect(page.locator('body')).toBeVisible(); }); @@ -28,13 +28,13 @@ test.describe('Form Builder', () => { // Look for form creation buttons or links const createFormButton = page.getByRole('button', { name: /create.*form/i }); const createFormLink = page.getByRole('link', { name: /create.*form/i }); - + // Try to find any create form element - const createElement = await createFormButton.count() > 0 ? createFormButton : createFormLink; - - if (await createElement.count() > 0) { + const createElement = (await createFormButton.count()) > 0 ? createFormButton : createFormLink; + + if ((await createElement.count()) > 0) { await createElement.click(); - + // Verify we're on the form builder page await expect(page.url()).toContain('form'); } else { @@ -49,18 +49,18 @@ test.describe('Form Builder', () => { test('should create a basic form with text field', async ({ page }) => { // This tests form creation API endpoints and data persistence - + // Check if we can find form builder elements const formElements = [ 'input[type="text"]', '[data-testid*="form"]', '[class*="form"]', - '[class*="builder"]' + '[class*="builder"]', ]; let builderFound = false; for (const selector of formElements) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { builderFound = true; break; } @@ -70,53 +70,57 @@ test.describe('Form Builder', () => { // Test form creation workflow // 1. Add a text field const addFieldButton = page.getByRole('button', { name: /add.*field|text.*field|field/i }); - if (await addFieldButton.count() > 0) { + if ((await addFieldButton.count()) > 0) { await addFieldButton.click(); - + // 2. Configure the field - const fieldNameInput = page.locator('input[placeholder*="name"], input[placeholder*="label"]').first(); - if (await fieldNameInput.count() > 0) { + const fieldNameInput = page + .locator('input[placeholder*="name"], input[placeholder*="label"]') + .first(); + if ((await fieldNameInput.count()) > 0) { await fieldNameInput.fill('First Name'); } - + // 3. Save the form (this tests POST API) const saveButton = page.getByRole('button', { name: /save|create/i }); - if (await saveButton.count() > 0) { + if ((await saveButton.count()) > 0) { await saveButton.click(); - + // Verify success state await expect(page.locator('text=success')).toBeVisible({ timeout: 5000 }); } } } else { - console.log('Form builder not found on current page - this may be expected for current implementation'); + console.log( + 'Form builder not found on current page - this may be expected for current implementation', + ); } }); test('should validate form fields', async ({ page }) => { // This tests client-side and server-side validation - + // Look for any form inputs const inputs = page.locator('input, textarea, select'); const inputCount = await inputs.count(); - + if (inputCount > 0) { // Try to submit empty form to test validation const submitButton = page.getByRole('button', { name: /submit|save|create/i }); - if (await submitButton.count() > 0) { + if ((await submitButton.count()) > 0) { await submitButton.click(); - + // Check for validation messages const validationMessages = [ 'text*="required"', 'text*="error"', '[class*="error"]', - '[role="alert"]' + '[role="alert"]', ]; - + for (const selector of validationMessages) { const errorElements = page.locator(selector); - if (await errorElements.count() > 0) { + if ((await errorElements.count()) > 0) { console.log(`Found validation message: ${await errorElements.first().textContent()}`); break; } @@ -128,33 +132,33 @@ test.describe('Form Builder', () => { test('should handle responsive design', async ({ page }) => { // Test mobile responsiveness - important for API design await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE - + // Verify page still loads and functions await expect(page.locator('body')).toBeVisible(); - + // Check if mobile navigation works const mobileMenu = page.locator('[class*="mobile"], [data-testid*="mobile"]'); - if (await mobileMenu.count() > 0) { + if ((await mobileMenu.count()) > 0) { await expect(mobileMenu).toBeVisible(); } - + // Reset to desktop await page.setViewportSize({ width: 1280, height: 720 }); }); test('should persist form data in localStorage', async ({ page }) => { // This tests client-side state persistence - + // Fill out any form inputs const textInputs = page.locator('input[type="text"], textarea'); const inputCount = await textInputs.count(); - + if (inputCount > 0) { await textInputs.first().fill('Test Data Persistence'); - + // Reload the page await page.reload(); - + // Check if data is persisted (this would test localStorage implementation) const persistedValue = await textInputs.first().inputValue(); if (persistedValue === 'Test Data Persistence') { @@ -169,27 +173,22 @@ test.describe('Form Builder', () => { test.describe('API Integration Tests', () => { test('should handle API errors gracefully', async ({ page }) => { // Test error handling for when backend is down - + // Mock network failure await page.route('**/api/**', (route) => { route.abort('failed'); }); - + await page.goto('/'); - + // Should still show page, but with error states await expect(page.locator('body')).toBeVisible(); - + // Look for error messages or fallback states - const errorStates = [ - 'text*="error"', - 'text*="failed"', - 'text*="try again"', - '[role="alert"]' - ]; - + const errorStates = ['text*="error"', 'text*="failed"', 'text*="try again"', '[role="alert"]']; + for (const selector of errorStates) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log('Found error handling'); break; } @@ -198,31 +197,31 @@ test.describe('API Integration Tests', () => { test('should handle slow API responses', async ({ page }) => { // Test loading states and timeouts - + // Mock slow response await page.route('**/api/**', async (route) => { - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); route.fulfill({ status: 200, - body: JSON.stringify({ success: true }) + body: JSON.stringify({ success: true }), }); }); - + await page.goto('/'); - + // Look for loading indicators const loadingStates = [ '[class*="loading"]', '[class*="spinner"]', 'text*="loading"', - '[role="progressbar"]' + '[role="progressbar"]', ]; - + for (const selector of loadingStates) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log('Found loading state handling'); break; } } }); -}); \ No newline at end of file +}); diff --git a/tests/form-submission.spec.ts b/tests/form-submission.spec.ts index 5ef4cca8..6a6acd8c 100644 --- a/tests/form-submission.spec.ts +++ b/tests/form-submission.spec.ts @@ -17,22 +17,22 @@ test.describe('Form Submission Flow', () => { test('should submit a form successfully', async ({ page }) => { // This tests the complete form submission pipeline // Frontend -> API -> Database -> Response - + // Look for form elements const forms = page.locator('form'); const formCount = await forms.count(); - + if (formCount > 0) { const form = forms.first(); - + // Fill out form fields const textInputs = form.locator('input[type="text"], input[type="email"], textarea'); const inputCount = await textInputs.count(); - + for (let i = 0; i < inputCount; i++) { const input = textInputs.nth(i); - const placeholder = await input.getAttribute('placeholder') || ''; - + const placeholder = (await input.getAttribute('placeholder')) || ''; + // Fill based on input type/placeholder if (placeholder.toLowerCase().includes('email')) { await input.fill('test@example.com'); @@ -42,23 +42,27 @@ test.describe('Form Submission Flow', () => { await input.fill(`Test Value ${i + 1}`); } } - + // Submit the form const submitButton = form.getByRole('button', { name: /submit|send|save/i }); - if (await submitButton.count() > 0) { + if ((await submitButton.count()) > 0) { await submitButton.click(); - + // Wait for success response (tests API response time) - await expect(page.locator('text*="success", text*="submitted", text*="thank you"')).toBeVisible({ - timeout: 10000 + await expect( + page.locator('text*="success", text*="submitted", text*="thank you"'), + ).toBeVisible({ + timeout: 10000, }); } } else { console.log('No forms found - creating a test scenario'); - + // Navigate to a form creation page if it exists - const createLinks = page.locator('a[href*="form"], button:has-text("create")', { hasText: /form|create/i }); - if (await createLinks.count() > 0) { + const createLinks = page.locator('a[href*="form"], button:has-text("create")', { + hasText: /form|create/i, + }); + if ((await createLinks.count()) > 0) { await createLinks.first().click(); console.log('Navigated to form creation page'); } @@ -67,16 +71,16 @@ test.describe('Form Submission Flow', () => { test('should validate required fields before submission', async ({ page }) => { // This tests both client-side and server-side validation - + const forms = page.locator('form'); - if (await forms.count() > 0) { + if ((await forms.count()) > 0) { const form = forms.first(); - + // Try to submit without filling required fields const submitButton = form.getByRole('button', { name: /submit|send|save/i }); - if (await submitButton.count() > 0) { + if ((await submitButton.count()) > 0) { await submitButton.click(); - + // Check for validation messages const validationSelectors = [ '[role="alert"]', @@ -84,19 +88,19 @@ test.describe('Form Submission Flow', () => { '[class*="error"]', 'text*="required"', 'text*="field is required"', - '[aria-invalid="true"]' + '[aria-invalid="true"]', ]; - + let validationFound = false; for (const selector of validationSelectors) { const elements = page.locator(selector); - if (await elements.count() > 0) { + if ((await elements.count()) > 0) { console.log(`Validation message found: ${await elements.first().textContent()}`); validationFound = true; break; } } - + if (!validationFound) { console.log('No client-side validation found - may be using server-side only'); } @@ -106,29 +110,29 @@ test.describe('Form Submission Flow', () => { test('should handle file uploads', async ({ page }) => { // This tests file upload API endpoints and storage - + const fileInputs = page.locator('input[type="file"]'); - if (await fileInputs.count() > 0) { + if ((await fileInputs.count()) > 0) { const fileInput = fileInputs.first(); - + // Create a test file const testFilePath = '/tmp/test-upload.txt'; await page.evaluate((content) => { const fs = require('fs'); fs.writeFileSync('/tmp/test-upload.txt', content); }, 'This is a test file for upload testing'); - + // Upload the file await fileInput.setInputFiles(testFilePath); - + // Submit the form const submitButton = page.getByRole('button', { name: /submit|upload|send/i }); - if (await submitButton.count() > 0) { + if ((await submitButton.count()) > 0) { await submitButton.click(); - + // Wait for upload success - await expect(page.locator('text*="upload", text*="success"')).toBeVisible({ - timeout: 15000 + await expect(page.locator('text*="upload", text*="success"')).toBeVisible({ + timeout: 15000, }); } } else { @@ -138,22 +142,24 @@ test.describe('Form Submission Flow', () => { test('should export form submissions', async ({ page }) => { // This tests data export APIs (CSV, Excel, PDF) - + // Look for export buttons - const exportButtons = page.locator('button:has-text("export"), button:has-text("download"), a[download]'); - - if (await exportButtons.count() > 0) { + const exportButtons = page.locator( + 'button:has-text("export"), button:has-text("download"), a[download]', + ); + + if ((await exportButtons.count()) > 0) { // Set up download handling const downloadPromise = page.waitForEvent('download'); - + await exportButtons.first().click(); - + const download = await downloadPromise; - + // Verify download expect(download.suggestedFilename()).toBeTruthy(); console.log(`Downloaded file: ${download.suggestedFilename()}`); - + // Save the file to verify content await download.saveAs(`/tmp/${download.suggestedFilename()}`); } else { @@ -163,30 +169,32 @@ test.describe('Form Submission Flow', () => { test('should paginate through large datasets', async ({ page }) => { // This tests database query optimization and pagination - + // Navigate to submissions or data view page - const dataLinks = page.locator('a:has-text("submissions"), a:has-text("data"), a:has-text("responses")'); - - if (await dataLinks.count() > 0) { + const dataLinks = page.locator( + 'a:has-text("submissions"), a:has-text("data"), a:has-text("responses")', + ); + + if ((await dataLinks.count()) > 0) { await dataLinks.first().click(); - + // Look for pagination controls const paginationElements = [ '[class*="pagination"]', 'button:has-text("next")', 'button:has-text("previous")', - '[aria-label*="page"]' + '[aria-label*="page"]', ]; - + for (const selector of paginationElements) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log('Pagination controls found'); - + // Test pagination const nextButton = page.locator('button:has-text("next")'); - if (await nextButton.count() > 0) { + if ((await nextButton.count()) > 0) { await nextButton.click(); - + // Verify page change (tests API pagination) await page.waitForLoadState('networkidle'); console.log('Pagination working correctly'); @@ -203,78 +211,77 @@ test.describe('Form Submission Flow', () => { test.describe('Real-time Features', () => { test('should handle concurrent form submissions', async ({ browser }) => { // This tests database concurrency and race conditions - + // Create multiple browser contexts (simulating multiple users) const context1 = await browser.newContext(); const context2 = await browser.newContext(); - + const page1 = await context1.newPage(); const page2 = await context2.newPage(); - + await page1.goto('/'); await page2.goto('/'); - + // Find forms on both pages const forms1 = page1.locator('form'); const forms2 = page2.locator('form'); - - if (await forms1.count() > 0 && await forms2.count() > 0) { + + if ((await forms1.count()) > 0 && (await forms2.count()) > 0) { // Fill and submit simultaneously const fillAndSubmit = async (page: any, userNum: number) => { const form = page.locator('form').first(); const inputs = form.locator('input[type="text"], textarea'); - - if (await inputs.count() > 0) { + + if ((await inputs.count()) > 0) { await inputs.first().fill(`User ${userNum} concurrent test`); - + const submitButton = form.getByRole('button', { name: /submit/i }); - if (await submitButton.count() > 0) { + if ((await submitButton.count()) > 0) { await submitButton.click(); } } }; - + // Submit from both users simultaneously - await Promise.all([ - fillAndSubmit(page1, 1), - fillAndSubmit(page2, 2) - ]); - + await Promise.all([fillAndSubmit(page1, 1), fillAndSubmit(page2, 2)]); + // Verify both submissions succeeded await expect(page1.locator('text*="success"')).toBeVisible({ timeout: 10000 }); await expect(page2.locator('text*="success"')).toBeVisible({ timeout: 10000 }); } - + await context1.close(); await context2.close(); }); test('should update data in real-time', async ({ page }) => { // This tests WebSocket connections or polling for real-time updates - + await page.goto('/'); - + // Mock real-time update await page.evaluate(() => { // Simulate receiving real-time data - window.dispatchEvent(new CustomEvent('realtime-update', { - detail: { type: 'new_submission', data: { id: '123' } } - })); + window.dispatchEvent( + new CustomEvent('realtime-update', { + detail: { type: 'new_submission', data: { id: '123' } }, + }), + ); }); - + // Wait for UI to update await page.waitForTimeout(1000); - + // Check if real-time update was handled const realTimeIndicators = [ '[class*="live"]', '[class*="real-time"]', 'text*="new"', - '[data-testid*="update"]' + '[data-testid*="update"]', ]; - + for (const selector of realTimeIndicators) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log('Real-time update handling found'); break; } @@ -285,28 +292,28 @@ test.describe('Real-time Features', () => { test.describe('Performance Tests', () => { test('should load large forms efficiently', async ({ page }) => { // This tests database query performance and frontend rendering - + const startTime = Date.now(); await page.goto('/'); - + // Wait for page to fully load await page.waitForLoadState('networkidle'); - + const loadTime = Date.now() - startTime; console.log(`Page load time: ${loadTime}ms`); - + // Performance should be under 3 seconds expect(loadTime).toBeLessThan(3000); - + // Check for performance optimization techniques const performanceElements = [ - '[loading="lazy"]', // Lazy loading - '[class*="skeleton"]', // Skeleton loading - '[class*="virtual"]' // Virtualization + '[loading="lazy"]', // Lazy loading + '[class*="skeleton"]', // Skeleton loading + '[class*="virtual"]', // Virtualization ]; - + for (const selector of performanceElements) { - if (await page.locator(selector).count() > 0) { + if ((await page.locator(selector).count()) > 0) { console.log(`Performance optimization found: ${selector}`); } } @@ -314,20 +321,20 @@ test.describe('Performance Tests', () => { test('should handle network timeouts gracefully', async ({ page }) => { // This tests timeout handling and retry logic - + // Mock slow network await page.route('**/api/**', async (route) => { - await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second delay + await new Promise((resolve) => setTimeout(resolve, 5000)); // 5 second delay route.continue(); }); - + const startTime = Date.now(); await page.goto('/', { timeout: 30000 }); - + const loadTime = Date.now() - startTime; console.log(`Slow network load time: ${loadTime}ms`); - + // Should still load eventually await expect(page.locator('body')).toBeVisible(); }); -}); \ No newline at end of file +});