page.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. "use client";
  2. import { useState, useEffect } from 'react';
  3. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
  4. import { Button } from '@/components/ui/button';
  5. import { Upload, FileText, Database, BarChart3, CheckCircle, Loader2, History, PlusCircle } from 'lucide-react';
  6. import { UploadForm } from '@/app/components/uploadForm';
  7. import { createCintasImportRecord, processCintasImportData } from '@/app/actions/cintas-workflow';
  8. import { getImports } from '@/app/actions/imports';
  9. interface FileData {
  10. id: string;
  11. filename: string;
  12. mimetype: string;
  13. size: number;
  14. createdAt: string;
  15. updatedAt: string;
  16. }
  17. interface CintasSummary {
  18. id: number;
  19. week: string;
  20. trrTotal: number;
  21. fourWkAverages: number;
  22. trrPlus4Wk: number;
  23. powerAdds: number;
  24. weekId: number;
  25. }
  26. interface ImportRecord {
  27. id: number;
  28. name: string;
  29. importDate: string;
  30. fileId: string | null;
  31. file?: {
  32. filename: string;
  33. createdAt: string;
  34. };
  35. layout?: {
  36. name: string;
  37. };
  38. cintasSummaries?: CintasSummary[];
  39. }
  40. export default function CintasCalendarSummaryPage() {
  41. const [viewMode, setViewMode] = useState<'imports' | 'new-import' | 'summary'>('imports');
  42. const [currentStep, setCurrentStep] = useState(1);
  43. const [uploadedFile, setUploadedFile] = useState<FileData | null>(null);
  44. const [isProcessing, setIsProcessing] = useState(false);
  45. const [importRecord, setImportRecord] = useState<any>(null);
  46. const [summaryData, setSummaryData] = useState<CintasSummary[]>([]);
  47. const [error, setError] = useState<string | null>(null);
  48. const [summaryExists, setSummaryExists] = useState(false);
  49. const [priorImports, setPriorImports] = useState<ImportRecord[]>([]);
  50. const [selectedImport, setSelectedImport] = useState<ImportRecord | null>(null);
  51. const [loadingImports, setLoadingImports] = useState(true);
  52. useEffect(() => {
  53. loadPriorImports();
  54. }, []);
  55. const loadPriorImports = async () => {
  56. try {
  57. setLoadingImports(true);
  58. const result = await getImports();
  59. if (result.success && result.data) {
  60. // Map the data to match our ImportRecord interface
  61. const mappedData = result.data.map((item: any) => ({
  62. id: item.id,
  63. name: item.name,
  64. importDate: item.importDate,
  65. fileId: item.fileId,
  66. file: item.file ? {
  67. filename: item.file.filename,
  68. createdAt: item.file.createdAt
  69. } : undefined,
  70. cintasSummaries: item.cintasSummaries || []
  71. }));
  72. setPriorImports(mappedData);
  73. } else {
  74. setError(result.error || 'Failed to load prior imports');
  75. setPriorImports([]);
  76. }
  77. } catch (err) {
  78. setError(err instanceof Error ? err.message : 'Failed to load prior imports');
  79. setPriorImports([]);
  80. } finally {
  81. setLoadingImports(false);
  82. }
  83. };
  84. const handleFileUploaded = (file: FileData) => {
  85. setUploadedFile(file);
  86. setCurrentStep(2);
  87. setError(null);
  88. };
  89. const handleCreateImportRecord = async () => {
  90. if (!uploadedFile) return;
  91. setIsProcessing(true);
  92. setError(null);
  93. try {
  94. const result = await createCintasImportRecord(uploadedFile.id, uploadedFile.filename);
  95. if (result.success) {
  96. setImportRecord(result.data);
  97. setCurrentStep(3);
  98. } else {
  99. setError(result.error || 'Failed to create import record');
  100. }
  101. } catch (err) {
  102. setError(err instanceof Error ? err.message : 'Unknown error occurred');
  103. } finally {
  104. setIsProcessing(false);
  105. }
  106. };
  107. const handleProcessImportData = async () => {
  108. if (!importRecord) return;
  109. setIsProcessing(true);
  110. setError(null);
  111. try {
  112. const result = await processCintasImportData(importRecord.id);
  113. if (result.success) {
  114. setCurrentStep(4);
  115. } else {
  116. setError(result.error || 'Failed to process import data');
  117. }
  118. } catch (err) {
  119. setError(err instanceof Error ? err.message : 'Unknown error occurred');
  120. } finally {
  121. setIsProcessing(false);
  122. }
  123. };
  124. const handleGenerateSummary = async () => {
  125. if (!importRecord) return;
  126. setIsProcessing(true);
  127. setError(null);
  128. try {
  129. const response = await fetch(`/api/imports/${importRecord.id}/summary`, {
  130. method: 'POST',
  131. headers: {
  132. 'Content-Type': 'application/json',
  133. },
  134. });
  135. const data = await response.json();
  136. if (response.ok) {
  137. setSummaryData(data.summary || []);
  138. setSummaryExists(true);
  139. if (data.summaryGenerated) {
  140. setCurrentStep(4);
  141. setViewMode('summary');
  142. }
  143. } else {
  144. throw new Error(data.error || 'Failed to generate summary');
  145. }
  146. } catch (err) {
  147. setError(err instanceof Error ? err.message : 'Failed to generate summary');
  148. } finally {
  149. setIsProcessing(false);
  150. }
  151. };
  152. const handleLoadImportSummary = async (importRecord: ImportRecord) => {
  153. try {
  154. setIsProcessing(true);
  155. setError(null);
  156. const response = await fetch(`/api/imports/${importRecord.id}/summary`);
  157. const data = await response.json();
  158. if (response.ok) {
  159. setSelectedImport(importRecord);
  160. setSummaryData(data.summary?.cintasSummaries || []);
  161. setSummaryExists(data.summaryExists);
  162. setViewMode('summary');
  163. } else {
  164. throw new Error(data.error || 'Failed to load summary');
  165. }
  166. } catch (err) {
  167. setError(err instanceof Error ? err.message : 'Failed to load summary');
  168. } finally {
  169. setIsProcessing(false);
  170. }
  171. };
  172. const handleStartNewImport = () => {
  173. setViewMode('new-import');
  174. setCurrentStep(1);
  175. setUploadedFile(null);
  176. setImportRecord(null);
  177. setSummaryData([]);
  178. setSummaryExists(false);
  179. setError(null);
  180. };
  181. const handleBackToImports = () => {
  182. setViewMode('imports');
  183. setSelectedImport(null);
  184. setSummaryData([]);
  185. loadPriorImports();
  186. };
  187. const formatDate = (dateString: string) => {
  188. return new Date(dateString).toLocaleDateString('en-US', {
  189. year: 'numeric',
  190. month: 'short',
  191. day: 'numeric',
  192. hour: '2-digit',
  193. minute: '2-digit'
  194. });
  195. };
  196. return (
  197. <div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
  198. <div className="container mx-auto py-6 px-4 max-w-6xl">
  199. <div className="mb-8">
  200. <h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Cintas Install Calendar Summary</h1>
  201. <p className="text-muted-foreground dark:text-gray-300">
  202. View prior imports or create new ones to process installation calendar data
  203. </p>
  204. </div>
  205. {error && (
  206. <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
  207. <p className="text-sm text-red-800">{error}</p>
  208. </div>
  209. )}
  210. {/* Navigation Buttons */}
  211. <div className="mb-6 flex gap-4">
  212. <Button
  213. variant={viewMode === 'imports' ? 'default' : 'outline'}
  214. onClick={handleBackToImports}
  215. className="flex items-center gap-2"
  216. >
  217. <History className="h-4 w-4" />
  218. Prior Imports
  219. </Button>
  220. <Button
  221. variant={viewMode === 'new-import' ? 'default' : 'outline'}
  222. onClick={handleStartNewImport}
  223. className="flex items-center gap-2"
  224. >
  225. <PlusCircle className="h-4 w-4" />
  226. New Import
  227. </Button>
  228. </div>
  229. {/* Prior Imports View */}
  230. {viewMode === 'imports' && (
  231. <Card>
  232. <CardHeader>
  233. <CardTitle>Prior Imports</CardTitle>
  234. <CardDescription>
  235. Select an import to view its summary results
  236. </CardDescription>
  237. </CardHeader>
  238. <CardContent>
  239. {loadingImports ? (
  240. <div className="flex justify-center py-8">
  241. <Loader2 className="h-8 w-8 animate-spin" />
  242. </div>
  243. ) : priorImports.length === 0 ? (
  244. <div className="text-center py-8">
  245. <p className="text-muted-foreground mb-4">No prior imports found</p>
  246. <Button onClick={handleStartNewImport}>
  247. Create First Import
  248. </Button>
  249. </div>
  250. ) : (
  251. <div className="space-y-4">
  252. {priorImports.map((importRecord) => (
  253. <div
  254. key={importRecord.id}
  255. className="border rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
  256. onClick={() => handleLoadImportSummary(importRecord)}
  257. >
  258. <div className="flex justify-between items-start">
  259. <div>
  260. <h3 className="font-semibold">{importRecord.name}</h3>
  261. <p className="text-sm text-muted-foreground">
  262. {formatDate(importRecord.importDate)}
  263. </p>
  264. {importRecord.file && (
  265. <p className="text-sm text-muted-foreground">
  266. File: {importRecord.file.filename}
  267. </p>
  268. )}
  269. </div>
  270. <div className="text-right">
  271. <span className="text-sm text-muted-foreground">
  272. {importRecord.cintasSummaries?.length || 0} weeks
  273. </span>
  274. </div>
  275. </div>
  276. </div>
  277. ))}
  278. </div>
  279. )}
  280. </CardContent>
  281. </Card>
  282. )}
  283. {/* New Import Workflow */}
  284. {viewMode === 'new-import' && (
  285. <div className="space-y-6">
  286. {/* Workflow Steps */}
  287. <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
  288. {[
  289. {
  290. id: 1,
  291. title: 'Upload Excel File',
  292. description: 'Upload the Cintas Install Calendar Excel file',
  293. icon: Upload,
  294. status: currentStep >= 1 ? (uploadedFile ? 'completed' : 'pending') : 'pending',
  295. },
  296. {
  297. id: 2,
  298. title: 'Create Import Record',
  299. description: 'Create import record with layout configuration',
  300. icon: FileText,
  301. status: currentStep >= 2 ? (importRecord ? 'completed' : 'pending') : 'disabled',
  302. },
  303. {
  304. id: 3,
  305. title: 'Import Data',
  306. description: 'Process Excel file and import data',
  307. icon: Database,
  308. status: currentStep > 3 ? 'completed' : (currentStep === 3 ? 'pending' : 'disabled'),
  309. },
  310. {
  311. id: 4,
  312. title: 'Generate Summary',
  313. description: 'Run summary calculations and display results',
  314. icon: BarChart3,
  315. status: currentStep >= 4 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
  316. },
  317. ].map((step) => {
  318. const Icon = step.icon;
  319. const getStatusColor = (status: string) => {
  320. switch (status) {
  321. case 'completed':
  322. return 'text-green-600 bg-green-50 border-green-200';
  323. case 'pending':
  324. return 'text-blue-600 bg-blue-50 border-blue-200';
  325. case 'disabled':
  326. return 'text-gray-400 bg-gray-50 border-gray-200';
  327. default:
  328. return 'text-gray-600 bg-gray-50 border-gray-200';
  329. }
  330. };
  331. return (
  332. <Card
  333. key={step.id}
  334. className={`border-2 ${getStatusColor(step.status)}`}
  335. >
  336. <CardHeader className="pb-3">
  337. <div className="flex items-center space-x-2">
  338. <Icon className="h-5 w-5" />
  339. <div>
  340. <CardTitle className="text-sm font-medium">
  341. Step {step.id}: {step.title}
  342. </CardTitle>
  343. </div>
  344. </div>
  345. </CardHeader>
  346. <CardContent>
  347. <CardDescription className="text-xs">
  348. {step.description}
  349. </CardDescription>
  350. </CardContent>
  351. </Card>
  352. );
  353. })}
  354. </div>
  355. {/* Step Content */}
  356. <div className="space-y-6">
  357. {currentStep === 1 && (
  358. <Card>
  359. <CardHeader>
  360. <CardTitle>Step 1: Upload Excel File</CardTitle>
  361. <CardDescription>
  362. Upload your Cintas Install Calendar Excel file to begin processing
  363. </CardDescription>
  364. </CardHeader>
  365. <CardContent>
  366. <div className="space-y-4">
  367. <UploadForm onFileUploaded={handleFileUploaded} />
  368. {uploadedFile && (
  369. <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
  370. <div className="flex items-center space-x-2">
  371. <CheckCircle className="h-5 w-5 text-green-600" />
  372. <div>
  373. <p className="text-sm font-medium text-green-800">File uploaded successfully!</p>
  374. <p className="text-sm text-green-600">
  375. {uploadedFile.filename} ({(uploadedFile.size / 1024 / 1024).toFixed(2)} MB)
  376. </p>
  377. </div>
  378. </div>
  379. </div>
  380. )}
  381. </div>
  382. </CardContent>
  383. </Card>
  384. )}
  385. {currentStep === 2 && (
  386. <Card>
  387. <CardHeader>
  388. <CardTitle>Step 2: Create Import Record</CardTitle>
  389. <CardDescription>
  390. Creating import record with Cintas Install Calendar layout configuration
  391. </CardDescription>
  392. </CardHeader>
  393. <CardContent>
  394. <div className="space-y-4">
  395. <p className="text-sm text-muted-foreground">
  396. File: {uploadedFile?.filename}
  397. </p>
  398. <Button
  399. onClick={handleCreateImportRecord}
  400. disabled={isProcessing}
  401. className="w-full"
  402. >
  403. {isProcessing ? (
  404. <>
  405. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  406. Creating Import Record...
  407. </>
  408. ) : (
  409. 'Create Import Record'
  410. )}
  411. </Button>
  412. </div>
  413. </CardContent>
  414. </Card>
  415. )}
  416. {currentStep === 3 && (
  417. <Card>
  418. <CardHeader>
  419. <CardTitle>Step 3: Import Data</CardTitle>
  420. <CardDescription>
  421. Processing Excel file and importing data into database
  422. </CardDescription>
  423. </CardHeader>
  424. <CardContent>
  425. <div className="space-y-4">
  426. <p className="text-sm text-muted-foreground">
  427. Import ID: {importRecord?.id}
  428. </p>
  429. <Button
  430. onClick={handleProcessImportData}
  431. disabled={isProcessing}
  432. className="w-full"
  433. >
  434. {isProcessing ? (
  435. <>
  436. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  437. Processing Import...
  438. </>
  439. ) : (
  440. 'Process Import'
  441. )}
  442. </Button>
  443. </div>
  444. </CardContent>
  445. </Card>
  446. )}
  447. {currentStep === 4 && (
  448. <Card>
  449. <CardHeader>
  450. <CardTitle>Step 4: Generate Summary</CardTitle>
  451. <CardDescription>
  452. Running summary calculations and displaying results
  453. </CardDescription>
  454. </CardHeader>
  455. <CardContent>
  456. <div className="space-y-4">
  457. <p className="text-sm text-muted-foreground">
  458. Import ID: {importRecord?.id}
  459. </p>
  460. <Button
  461. onClick={handleGenerateSummary}
  462. disabled={isProcessing}
  463. className="w-full"
  464. >
  465. {isProcessing ? (
  466. <>
  467. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  468. {summaryExists ? 'Loading Summary...' : 'Generating Summary...'}
  469. </>
  470. ) : (
  471. summaryExists ? 'Load Summary' : 'Generate Summary'
  472. )}
  473. </Button>
  474. </div>
  475. </CardContent>
  476. </Card>
  477. )}
  478. </div>
  479. </div>
  480. )}
  481. {/* Summary Results View */}
  482. {viewMode === 'summary' && selectedImport && (
  483. <Card>
  484. <CardHeader>
  485. <div className="flex justify-between items-start">
  486. <div>
  487. <CardTitle>Summary Results</CardTitle>
  488. <CardDescription>
  489. {selectedImport.name} - {formatDate(selectedImport.importDate)}
  490. </CardDescription>
  491. </div>
  492. <Button
  493. variant="outline"
  494. onClick={handleBackToImports}
  495. className="flex items-center gap-2"
  496. >
  497. <History className="h-4 w-4" />
  498. Back to Imports
  499. </Button>
  500. </div>
  501. </CardHeader>
  502. <CardContent>
  503. {isProcessing ? (
  504. <div className="flex justify-center py-8">
  505. <Loader2 className="h-8 w-8 animate-spin" />
  506. </div>
  507. ) : summaryData.length === 0 ? (
  508. <div className="text-center py-8">
  509. <p className="text-muted-foreground mb-4">No summary data available</p>
  510. <Button onClick={() => handleGenerateSummary()}>
  511. Generate Summary
  512. </Button>
  513. </div>
  514. ) : (
  515. <div className="overflow-x-auto">
  516. <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
  517. <thead className="bg-gray-50 dark:bg-gray-800">
  518. <tr>
  519. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Week</th>
  520. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">TRR Total</th>
  521. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">4 Week Avg</th>
  522. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">TRR + 4Wk</th>
  523. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Power Adds</th>
  524. </tr>
  525. </thead>
  526. <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700">
  527. {summaryData.map((item, index) => (
  528. <tr
  529. key={`${item.id}-${index}`}
  530. className={index % 2 === 0
  531. ? 'bg-white dark:bg-gray-800'
  532. : 'bg-gray-50 dark:bg-gray-700/50'
  533. }
  534. >
  535. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.week}</td>
  536. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.trrTotal}</td>
  537. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.fourWkAverages}</td>
  538. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.trrPlus4Wk}</td>
  539. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{item.powerAdds}</td>
  540. </tr>
  541. ))}
  542. </tbody>
  543. </table>
  544. </div>
  545. )}
  546. </CardContent>
  547. </Card>
  548. )}
  549. </div>
  550. </div>
  551. );
  552. }