fix(platform-service): fix OFFSET/LIMIT pairing, survey analytics productId, unused imports
- broadcasts/repository: fix OFFSET/LIMIT must be paired (Cosmos SQL requires both) - broadcasts/repository: remove unused TargetingContext import - surveys/repository: fix OFFSET/LIMIT in listSurveys + listResponsesForSurvey - surveys/repository: fix getSurveyAnalytics to accept and pass productId - surveys/repository: fix isComplete boolean param cast for Cosmos SDK types - surveys/routes: fix parseInt safety for limit/offset query params (NaN guard) - surveys/routes: pass productId to getSurveyAnalytics call
This commit is contained in:
parent
652a8e5d15
commit
4071429871
@ -9,7 +9,6 @@ import {
|
|||||||
BroadcastDelivery,
|
BroadcastDelivery,
|
||||||
InAppMessage,
|
InAppMessage,
|
||||||
BroadcastRead,
|
BroadcastRead,
|
||||||
TargetingContext,
|
|
||||||
type BroadcastStatus,
|
type BroadcastStatus,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
@ -66,9 +65,7 @@ export async function listBroadcasts(
|
|||||||
query += ` LIMIT ${options.limit}`;
|
query += ` LIMIT ${options.limit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resources } = await container.items
|
const { resources } = await container.items.query<Broadcast>({ query, parameters }).fetchAll();
|
||||||
.query<Broadcast>({ query, parameters })
|
|
||||||
.fetchAll();
|
|
||||||
|
|
||||||
return { broadcasts: resources, total };
|
return { broadcasts: resources, total };
|
||||||
}
|
}
|
||||||
@ -212,8 +209,9 @@ export async function listDeliveriesForBroadcast(
|
|||||||
.fetchAll();
|
.fetchAll();
|
||||||
const total = countResult[0] ?? 0;
|
const total = countResult[0] ?? 0;
|
||||||
|
|
||||||
if (options?.offset) query += ` OFFSET ${options.offset}`;
|
if (options?.limit) {
|
||||||
if (options?.limit) query += ` LIMIT ${options.limit}`;
|
query += ` OFFSET ${options.offset ?? 0} LIMIT ${options.limit}`;
|
||||||
|
}
|
||||||
|
|
||||||
const { resources } = await container.items
|
const { resources } = await container.items
|
||||||
.query<BroadcastDelivery>({ query, parameters })
|
.query<BroadcastDelivery>({ query, parameters })
|
||||||
@ -252,14 +250,12 @@ export async function getInAppMessagesForUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only show non-expired messages
|
// Only show non-expired messages
|
||||||
query += " AND (c.expiresAt = null OR c.expiresAt > @now)";
|
query += ' AND (c.expiresAt = null OR c.expiresAt > @now)';
|
||||||
parameters.push({ name: '@now', value: new Date().toISOString() });
|
parameters.push({ name: '@now', value: new Date().toISOString() });
|
||||||
|
|
||||||
query += ' ORDER BY c.createdAt DESC';
|
query += ' ORDER BY c.createdAt DESC';
|
||||||
|
|
||||||
const { resources } = await container.items
|
const { resources } = await container.items.query<InAppMessage>({ query, parameters }).fetchAll();
|
||||||
.query<InAppMessage>({ query, parameters })
|
|
||||||
.fetchAll();
|
|
||||||
|
|
||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
@ -302,7 +298,9 @@ export async function deleteExpiredInAppMessages(userId: string): Promise<number
|
|||||||
{ name: '@now', value: now },
|
{ name: '@now', value: now },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { resources } = await container.items.query<{ id: string }>({ query, parameters }).fetchAll();
|
const { resources } = await container.items
|
||||||
|
.query<{ id: string }>({ query, parameters })
|
||||||
|
.fetchAll();
|
||||||
|
|
||||||
let deleted = 0;
|
let deleted = 0;
|
||||||
for (const { id } of resources) {
|
for (const { id } of resources) {
|
||||||
|
|||||||
@ -56,8 +56,9 @@ export async function listSurveys(
|
|||||||
.fetchAll();
|
.fetchAll();
|
||||||
const total = countResult[0] ?? 0;
|
const total = countResult[0] ?? 0;
|
||||||
|
|
||||||
if (options?.offset) query += ` OFFSET ${options.offset}`;
|
if (options?.limit) {
|
||||||
if (options?.limit) query += ` LIMIT ${options.limit}`;
|
query += ` OFFSET ${options.offset ?? 0} LIMIT ${options.limit}`;
|
||||||
|
}
|
||||||
|
|
||||||
const { resources } = await container.items.query<Survey>({ query, parameters }).fetchAll();
|
const { resources } = await container.items.query<Survey>({ query, parameters }).fetchAll();
|
||||||
return { surveys: resources, total };
|
return { surveys: resources, total };
|
||||||
@ -164,7 +165,7 @@ export async function listResponsesForSurvey(
|
|||||||
|
|
||||||
if (options?.isComplete !== undefined) {
|
if (options?.isComplete !== undefined) {
|
||||||
query += ' AND c.isComplete = @isComplete';
|
query += ' AND c.isComplete = @isComplete';
|
||||||
parameters.push({ name: '@isComplete', value: options.isComplete.toString() });
|
parameters.push({ name: '@isComplete', value: options.isComplete as unknown as string });
|
||||||
}
|
}
|
||||||
|
|
||||||
query += ' ORDER BY c.createdAt DESC';
|
query += ' ORDER BY c.createdAt DESC';
|
||||||
@ -175,10 +176,13 @@ export async function listResponsesForSurvey(
|
|||||||
.fetchAll();
|
.fetchAll();
|
||||||
const total = countResult[0] ?? 0;
|
const total = countResult[0] ?? 0;
|
||||||
|
|
||||||
if (options?.offset) query += ` OFFSET ${options.offset}`;
|
if (options?.limit) {
|
||||||
if (options?.limit) query += ` LIMIT ${options.limit}`;
|
query += ` OFFSET ${options.offset ?? 0} LIMIT ${options.limit}`;
|
||||||
|
}
|
||||||
|
|
||||||
const { resources } = await container.items.query<SurveyResponse>({ query, parameters }).fetchAll();
|
const { resources } = await container.items
|
||||||
|
.query<SurveyResponse>({ query, parameters })
|
||||||
|
.fetchAll();
|
||||||
return { responses: resources, total };
|
return { responses: resources, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,8 +192,10 @@ export async function listRespondents(surveyId: string): Promise<string[]> {
|
|||||||
const query = 'SELECT c.userId FROM c WHERE c.surveyId = @surveyId AND c.isComplete = true';
|
const query = 'SELECT c.userId FROM c WHERE c.surveyId = @surveyId AND c.isComplete = true';
|
||||||
const parameters = [{ name: '@surveyId', value: surveyId }];
|
const parameters = [{ name: '@surveyId', value: surveyId }];
|
||||||
|
|
||||||
const { resources } = await container.items.query<{ userId: string }>({ query, parameters }).fetchAll();
|
const { resources } = await container.items
|
||||||
return resources.map((r) => r.userId);
|
.query<{ userId: string }>({ query, parameters })
|
||||||
|
.fetchAll();
|
||||||
|
return resources.map(r => r.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -300,7 +306,10 @@ export interface QuestionAnalytics {
|
|||||||
sampleResponses?: string[];
|
sampleResponses?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSurveyAnalytics(surveyId: string): Promise<{
|
export async function getSurveyAnalytics(
|
||||||
|
surveyId: string,
|
||||||
|
productId: string
|
||||||
|
): Promise<{
|
||||||
totalStarts: number;
|
totalStarts: number;
|
||||||
totalCompletions: number;
|
totalCompletions: number;
|
||||||
completionRate: number;
|
completionRate: number;
|
||||||
@ -310,21 +319,21 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
const { responses } = await listResponsesForSurvey(surveyId);
|
const { responses } = await listResponsesForSurvey(surveyId);
|
||||||
|
|
||||||
const totalStarts = responses.length;
|
const totalStarts = responses.length;
|
||||||
const completedResponses = responses.filter((r) => r.isComplete);
|
const completedResponses = responses.filter(r => r.isComplete);
|
||||||
const totalCompletions = completedResponses.length;
|
const totalCompletions = completedResponses.length;
|
||||||
|
|
||||||
// Calculate average time
|
// Calculate average time
|
||||||
const times = completedResponses
|
const times = completedResponses
|
||||||
.map((r) => {
|
.map(r => {
|
||||||
if (!r.completedAt || !r.startedAt) return 0;
|
if (!r.completedAt || !r.startedAt) return 0;
|
||||||
return (new Date(r.completedAt).getTime() - new Date(r.startedAt).getTime()) / 1000;
|
return (new Date(r.completedAt).getTime() - new Date(r.startedAt).getTime()) / 1000;
|
||||||
})
|
})
|
||||||
.filter((t) => t > 0);
|
.filter(t => t > 0);
|
||||||
|
|
||||||
const avgTimeSeconds = times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0;
|
const avgTimeSeconds = times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0;
|
||||||
|
|
||||||
// Get survey to access question definitions
|
// Get survey to access question definitions
|
||||||
const survey = await getSurvey(surveyId, ''); // productId not needed for this query
|
const survey = await getSurvey(surveyId, productId);
|
||||||
if (!survey) {
|
if (!survey) {
|
||||||
return {
|
return {
|
||||||
totalStarts,
|
totalStarts,
|
||||||
@ -336,7 +345,7 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Aggregate per-question analytics
|
// Aggregate per-question analytics
|
||||||
const questionAnalytics: QuestionAnalytics[] = survey.questions.map((q) => {
|
const questionAnalytics: QuestionAnalytics[] = survey.questions.map(q => {
|
||||||
const qa: QuestionAnalytics = {
|
const qa: QuestionAnalytics = {
|
||||||
questionId: q.id,
|
questionId: q.id,
|
||||||
questionType: q.type,
|
questionType: q.type,
|
||||||
@ -344,7 +353,7 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const answers = responses
|
const answers = responses
|
||||||
.map((r) => r.answers[q.id])
|
.map(r => r.answers[q.id])
|
||||||
.filter((a): a is QuestionAnswer => a !== undefined);
|
.filter((a): a is QuestionAnswer => a !== undefined);
|
||||||
|
|
||||||
qa.totalResponses = answers.length;
|
qa.totalResponses = answers.length;
|
||||||
@ -352,7 +361,7 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
// Type-specific aggregation
|
// Type-specific aggregation
|
||||||
if (q.type === 'single_choice' || q.type === 'dropdown') {
|
if (q.type === 'single_choice' || q.type === 'dropdown') {
|
||||||
qa.optionCounts = {};
|
qa.optionCounts = {};
|
||||||
answers.forEach((a) => {
|
answers.forEach(a => {
|
||||||
if (a.type === 'single_choice') {
|
if (a.type === 'single_choice') {
|
||||||
qa.optionCounts![a.optionId] = (qa.optionCounts![a.optionId] || 0) + 1;
|
qa.optionCounts![a.optionId] = (qa.optionCounts![a.optionId] || 0) + 1;
|
||||||
}
|
}
|
||||||
@ -361,9 +370,9 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
|
|
||||||
if (q.type === 'multiple_choice') {
|
if (q.type === 'multiple_choice') {
|
||||||
qa.optionCounts = {};
|
qa.optionCounts = {};
|
||||||
answers.forEach((a) => {
|
answers.forEach(a => {
|
||||||
if (a.type === 'multiple_choice') {
|
if (a.type === 'multiple_choice') {
|
||||||
a.optionIds.forEach((id) => {
|
a.optionIds.forEach(id => {
|
||||||
qa.optionCounts![id] = (qa.optionCounts![id] || 0) + 1;
|
qa.optionCounts![id] = (qa.optionCounts![id] || 0) + 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -375,7 +384,7 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
.filter((a): a is { type: 'rating' | 'nps'; value: number } =>
|
.filter((a): a is { type: 'rating' | 'nps'; value: number } =>
|
||||||
['rating', 'nps'].includes(a.type)
|
['rating', 'nps'].includes(a.type)
|
||||||
)
|
)
|
||||||
.map((a) => a.value);
|
.map(a => a.value);
|
||||||
|
|
||||||
if (values.length > 0) {
|
if (values.length > 0) {
|
||||||
qa.average = values.reduce((a, b) => a + b, 0) / values.length;
|
qa.average = values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
@ -384,7 +393,7 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
|
|
||||||
// Build distribution
|
// Build distribution
|
||||||
qa.distribution = {};
|
qa.distribution = {};
|
||||||
values.forEach((v) => {
|
values.forEach(v => {
|
||||||
qa.distribution![v] = (qa.distribution![v] || 0) + 1;
|
qa.distribution![v] = (qa.distribution![v] || 0) + 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -393,13 +402,13 @@ export async function getSurveyAnalytics(surveyId: string): Promise<{
|
|||||||
if (q.type === 'text_short' || q.type === 'text_long') {
|
if (q.type === 'text_short' || q.type === 'text_long') {
|
||||||
qa.sampleResponses = answers
|
qa.sampleResponses = answers
|
||||||
.filter((a): a is { type: 'text'; value: string } => a.type === 'text')
|
.filter((a): a is { type: 'text'; value: string } => a.type === 'text')
|
||||||
.map((a) => a.value)
|
.map(a => a.value)
|
||||||
.slice(0, 10); // Sample first 10
|
.slice(0, 10); // Sample first 10
|
||||||
}
|
}
|
||||||
|
|
||||||
if (q.type === 'ranking') {
|
if (q.type === 'ranking') {
|
||||||
qa.optionCounts = {};
|
qa.optionCounts = {};
|
||||||
answers.forEach((a) => {
|
answers.forEach(a => {
|
||||||
if (a.type === 'ranking') {
|
if (a.type === 'ranking') {
|
||||||
// Count first-choice rankings
|
// Count first-choice rankings
|
||||||
const firstChoice = a.rankedOptionIds[0];
|
const firstChoice = a.rankedOptionIds[0];
|
||||||
@ -430,24 +439,23 @@ export function exportResponsesToCSV(responses: SurveyResponse[]): string {
|
|||||||
if (responses.length === 0) return '';
|
if (responses.length === 0) return '';
|
||||||
|
|
||||||
// Get all unique question IDs
|
// Get all unique question IDs
|
||||||
const questionIds = Array.from(
|
const questionIds = Array.from(new Set(responses.flatMap(r => Object.keys(r.answers))));
|
||||||
new Set(responses.flatMap((r) => Object.keys(r.answers)))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build headers
|
// Build headers
|
||||||
const headers = ['responseId', 'userId', 'startedAt', 'completedAt', 'isComplete', ...questionIds];
|
const headers = [
|
||||||
|
'responseId',
|
||||||
|
'userId',
|
||||||
|
'startedAt',
|
||||||
|
'completedAt',
|
||||||
|
'isComplete',
|
||||||
|
...questionIds,
|
||||||
|
];
|
||||||
|
|
||||||
// Build rows
|
// Build rows
|
||||||
const rows = responses.map((r) => {
|
const rows = responses.map(r => {
|
||||||
const base = [
|
const base = [r.id, r.userId, r.startedAt, r.completedAt ?? '', r.isComplete ? 'yes' : 'no'];
|
||||||
r.id,
|
|
||||||
r.userId,
|
|
||||||
r.startedAt,
|
|
||||||
r.completedAt ?? '',
|
|
||||||
r.isComplete ? 'yes' : 'no',
|
|
||||||
];
|
|
||||||
|
|
||||||
const answers = questionIds.map((qid) => {
|
const answers = questionIds.map(qid => {
|
||||||
const ans = r.answers[qid];
|
const ans = r.answers[qid];
|
||||||
if (!ans) return '';
|
if (!ans) return '';
|
||||||
|
|
||||||
@ -475,5 +483,5 @@ export function exportResponsesToCSV(responses: SurveyResponse[]): string {
|
|||||||
|
|
||||||
// Combine
|
// Combine
|
||||||
const escape = (s: string) => (s.includes(',') || s.includes('"') ? `"${s}"` : s);
|
const escape = (s: string) => (s.includes(',') || s.includes('"') ? `"${s}"` : s);
|
||||||
return [headers.join(','), ...rows.map((r) => r.map(escape).join(','))].join('\n');
|
return [headers.join(','), ...rows.map(r => r.map(escape).join(','))].join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ function requireAdmin(req: { jwtPayload?: { sub: string; role?: string } }): str
|
|||||||
|
|
||||||
async function adminRoutes(app: FastifyInstance): Promise<void> {
|
async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||||
// List all surveys
|
// List all surveys
|
||||||
app.get('/', async (req) => {
|
app.get('/', async req => {
|
||||||
const adminId = requireAdmin(req);
|
const adminId = requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add computed completion rate
|
// Add computed completion rate
|
||||||
const surveysWithRate = surveys.map((s) => ({
|
const surveysWithRate = surveys.map(s => ({
|
||||||
...s,
|
...s,
|
||||||
computedCompletionRate: computeCompletionRate(s.metrics),
|
computedCompletionRate: computeCompletionRate(s.metrics),
|
||||||
}));
|
}));
|
||||||
@ -67,7 +67,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get single survey with questions
|
// Get single survey with questions
|
||||||
app.get<{ Params: { id: string } }>('/:id', async (req) => {
|
app.get<{ Params: { id: string } }>('/:id', async req => {
|
||||||
requireAdmin(req);
|
requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -112,7 +112,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update survey
|
// Update survey
|
||||||
app.put<{ Params: { id: string } }>('/:id', async (req) => {
|
app.put<{ Params: { id: string } }>('/:id', async req => {
|
||||||
const adminId = requireAdmin(req);
|
const adminId = requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -148,7 +148,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Duplicate survey
|
// Duplicate survey
|
||||||
app.post<{ Params: { id: string } }>('/:id/duplicate', async (req) => {
|
app.post<{ Params: { id: string } }>('/:id/duplicate', async req => {
|
||||||
const adminId = requireAdmin(req);
|
const adminId = requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -181,7 +181,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Pause survey
|
// Pause survey
|
||||||
app.post<{ Params: { id: string } }>('/:id/pause', async (req) => {
|
app.post<{ Params: { id: string } }>('/:id/pause', async req => {
|
||||||
const adminId = requireAdmin(req);
|
const adminId = requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -200,31 +200,41 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// List responses
|
// List responses
|
||||||
app.get<{ Params: { id: string } }>('/:id/responses', async (req) => {
|
app.get<{ Params: { id: string } }>('/:id/responses', async req => {
|
||||||
requireAdmin(req);
|
requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const { isComplete, limit = 50, offset = 0 } = req.query as {
|
const {
|
||||||
|
isComplete,
|
||||||
|
limit: limitStr,
|
||||||
|
offset: offsetStr,
|
||||||
|
} = req.query as {
|
||||||
isComplete?: string;
|
isComplete?: string;
|
||||||
limit?: string;
|
limit?: string;
|
||||||
offset?: string;
|
offset?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parsedLimit = limitStr ? parseInt(limitStr, 10) : 50;
|
||||||
|
const parsedOffset = offsetStr ? parseInt(offsetStr, 10) : 0;
|
||||||
|
const safeLimit =
|
||||||
|
Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 200) : 50;
|
||||||
|
const safeOffset = Number.isFinite(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
|
||||||
|
|
||||||
const survey = await repo.getSurvey(id, productId);
|
const survey = await repo.getSurvey(id, productId);
|
||||||
if (!survey) throw new NotFoundError('Survey not found');
|
if (!survey) throw new NotFoundError('Survey not found');
|
||||||
|
|
||||||
const { responses, total } = await repo.listResponsesForSurvey(id, {
|
const { responses, total } = await repo.listResponsesForSurvey(id, {
|
||||||
isComplete: isComplete === 'true' ? true : isComplete === 'false' ? false : undefined,
|
isComplete: isComplete === 'true' ? true : isComplete === 'false' ? false : undefined,
|
||||||
limit: parseInt(String(limit), 10),
|
limit: safeLimit,
|
||||||
offset: parseInt(String(offset), 10),
|
offset: safeOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { responses, total };
|
return { responses, total };
|
||||||
});
|
});
|
||||||
|
|
||||||
// List respondents (just user IDs)
|
// List respondents (just user IDs)
|
||||||
app.get<{ Params: { id: string } }>('/:id/respondents', async (req) => {
|
app.get<{ Params: { id: string } }>('/:id/respondents', async req => {
|
||||||
requireAdmin(req);
|
requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -237,7 +247,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get analytics
|
// Get analytics
|
||||||
app.get<{ Params: { id: string } }>('/:id/analytics', async (req) => {
|
app.get<{ Params: { id: string } }>('/:id/analytics', async req => {
|
||||||
requireAdmin(req);
|
requireAdmin(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -245,7 +255,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const survey = await repo.getSurvey(id, productId);
|
const survey = await repo.getSurvey(id, productId);
|
||||||
if (!survey) throw new NotFoundError('Survey not found');
|
if (!survey) throw new NotFoundError('Survey not found');
|
||||||
|
|
||||||
const analytics = await repo.getSurveyAnalytics(id);
|
const analytics = await repo.getSurveyAnalytics(id, productId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
surveyId: id,
|
surveyId: id,
|
||||||
@ -287,7 +297,7 @@ async function adminRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
|
|
||||||
async function publicRoutes(app: FastifyInstance): Promise<void> {
|
async function publicRoutes(app: FastifyInstance): Promise<void> {
|
||||||
// Get active survey for user (rate limited in server.ts)
|
// Get active survey for user (rate limited in server.ts)
|
||||||
app.get('/active', async (req) => {
|
app.get('/active', async req => {
|
||||||
const userId = requireAuth(req);
|
const userId = requireAuth(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
|
|
||||||
@ -312,7 +322,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
osVersion: headers['x-os-version'] ?? '0.0.0',
|
osVersion: headers['x-os-version'] ?? '0.0.0',
|
||||||
countryCode: headers['x-country-code'],
|
countryCode: headers['x-country-code'],
|
||||||
regionCode: headers['x-region-code'],
|
regionCode: headers['x-region-code'],
|
||||||
userSegments: (segments as string[]) as import('../broadcasts/types.js').UserSegment[],
|
userSegments: segments as string[] as import('../broadcasts/types.js').UserSegment[],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find all active surveys
|
// Find all active surveys
|
||||||
@ -356,7 +366,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Start survey session
|
// Start survey session
|
||||||
app.post<{ Params: { id: string } }>('/:id/start', async (req) => {
|
app.post<{ Params: { id: string } }>('/:id/start', async req => {
|
||||||
const userId = requireAuth(req);
|
const userId = requireAuth(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -424,7 +434,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Submit answer
|
// Submit answer
|
||||||
app.post<{ Params: { id: string } }>('/:id/response', async (req) => {
|
app.post<{ Params: { id: string } }>('/:id/response', async req => {
|
||||||
const userId = requireAuth(req);
|
const userId = requireAuth(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -435,7 +445,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!survey) throw new NotFoundError('Survey not found');
|
if (!survey) throw new NotFoundError('Survey not found');
|
||||||
|
|
||||||
// Find the question
|
// Find the question
|
||||||
const question = survey.questions.find((q) => q.id === questionId);
|
const question = survey.questions.find(q => q.id === questionId);
|
||||||
if (!question) throw new NotFoundError('Question not found');
|
if (!question) throw new NotFoundError('Question not found');
|
||||||
|
|
||||||
// Validate answer type matches question type
|
// Validate answer type matches question type
|
||||||
@ -454,7 +464,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const updatedAnswers = { ...response.answers, [questionId]: answer };
|
const updatedAnswers = { ...response.answers, [questionId]: answer };
|
||||||
|
|
||||||
// Find next question index (skipping conditional questions)
|
// Find next question index (skipping conditional questions)
|
||||||
let nextIndex = response.currentQuestionIndex + 1;
|
const nextIndex = response.currentQuestionIndex + 1;
|
||||||
|
|
||||||
response = await repo.updateResponse(id, userId, {
|
response = await repo.updateResponse(id, userId, {
|
||||||
answers: updatedAnswers,
|
answers: updatedAnswers,
|
||||||
@ -471,7 +481,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Complete survey
|
// Complete survey
|
||||||
app.post<{ Params: { id: string } }>('/:id/complete', async (req) => {
|
app.post<{ Params: { id: string } }>('/:id/complete', async req => {
|
||||||
const userId = requireAuth(req);
|
const userId = requireAuth(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@ -485,12 +495,10 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// Check for required questions
|
// Check for required questions
|
||||||
const unansweredRequired = survey.questions.filter(
|
const unansweredRequired = survey.questions.filter(q => q.required && !response.answers[q.id]);
|
||||||
(q) => q.required && !response.answers[q.id]
|
|
||||||
);
|
|
||||||
if (unansweredRequired.length > 0) {
|
if (unansweredRequired.length > 0) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
`Required questions unanswered: ${unansweredRequired.map((q) => q.id).join(', ')}`
|
`Required questions unanswered: ${unansweredRequired.map(q => q.id).join(', ')}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,7 +562,7 @@ async function publicRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Dismiss survey
|
// Dismiss survey
|
||||||
app.post<{ Params: { id: string } }>('/:id/dismiss', async (req) => {
|
app.post<{ Params: { id: string } }>('/:id/dismiss', async req => {
|
||||||
const userId = requireAuth(req);
|
const userId = requireAuth(req);
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user