imports.ts 13 KB

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