New-Thanasoft/thanasoft-front/FILE_MANAGEMENT_FRONTEND.md
2025-12-01 17:02:01 +03:00

816 lines
20 KiB
Markdown

# File Management Frontend System
## Overview
Complete frontend file management system with Vue.js 3, TypeScript, and Pinia for state management. This system provides file upload, organization, filtering, and management capabilities.
## Architecture
- **FileService**: API communication and data transformation
- **FileStore**: Pinia store for state management
- **TypeScript Interfaces**: Type safety for all data structures
## Installation
The file service and store are ready to use. Import them in your components:
```typescript
import { useFileStore } from "@/stores/fileStore";
import FileService from "@/services/file";
```
## FileStore Usage
### Basic Setup
```typescript
import { useFileStore } from "@/stores/fileStore";
const fileStore = useFileStore();
// Reactive state
const {
files,
isLoading,
hasError,
getError,
getPagination,
totalSizeFormatted,
} = storeToRefs(fileStore);
// Actions
const { fetchFiles, uploadFile, deleteFile, searchFiles } = fileStore;
```
### Store State Properties
#### Files Management
- `files` - Array of all files
- `currentFile` - Currently selected/viewed file
- `selectedFiles` - Array of selected file IDs for bulk operations
- `filters` - Current filtering criteria
- `pagination` - Pagination metadata
#### Loading States
- `loading` - General loading state
- `uploadProgress` - Upload progress (0-100%)
- `error` - Error message
- `hasError` - Boolean error state
#### Statistics
- `organizedFiles` - Files grouped by category/subcategory
- `storageStats` - Storage usage statistics
### Store Actions
#### File Retrieval
```typescript
// Get all files with pagination and filters
await fileStore.fetchFiles({
page: 1,
per_page: 15,
search: "document",
category: "devis",
sort_by: "uploaded_at",
sort_direction: "desc",
});
// Get specific file by ID
const file = await fileStore.fetchFile(123);
// Search files
await fileStore.searchFiles("invoice", { page: 1, per_page: 10 });
// Get files by category
await fileStore.fetchFilesByCategory("devis", { per_page: 20 });
// Get files by client
await fileStore.fetchFilesByClient(456, { per_page: 15 });
```
#### File Upload
```typescript
// Upload a single file
const fileInput = ref<HTMLInputElement>();
const handleFileUpload = async (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
try {
await fileStore.uploadFile({
file,
category: "devis",
client_id: 123,
subcategory: "annual",
description: "Annual quote document",
tags: ["quote", "annual"],
is_public: false,
});
console.log("File uploaded successfully");
} catch (error) {
console.error("Upload failed:", error);
}
}
};
```
#### File Management
```typescript
// Update file metadata
await fileStore.updateFile({
id: 123,
file_name: "updated_filename.pdf",
description: "Updated description",
tags: ["updated", "tag"],
category: "facture",
});
// Delete single file
await fileStore.deleteFile(123);
// Delete multiple files
await fileStore.deleteMultipleFiles([123, 124, 125]);
// Download file
await fileStore.downloadFile(123);
// Generate download URL
const downloadUrl = await fileStore.generateDownloadUrl(123);
```
#### Filtering and Organization
```typescript
// Set filters
fileStore.setFilters({
category: "devis",
client_id: 123,
date_from: "2024-01-01",
date_to: "2024-12-31",
mime_type: "application/pdf",
});
// Clear filters
fileStore.clearFilters();
// Get organized structure
await fileStore.fetchOrganizedStructure();
// Get storage statistics
await fileStore.fetchStorageStatistics();
```
#### Selection Management
```typescript
// Select/deselect files
fileStore.selectFile(123);
fileStore.deselectFile(123);
// Select all/none
fileStore.selectAllFiles();
fileStore.deselectAllFiles();
// Get selected files
const selectedFiles = computed(() => fileStore.getSelectedFiles.value);
```
### Computed Properties
```typescript
// Basic getters
const allFiles = computed(() => fileStore.allFiles);
const isLoading = computed(() => fileStore.isLoading);
const hasError = computed(() => fileStore.hasError);
const totalSize = computed(() => fileStore.totalSizeFormatted);
// Filtered views
const imageFiles = computed(() => fileStore.imageFiles);
const pdfFiles = computed(() => fileStore.pdfFiles);
const recentFiles = computed(() => fileStore.recentFiles);
// Grouped views
const filesByCategory = computed(() => fileStore.filesByCategory);
const filesByClient = computed(() => fileStore.filesByClient);
// Pagination
const pagination = computed(() => fileStore.getPagination);
```
## FileService Usage
### Direct Service Methods
```typescript
import FileService from "@/services/file";
// File validation before upload
const validation = FileService.validateFile(file, 10 * 1024 * 1024); // 10MB
if (!validation.valid) {
console.error(validation.error);
return;
}
// Format file size
const sizeFormatted = FileService.formatFileSize(1024000); // "1000.00 KB"
// Get file icon
const icon = FileService.getFileIcon("application/pdf"); // "📄"
// Check file type
const isImage = FileService.isImageFile("image/jpeg"); // true
const isPdf = FileService.isPdfFile("application/pdf"); // true
// Get file extension
const extension = FileService.getFileExtension("document.pdf"); // "pdf"
```
## Component Examples
### File List Component
```vue
<template>
<div class="file-management">
<!-- Header with actions -->
<div class="flex justify-between items-center mb-4">
<h2>Fichiers ({{ fileStore.files.length }})</h2>
<div class="flex gap-2">
<SoftButton @click="refreshFiles" :loading="fileStore.isLoading">
Actualiser
</SoftButton>
<SoftButton @click="showUploadModal = true" color="primary">
Télécharger
</SoftButton>
</div>
</div>
<!-- Filters -->
<div class="filters mb-4">
<input
v-model="searchQuery"
@input="handleSearch"
placeholder="Rechercher des fichiers..."
class="form-control"
/>
<select v-model="selectedCategory" @change="handleCategoryChange">
<option value="">Toutes les catégories</option>
<option value="devis">Devis</option>
<option value="facture">Factures</option>
<option value="contrat">Contrats</option>
<option value="document">Documents</option>
<option value="image">Images</option>
<option value="autre">Autres</option>
</select>
</div>
<!-- File list -->
<div v-if="fileStore.isLoading" class="text-center">
<div class="spinner">Chargement...</div>
</div>
<div v-else-if="fileStore.hasError" class="alert alert-danger">
{{ fileStore.getError }}
</div>
<div v-else class="file-grid">
<div
v-for="file in fileStore.files"
:key="file.id"
class="file-card"
:class="{ selected: fileStore.selectedFiles.includes(file.id) }"
@click="toggleFileSelection(file.id)"
>
<div class="file-icon">
{{ FileService.getFileIcon(file.mime_type) }}
</div>
<div class="file-info">
<h4>{{ file.file_name }}</h4>
<p>{{ file.size_formatted }} {{ file.category }}</p>
<small>{{ formatDate(file.uploaded_at) }}</small>
</div>
<div class="file-actions">
<button @click.stop="downloadFile(file.id)" class="btn btn-sm">
Télécharger
</button>
<button @click.stop="editFile(file.id)" class="btn btn-sm">
Modifier
</button>
<button
@click.stop="confirmDelete(file.id)"
class="btn btn-sm text-danger"
>
Supprimer
</button>
</div>
</div>
</div>
<!-- Pagination -->
<div class="pagination mt-4">
<SoftPagination
:current-page="fileStore.getPagination.current_page"
:last-page="fileStore.getPagination.last_page"
@page-changed="handlePageChange"
/>
</div>
<!-- Upload Modal -->
<FileUploadModal
v-if="showUploadModal"
@close="showUploadModal = false"
@uploaded="handleFileUploaded"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useFileStore } from "@/stores/fileStore";
import FileService from "@/services/file";
const fileStore = useFileStore();
const showUploadModal = ref(false);
const searchQuery = ref("");
const selectedCategory = ref("");
onMounted(() => {
fileStore.fetchFiles();
});
const handleSearch = () => {
fileStore.setFilters({ search: searchQuery.value });
fileStore.fetchFiles({ page: 1 });
};
const handleCategoryChange = () => {
fileStore.setFilters({ category: selectedCategory.value });
fileStore.fetchFiles({ page: 1 });
};
const handlePageChange = (page: number) => {
fileStore.fetchFiles({ page });
};
const toggleFileSelection = (fileId: number) => {
if (fileStore.selectedFiles.includes(fileId)) {
fileStore.deselectFile(fileId);
} else {
fileStore.selectFile(fileId);
}
};
const downloadFile = async (fileId: number) => {
try {
await fileStore.downloadFile(fileId);
} catch (error) {
console.error("Download failed:", error);
}
};
const editFile = (fileId: number) => {
// Navigate to file edit page or open modal
router.push(`/files/${fileId}/edit`);
};
const confirmDelete = (fileId: number) => {
if (confirm("Êtes-vous sûr de vouloir supprimer ce fichier ?")) {
fileStore.deleteFile(fileId);
}
};
const handleFileUploaded = () => {
showUploadModal.value = false;
fileStore.fetchFiles({ page: 1 });
};
const refreshFiles = () => {
fileStore.fetchFiles();
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("fr-FR");
};
</script>
```
### File Upload Component
```vue
<template>
<div class="upload-modal">
<div class="modal-header">
<h3>Télécharger un fichier</h3>
<button @click="$emit('close')" class="close-btn">&times;</button>
</div>
<form @submit.prevent="handleUpload" class="upload-form">
<!-- File selection -->
<div class="form-group">
<label>Fichier *</label>
<input
type="file"
@change="handleFileSelect"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
required
/>
<div v-if="selectedFile" class="file-preview">
<p>
{{ selectedFile.name }} ({{
FileService.formatFileSize(selectedFile.size)
}})
</p>
</div>
</div>
<!-- Category -->
<div class="form-group">
<label>Catégorie *</label>
<select v-model="form.category" required>
<option value="">Sélectionner une catégorie</option>
<option value="devis">Devis</option>
<option value="facture">Facture</option>
<option value="contrat">Contrat</option>
<option value="document">Document</option>
<option value="image">Image</option>
<option value="autre">Autre</option>
</select>
</div>
<!-- Client ID -->
<div class="form-group">
<label>Client (optionnel)</label>
<input
v-model.number="form.client_id"
type="number"
placeholder="ID du client"
/>
</div>
<!-- Subcategory -->
<div class="form-group">
<label>Sous-catégorie (optionnel)</label>
<input
v-model="form.subcategory"
type="text"
placeholder="Ex: annual, monthly"
/>
</div>
<!-- Description -->
<div class="form-group">
<label>Description</label>
<textarea
v-model="form.description"
placeholder="Description du fichier"
rows="3"
></textarea>
</div>
<!-- Tags -->
<div class="form-group">
<label>Étiquettes</label>
<input
v-model="tagInput"
@keydown.enter.prevent="addTag"
placeholder="Ajouter une étiquette et appuyer sur Entrée"
/>
<div class="tags">
<span v-for="(tag, index) in form.tags" :key="index" class="tag">
{{ tag }}
<button type="button" @click="removeTag(index)">&times;</button>
</span>
</div>
</div>
<!-- Public checkbox -->
<div class="form-group">
<label class="checkbox-label">
<input v-model="form.is_public" type="checkbox" />
Fichier public
</label>
</div>
<!-- Progress bar -->
<div v-if="fileStore.isLoading" class="progress-bar">
<div
class="progress-fill"
:style="{ width: fileStore.getUploadProgress + '%' }"
></div>
</div>
<!-- Error message -->
<div v-if="fileStore.hasError" class="alert alert-danger">
{{ fileStore.getError }}
</div>
<!-- Actions -->
<div class="modal-actions">
<button
type="button"
@click="$emit('close')"
:disabled="fileStore.isLoading"
>
Annuler
</button>
<button
type="submit"
:disabled="!selectedFile || !form.category || fileStore.isLoading"
class="btn-primary"
>
{{ fileStore.isLoading ? "Téléchargement..." : "Télécharger" }}
</button>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { useFileStore } from "@/stores/fileStore";
import FileService from "@/services/file";
const emit = defineEmits<{
close: [];
uploaded: [];
}>();
const fileStore = useFileStore();
const selectedFile = ref<File | null>(null);
const tagInput = ref("");
const form = reactive({
category: "",
client_id: undefined as number | undefined,
subcategory: "",
description: "",
tags: [] as string[],
is_public: false,
});
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
// Validate file
const validation = FileService.validateFile(file);
if (!validation.valid) {
alert(validation.error);
target.value = "";
return;
}
selectedFile.value = file;
}
};
const addTag = () => {
const tag = tagInput.value.trim();
if (tag && !form.tags.includes(tag) && form.tags.length < 10) {
form.tags.push(tag);
tagInput.value = "";
}
};
const removeTag = (index: number) => {
form.tags.splice(index, 1);
};
const handleUpload = async () => {
if (!selectedFile.value || !form.category) return;
try {
await fileStore.uploadFile({
file: selectedFile.value,
...form,
});
emit("uploaded");
} catch (error) {
console.error("Upload failed:", error);
}
};
</script>
```
### File Statistics Component
```vue
<template>
<div class="file-statistics">
<div class="stats-header">
<h3>Statistiques de Stockage</h3>
<button @click="refreshStats" :disabled="fileStore.isLoading">
Actualiser
</button>
</div>
<div v-if="fileStore.storageStats" class="stats-content">
<!-- Overview cards -->
<div class="stats-grid">
<div class="stat-card">
<h4>Total des Fichiers</h4>
<p class="stat-number">{{ fileStore.storageStats.total_files }}</p>
</div>
<div class="stat-card">
<h4>Espace Utilisé</h4>
<p class="stat-number">
{{ fileStore.storageStats.total_size_formatted }}
</p>
</div>
</div>
<!-- Files by type -->
<div class="stats-section">
<h4>Par Type de Fichier</h4>
<div class="type-grid">
<div
v-for="(typeData, mimeType) in fileStore.storageStats.by_type"
:key="mimeType"
class="type-card"
>
<div class="type-icon">{{ FileService.getFileIcon(mimeType) }}</div>
<div class="type-info">
<p class="type-name">{{ getTypeDisplayName(mimeType) }}</p>
<p class="type-count">{{ typeData.count }} fichiers</p>
<p class="type-size">
{{ FileService.formatFileSize(typeData.total_size) }}
</p>
</div>
</div>
</div>
</div>
<!-- Files by category -->
<div class="stats-section">
<h4>Par Catégorie</h4>
<div class="category-grid">
<div
v-for="(categoryData, category) in fileStore.storageStats
.by_category"
:key="category"
class="category-card"
>
<h5>{{ getCategoryDisplayName(category) }}</h5>
<div class="category-stats">
<span>{{ categoryData.count }} fichiers</span>
<span>{{
FileService.formatFileSize(categoryData.total_size)
}}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="fileStore.isLoading" class="loading">
Chargement des statistiques...
</div>
<div v-else-if="fileStore.hasError" class="error">
{{ fileStore.getError }}
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { useFileStore } from "@/stores/fileStore";
import FileService from "@/services/file";
const fileStore = useFileStore();
onMounted(() => {
fileStore.fetchStorageStatistics();
});
const refreshStats = () => {
fileStore.fetchStorageStatistics();
};
const getTypeDisplayName = (mimeType: string): string => {
const typeMap: Record<string, string> = {
"application/pdf": "PDF",
"image/jpeg": "Image JPEG",
"image/png": "Image PNG",
"application/msword": "Document Word",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
"Document Word",
};
return typeMap[mimeType] || mimeType;
};
const getCategoryDisplayName = (category: string): string => {
const categoryMap: Record<string, string> = {
devis: "Devis",
facture: "Factures",
contrat: "Contrats",
document: "Documents",
image: "Images",
autre: "Autres",
};
return categoryMap[category] || category;
};
</script>
```
## Best Practices
### Error Handling
```typescript
try {
await fileStore.uploadFile(payload);
// Success handling
} catch (error: any) {
// Display user-friendly error message
const message = error.response?.data?.message || error.message;
// Show in toast/notification
}
```
### File Validation
```typescript
// Always validate files before upload
const validation = FileService.validateFile(file);
if (!validation.valid) {
showError(validation.error);
return;
}
```
### Progress Tracking
```vue
<template>
<div v-if="fileStore.isLoading" class="upload-progress">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: fileStore.getUploadProgress + '%' }"
></div>
</div>
<p>{{ fileStore.getUploadProgress }}%</p>
</div>
</template>
```
### Bulk Operations
```typescript
// Delete multiple files
const deleteSelected = async () => {
if (fileStore.selectedFiles.length === 0) return;
if (confirm(`Supprimer ${fileStore.selectedFiles.length} fichiers ?`)) {
await fileStore.deleteMultipleFiles(fileStore.selectedFiles);
fileStore.deselectAllFiles();
}
};
```
### Performance Optimization
```typescript
// Use computed properties for filtered views
const pdfFiles = computed(() => fileStore.pdfFiles);
const imageFiles = computed(() => fileStore.imageFiles);
// Cache expensive operations
const fileStats = computed(() => {
return {
totalSize: fileStore.totalSizeFormatted,
fileCount: fileStore.files.length,
};
});
```
## Integration with Backend
The frontend system integrates with the Laravel backend API through the FileService. All API endpoints are mapped:
- `GET /api/files``FileService.getAllFiles()`
- `POST /api/files``FileService.uploadFile()`
- `GET /api/files/{id}``FileService.getFile()`
- `PUT /api/files/{id}``FileService.updateFile()`
- `DELETE /api/files/{id}``FileService.deleteFile()`
- And specialized endpoints for categories, clients, statistics, etc.
This ensures full compatibility with the backend file management system while providing a rich, type-safe frontend experience.