feat: add CSV rates CRUD management to frontend dashboard
Some checks failed
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m38s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Successful in 25m29s
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Deployment Summary (push) Has been skipped
CI/CD Pipeline / Discord Notification (Success) (push) Has been skipped
CI/CD Pipeline / Discord Notification (Failure) (push) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
David 2025-11-17 19:11:37 +01:00
parent 0ddd57c5b0
commit 27caca0734
4 changed files with 162 additions and 2 deletions

View File

@ -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`,
};
}
}

View File

@ -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) => {

View File

@ -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<File | null>(null);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState<string | null>(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() {
</p>
</div>
{/* Company Email */}
<div className="space-y-2">
<Label htmlFor="company-email">
Email de la compagnie <span className="text-red-500">*</span>
</Label>
<Input
id="company-email"
type="email"
value={companyEmail}
onChange={e => setCompanyEmail(e.target.value)}
placeholder="Ex: bookings@sscconsolidation.com"
required
disabled={loading}
/>
<p className="text-xs text-muted-foreground">
Email pour les demandes de réservation auprès de cette compagnie
</p>
</div>
{/* File Input */}
<div className="space-y-2">
<Label htmlFor="file-input">
@ -186,7 +209,7 @@ export function CsvUpload() {
{/* Actions */}
<div className="flex gap-2">
<Button type="submit" disabled={loading || !file || !companyName}>
<Button type="submit" disabled={loading || !file || !companyName || !companyEmail}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />