diff --git a/thanasoft-back/database/seeders/DatabaseSeeder.php b/thanasoft-back/database/seeders/DatabaseSeeder.php index e9b34b3..23e5c87 100644 --- a/thanasoft-back/database/seeders/DatabaseSeeder.php +++ b/thanasoft-back/database/seeders/DatabaseSeeder.php @@ -30,5 +30,10 @@ class DatabaseSeeder extends Seeder $this->call(QuoteSeeder::class); $this->call(InvoiceSeeder::class); $this->call(AvoirSeeder::class); + + // ── Stock data ──────────────────────────────────────────────────────── + $this->call(WarehouseSeeder::class); + $this->call(StockItemSeeder::class); + $this->call(StockMoveSeeder::class); } } diff --git a/thanasoft-back/database/seeders/StockItemSeeder.php b/thanasoft-back/database/seeders/StockItemSeeder.php new file mode 100644 index 0000000..58ce6f0 --- /dev/null +++ b/thanasoft-back/database/seeders/StockItemSeeder.php @@ -0,0 +1,74 @@ + [ + ['warehouse_id' => 1, 'qty_on_hand' => 150.000, 'safety_stock' => 50.000], // Paris: Normal + ['warehouse_id' => 2, 'qty_on_hand' => 45.000, 'safety_stock' => 50.000], // Marseille: LOW STOCK - ALERT + ['warehouse_id' => 3, 'qty_on_hand' => 120.000, 'safety_stock' => 40.000], // Lyon: Normal + ], + // Product 2: Medium rotation + 2 => [ + ['warehouse_id' => 1, 'qty_on_hand' => 80.000, 'safety_stock' => 30.000], // Paris: Normal + ['warehouse_id' => 2, 'qty_on_hand' => 25.000, 'safety_stock' => 35.000], // Marseille: LOW STOCK - ALERT + ['warehouse_id' => 3, 'qty_on_hand' => 90.000, 'safety_stock' => 30.000], // Lyon: Normal + ], + // Product 3: Low rotation, strategic stock + 3 => [ + ['warehouse_id' => 1, 'qty_on_hand' => 200.000, 'safety_stock' => 80.000], // Paris: Normal + ['warehouse_id' => 2, 'qty_on_hand' => 75.000, 'safety_stock' => 80.000], // Marseille: LOW STOCK - ALERT + ['warehouse_id' => 3, 'qty_on_hand' => 180.000, 'safety_stock' => 70.000], // Lyon: Normal + ], + // Product 4: High value, low volume + 4 => [ + ['warehouse_id' => 1, 'qty_on_hand' => 15.000, 'safety_stock' => 8.000], // Paris: Normal + ['warehouse_id' => 2, 'qty_on_hand' => 5.000, 'safety_stock' => 10.000], // Marseille: LOW STOCK - ALERT + ['warehouse_id' => 3, 'qty_on_hand' => 12.000, 'safety_stock' => 8.000], // Lyon: Normal + ], + // Product 5: Medium rotation + 5 => [ + ['warehouse_id' => 1, 'qty_on_hand' => 120.000, 'safety_stock' => 40.000], // Paris: Normal + ['warehouse_id' => 2, 'qty_on_hand' => 35.000, 'safety_stock' => 45.000], // Marseille: LOW STOCK - ALERT + ['warehouse_id' => 3, 'qty_on_hand' => 110.000, 'safety_stock' => 40.000], // Lyon: Normal + ], + // Product 6: High rotation, high volume + 6 => [ + ['warehouse_id' => 1, 'qty_on_hand' => 500.000, 'safety_stock' => 150.000], // Paris: Normal + ['warehouse_id' => 2, 'qty_on_hand' => 140.000, 'safety_stock' => 160.000], // Marseille: LOW STOCK - ALERT + ['warehouse_id' => 3, 'qty_on_hand' => 480.000, 'safety_stock' => 150.000], // Lyon: Normal + ], + ]; + + foreach ($stockData as $productId => $warehouseStocks) { + foreach ($warehouseStocks as $stock) { + DB::table('stock_items')->updateOrInsert( + [ + 'product_id' => $productId, + 'warehouse_id' => $stock['warehouse_id'], + ], + [ + 'qty_on_hand_base' => $stock['qty_on_hand'], + 'safety_stock_base' => $stock['safety_stock'], + 'updated_at' => now(), + 'created_at' => now(), + ] + ); + } + } + } +} diff --git a/thanasoft-back/database/seeders/StockMoveSeeder.php b/thanasoft-back/database/seeders/StockMoveSeeder.php new file mode 100644 index 0000000..d1d40be --- /dev/null +++ b/thanasoft-back/database/seeders/StockMoveSeeder.php @@ -0,0 +1,74 @@ + 1, 'from_id' => null, 'to_id' => 1, 'qty' => 500, 'type' => 'intake', 'date' => '2025-05-10'], + ['product_id' => 2, 'from_id' => 1, 'to_id' => 2, 'qty' => 120, 'type' => 'transfer', 'date' => '2025-05-15'], + ['product_id' => 3, 'from_id' => null, 'to_id' => 2, 'qty' => 300, 'type' => 'intake', 'date' => '2025-05-20'], + + // June 2025 + ['product_id' => 4, 'from_id' => 1, 'to_id' => null, 'qty' => 45, 'type' => 'sale', 'date' => '2025-06-05'], + ['product_id' => 5, 'from_id' => 2, 'to_id' => 3, 'qty' => 200, 'type' => 'transfer', 'date' => '2025-06-12'], + ['product_id' => 6, 'from_id' => null, 'to_id' => 1, 'qty' => 1000, 'type' => 'intake', 'date' => '2025-06-18'], + + // July 2025 + ['product_id' => 1, 'from_id' => 1, 'to_id' => null, 'qty' => 150, 'type' => 'sale', 'date' => '2025-07-08'], + ['product_id' => 2, 'from_id' => 2, 'to_id' => 1, 'qty' => 80, 'type' => 'transfer', 'date' => '2025-07-14'], + ['product_id' => 3, 'from_id' => 2, 'to_id' => null, 'qty' => 100, 'type' => 'sale', 'date' => '2025-07-22'], + + // August 2025 + ['product_id' => 4, 'from_id' => null, 'to_id' => 2, 'qty' => 50, 'type' => 'intake', 'date' => '2025-08-05'], + ['product_id' => 5, 'from_id' => 3, 'to_id' => 1, 'qty' => 90, 'type' => 'transfer', 'date' => '2025-08-11'], + + // ... skip ahead to recent months for better visibility ... + + // March 2026 + ['product_id' => 6, 'from_id' => null, 'to_id' => 3, 'qty' => 450, 'type' => 'intake', 'date' => '2026-03-10'], + ['product_id' => 1, 'from_id' => 3, 'to_id' => 2, 'qty' => 160, 'type' => 'transfer', 'date' => '2026-03-18'], + ['product_id' => 2, 'from_id' => 1, 'to_id' => null, 'qty' => 95, 'type' => 'sale', 'date' => '2026-03-25'], + + // April 2026 + ['product_id' => 3, 'from_id' => null, 'to_id' => 1, 'qty' => 280, 'type' => 'intake', 'date' => '2026-04-02'], + ['product_id' => 4, 'from_id' => 2, 'to_id' => 3, 'qty' => 15, 'type' => 'transfer', 'date' => '2026-04-08'], + ['product_id' => 5, 'from_id' => 1, 'to_id' => null, 'qty' => 110, 'type' => 'sale', 'date' => '2026-04-15'], + + // May 2026 (Recent - visible in dashboard) + ['product_id' => 6, 'from_id' => 3, 'to_id' => 1, 'qty' => 320, 'type' => 'transfer', 'date' => '2026-05-02'], + ['product_id' => 1, 'from_id' => null, 'to_id' => 2, 'qty' => 200, 'type' => 'intake', 'date' => '2026-05-05'], + ['product_id' => 2, 'from_id' => 2, 'to_id' => null, 'qty' => 75, 'type' => 'sale', 'date' => '2026-05-08'], + ['product_id' => 3, 'from_id' => 1, 'to_id' => 3, 'qty' => 140, 'type' => 'transfer', 'date' => '2026-05-10'], + ['product_id' => 4, 'from_id' => null, 'to_id' => 3, 'qty' => 25, 'type' => 'intake', 'date' => '2026-05-12'], + ]; + + foreach ($movements as $movement) { + DB::table('stock_moves')->insert([ + 'product_id' => $movement['product_id'], + 'from_warehouse_id' => $movement['from_id'], + 'to_warehouse_id' => $movement['to_id'], + 'qty_base' => $movement['qty'], + 'move_type' => $movement['type'], + 'ref_type' => null, + 'ref_id' => null, + 'moved_at' => $movement['date'], + 'created_at' => $movement['date'], + 'updated_at' => $movement['date'], + ]); + } + } +} diff --git a/thanasoft-back/database/seeders/WarehouseSeeder.php b/thanasoft-back/database/seeders/WarehouseSeeder.php new file mode 100644 index 0000000..134ffa1 --- /dev/null +++ b/thanasoft-back/database/seeders/WarehouseSeeder.php @@ -0,0 +1,46 @@ + 'Entrepôt Paris (Principal)', + 'address_line1' => '123 Rue de la Paix', + 'address_line2' => 'Bâtiment A', + 'postal_code' => '75001', + 'city' => 'Paris', + 'country_code' => 'FR', + ], + [ + 'name' => 'Entrepôt Marseille', + 'address_line1' => '456 Avenue de la Mer', + 'address_line2' => 'Zone logistique Sud', + 'postal_code' => '13000', + 'city' => 'Marseille', + 'country_code' => 'FR', + ], + [ + 'name' => 'Entrepôt Lyon', + 'address_line1' => '789 Boulevard de l\'Est', + 'address_line2' => 'Parc industriel', + 'postal_code' => '69000', + 'city' => 'Lyon', + 'country_code' => 'FR', + ], + ]; + + foreach ($warehouses as $warehouse) { + Warehouse::firstOrCreate( + ['name' => $warehouse['name']], + $warehouse + ); + } + } +} diff --git a/thanasoft-front/src/components/Atom/Stats/StockKpiCard.vue b/thanasoft-front/src/components/Atom/Stats/StockKpiCard.vue new file mode 100644 index 0000000..8c4038a --- /dev/null +++ b/thanasoft-front/src/components/Atom/Stats/StockKpiCard.vue @@ -0,0 +1,153 @@ + + + diff --git a/thanasoft-front/src/components/Molecule/Stats/RotationCard.vue b/thanasoft-front/src/components/Molecule/Stats/RotationCard.vue new file mode 100644 index 0000000..759f04d --- /dev/null +++ b/thanasoft-front/src/components/Molecule/Stats/RotationCard.vue @@ -0,0 +1,69 @@ + + + diff --git a/thanasoft-front/src/components/Molecule/Stats/WarehouseMovementCard.vue b/thanasoft-front/src/components/Molecule/Stats/WarehouseMovementCard.vue new file mode 100644 index 0000000..e53cf61 --- /dev/null +++ b/thanasoft-front/src/components/Molecule/Stats/WarehouseMovementCard.vue @@ -0,0 +1,63 @@ + + + diff --git a/thanasoft-front/src/components/Molecule/Stats/WarehouseStockCard.vue b/thanasoft-front/src/components/Molecule/Stats/WarehouseStockCard.vue new file mode 100644 index 0000000..aebe8d2 --- /dev/null +++ b/thanasoft-front/src/components/Molecule/Stats/WarehouseStockCard.vue @@ -0,0 +1,72 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Stock/StockStatsDashboard.vue b/thanasoft-front/src/components/Organism/Stock/StockStatsDashboard.vue new file mode 100644 index 0000000..a2980a0 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Stock/StockStatsDashboard.vue @@ -0,0 +1,101 @@ + + + diff --git a/thanasoft-front/src/services/stockStatistics.ts b/thanasoft-front/src/services/stockStatistics.ts new file mode 100644 index 0000000..9966512 --- /dev/null +++ b/thanasoft-front/src/services/stockStatistics.ts @@ -0,0 +1,66 @@ +import axios from 'axios'; + +export interface ProductRotation { + product_id: number; + product_name: string; + moved_qty_last_12_months: number; + qty_on_hand: number; + rotation_rate: number | null; +} + +export interface WarehouseMovement { + warehouse_id: number; + warehouse_name: string; + incoming_moves: number; + incoming_qty: number; + outgoing_moves: number; + outgoing_qty: number; + total_moves: number; +} + +export interface StockByWarehouse { + warehouse_id: number; + warehouse_name: string; + qty_on_hand: number; + stock_value: number; + alert_count: number; +} + +export interface StockStatistics { + total_products: number; + low_stock_products: number; + expiring_products: number; + total_value: number; + stock_alerts_count: number; + stock_rotation_by_product: ProductRotation[]; + warehouse_movements: WarehouseMovement[]; + stock_by_warehouse: StockByWarehouse[]; +} + +export interface StockStatisticsResponse { + data: StockStatistics; +} + +const API_BASE = process.env.VUE_APP_API_BASE_URL || ''; +const TOKEN = localStorage.getItem('auth_token'); + +export const StockStatisticsService = { + async getStatistics(): Promise { + try { + const response = await axios.get( + `${API_BASE}/api/products/statistics`, + { + headers: { + Authorization: `Bearer ${TOKEN}`, + }, + } + ); + return response.data.data; + } catch (error: any) { + throw new Error( + error.response?.data?.message || + 'Erreur lors de la récupération des statistiques de stock' + ); + } + }, +}; diff --git a/thanasoft-front/src/stores/stockStatisticsStore.ts b/thanasoft-front/src/stores/stockStatisticsStore.ts new file mode 100644 index 0000000..39a1f61 --- /dev/null +++ b/thanasoft-front/src/stores/stockStatisticsStore.ts @@ -0,0 +1,45 @@ +import { defineStore } from 'pinia'; +import { StockStatisticsService } from '@/services/stockStatistics'; +import type { StockStatistics } from '@/services/stockStatistics'; + +interface StockStatisticsState { + statistics: StockStatistics | null; + loading: boolean; + error: string | null; +} + +export const useStockStatisticsStore = defineStore('stockStatistics', { + state: (): StockStatisticsState => ({ + statistics: null, + loading: false, + error: null, + }), + + getters: { + isLoading: (state) => state.loading, + hasError: (state) => state.error !== null, + getError: (state) => state.error || '', + hasData: (state) => state.statistics !== null, + }, + + actions: { + async fetchStatistics() { + this.loading = true; + this.error = null; + try { + this.statistics = await StockStatisticsService.getStatistics(); + } catch (err: any) { + this.error = err.message; + this.statistics = null; + } finally { + this.loading = false; + } + }, + + clearStatistics() { + this.statistics = null; + this.error = null; + this.loading = false; + }, + }, +}); diff --git a/thanasoft-front/src/views/pages/Stock/Stock.vue b/thanasoft-front/src/views/pages/Stock/Stock.vue index 0db9142..4dee158 100644 --- a/thanasoft-front/src/views/pages/Stock/Stock.vue +++ b/thanasoft-front/src/views/pages/Stock/Stock.vue @@ -1,11 +1,81 @@ -