page.tsx 23 KB

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