imports.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. 'use server';
  2. import { prisma } from '@/lib/prisma';
  3. import { revalidatePath } from 'next/cache';
  4. import { z } from 'zod';
  5. import { ImportProcessor } from '@/app/lib/excel-import/import-processor';
  6. // Validation schemas
  7. const createImportSchema = z.object({
  8. name: z.string().min(1, 'Import name is required'),
  9. layoutId: z.number().int().positive('Layout configuration is required'),
  10. fileId: z.string().optional(),
  11. });
  12. const updateImportSchema = z.object({
  13. id: z.number().int().positive(),
  14. name: z.string().min(1, 'Import name is required'),
  15. fileId: z.string().optional(),
  16. });
  17. // Create a new import
  18. export async function createImport(data: {
  19. name: string;
  20. layoutId: number;
  21. fileId?: string;
  22. }) {
  23. try {
  24. const validatedData = createImportSchema.parse(data);
  25. const importRecord = await prisma.import.create({
  26. data: {
  27. name: validatedData.name,
  28. layoutId: validatedData.layoutId,
  29. importDate: new Date(),
  30. ...(validatedData.fileId && { fileId: validatedData.fileId }),
  31. },
  32. include: {
  33. layout: true,
  34. },
  35. });
  36. revalidatePath('/imports');
  37. return { success: true, data: importRecord };
  38. } catch (error) {
  39. console.error('Error creating import:', error);
  40. return { success: false, error: 'Failed to create import' };
  41. }
  42. }
  43. // Get all imports for the current user
  44. export async function getImports(userId?: string) {
  45. try {
  46. const imports = await prisma.import.findMany({
  47. where: userId ? {
  48. fileId: {
  49. not: null,
  50. },
  51. } : {},
  52. include: {
  53. layout: true,
  54. },
  55. orderBy: {
  56. importDate: 'desc',
  57. },
  58. });
  59. // Filter by userId manually since Prisma doesn't have direct relation
  60. let filteredImports = imports;
  61. if (userId) {
  62. const fileIds = await prisma.file.findMany({
  63. where: {
  64. userId: userId,
  65. },
  66. select: {
  67. id: true,
  68. },
  69. });
  70. const userFileIds = new Set(fileIds.map(f => f.id));
  71. filteredImports = imports.filter(imp =>
  72. imp.fileId && userFileIds.has(imp.fileId)
  73. );
  74. }
  75. return { success: true, data: filteredImports };
  76. } catch (error) {
  77. console.error('Error fetching imports:', error);
  78. return { success: false, error: 'Failed to fetch imports' };
  79. }
  80. }
  81. // Get a single import by ID
  82. export async function getImportById(id: number) {
  83. try {
  84. const importRecord = await prisma.import.findUnique({
  85. where: { id },
  86. include: {
  87. layout: {
  88. include: {
  89. sections: {
  90. include: {
  91. fields: true,
  92. },
  93. },
  94. },
  95. },
  96. },
  97. });
  98. if (!importRecord) {
  99. return { success: false, error: 'Import not found' };
  100. }
  101. // Fetch file separately if fileId exists
  102. let file = undefined;
  103. if (importRecord.fileId) {
  104. file = await prisma.file.findUnique({
  105. where: { id: importRecord.fileId },
  106. });
  107. }
  108. return { success: true, data: { ...importRecord, file } };
  109. } catch (error) {
  110. console.error('Error fetching import:', error);
  111. return { success: false, error: 'Failed to fetch import' };
  112. }
  113. }
  114. // Update an import
  115. export async function updateImport(data: {
  116. id: number;
  117. name: string;
  118. fileId?: string;
  119. }) {
  120. try {
  121. const validatedData = updateImportSchema.parse(data);
  122. const importRecord = await prisma.import.update({
  123. where: { id: validatedData.id },
  124. data: {
  125. name: validatedData.name,
  126. ...(validatedData.fileId !== undefined && { fileId: validatedData.fileId }),
  127. },
  128. include: {
  129. layout: true,
  130. },
  131. });
  132. revalidatePath('/imports');
  133. return { success: true, data: importRecord };
  134. } catch (error) {
  135. console.error('Error updating import:', error);
  136. return { success: false, error: 'Failed to update import' };
  137. }
  138. }
  139. // Delete an import
  140. export async function deleteImport(id: number) {
  141. try {
  142. await prisma.import.delete({
  143. where: { id },
  144. });
  145. revalidatePath('/imports');
  146. return { success: true };
  147. } catch (error) {
  148. console.error('Error deleting import:', error);
  149. return { success: false, error: 'Failed to delete import' };
  150. }
  151. }
  152. // Calculate Cintas summaries for an import
  153. export async function calculateCintasSummaries(importId: number) {
  154. try {
  155. // This would typically call a stored procedure or perform calculations
  156. // For now, we'll simulate the calculation
  157. // In a real implementation, you might call:
  158. // await prisma.$executeRaw`CALL cintas_calculate_summary(${importId})`;
  159. // For demo purposes, we'll create some sample data
  160. const summaries = [
  161. {
  162. importId,
  163. week: '2024-W01',
  164. trrTotal: 100,
  165. fourWkAverages: 95,
  166. trrPlus4Wk: 195,
  167. powerAdds: 25,
  168. weekId: 1,
  169. },
  170. {
  171. importId,
  172. week: '2024-W02',
  173. trrTotal: 110,
  174. fourWkAverages: 100,
  175. trrPlus4Wk: 210,
  176. powerAdds: 30,
  177. weekId: 2,
  178. },
  179. ];
  180. // Clear existing summaries for this import
  181. await prisma.cintasSummary.deleteMany({
  182. where: { importId },
  183. });
  184. // Create new summaries
  185. const createdSummaries = await Promise.all(
  186. summaries.map(summary =>
  187. prisma.cintasSummary.create({
  188. data: summary,
  189. })
  190. )
  191. );
  192. return { success: true, data: createdSummaries };
  193. } catch (error) {
  194. console.error('Error calculating Cintas summaries:', error);
  195. return { success: false, error: 'Failed to calculate summaries' };
  196. }
  197. }
  198. // Get available layout configurations
  199. export async function getLayoutConfigurations() {
  200. try {
  201. const layouts = await prisma.layoutConfiguration.findMany({
  202. include: {
  203. sections: {
  204. include: {
  205. fields: true,
  206. },
  207. },
  208. },
  209. orderBy: {
  210. name: 'asc',
  211. },
  212. });
  213. return { success: true, data: layouts };
  214. } catch (error) {
  215. console.error('Error fetching layout configurations:', error);
  216. return { success: false, error: 'Failed to fetch layout configurations' };
  217. }
  218. }
  219. // Get import summary (GET /api/imports/[id]/summary)
  220. export async function getImportSummary(importId: number) {
  221. try {
  222. if (!importId || isNaN(importId)) {
  223. return { success: false, error: 'Invalid import ID' };
  224. }
  225. // Check if import exists
  226. const importRecord = await prisma.import.findUnique({
  227. where: { id: importId }
  228. });
  229. if (!importRecord) {
  230. return { success: false, error: 'Import not found' };
  231. }
  232. // Get basic summary data
  233. const totalRecords = await prisma.cintasInstallCalendar.count({
  234. where: { importId }
  235. });
  236. const cintasSummaries = await prisma.cintasSummary.findMany({
  237. where: { importId },
  238. orderBy: { weekId: 'desc' }
  239. });
  240. // Get file info
  241. const file = importRecord.fileId ? await prisma.file.findUnique({
  242. where: { id: importRecord.fileId }
  243. }) : null;
  244. const summary = {
  245. totalRecords,
  246. totalWeeks: cintasSummaries.length,
  247. cintasSummaries: cintasSummaries.map((summary: any) => ({
  248. id: summary.id,
  249. week: summary.week,
  250. trrTotal: summary.trrTotal,
  251. fourWkAverages: summary.fourWkAverages,
  252. trrPlus4Wk: summary.trrPlus4Wk,
  253. powerAdds: summary.powerAdds,
  254. weekId: summary.weekId
  255. }))
  256. };
  257. return {
  258. success: true,
  259. data: {
  260. importId,
  261. fileName: file?.filename || 'Unknown',
  262. uploadDate: importRecord.createdAt,
  263. summary,
  264. summaryExists: cintasSummaries.length > 0
  265. }
  266. };
  267. } catch (error) {
  268. console.error('Error fetching import summary:', error);
  269. return { success: false, error: 'Failed to fetch import summary' };
  270. }
  271. }
  272. // Generate import summary (POST /api/imports/[id]/summary)
  273. export async function generateImportSummary(importId: number) {
  274. try {
  275. if (!importId || isNaN(importId)) {
  276. return { success: false, error: 'Invalid import ID' };
  277. }
  278. // Check if import exists
  279. const importRecord = await prisma.import.findUnique({
  280. where: { id: importId }
  281. });
  282. if (!importRecord) {
  283. return { success: false, error: 'Import not found' };
  284. }
  285. // Check if summary already exists
  286. const existingSummaries = await prisma.cintasSummary.count({
  287. where: { importId }
  288. });
  289. if (existingSummaries > 0) {
  290. // Return existing summary
  291. const cintasSummaries = await prisma.cintasSummary.findMany({
  292. where: { importId },
  293. orderBy: { weekId: 'desc' }
  294. });
  295. return {
  296. success: true,
  297. data: {
  298. importId,
  299. summaryGenerated: false,
  300. message: 'Summary already exists',
  301. summary: cintasSummaries.map((summary: any) => ({
  302. id: summary.id,
  303. week: summary.week,
  304. trrTotal: summary.trrTotal,
  305. fourWkAverages: summary.fourWkAverages,
  306. trrPlus4Wk: summary.trrPlus4Wk,
  307. powerAdds: summary.powerAdds,
  308. weekId: summary.weekId
  309. }))
  310. }
  311. };
  312. }
  313. // Generate new summary using stored procedure
  314. await prisma.$executeRawUnsafe(
  315. `CALL cintas_calculate_summary(${importId})`
  316. );
  317. // Fetch the newly generated summary
  318. const cintasSummaries = await prisma.cintasSummary.findMany({
  319. where: { importId },
  320. orderBy: { weekId: 'desc' }
  321. });
  322. return {
  323. success: true,
  324. data: {
  325. importId,
  326. summaryGenerated: true,
  327. message: 'Summary generated successfully',
  328. summary: cintasSummaries.map((summary: any) => ({
  329. id: summary.id,
  330. week: summary.week,
  331. trrTotal: summary.trrTotal,
  332. fourWkAverages: summary.fourWkAverages,
  333. trrPlus4Wk: summary.trrPlus4Wk,
  334. powerAdds: summary.powerAdds,
  335. weekId: summary.weekId
  336. }))
  337. }
  338. };
  339. } catch (error) {
  340. console.error('Error generating summary:', error);
  341. return { success: false, error: 'Failed to generate summary' };
  342. }
  343. }
  344. // Get import progress (GET /api/imports/[id]/progress)
  345. export async function getImportProgress(importId: number) {
  346. try {
  347. if (!importId || isNaN(importId)) {
  348. return { success: false, error: 'Invalid import ID' };
  349. }
  350. // Check if import exists
  351. const importRecord = await prisma.import.findUnique({
  352. where: { id: importId }
  353. });
  354. if (!importRecord) {
  355. return { success: false, error: 'Import not found' };
  356. }
  357. // Check all possible tables for records
  358. const [cintasCount, gowDataCount, gowFacCount, gowCorpCount] = await Promise.all([
  359. prisma.cintasInstallCalendar.count({ where: { importId } }),
  360. prisma.gowData.count({ where: { importId } }),
  361. prisma.gowFacId.count({ where: { importId } }),
  362. prisma.gowCorpRef.count({ where: { importId } }),
  363. ]);
  364. const totalRecords = cintasCount + gowDataCount + gowFacCount + gowCorpCount;
  365. // Since we don't have status fields, we'll use record count as proxy
  366. const hasRecords = totalRecords > 0;
  367. return {
  368. success: true,
  369. data: {
  370. importId,
  371. status: hasRecords ? 'completed' : 'pending',
  372. progress: hasRecords ? 100 : 0,
  373. processedRecords: totalRecords,
  374. totalRecords: totalRecords,
  375. errorMessage: null,
  376. lastUpdated: importRecord.updatedAt,
  377. timestamp: new Date().toISOString()
  378. }
  379. };
  380. } catch (error) {
  381. console.error('Error fetching import progress:', error);
  382. return { success: false, error: 'Failed to fetch import progress' };
  383. }
  384. }
  385. // Trigger import process (POST /api/imports/[id]/trigger)
  386. export async function triggerImportProcess(importId: number) {
  387. try {
  388. if (!importId || isNaN(importId)) {
  389. return { success: false, error: 'Invalid import ID' };
  390. }
  391. // Validate import exists
  392. const importRecord = await prisma.import.findUnique({
  393. where: { id: importId }
  394. });
  395. if (!importRecord) {
  396. return { success: false, error: 'Import not found' };
  397. }
  398. if (!importRecord.fileId) {
  399. return { success: false, error: 'No file attached to import' };
  400. }
  401. // Check if layout exists
  402. const layout = await prisma.layoutConfiguration.findUnique({
  403. where: { id: importRecord.layoutId }
  404. });
  405. if (!layout) {
  406. return { success: false, error: 'No layout configuration found' };
  407. }
  408. // Check if data already exists for this import
  409. const existingRecords = await prisma.cintasInstallCalendar.count({
  410. where: { importId }
  411. });
  412. if (existingRecords > 0) {
  413. return {
  414. success: true,
  415. message: 'Import already processed',
  416. importId,
  417. existingRecords
  418. };
  419. }
  420. // Use the ImportProcessor to actually process the import
  421. const processor = new ImportProcessor();
  422. const result = await processor.processImport(importId);
  423. if (result.success) {
  424. revalidatePath('/imports');
  425. return {
  426. success: true,
  427. message: 'Import process completed successfully',
  428. importId,
  429. totalInserted: result.totalInserted
  430. };
  431. } else {
  432. return {
  433. success: false,
  434. error: 'Import processing failed',
  435. errors: result.errors
  436. };
  437. }
  438. } catch (error) {
  439. console.error('Error triggering import:', error);
  440. return { success: false, error: 'Failed to trigger import' };
  441. }
  442. }
  443. // Update import progress (for internal use by ImportProcessor)
  444. export async function updateImportProgress(
  445. importId: number,
  446. progress: {
  447. processedRecords: number;
  448. totalRecords: number;
  449. status: string;
  450. errorMessage?: string;
  451. }
  452. ) {
  453. try {
  454. // Since the Import model doesn't have these fields, we'll just return success
  455. // In a real implementation, you would need to add these fields to the schema
  456. console.log(`Import ${importId} progress: ${progress.processedRecords}/${progress.totalRecords} (${progress.status})`);
  457. return { success: true };
  458. } catch (error) {
  459. console.error('Error updating import progress:', error);
  460. return { success: false, error: 'Failed to update progress' };
  461. }
  462. }