page.tsx 23 KB

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