feat: implement CSV export API with PocketBase integration
This commit is contained in:
@@ -0,0 +1,218 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import PocketBase from "pocketbase";
|
||||||
|
|
||||||
|
const pb = new PocketBase("https://base.mossport.info");
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
return res.status(405).json({ message: "Method not allowed" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get query parameters
|
||||||
|
const { collection = "forms", type, token } = req.query;
|
||||||
|
|
||||||
|
// Authenticate if token is provided
|
||||||
|
if (token && typeof token === "string") {
|
||||||
|
try {
|
||||||
|
pb.authStore.save(token, null);
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ message: "Invalid token" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filter based on type
|
||||||
|
let filter = "";
|
||||||
|
if (type && typeof type === "string") {
|
||||||
|
filter = `type = "${type}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch records from PocketBase
|
||||||
|
const records = await pb.collection(collection as string).getFullList({
|
||||||
|
filter: filter || undefined,
|
||||||
|
sort: "-created",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (records.length === 0) {
|
||||||
|
return res
|
||||||
|
.status(200)
|
||||||
|
.setHeader("Content-Type", "text/csv")
|
||||||
|
.send("No data found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to CSV
|
||||||
|
const csvData = convertToCSV(records, type as string);
|
||||||
|
|
||||||
|
// Set headers for CSV download
|
||||||
|
const filename = `${collection}_${type || "all"}_${
|
||||||
|
new Date().toISOString().split("T")[0]
|
||||||
|
}.csv`;
|
||||||
|
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||||
|
|
||||||
|
// Add BOM for proper UTF-8 encoding in Excel
|
||||||
|
res.send("\uFEFF" + csvData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("CSV export error:", error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: "Failed to export data",
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToCSV(records: any[], type?: string): string {
|
||||||
|
if (records.length === 0) return "";
|
||||||
|
|
||||||
|
// Define headers based on type
|
||||||
|
let headers: string[] = [];
|
||||||
|
|
||||||
|
if (type === "exhibition") {
|
||||||
|
headers = [
|
||||||
|
"ID",
|
||||||
|
"Дата создания",
|
||||||
|
"Статус",
|
||||||
|
"Одобрено",
|
||||||
|
"Имя",
|
||||||
|
"Телефон",
|
||||||
|
"Email",
|
||||||
|
"Марка автомобиля",
|
||||||
|
"Модель автомобиля",
|
||||||
|
"Описание",
|
||||||
|
"Количество фотографий",
|
||||||
|
];
|
||||||
|
} else if (type === "fight") {
|
||||||
|
headers = [
|
||||||
|
"ID",
|
||||||
|
"Дата создания",
|
||||||
|
"Статус",
|
||||||
|
"Одобрено",
|
||||||
|
"Фамилия",
|
||||||
|
"Имя",
|
||||||
|
"Отчество",
|
||||||
|
"Дата рождения",
|
||||||
|
"Гражданство",
|
||||||
|
"Телефон",
|
||||||
|
"Email",
|
||||||
|
"Марка автомобиля",
|
||||||
|
"Модель автомобиля",
|
||||||
|
"Двигатель",
|
||||||
|
"Мощность",
|
||||||
|
"Дополнительная информация",
|
||||||
|
"Количество фотографий",
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Generic headers for other types
|
||||||
|
headers = ["ID", "Дата создания", "Тип", "Статус", "Одобрено", "Данные"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to properly escape CSV fields
|
||||||
|
const escapeCSVField = (field: any): string => {
|
||||||
|
if (field === null || field === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let str = String(field);
|
||||||
|
|
||||||
|
// Remove any control characters and normalize whitespace
|
||||||
|
str = str.replace(/[\r\n\t]/g, " ").replace(/\s+/g, " ").trim();
|
||||||
|
|
||||||
|
// If field contains comma, quote, or starts/ends with whitespace, wrap in quotes
|
||||||
|
if (
|
||||||
|
str.includes(",") ||
|
||||||
|
str.includes('"') ||
|
||||||
|
str.includes("\n") ||
|
||||||
|
str !== str.trim()
|
||||||
|
) {
|
||||||
|
// Escape existing quotes by doubling them
|
||||||
|
str = str.replace(/"/g, '""');
|
||||||
|
return `"${str}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create CSV content
|
||||||
|
const csvRows: string[] = [];
|
||||||
|
|
||||||
|
// Add header row
|
||||||
|
csvRows.push(headers.map((header) => escapeCSVField(header)).join(","));
|
||||||
|
|
||||||
|
// Add data rows
|
||||||
|
records.forEach((record) => {
|
||||||
|
const data =
|
||||||
|
typeof record.data === "string" ? JSON.parse(record.data) : record.data;
|
||||||
|
|
||||||
|
let row: any[] = [];
|
||||||
|
|
||||||
|
if (type === "exhibition") {
|
||||||
|
row = [
|
||||||
|
record.id || "",
|
||||||
|
new Date(record.created).toLocaleDateString("ru-RU") +
|
||||||
|
" " +
|
||||||
|
new Date(record.created).toLocaleTimeString("ru-RU"),
|
||||||
|
record.status || "pending",
|
||||||
|
record.approved ? "Да" : "Нет",
|
||||||
|
data?.name || "",
|
||||||
|
data?.phone || "",
|
||||||
|
data?.email || "",
|
||||||
|
data?.carBrand || "",
|
||||||
|
data?.carModel || "",
|
||||||
|
data?.description || "",
|
||||||
|
(record.images?.length || 0),
|
||||||
|
];
|
||||||
|
} else if (type === "fight") {
|
||||||
|
row = [
|
||||||
|
record.id || "",
|
||||||
|
new Date(record.created).toLocaleDateString("ru-RU") +
|
||||||
|
" " +
|
||||||
|
new Date(record.created).toLocaleTimeString("ru-RU"),
|
||||||
|
record.status || "pending",
|
||||||
|
record.approved ? "Да" : "Нет",
|
||||||
|
data?.lastName || "",
|
||||||
|
data?.firstName || "",
|
||||||
|
data?.middleName || "",
|
||||||
|
data?.birthDate || "",
|
||||||
|
data?.citizenship || "",
|
||||||
|
data?.phone || "",
|
||||||
|
data?.email || "",
|
||||||
|
data?.carBrand || "",
|
||||||
|
data?.carModel || "",
|
||||||
|
data?.engine || "",
|
||||||
|
data?.power || "",
|
||||||
|
data?.additionalInfo || "",
|
||||||
|
(record.images?.length || 0),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// For generic data, flatten the JSON properly
|
||||||
|
const flattenedData = data
|
||||||
|
? Object.entries(data)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join("; ")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
row = [
|
||||||
|
record.id || "",
|
||||||
|
new Date(record.created).toLocaleDateString("ru-RU") +
|
||||||
|
" " +
|
||||||
|
new Date(record.created).toLocaleTimeString("ru-RU"),
|
||||||
|
record.type || "",
|
||||||
|
record.status || "pending",
|
||||||
|
record.approved ? "Да" : "Нет",
|
||||||
|
flattenedData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape and format each field properly
|
||||||
|
const escapedRow = row.map((field) => escapeCSVField(field));
|
||||||
|
csvRows.push(escapedRow.join(","));
|
||||||
|
});
|
||||||
|
|
||||||
|
return csvRows.join("\r\n");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user