240 lines
7.4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|