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
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:
parent
0ddd57c5b0
commit
27caca0734
@ -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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user