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

20 KiB

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:

import { useFileStore } from "@/stores/fileStore";
import FileService from "@/services/file";

FileStore Usage

Basic Setup

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

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

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

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

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

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

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

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

<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

<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

<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

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

// Always validate files before upload
const validation = FileService.validateFile(file);
if (!validation.valid) {
  showError(validation.error);
  return;
}

Progress Tracking

<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

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

// 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/filesFileService.getAllFiles()
  • POST /api/filesFileService.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.