feat: show recent exports

This commit is contained in:
Marc Seitz
2025-07-29 12:46:43 +02:00
parent a614650fea
commit de733d53bc
6 changed files with 602 additions and 136 deletions

View File

@@ -3,6 +3,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useSession } from "next-auth/react";
import { toast } from "sonner";
import { ExportJob } from "@/lib/redis-job-store";
import { Button } from "../ui/button";
interface ExportStatus {
status: string;
progress?: string;
@@ -31,6 +35,9 @@ export function ExportVisitsModal({
const [exportStatus, setExportStatus] = useState<ExportStatus | null>(null);
const [showModal, setShowModal] = useState(false);
const [viewCount, setViewCount] = useState<number | null>(null);
const [existingExports, setExistingExports] = useState<ExportJob[]>([]);
const [showNewExport, setShowNewExport] = useState(false);
const [loading, setLoading] = useState(true);
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
const exportStartedRef = useRef<boolean>(false);
@@ -43,18 +50,45 @@ export function ExportVisitsModal({
};
}, []);
const startExport = useCallback(async () => {
// Fetch existing exports when modal opens
useEffect(() => {
const fetchExistingExports = async () => {
try {
setLoading(true);
setShowModal(true);
const endpoint = groupId
? `/api/teams/${teamId}/datarooms/${dataroomId}/groups/${groupId}/export-visits`
: `/api/teams/${teamId}/datarooms/${dataroomId}/export-visits`;
const response = await fetch(endpoint, { method: "GET" });
if (response.ok) {
const exports = await response.json();
setExistingExports(exports);
} else {
console.error("Failed to fetch existing exports");
}
} catch (error) {
console.error("Error fetching existing exports:", error);
} finally {
setLoading(false);
}
};
fetchExistingExports();
}, [teamId, dataroomId, groupId]);
const startNewExport = useCallback(async () => {
// Prevent double triggering
if (exportStartedRef.current) {
console.warn("Export already started, skipping duplicate request");
return;
}
exportStartedRef.current = true;
setShowNewExport(true);
try {
// Show modal immediately
setShowModal(true);
// Get view count first
try {
const viewCountResponse = await fetch(
@@ -72,10 +106,11 @@ export function ExportVisitsModal({
}
// Trigger the background export job
const response = await fetch(
`/api/teams/${teamId}/datarooms/${dataroomId}${groupId ? `/groups/${groupId}` : ""}/export-visits`,
{ method: "POST" },
);
const endpoint = groupId
? `/api/teams/${teamId}/datarooms/${dataroomId}/groups/${groupId}/export-visits`
: `/api/teams/${teamId}/datarooms/${dataroomId}/export-visits`;
const response = await fetch(endpoint, { method: "POST" });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -174,11 +209,6 @@ export function ExportVisitsModal({
}
}, [teamId, dataroomId, groupId, dataroomName, groupName]);
// Start export immediately when component mounts
useEffect(() => {
startExport();
}, [startExport]);
// Send export via email
const sendExportEmail = async () => {
if (!exportStatus?.exportId || !session?.user?.email) return;
@@ -224,6 +254,49 @@ export function ExportVisitsModal({
}
};
// Download existing export
const downloadExport = async (exportId: string, resourceName: string) => {
try {
const downloadUrl = `/api/teams/${teamId}/export-jobs/${exportId}?download=true`;
const link = window.document.createElement("a");
link.href = downloadUrl;
link.setAttribute(
"download",
`${resourceName || dataroomName}_${groupName ? `${groupName}_` : ""}visits_${new Date().toISOString().split("T")[0]}.csv`,
);
link.rel = "noopener noreferrer";
link.style.display = "none";
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
} catch (error) {
console.error("Download failed:", error);
toast.error("Failed to download export");
}
};
// Format date for display
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
// Get status badge color
const getStatusColor = (status: string) => {
switch (status) {
case "COMPLETED":
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
case "PROCESSING":
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
case "PENDING":
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
case "FAILED":
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300";
default:
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
}
};
// Handle close - cleanup and call parent onClose
const handleClose = () => {
if (pollIntervalRef.current) {
@@ -232,6 +305,10 @@ export function ExportVisitsModal({
}
setShowModal(false);
setExportStatus(null);
setShowNewExport(false);
setExistingExports([]); // Reset existing exports
setViewCount(null); // Reset view count
setLoading(true); // Reset loading state
exportStartedRef.current = false; // Reset for potential reuse
onClose();
};
@@ -247,7 +324,7 @@ export function ExportVisitsModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="mx-4 w-full max-w-md rounded-lg bg-white p-6">
<div className="mx-4 w-full max-w-md rounded-lg bg-white p-6 dark:bg-gray-800">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">Export Visits</h3>
<button
@@ -270,60 +347,141 @@ export function ExportVisitsModal({
</button>
</div>
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-muted-foreground"></div>
<span className="text-sm text-gray-600">
{exportStatus?.progress || "Processing export..."}
</span>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-primary"></div>
</div>
<div className="text-sm text-gray-600">
Exporting visits for: {displayName}
</div>
{viewCount !== null && (
<div className="text-sm text-gray-600">
Found {viewCount} visit{viewCount !== 1 ? "s" : ""} to export
) : showNewExport ? (
// Show export progress
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-muted-foreground"></div>
<span className="text-sm text-gray-600">
{exportStatus?.progress || "Processing export..."}
</span>
</div>
)}
{viewCount !== null && viewCount > 10 && session?.user?.email && (
<div className="rounded-md bg-gray-50 p-3 text-sm dark:bg-gray-900">
<p className="mb-2 font-medium text-muted-foreground">
Large export detected ({viewCount} visits)
</p>
<p className="mb-3 text-muted-foreground">
This export may take several minutes. We recommend getting it
emailed to you when ready.
</p>
<div className="text-sm text-gray-600">
Exporting visits for: {displayName}
</div>
{viewCount !== null && (
<div className="text-sm text-gray-600">
Found {viewCount} visit{viewCount !== 1 ? "s" : ""} to export
</div>
)}
{viewCount !== null && viewCount > 10 && session?.user?.email && (
<div className="rounded-md bg-gray-50 p-3 text-sm dark:bg-gray-900">
<p className="mb-2 font-medium text-muted-foreground">
Large export detected ({viewCount} visits)
</p>
<p className="mb-3 text-muted-foreground">
This export may take several minutes. We recommend getting it
emailed to you when ready.
</p>
<button
onClick={sendExportEmail}
className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/80"
>
Email to {session.user.email}
</button>
</div>
)}
{(!viewCount || viewCount <= 10) && (
<div className="text-sm text-gray-500">
Your export will be ready shortly...
</div>
)}
{/* Cancel button - only show if export is in progress */}
{exportStatus?.exportId && (
<div className="flex gap-2 px-3">
<Button
onClick={cancelExport}
variant="outline"
size="sm"
className="flex-1 rounded-md border border-red-300 bg-white px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-50 dark:border-red-600 dark:bg-gray-900 dark:text-red-400 dark:hover:bg-red-950"
>
Cancel Export
</Button>
</div>
)}
</div>
) : (
// Show existing exports and new export option
<div className="space-y-4">
<div className="text-sm text-gray-600">
Exports for: {displayName}
</div>
{existingExports.length > 0 ? (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
Recent Exports
</h4>
<div className="max-h-48 space-y-2 overflow-y-auto">
{existingExports.map((exportJob) => (
<div
key={exportJob.id}
className="flex items-center justify-between rounded-md border border-gray-200 p-3 dark:border-gray-700"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span
className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${getStatusColor(exportJob.status)}`}
>
{exportJob.status}
</span>
<span className="text-xs text-gray-500">
{formatDate(exportJob.createdAt)}
</span>
</div>
{exportJob.groupId && (
<div className="mt-1 text-xs text-gray-500">
Group: {exportJob.groupId}
</div>
)}
{exportJob.error && (
<p className="mt-1 text-xs text-red-600">
{exportJob.error}
</p>
)}
</div>
{exportJob.status === "COMPLETED" && exportJob.result && (
<button
onClick={() =>
downloadExport(
exportJob.id,
exportJob.resourceName || dataroomName,
)
}
className="ml-2 rounded-md bg-primary px-3 py-1 text-xs text-primary-foreground transition-colors hover:bg-primary/80"
>
Download
</button>
)}
</div>
))}
</div>
</div>
) : (
<div className="text-center text-sm text-gray-500">
No previous exports found
</div>
)}
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
<button
onClick={sendExportEmail}
onClick={startNewExport}
className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/80"
>
Email to {session.user.email}
Start New Export
</button>
</div>
)}
{(!viewCount || viewCount <= 10) && (
<div className="text-sm text-gray-500">
Your export will be ready shortly...
</div>
)}
{/* Cancel button - only show if export is in progress */}
{exportStatus?.exportId && (
<div className="flex gap-2">
<button
onClick={cancelExport}
className="flex-1 rounded-md border border-red-300 bg-white px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-50 dark:border-red-600 dark:bg-gray-900 dark:text-red-400 dark:hover:bg-red-950"
>
Cancel Export
</button>
</div>
)}
</div>
</div>
)}
</div>
</div>
);

View File

@@ -4,6 +4,8 @@ import { Document } from "@prisma/client";
import { useSession } from "next-auth/react";
import { toast } from "sonner";
import { ExportJob } from "@/lib/redis-job-store";
import { Button } from "../ui/button";
interface ExportStatus {
@@ -28,6 +30,9 @@ export function ExportVisitsModal({
const [exportStatus, setExportStatus] = useState<ExportStatus | null>(null);
const [showModal, setShowModal] = useState(false);
const [viewCount, setViewCount] = useState<number | null>(null);
const [existingExports, setExistingExports] = useState<ExportJob[]>([]);
const [showNewExport, setShowNewExport] = useState(false);
const [loading, setLoading] = useState(true);
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
const exportStartedRef = useRef<boolean>(false);
@@ -40,18 +45,44 @@ export function ExportVisitsModal({
};
}, []);
const startExport = useCallback(async () => {
// Fetch existing exports when modal opens
useEffect(() => {
const fetchExistingExports = async () => {
try {
setLoading(true);
setShowModal(true);
const response = await fetch(
`/api/teams/${teamId}/documents/${document.id}/export-visits`,
{ method: "GET" },
);
if (response.ok) {
const exports = await response.json();
setExistingExports(exports);
} else {
console.error("Failed to fetch existing exports");
}
} catch (error) {
console.error("Error fetching existing exports:", error);
} finally {
setLoading(false);
}
};
fetchExistingExports();
}, [teamId, document.id]);
const startNewExport = useCallback(async () => {
// Prevent double triggering
if (exportStartedRef.current) {
console.warn("Export already started, skipping duplicate request");
return;
}
exportStartedRef.current = true;
setShowNewExport(true);
try {
// Show modal immediately
setShowModal(true);
// Get view count first
try {
const viewCountResponse = await fetch(
@@ -169,12 +200,7 @@ export function ExportVisitsModal({
);
handleClose();
}
}, [document.id, teamId]);
// Start export immediately when component mounts
useEffect(() => {
startExport();
}, [startExport]);
}, [document.id, teamId, document.name]);
// Send export via email
const sendExportEmail = async () => {
@@ -221,6 +247,49 @@ export function ExportVisitsModal({
}
};
// Download existing export
const downloadExport = async (exportId: string, resourceName: string) => {
try {
const downloadUrl = `/api/teams/${teamId}/export-jobs/${exportId}?download=true`;
const link = window.document.createElement("a");
link.href = downloadUrl;
link.setAttribute(
"download",
`${resourceName || document.name}_visits_${new Date().toISOString().split("T")[0]}.csv`,
);
link.rel = "noopener noreferrer";
link.style.display = "none";
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
} catch (error) {
console.error("Download failed:", error);
toast.error("Failed to download export");
}
};
// Format date for display
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
// Get status badge color
const getStatusColor = (status: string) => {
switch (status) {
case "COMPLETED":
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
case "PROCESSING":
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
case "PENDING":
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
case "FAILED":
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300";
default:
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300";
}
};
// Handle close - cleanup and call parent onClose
const handleClose = () => {
if (pollIntervalRef.current) {
@@ -229,6 +298,10 @@ export function ExportVisitsModal({
}
setShowModal(false);
setExportStatus(null);
setShowNewExport(false);
setExistingExports([]); // Reset existing exports
setViewCount(null); // Reset view count
setLoading(true); // Reset loading state
exportStartedRef.current = false; // Reset for potential reuse
onClose();
};
@@ -240,7 +313,7 @@ export function ExportVisitsModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="mx-4 w-full max-w-md rounded-lg bg-white p-6">
<div className="mx-4 w-full max-w-md rounded-lg bg-white p-6 dark:bg-gray-800">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">Export Visits</h3>
<button
@@ -263,58 +336,132 @@ export function ExportVisitsModal({
</button>
</div>
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-muted-foreground"></div>
<span className="text-sm text-gray-600">
{exportStatus?.progress || "Processing export..."}
</span>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-primary"></div>
</div>
{viewCount !== null && (
<div className="text-sm text-gray-600">
Found {viewCount} view{viewCount !== 1 ? "s" : ""} to export
) : showNewExport ? (
// Show export progress
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-muted-foreground"></div>
<span className="text-sm text-gray-600">
{exportStatus?.progress || "Processing export..."}
</span>
</div>
)}
{viewCount !== null && viewCount > 10 && session?.user?.email && (
<div className="rounded-md bg-gray-50 p-3 text-sm dark:bg-gray-900">
<p className="mb-2 font-medium text-muted-foreground">
Large export detected ({viewCount} views)
</p>
<p className="mb-3 text-muted-foreground">
This export may take several minutes. We recommend getting it
emailed to you when ready.
</p>
{viewCount !== null && (
<div className="text-sm text-gray-600">
Found {viewCount} view{viewCount !== 1 ? "s" : ""} to export
</div>
)}
{viewCount !== null && viewCount > 10 && session?.user?.email && (
<div className="rounded-md bg-gray-50 p-3 text-sm dark:bg-gray-900">
<p className="mb-2 font-medium text-muted-foreground">
Large export detected ({viewCount} views)
</p>
<p className="mb-3 text-muted-foreground">
This export may take several minutes. We recommend getting it
emailed to you when ready.
</p>
<button
onClick={sendExportEmail}
className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/80"
>
Email to {session.user.email}
</button>
</div>
)}
{(!viewCount || viewCount <= 10) && (
<div className="text-sm text-gray-500">
Your export will be ready shortly...
</div>
)}
{/* Cancel button - only show if export is in progress */}
{exportStatus?.exportId && (
<div className="flex gap-2 px-3">
<Button
onClick={cancelExport}
variant="outline"
size="sm"
className="flex-1 rounded-md border border-red-300 bg-white px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-50 dark:border-red-600 dark:bg-gray-900 dark:text-red-400 dark:hover:bg-red-950"
>
Cancel Export
</Button>
</div>
)}
</div>
) : (
// Show existing exports and new export option
<div className="space-y-4">
<div className="text-sm text-gray-600">
Exports for: {document.name}
</div>
{existingExports.length > 0 ? (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
Recent Exports
</h4>
<div className="max-h-48 space-y-2 overflow-y-auto">
{existingExports.map((exportJob) => (
<div
key={exportJob.id}
className="flex items-center justify-between rounded-md border border-gray-200 p-3 dark:border-gray-700"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span
className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${getStatusColor(exportJob.status)}`}
>
{exportJob.status}
</span>
<span className="text-xs text-gray-500">
{formatDate(exportJob.createdAt)}
</span>
</div>
{exportJob.error && (
<p className="mt-1 text-xs text-red-600">
{exportJob.error}
</p>
)}
</div>
{exportJob.status === "COMPLETED" && exportJob.result && (
<button
onClick={() =>
downloadExport(
exportJob.id,
exportJob.resourceName || document.name,
)
}
className="ml-2 rounded-md bg-primary px-3 py-1 text-xs text-primary-foreground transition-colors hover:bg-primary/80"
>
Download
</button>
)}
</div>
))}
</div>
</div>
) : (
<div className="text-center text-sm text-gray-500">
No previous exports found
</div>
)}
<div className="border-t border-gray-200 pt-4 dark:border-gray-700">
<button
onClick={sendExportEmail}
onClick={startNewExport}
className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/80"
>
Email to {session.user.email}
Start New Export
</button>
</div>
)}
{(!viewCount || viewCount <= 10) && (
<div className="text-sm text-gray-500">
Your export will be ready shortly...
</div>
)}
{/* Cancel button - only show if export is in progress */}
{exportStatus?.exportId && (
<div className="flex gap-2 px-3">
<Button
onClick={cancelExport}
variant="outline"
size="sm"
className="flex-1 rounded-md border border-red-300 bg-white px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-50 hover:text-red-800 dark:border-red-600 dark:bg-gray-900 dark:text-red-400 dark:hover:bg-red-950"
>
Cancel Export
</Button>
</div>
)}
</div>
</div>
)}
</div>
</div>
);

View File

@@ -254,6 +254,27 @@ export class RedisJobStore {
);
}
async getResourceJobs(
resourceId: string,
teamId: string,
type?: "document" | "dataroom" | "dataroom-group",
groupId?: string,
limit: number = 10,
): Promise<ExportJob[]> {
const teamJobs = await this.getTeamJobs(teamId, limit * 2); // Get more to filter
// Filter jobs by resource and type
return teamJobs
.filter((job) => {
const matchesResource = job.resourceId === resourceId;
const matchesType = !type || job.type === type;
const matchesGroup = !groupId || job.groupId === groupId;
const matchStatus = job.status !== "FAILED";
return matchesResource && matchesType && matchesGroup && matchStatus;
})
.slice(0, limit);
}
async getUserTeamJobs(
userId: string,
teamId: string,

View File

@@ -3,15 +3,63 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { jobStore } from "@/lib/redis-job-store";
import { exportVisitsTask } from "@/lib/trigger/export-visits";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "GET") {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}
const { teamId, id: dataroomId } = req.query as {
teamId: string;
id: string;
};
const userId = (session.user as CustomUser).id;
try {
// Verify team access
const team = await prisma.team.findUnique({
where: {
id: teamId,
users: {
some: {
userId: userId,
},
},
},
select: { plan: true },
});
if (!team) {
return res.status(404).end("Team not found");
}
// Get existing exports for this dataroom
const existingExports = await jobStore.getResourceJobs(
dataroomId,
teamId,
"dataroom",
undefined,
10,
);
return res.status(200).json(existingExports);
} catch (error) {
console.error("Error fetching existing exports:", error);
return res
.status(500)
.json({ message: "Failed to fetch existing exports" });
}
}
if (req.method !== "POST") {
// Changed to POST to trigger background job
res.setHeader("Allow", ["POST"]);
@@ -89,11 +137,7 @@ export default async function handler(
},
{
idempotencyKey: exportJob.id,
tags: [
`team_${teamId}`,
`user_${userId}`,
`export_${exportJob.id}`,
],
tags: [`team_${teamId}`, `user_${userId}`, `export_${exportJob.id}`],
},
);
@@ -105,7 +149,8 @@ export default async function handler(
return res.status(200).json({
exportId: updatedJob?.id || exportJob.id,
status: updatedJob?.status || exportJob.status,
message: "Export job created successfully. You will be notified when it's ready.",
message:
"Export job created successfully. You will be notified when it's ready.",
});
} catch (error) {
console.error("Error creating export job:", error);

View File

@@ -3,15 +3,68 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { jobStore } from "@/lib/redis-job-store";
import { exportVisitsTask } from "@/lib/trigger/export-visits";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "GET") {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}
const {
teamId,
id: dataroomId,
groupId,
} = req.query as {
teamId: string;
id: string;
groupId: string;
};
const userId = (session.user as CustomUser).id;
try {
// Verify team access
const team = await prisma.team.findUnique({
where: {
id: teamId,
users: {
some: {
userId: userId,
},
},
},
select: { plan: true },
});
if (!team) {
return res.status(404).end("Team not found");
}
// Get existing exports for this dataroom group
const existingExports = await jobStore.getResourceJobs(
dataroomId,
teamId,
"dataroom",
groupId,
10,
);
return res.status(200).json(existingExports);
} catch (error) {
console.error("Error fetching existing exports:", error);
return res
.status(500)
.json({ message: "Failed to fetch existing exports" });
}
}
if (req.method !== "POST") {
// Changed to POST to trigger background job
res.setHeader("Allow", ["POST"]);
@@ -96,11 +149,7 @@ export default async function handler(
},
{
idempotencyKey: exportJob.id,
tags: [
`team_${teamId}`,
`user_${userId}`,
`export_${exportJob.id}`,
],
tags: [`team_${teamId}`, `user_${userId}`, `export_${exportJob.id}`],
},
);
@@ -112,7 +161,8 @@ export default async function handler(
return res.status(200).json({
exportId: updatedJob?.id || exportJob.id,
status: updatedJob?.status || exportJob.status,
message: "Export job created successfully. You will be notified when it's ready.",
message:
"Export job created successfully. You will be notified when it's ready.",
});
} catch (error) {
console.error("Error creating export job:", error);

View File

@@ -3,15 +3,63 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { getServerSession } from "next-auth/next";
import prisma from "@/lib/prisma";
import { jobStore } from "@/lib/redis-job-store";
import { exportVisitsTask } from "@/lib/trigger/export-visits";
import prisma from "@/lib/prisma";
import { CustomUser } from "@/lib/types";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method === "GET") {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).end("Unauthorized");
}
const { teamId, id: docId } = req.query as {
teamId: string;
id: string;
};
const userId = (session.user as CustomUser).id;
try {
// Verify team access
const team = await prisma.team.findUnique({
where: {
id: teamId,
users: {
some: {
userId: userId,
},
},
},
select: { plan: true },
});
if (!team) {
return res.status(404).end("Team not found");
}
// Get existing exports for this document
const existingExports = await jobStore.getResourceJobs(
docId,
teamId,
"document",
undefined,
10,
);
return res.status(200).json(existingExports);
} catch (error) {
console.error("Error fetching existing exports:", error);
return res
.status(500)
.json({ message: "Failed to fetch existing exports" });
}
}
if (req.method !== "POST") {
// Changed to POST to trigger background job
res.setHeader("Allow", ["POST"]);
@@ -86,11 +134,7 @@ export default async function handler(
},
{
idempotencyKey: exportJob.id,
tags: [
`team_${teamId}`,
`user_${userId}`,
`export_${exportJob.id}`,
],
tags: [`team_${teamId}`, `user_${userId}`, `export_${exportJob.id}`],
},
);
@@ -102,7 +146,8 @@ export default async function handler(
return res.status(200).json({
exportId: updatedJob?.id || exportJob.id,
status: updatedJob?.status || exportJob.status,
message: "Export job created successfully. You will be notified when it's ready.",
message:
"Export job created successfully. You will be notified when it's ready.",
});
} catch (error) {
console.error("Error creating export job:", error);