From 27caca073486a62216fd2d9d14588cc794aac7bf Mon Sep 17 00:00:00 2001 From: David Date: Mon, 17 Nov 2025 19:11:37 +0100 Subject: [PATCH] feat: add CSV rates CRUD management to frontend dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added comprehensive CSV rates management interface to the frontend dashboard with full CRUD operations. ## Backend Changes - Added `GET /api/v1/admin/csv-rates/files` endpoint to list all uploaded CSV files with metadata - Added `DELETE /api/v1/admin/csv-rates/files/:filename` endpoint to delete CSV files and their configurations - Both endpoints provide frontend-compatible responses with file info (filename, size, rowCount, uploadedAt) - File deletion includes both filesystem cleanup and database configuration removal ## Frontend Changes - Added "CSV Rates" navigation item to dashboard sidebar (ADMIN only) - Moved CSV rates page from `/app/admin/csv-rates` to `/app/dashboard/admin/csv-rates` for proper dashboard integration - Updated CsvUpload component to include required `companyEmail` field - Component now properly validates and sends all required fields (companyName, companyEmail, file) - Enhanced form validation with email input type ## Features - ✅ Upload CSV rate files with company name and email - ✅ List all uploaded CSV files with metadata (filename, size, row count, upload date) - ✅ Delete CSV files with confirmation dialog - ✅ Real-time file validation (format, size limit 10MB) - ✅ Auto-refresh after successful operations - ✅ ADMIN role-based access control - ✅ Integrated into dashboard navigation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../controllers/admin/csv-rates.controller.ts | 133 ++++++++++++++++++ .../dashboard}/admin/csv-rates/page.tsx | 0 apps/frontend/app/dashboard/layout.tsx | 4 + .../src/components/admin/CsvUpload.tsx | 27 +++- 4 files changed, 162 insertions(+), 2 deletions(-) rename apps/frontend/{src/app => app/dashboard}/admin/csv-rates/page.tsx (100%) diff --git a/apps/backend/src/application/controllers/admin/csv-rates.controller.ts b/apps/backend/src/application/controllers/admin/csv-rates.controller.ts index dc2c069..67bda15 100644 --- a/apps/backend/src/application/controllers/admin/csv-rates.controller.ts +++ b/apps/backend/src/application/controllers/admin/csv-rates.controller.ts @@ -348,4 +348,137 @@ export class CsvRatesAdminController { this.logger.log(`Deleted CSV config for company: ${companyName}`); } + + /** + * List all CSV files (Frontend compatibility endpoint) + * Maps to GET /files for compatibility with frontend API client + */ + @Get('files') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'List all CSV files (ADMIN only)', + description: 'Returns list of all uploaded CSV files with metadata. Alias for /config endpoint.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'List of CSV files', + schema: { + type: 'object', + properties: { + files: { + type: 'array', + items: { + type: 'object', + properties: { + filename: { type: 'string', example: 'ssc-consolidation.csv' }, + size: { type: 'number', example: 2048 }, + uploadedAt: { type: 'string', format: 'date-time' }, + rowCount: { type: 'number', example: 150 }, + }, + }, + }, + }, + }, + }) + async listFiles(): Promise<{ files: any[] }> { + this.logger.log('Fetching all CSV files (frontend compatibility)'); + + const configs = await this.csvConfigRepository.findAll(); + const fs = require('fs'); + const path = require('path'); + + // Map configs to file info format expected by frontend + const files = configs.map((config) => { + const filePath = path.join( + process.cwd(), + 'apps/backend/src/infrastructure/storage/csv-storage/rates', + config.csvFilePath + ); + + let fileSize = 0; + try { + const stats = fs.statSync(filePath); + fileSize = stats.size; + } catch (error) { + this.logger.warn(`Could not get file size for ${config.csvFilePath}`); + } + + return { + filename: config.csvFilePath, + size: fileSize, + uploadedAt: config.uploadedAt.toISOString(), + rowCount: config.rowCount, + }; + }); + + return { files }; + } + + /** + * Delete CSV file (Frontend compatibility endpoint) + * Maps to DELETE /files/:filename + */ + @Delete('files/:filename') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Delete CSV file by filename (ADMIN only)', + description: 'Deletes a CSV file and its configuration from the system.', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'File deleted successfully', + schema: { + type: 'object', + properties: { + success: { type: 'boolean', example: true }, + message: { type: 'string', example: 'File deleted successfully' }, + }, + }, + }) + @ApiResponse({ + status: 404, + description: 'File not found', + }) + async deleteFile( + @Param('filename') filename: string, + @CurrentUser() user: UserPayload + ): Promise<{ success: boolean; message: string }> { + this.logger.warn(`[Admin: ${user.email}] Deleting CSV file: ${filename}`); + + // Find config by file path + const configs = await this.csvConfigRepository.findAll(); + const config = configs.find((c) => c.csvFilePath === filename); + + if (!config) { + throw new BadRequestException(`No configuration found for file: ${filename}`); + } + + // Delete the file from filesystem + const fs = require('fs'); + const path = require('path'); + const filePath = path.join( + process.cwd(), + 'apps/backend/src/infrastructure/storage/csv-storage/rates', + filename + ); + + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + this.logger.log(`Deleted file: ${filePath}`); + } + } catch (error: any) { + this.logger.error(`Failed to delete file ${filePath}: ${error.message}`); + } + + // Delete the configuration + await this.csvConfigRepository.delete(config.companyName); + + this.logger.log(`Deleted CSV config and file for: ${config.companyName}`); + + return { + success: true, + message: `File ${filename} deleted successfully`, + }; + } } diff --git a/apps/frontend/src/app/admin/csv-rates/page.tsx b/apps/frontend/app/dashboard/admin/csv-rates/page.tsx similarity index 100% rename from apps/frontend/src/app/admin/csv-rates/page.tsx rename to apps/frontend/app/dashboard/admin/csv-rates/page.tsx diff --git a/apps/frontend/app/dashboard/layout.tsx b/apps/frontend/app/dashboard/layout.tsx index c1119e9..5c35561 100644 --- a/apps/frontend/app/dashboard/layout.tsx +++ b/apps/frontend/app/dashboard/layout.tsx @@ -25,6 +25,10 @@ export default function DashboardLayout({ children }: { children: React.ReactNod { name: 'My Profile', href: '/dashboard/profile', icon: '👤' }, { name: 'Organization', href: '/dashboard/settings/organization', icon: '🏢' }, { name: 'Users', href: '/dashboard/settings/users', icon: '👥' }, + // ADMIN only navigation items + ...(user?.role === 'ADMIN' ? [ + { name: 'CSV Rates', href: '/dashboard/admin/csv-rates', icon: '📄' }, + ] : []), ]; const isActive = (href: string) => { diff --git a/apps/frontend/src/components/admin/CsvUpload.tsx b/apps/frontend/src/components/admin/CsvUpload.tsx index 662288d..6ef5c74 100644 --- a/apps/frontend/src/components/admin/CsvUpload.tsx +++ b/apps/frontend/src/components/admin/CsvUpload.tsx @@ -20,6 +20,7 @@ import { uploadCsvRates } from '@/lib/api/admin/csv-rates'; export function CsvUpload() { const router = useRouter(); const [companyName, setCompanyName] = useState(''); + const [companyEmail, setCompanyEmail] = useState(''); const [file, setFile] = useState(null); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(null); @@ -47,7 +48,7 @@ export function CsvUpload() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!file || !companyName) { + if (!file || !companyName || !companyEmail) { setError('Veuillez remplir tous les champs'); return; } @@ -60,11 +61,13 @@ export function CsvUpload() { const formData = new FormData(); formData.append('file', file); formData.append('companyName', companyName); + formData.append('companyEmail', companyEmail); const result = await uploadCsvRates(formData); setSuccess(`✅ Succès ! ${result.rowCount} tarifs uploadés pour ${result.companyName}`); setCompanyName(''); + setCompanyEmail(''); setFile(null); // Reset file input @@ -86,6 +89,7 @@ export function CsvUpload() { const handleReset = () => { setCompanyName(''); + setCompanyEmail(''); setFile(null); setError(null); setSuccess(null); @@ -129,6 +133,25 @@ export function CsvUpload() {

+ {/* Company Email */} +
+ + setCompanyEmail(e.target.value)} + placeholder="Ex: bookings@sscconsolidation.com" + required + disabled={loading} + /> +

+ Email pour les demandes de réservation auprès de cette compagnie +

+
+ {/* File Input */}