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 */}