page.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. /* eslint-disable @typescript-eslint/no-explicit-any */
  2. "use client";
  3. import { useState } from 'react';
  4. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
  5. import { Button } from '@/components/ui/button';
  6. import { Upload, FileText, Database, BarChart3, CheckCircle, Loader2 } from 'lucide-react';
  7. import { UploadForm } from '@/app/components/uploadForm';
  8. import { createCintasImportRecord, processCintasImportData } from '@/app/actions/cintas-workflow';
  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. export default function CintasCalendarSummaryPage() {
  27. const [currentStep, setCurrentStep] = useState(1);
  28. const [uploadedFile, setUploadedFile] = useState<FileData | null>(null);
  29. const [isProcessing, setIsProcessing] = useState(false);
  30. const [importRecord, setImportRecord] = useState<any>(null);
  31. const [summaryData, setSummaryData] = useState<CintasSummary[]>([]);
  32. const [error, setError] = useState<string | null>(null);
  33. const handleFileUploaded = (file: FileData) => {
  34. setUploadedFile(file);
  35. setCurrentStep(2);
  36. setError(null);
  37. };
  38. const handleCreateImportRecord = async () => {
  39. if (!uploadedFile) return;
  40. setIsProcessing(true);
  41. setError(null);
  42. try {
  43. const result = await createCintasImportRecord(uploadedFile.id, uploadedFile.filename);
  44. if (result.success) {
  45. setImportRecord(result.data);
  46. setCurrentStep(3);
  47. } else {
  48. setError(result.error || 'Failed to create import record');
  49. }
  50. } catch (err) {
  51. setError(err instanceof Error ? err.message : 'Unknown error occurred');
  52. } finally {
  53. setIsProcessing(false);
  54. }
  55. };
  56. const handleProcessImportData = async () => {
  57. if (!importRecord) return;
  58. setIsProcessing(true);
  59. setError(null);
  60. try {
  61. const result = await processCintasImportData(importRecord.id);
  62. if (result.success) {
  63. setCurrentStep(4);
  64. // After processing, fetch the summary data
  65. await handleGenerateSummary();
  66. } else {
  67. setError(result.error || 'Failed to process import data');
  68. }
  69. } catch (err) {
  70. setError(err instanceof Error ? err.message : 'Unknown error occurred');
  71. } finally {
  72. setIsProcessing(false);
  73. }
  74. };
  75. const handleGenerateSummary = async () => {
  76. if (!importRecord) return;
  77. setIsProcessing(true);
  78. setError(null);
  79. try {
  80. // This would typically call an API endpoint to run the stored procedure
  81. // For now, we'll simulate the summary generation
  82. const response = await fetch(`/api/imports/${importRecord.id}/summary`);
  83. if (response.ok) {
  84. const data = await response.json();
  85. setSummaryData(data);
  86. } else {
  87. throw new Error('Failed to generate summary');
  88. }
  89. } catch (err) {
  90. setError(err instanceof Error ? err.message : 'Failed to generate summary');
  91. } finally {
  92. setIsProcessing(false);
  93. }
  94. };
  95. const steps = [
  96. {
  97. id: 1,
  98. title: 'Upload Excel File',
  99. description: 'Upload the Cintas Install Calendar Excel file to blob storage',
  100. icon: Upload,
  101. status: currentStep >= 1 ? (uploadedFile ? 'completed' : 'pending') : 'pending',
  102. },
  103. {
  104. id: 2,
  105. title: 'Create Import Record',
  106. description: 'Create an import record with Cintas Install Calendar layout configuration',
  107. icon: FileText,
  108. status: currentStep >= 2 ? (importRecord ? 'completed' : 'pending') : 'disabled',
  109. },
  110. {
  111. id: 3,
  112. title: 'Import Data',
  113. description: 'Read the Excel file and import data into PostgreSQL database',
  114. icon: Database,
  115. status: currentStep >= 3 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
  116. },
  117. {
  118. id: 4,
  119. title: 'Generate Summary',
  120. description: 'Run summary calculations and display results',
  121. icon: BarChart3,
  122. status: currentStep >= 4 ? (summaryData.length > 0 ? 'completed' : 'pending') : 'disabled',
  123. },
  124. ];
  125. const getStepStatusColor = (status: string) => {
  126. switch (status) {
  127. case 'completed':
  128. return 'text-green-600 bg-green-50 border-green-200';
  129. case 'pending':
  130. return 'text-blue-600 bg-blue-50 border-blue-200';
  131. case 'disabled':
  132. return 'text-gray-400 bg-gray-50 border-gray-200';
  133. default:
  134. return 'text-gray-600 bg-gray-50 border-gray-200';
  135. }
  136. };
  137. return (
  138. <div className="container mx-auto py-6 px-4 max-w-6xl">
  139. <div className="mb-8">
  140. <h1 className="text-3xl font-bold tracking-tight">Cintas Install Calendar Summary</h1>
  141. <p className="text-muted-foreground">
  142. Follow the workflow steps to upload and process the installation calendar data
  143. </p>
  144. </div>
  145. {error && (
  146. <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
  147. <p className="text-sm text-red-800">{error}</p>
  148. </div>
  149. )}
  150. {/* Workflow Steps */}
  151. <div className="mb-8">
  152. <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
  153. {steps.map((step) => {
  154. const Icon = step.icon;
  155. return (
  156. <Card
  157. key={step.id}
  158. className={`border-2 ${getStepStatusColor(step.status)}`}
  159. >
  160. <CardHeader className="pb-3">
  161. <div className="flex items-center space-x-2">
  162. <Icon className="h-5 w-5" />
  163. <div>
  164. <CardTitle className="text-sm font-medium">
  165. Step {step.id}: {step.title}
  166. </CardTitle>
  167. </div>
  168. </div>
  169. </CardHeader>
  170. <CardContent>
  171. <CardDescription className="text-xs">
  172. {step.description}
  173. </CardDescription>
  174. </CardContent>
  175. </Card>
  176. );
  177. })}
  178. </div>
  179. </div>
  180. {/* Step Content */}
  181. <div className="grid gap-6">
  182. {currentStep === 1 && (
  183. <Card>
  184. <CardHeader>
  185. <CardTitle>Step 1: Upload Excel File</CardTitle>
  186. <CardDescription>
  187. Upload your Cintas Install Calendar Excel file to begin processing
  188. </CardDescription>
  189. </CardHeader>
  190. <CardContent>
  191. <div className="space-y-4">
  192. <UploadForm onFileUploaded={handleFileUploaded} />
  193. {uploadedFile && (
  194. <div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
  195. <div className="flex items-center space-x-2">
  196. <CheckCircle className="h-5 w-5 text-green-600" />
  197. <div>
  198. <p className="text-sm font-medium text-green-800">File uploaded successfully!</p>
  199. <p className="text-sm text-green-600">
  200. {uploadedFile.filename} ({(uploadedFile.size / 1024 / 1024).toFixed(2)} MB)
  201. </p>
  202. </div>
  203. </div>
  204. </div>
  205. )}
  206. </div>
  207. </CardContent>
  208. </Card>
  209. )}
  210. {currentStep === 2 && (
  211. <Card>
  212. <CardHeader>
  213. <CardTitle>Step 2: Create Import Record</CardTitle>
  214. <CardDescription>
  215. Creating import record with Cintas Install Calendar layout configuration
  216. </CardDescription>
  217. </CardHeader>
  218. <CardContent>
  219. <div className="space-y-4">
  220. <p className="text-sm text-muted-foreground">
  221. File: {uploadedFile?.filename}
  222. </p>
  223. <Button
  224. onClick={handleCreateImportRecord}
  225. disabled={isProcessing}
  226. className="w-full"
  227. >
  228. {isProcessing ? (
  229. <>
  230. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  231. Creating Import Record...
  232. </>
  233. ) : (
  234. 'Create Import Record'
  235. )}
  236. </Button>
  237. </div>
  238. </CardContent>
  239. </Card>
  240. )}
  241. {currentStep === 3 && (
  242. <Card>
  243. <CardHeader>
  244. <CardTitle>Step 3: Import Data</CardTitle>
  245. <CardDescription>
  246. Processing Excel file and importing data into database
  247. </CardDescription>
  248. </CardHeader>
  249. <CardContent>
  250. <div className="space-y-4">
  251. <p className="text-sm text-muted-foreground">
  252. Import ID: {importRecord?.id}
  253. </p>
  254. <Button
  255. onClick={handleProcessImportData}
  256. disabled={isProcessing}
  257. className="w-full"
  258. >
  259. {isProcessing ? (
  260. <>
  261. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  262. Processing Import...
  263. </>
  264. ) : (
  265. 'Process Import'
  266. )}
  267. </Button>
  268. </div>
  269. </CardContent>
  270. </Card>
  271. )}
  272. {currentStep === 4 && (
  273. <Card>
  274. <CardHeader>
  275. <CardTitle>Step 4: Generate Summary</CardTitle>
  276. <CardDescription>
  277. Running summary calculations and displaying results
  278. </CardDescription>
  279. </CardHeader>
  280. <CardContent>
  281. <div className="space-y-4">
  282. <Button
  283. onClick={handleGenerateSummary}
  284. disabled={isProcessing}
  285. className="w-full"
  286. >
  287. {isProcessing ? (
  288. <>
  289. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  290. Generating Summary...
  291. </>
  292. ) : (
  293. 'Generate Summary'
  294. )}
  295. </Button>
  296. {summaryData.length > 0 && (
  297. <div className="mt-6">
  298. <h3 className="text-lg font-semibold mb-4">Summary Results</h3>
  299. <div className="overflow-x-auto">
  300. <table className="min-w-full divide-y divide-gray-200">
  301. <thead className="bg-gray-50">
  302. <tr>
  303. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Week</th>
  304. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">TRR Total</th>
  305. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">4 Week Avg</th>
  306. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">TRR + 4Wk</th>
  307. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Power Adds</th>
  308. </tr>
  309. </thead>
  310. <tbody className="bg-white divide-y divide-gray-200">
  311. {summaryData.map((item) => (
  312. <tr key={item.id}>
  313. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{item.week}</td>
  314. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{item.trrTotal}</td>
  315. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{item.fourWkAverages}</td>
  316. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{item.trrPlus4Wk}</td>
  317. <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{item.powerAdds}</td>
  318. </tr>
  319. ))}
  320. </tbody>
  321. </table>
  322. </div>
  323. </div>
  324. )}
  325. </div>
  326. </CardContent>
  327. </Card>
  328. )}
  329. {/* Navigation */}
  330. <div className="flex justify-between">
  331. <Button
  332. variant="outline"
  333. onClick={() => setCurrentStep(Math.max(1, currentStep - 1))}
  334. disabled={currentStep === 1}
  335. >
  336. Previous
  337. </Button>
  338. <Button
  339. onClick={() => setCurrentStep(Math.min(4, currentStep + 1))}
  340. disabled={currentStep === 4 || (currentStep === 1 && !uploadedFile)}
  341. >
  342. {currentStep === 4 ? 'Complete' : 'Next'}
  343. </Button>
  344. </div>
  345. </div>
  346. </div>
  347. );
  348. }