Feat: stat front end

This commit is contained in:
kevin 2026-05-13 10:37:34 +03:00
parent 39c21d3d09
commit 9a52bddd1a
12 changed files with 845 additions and 7 deletions

View File

@ -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);
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Database\Seeders;
use App\Models\Product;
use App\Models\Warehouse;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class StockItemSeeder extends Seeder
{
public function run(): void
{
$products = Product::all();
$warehouses = Warehouse::all();
// Define realistic stock quantities for each product × warehouse
$stockData = [
// Product 1: High rotation, variable stock
1 => [
['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(),
]
);
}
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Database\Seeders;
use App\Models\Product;
use App\Models\Warehouse;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class StockMoveSeeder extends Seeder
{
public function run(): void
{
$products = Product::all();
$warehouses = Warehouse::all();
// Sample movements over the past 12 months
// Mix of transfers, intakes, adjustments, sales
$movements = [
// May 2025
['product_id' => 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'],
]);
}
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Database\Seeders;
use App\Models\Warehouse;
use Illuminate\Database\Seeder;
class WarehouseSeeder extends Seeder
{
public function run(): void
{
$warehouses = [
[
'name' => '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
);
}
}
}

View File

@ -0,0 +1,153 @@
<template>
<div class="overflow-hidden card">
<div class="p-3 pb-0 card-header">
<h6 class="mb-0 font-weight-bolder">{{ label }}</h6>
<p class="mb-0 text-sm text-secondary">{{ subtitle }}</p>
</div>
<div class="p-0 card-body">
<div v-if="chartId && chart" class="p-3">
<canvas :id="chartId" />
</div>
<div class="p-3 pt-0">
<div class="row align-items-center">
<div class="col-8">
<h5 class="mb-0 font-weight-bolder">{{ value }}</h5>
<span v-if="trend" class="text-sm text-secondary" v-html="trend"></span>
</div>
<div v-if="suffix" class="col-4 text-end">
<span class="text-lg font-weight-bolder text-secondary">{{ suffix }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, onMounted, watch } from 'vue';
import Chart from 'chart.js/auto';
interface MiniChartData {
labels: string[];
datasets: Array<{
label: string;
data: number[];
borderColor?: string;
backgroundColor?: string;
tension?: number;
fill?: boolean;
borderWidth?: number;
}>;
color?: string;
}
export default defineComponent({
name: 'StockKpiCard',
props: {
label: {
type: String,
required: true,
},
value: {
type: [String, Number],
required: true,
},
suffix: {
type: String,
default: '',
},
trend: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
chartId: {
type: String,
default: '',
},
chartHeight: {
type: Number,
default: 100,
},
chart: {
type: Object as PropType<MiniChartData>,
default: null,
},
},
setup(props) {
const chartInstance = { current: null as Chart | null };
const initChart = () => {
if (!props.chartId || !props.chart) return;
const canvas = document.getElementById(props.chartId) as HTMLCanvasElement;
if (!canvas) return;
if (chartInstance.current) {
chartInstance.current.destroy();
}
const ctx = canvas.getContext('2d');
if (!ctx) return;
const color = props.chart.color || '#344767';
chartInstance.current = new Chart(ctx, {
type: 'line',
data: {
labels: props.chart.labels,
datasets: [
{
label: props.chart.datasets[0]?.label || 'Data',
data: props.chart.datasets[0]?.data || [],
borderColor: color,
backgroundColor: `rgba(100, 115, 130, 0.15)`,
tension: 0.4,
fill: true,
borderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
y: {
beginAtZero: true,
grid: {
drawBorder: false,
color: 'rgba(0,0,0,0.05)',
},
},
x: {
grid: {
drawBorder: false,
display: false,
},
},
},
},
});
};
onMounted(() => {
initChart();
});
watch(() => props.chart, () => {
initChart();
});
return {};
},
});
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="overflow-hidden card">
<div class="p-3 pb-0 card-header">
<h6 class="mb-0 font-weight-bolder">Top produits par rotation</h6>
<p class="mb-0 text-sm text-secondary">Produits les plus consommés (12 mois)</p>
</div>
<div class="p-0 card-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="text-uppercase text-secondary text-xxs font-weight-bolder ps-2">
<tr>
<th>Produit</th>
<th>Stock</th>
<th>Rotations</th>
<th>Taux</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.product_id">
<td class="ps-2">
<p class="text-sm font-weight-bold mb-0">{{ item.product_name }}</p>
</td>
<td>
<p class="text-sm mb-0">{{ formatNumber(item.qty_on_hand) }}</p>
</td>
<td>
<p class="text-sm mb-0">{{ formatNumber(item.moved_qty_last_12_months) }}</p>
</td>
<td>
<div class="d-flex align-items-center">
<span v-if="item.rotation_rate" class="badge badge-sm bg-info">
{{ formatRotationRate(item.rotation_rate) }}x
</span>
<span v-else class="badge badge-sm bg-secondary">N/A</span>
</div>
</td>
</tr>
<tr v-if="!items.length">
<td colspan="4" class="text-center text-secondary py-3">Aucun mouvement de stock</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import type { ProductRotation } from '@/services/stockStatistics';
export default defineComponent({
name: 'RotationCard',
props: {
items: {
type: Array as PropType<ProductRotation[]>,
default: () => [],
},
},
methods: {
formatNumber(n: number) {
return Math.round(n * 100) / 100;
},
formatRotationRate(rate: number) {
return Math.round(rate * 100) / 100;
},
},
});
</script>

View File

@ -0,0 +1,63 @@
<template>
<div class="overflow-hidden card">
<div class="p-3 pb-0 card-header">
<h6 class="mb-0 font-weight-bolder">Mouvements par entrepôt</h6>
<p class="mb-0 text-sm text-secondary">Flux entrants et sortants</p>
</div>
<div class="p-0 card-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="text-uppercase text-secondary text-xxs font-weight-bolder ps-2">
<tr>
<th>Entrepôt</th>
<th>Entrées</th>
<th>Sorties</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr v-for="warehouse in items" :key="warehouse.warehouse_id">
<td class="ps-2">
<p class="text-sm font-weight-bold mb-0">{{ warehouse.warehouse_name }}</p>
</td>
<td>
<span class="badge badge-sm bg-success me-1">{{ warehouse.incoming_moves }}</span>
<small class="text-secondary">{{ formatNumber(warehouse.incoming_qty) }}</small>
</td>
<td>
<span class="badge badge-sm bg-warning me-1">{{ warehouse.outgoing_moves }}</span>
<small class="text-secondary">{{ formatNumber(warehouse.outgoing_qty) }}</small>
</td>
<td>
<p class="text-sm font-weight-bold mb-0">{{ warehouse.total_moves }}</p>
</td>
</tr>
<tr v-if="!items.length">
<td colspan="4" class="text-center text-secondary py-3">Aucun entrepôt</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import type { WarehouseMovement } from '@/services/stockStatistics';
export default defineComponent({
name: 'WarehouseMovementCard',
props: {
items: {
type: Array as PropType<WarehouseMovement[]>,
default: () => [],
},
},
methods: {
formatNumber(n: number) {
return Math.round(n * 100) / 100;
},
},
});
</script>

View File

@ -0,0 +1,72 @@
<template>
<div class="overflow-hidden card">
<div class="p-3 pb-0 card-header">
<h6 class="mb-0 font-weight-bolder">Stock par entrepôt</h6>
<p class="mb-0 text-sm text-secondary">Valeur et alertes</p>
</div>
<div class="p-0 card-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead class="text-uppercase text-secondary text-xxs font-weight-bolder ps-2">
<tr>
<th>Entrepôt</th>
<th>Quantité</th>
<th>Valeur</th>
<th>Alertes</th>
</tr>
</thead>
<tbody>
<tr v-for="warehouse in items" :key="warehouse.warehouse_id">
<td class="ps-2">
<p class="text-sm font-weight-bold mb-0">{{ warehouse.warehouse_name }}</p>
</td>
<td>
<p class="text-sm mb-0">{{ formatNumber(warehouse.qty_on_hand) }}</p>
</td>
<td>
<p class="text-sm font-weight-bold mb-0">{{ formatCurrency(warehouse.stock_value) }}</p>
</td>
<td>
<span v-if="warehouse.alert_count > 0" class="badge badge-sm bg-danger">
{{ warehouse.alert_count }}
</span>
<span v-else class="badge badge-sm bg-success"></span>
</td>
</tr>
<tr v-if="!items.length">
<td colspan="4" class="text-center text-secondary py-3">Aucun entrepôt</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import type { StockByWarehouse } from '@/services/stockStatistics';
export default defineComponent({
name: 'WarehouseStockCard',
props: {
items: {
type: Array as PropType<StockByWarehouse[]>,
default: () => [],
},
},
methods: {
formatNumber(n: number) {
return Math.round(n * 100) / 100;
},
formatCurrency(n: number) {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact',
maximumFractionDigits: 1,
}).format(n);
},
},
});
</script>

View File

@ -0,0 +1,101 @@
<template>
<div>
<!-- Row 1: KPI cards -->
<div class="row g-4 mb-4">
<div class="col-xl-3 col-sm-6">
<stock-kpi-card
label="Total produits"
:value="stats.total_products"
subtitle="En stock"
/>
</div>
<div class="col-xl-3 col-sm-6">
<stock-kpi-card
label="Stock faible"
:value="stats.low_stock_products"
:trend="lowStockTrend"
subtitle="À ≤ minimum"
/>
</div>
<div class="col-xl-3 col-sm-6">
<stock-kpi-card
label="Alertes actives"
:value="stats.stock_alerts_count"
subtitle="Dessous du seuil de sécurité"
/>
</div>
<div class="col-xl-3 col-sm-6">
<stock-kpi-card
label="Valeur totale"
:value="stockValueLabel"
subtitle="Immobilisation financière"
/>
</div>
</div>
<!-- Row 2: Top rotation + Warehouse movements -->
<div class="row g-4 mb-4">
<div class="col-xl-6">
<rotation-card :items="stats.stock_rotation_by_product" />
</div>
<div class="col-xl-6">
<warehouse-movement-card :items="stats.warehouse_movements" />
</div>
</div>
<!-- Row 3: Warehouse stock -->
<div class="row g-4">
<div class="col-12">
<warehouse-stock-card :items="stats.stock_by_warehouse" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from 'vue';
import type { StockStatistics } from '@/services/stockStatistics';
import StockKpiCard from '@/components/Atom/Stats/StockKpiCard.vue';
import RotationCard from '@/components/Molecule/Stats/RotationCard.vue';
import WarehouseMovementCard from '@/components/Molecule/Stats/WarehouseMovementCard.vue';
import WarehouseStockCard from '@/components/Molecule/Stats/WarehouseStockCard.vue';
export default defineComponent({
name: 'StockStatsDashboard',
components: {
StockKpiCard,
RotationCard,
WarehouseMovementCard,
WarehouseStockCard,
},
props: {
stats: {
type: Object as PropType<StockStatistics>,
required: true,
},
},
setup(props) {
const stockValueLabel = computed(() => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact',
maximumFractionDigits: 1,
}).format(props.stats.total_value);
});
const lowStockTrend = computed(() => {
const total = props.stats.total_products;
const lowStock = props.stats.low_stock_products;
const pct = total > 0 ? Math.round((lowStock / total) * 100) : 0;
return `<span class="text-danger">${pct}%</span> du catalogue`;
});
return {
stockValueLabel,
lowStockTrend,
};
},
});
</script>

View File

@ -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<StockStatistics> {
try {
const response = await axios.get<StockStatisticsResponse>(
`${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'
);
}
},
};

View File

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

View File

@ -1,11 +1,81 @@
<template>
<main
class="main-content position-relative max-height-vh-100 h-100 border-radius-lg"
>
<div class="py-4 container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex align-items-center justify-content-between">
<div>
<h1>Gestion stock</h1>
<h4 class="mb-0 font-weight-bolder">Statistiques stock</h4>
<p class="mb-0 text-sm text-secondary">
Analyse des stocks, rotations, et mouvements d'entrepôt
</p>
</div>
<button
class="btn btn-sm btn-outline-secondary mb-0"
:disabled="store.isLoading"
@click="store.fetchStatistics()"
>
<i class="fas fa-sync-alt me-1" />
Actualiser
</button>
</div>
</div>
</div>
<!-- Loading -->
<div v-if="store.isLoading" class="row">
<div class="col-12 text-center py-6">
<div class="spinner-border text-secondary" role="status">
<span class="visually-hidden">Chargement</span>
</div>
<p class="mt-3 text-secondary">Chargement des statistiques</p>
</div>
</div>
<!-- Error -->
<div v-else-if="store.hasError" class="row">
<div class="col-12">
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2" />
{{ store.getError }}
</div>
</div>
</div>
<!-- Dashboard -->
<stock-stats-dashboard
v-else-if="store.hasData"
:stats="store.statistics"
/>
<app-footer />
</div>
</main>
</template>
<script>
export default {
name: "GestionStock",
};
<script lang="ts">
import { defineComponent, onMounted } from "vue";
import { useStockStatisticsStore } from "@/stores/stockStatisticsStore";
import StockStatsDashboard from "@/components/Organism/Stock/StockStatsDashboard.vue";
import AppFooter from "@/examples/Footer.vue";
export default defineComponent({
name: "StatistiquesStock",
components: {
StockStatsDashboard,
AppFooter,
},
setup() {
const store = useStockStatisticsStore();
onMounted(() => {
store.fetchStatistics();
});
return { store };
},
});
</script>