NeahStable/app/api/twenty-crm/tasks/route.ts
2026-01-15 22:33:49 +01:00

240 lines
7.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/options";
import { logger } from "@/lib/logger";
interface TwentyTask {
id: string;
title: string;
body?: string;
dueAt?: string;
completedAt?: string;
type?: string;
assigneeId?: string;
assignee?: {
id: string;
firstName?: string;
lastName?: string;
email?: string;
};
}
/**
* Fetch tasks from Twenty CRM using GraphQL API
*/
async function fetchTwentyTasks(): Promise<TwentyTask[]> {
try {
if (!process.env.TWENTY_CRM_API_URL) {
logger.error('[TWENTY_CRM_TASKS] TWENTY_CRM_API_URL is not set in environment variables');
return [];
}
if (!process.env.TWENTY_CRM_API_KEY) {
logger.error('[TWENTY_CRM_TASKS] TWENTY_CRM_API_KEY is not set in environment variables');
return [];
}
const apiUrl = process.env.TWENTY_CRM_API_URL.endsWith('/graphql')
? process.env.TWENTY_CRM_API_URL
: `${process.env.TWENTY_CRM_API_URL}/graphql`;
// Calculate today's date at midnight for filtering overdue tasks
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayISO = today.toISOString();
// GraphQL query to fetch tasks from Twenty CRM
// Twenty CRM uses different query structures - trying tasks query first
// If this doesn't work, we may need to use REST API or check the actual schema
const query = `
query GetOverdueTasks {
tasks(
where: {
completedAt: { is: NULL }
dueAt: { lt: "${todayISO}" }
}
orderBy: { dueAt: AscNullsLast }
) {
edges {
node {
id
title
body
dueAt
completedAt
assigneeId
assignee {
id
firstName
lastName
email
}
}
}
}
}
`;
logger.debug('[TWENTY_CRM_TASKS] Fetching tasks from Twenty CRM', {
apiUrl: apiUrl.replace(/\/graphql$/, ''), // Log without /graphql for security
});
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.TWENTY_CRM_API_KEY}`,
},
body: JSON.stringify({ query }),
});
const responseText = await response.text();
if (!response.ok) {
logger.error('[TWENTY_CRM_TASKS] Failed to fetch tasks from Twenty CRM', {
status: response.status,
statusText: response.statusText,
response: responseText.substring(0, 500), // Log first 500 chars
});
return [];
}
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
logger.error('[TWENTY_CRM_TASKS] Failed to parse Twenty CRM response', {
error: e instanceof Error ? e.message : String(e),
response: responseText.substring(0, 500),
});
return [];
}
// Check for GraphQL errors
if (data.errors) {
logger.error('[TWENTY_CRM_TASKS] GraphQL errors from Twenty CRM', {
errors: data.errors,
});
return [];
}
// Try different possible response structures
// Twenty CRM may use different field names depending on version
let activitiesData = null;
if (data.data?.tasks?.edges) {
activitiesData = data.data.tasks.edges;
} else if (data.data?.activities?.edges) {
activitiesData = data.data.activities.edges;
} else if (data.data?.findManyTasks?.edges) {
activitiesData = data.data.findManyTasks.edges;
} else if (data.data?.findManyActivities?.edges) {
activitiesData = data.data.findManyActivities.edges;
} else if (Array.isArray(data.data?.tasks)) {
// Direct array response
activitiesData = data.data.tasks.map((node: any) => ({ node }));
} else if (Array.isArray(data.data?.activities)) {
// Direct array response
activitiesData = data.data.activities.map((node: any) => ({ node }));
} else if (Array.isArray(data.data)) {
// Root level array
activitiesData = data.data.map((node: any) => ({ node }));
}
if (!activitiesData) {
logger.warn('[TWENTY_CRM_TASKS] Unexpected response format from Twenty CRM', {
dataKeys: Object.keys(data.data || {}),
fullData: JSON.stringify(data.data).substring(0, 1000),
});
return [];
}
// Transform Twenty CRM tasks to match our Task interface
const tasks: TwentyTask[] = activitiesData.map((edge: any) => {
const node = edge.node || edge; // Handle both edge.node and direct node
return {
id: node.id,
title: node.title || 'Untitled Task',
body: node.body || null,
dueAt: node.dueAt || null,
completedAt: node.completedAt || null,
type: node.type || 'Task',
assigneeId: node.assigneeId || null,
assignee: node.assignee ? {
id: node.assignee.id,
firstName: node.assignee.firstName || null,
lastName: node.assignee.lastName || null,
email: node.assignee.email || null,
} : null,
};
});
logger.debug('[TWENTY_CRM_TASKS] Successfully fetched tasks from Twenty CRM', {
count: tasks.length,
});
return tasks;
} catch (error) {
logger.error('[TWENTY_CRM_TASKS] Error fetching tasks from Twenty CRM', {
error: error instanceof Error ? error.message : String(error),
});
return [];
}
}
export async function GET(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Check for force refresh parameter
const url = new URL(request.url);
const forceRefresh = url.searchParams.get('refresh') === 'true';
logger.debug('[TWENTY_CRM_TASKS] Fetching tasks for user', {
emailHash: Buffer.from(session.user.email.toLowerCase()).toString('base64').slice(0, 12),
forceRefresh,
});
const tasks = await fetchTwentyTasks();
// Transform to match Leantime task format for consistency
const transformedTasks = tasks.map((task) => ({
id: `twenty-${task.id}`, // Prefix to avoid conflicts with Leantime IDs
headline: task.title,
description: task.body || null,
dateToFinish: task.dueAt || null,
projectName: 'Twenty CRM',
projectId: 0,
status: task.completedAt ? 5 : 1, // 5 = Done, 1 = New
editorId: task.assigneeId || null,
editorFirstname: task.assignee?.firstName || null,
editorLastname: task.assignee?.lastName || null,
authorFirstname: null,
authorLastname: null,
milestoneHeadline: null,
editTo: null,
editFrom: null,
type: 'twenty-crm',
dependingTicketId: null,
source: 'twenty-crm', // Add source identifier
url: process.env.TWENTY_CRM_URL ? `${process.env.TWENTY_CRM_URL}/object/activity/${task.id}` : null,
}));
logger.debug('[TWENTY_CRM_TASKS] Transformed tasks', {
count: transformedTasks.length,
});
return NextResponse.json(transformedTasks);
} catch (error) {
logger.error('[TWENTY_CRM_TASKS] Error in tasks route', {
error: error instanceof Error ? error.message : String(error),
});
return NextResponse.json(
{ error: "Failed to fetch tasks" },
{ status: 500 }
);
}
}