New-Thanasoft/thanasoft-front/src/components/Organism/Stock/WarehouseDetailPresentation.vue

683 lines
21 KiB
Vue

<template>
<div class="py-4 container-fluid">
<div class="row">
<div class="col-lg-6">
<h4>
{{ isEditMode ? "Modifier l'entrepôt" : "Détails de l'entrepôt" }}
</h4>
<p>
{{
isEditMode
? "Modifiez les informations de l'entrepôt ci-dessous."
: "Informations détaillées, produits stockés et mouvements."
}}
</p>
</div>
<div
class="text-right col-lg-6 d-flex flex-column justify-content-center"
>
<div class="w-100 mt-lg-0">
<div v-if="!isEditMode" class="d-flex align-items-center gap-2 w-100">
<div class="flex-grow-1 d-flex justify-content-center">
<button
type="button"
class="btn btn-outline-secondary btn-sm"
@click="handleBack"
>
<i class="fas fa-arrow-left me-2"></i>
Retour à la liste
</button>
</div>
<div class="d-flex gap-2 ms-auto">
<button
type="button"
class="mt-2 mb-0 btn bg-gradient-info"
@click="toggleEditMode"
>
<i class="fas fa-edit me-2"></i>
Modifier
</button>
<button
type="button"
class="mt-2 mb-0 btn bg-gradient-danger"
:disabled="deleting"
@click="handleDelete"
>
<i class="fas fa-trash me-2"></i>
Supprimer
</button>
</div>
</div>
<div v-else class="d-flex gap-2 justify-content-end">
<button
type="button"
class="mt-2 mb-0 btn bg-gradient-secondary"
@click="cancelEdit"
>
Annuler
</button>
<button
type="button"
class="mt-2 mb-0 btn bg-gradient-success"
:disabled="saving"
@click="saveWarehouse"
>
<i v-if="saving" class="fas fa-spinner fa-spin me-2"></i>
<i v-else class="fas fa-save me-2"></i>
Sauvegarder
</button>
</div>
</div>
</div>
</div>
<div v-if="loading" class="loading-container">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p>Chargement des informations de l'entrepôt...</p>
</div>
</div>
<div v-else-if="error" class="error-container">
<div class="error-message">
<i class="fas fa-exclamation-triangle fa-2x text-danger mb-3"></i>
<h5>Erreur de chargement</h5>
<p>{{ error }}</p>
<button
type="button"
class="btn btn-outline-primary btn-sm"
@click="loadWarehouseData"
>
Réessayer
</button>
</div>
</div>
<div v-else-if="warehouse" class="warehouse-details-content mt-3">
<div class="row g-4">
<div class="col-xl-3 col-lg-4">
<div class="card warehouse-side-nav-card">
<div class="card-body pb-2">
<div class="warehouse-sidebar-profile">
<div class="warehouse-sidebar-icon">
<i class="fas fa-warehouse"></i>
</div>
<h6 class="warehouse-sidebar-title text-center mb-1">
{{ warehouse.name }}
</h6>
<p class="warehouse-sidebar-reference text-center mb-0">
{{ warehouse.city || "Ville non renseignée" }}
</p>
<div class="warehouse-sidebar-badges mt-3">
<span class="badge bg-primary">{{
warehouse.country_code || "--"
}}</span>
<span class="badge bg-info text-dark"
>{{ warehouseProducts.length }} produits</span
>
</div>
</div>
</div>
<hr class="horizontal dark my-2 mx-3" />
<div class="card-body pt-2">
<ul class="nav nav-pills flex-column warehouse-nav">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'details' }"
href="javascript:;"
@click="activeTab = 'details'"
>
<i class="fas fa-info-circle me-2"></i>
<span class="text-sm">Informations</span>
</a>
</li>
<li class="nav-item pt-2">
<a
class="nav-link"
:class="{ active: activeTab === 'products' }"
href="javascript:;"
@click="activeTab = 'products'"
>
<i class="fas fa-boxes me-2"></i>
<span class="text-sm">Produits</span>
</a>
</li>
<li class="nav-item pt-2">
<a
class="nav-link"
:class="{ active: activeTab === 'movements' }"
href="javascript:;"
@click="activeTab = 'movements'"
>
<i class="fas fa-exchange-alt me-2"></i>
<span class="text-sm">Mouvements</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col-xl-9 col-lg-8">
<div v-show="activeTab === 'details'">
<div class="card">
<div class="card-header bg-gradient-primary text-white">
<h5 class="mb-0">Informations de l'entrepôt</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<label>Nom de l'entrepôt *</label>
<input
v-if="isEditMode"
v-model="formData.name"
class="form-control"
type="text"
placeholder="Nom de l'entrepôt"
/>
<p v-else class="form-control-static">
{{ warehouse.name }}
</p>
</div>
<div class="col-md-6">
<label>Pays</label>
<input
v-if="isEditMode"
v-model="formData.country_code"
class="form-control"
type="text"
maxlength="2"
placeholder="FR"
/>
<p v-else class="form-control-static">
{{ warehouse.country_code || "-" }}
</p>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<label>Adresse 1</label>
<input
v-if="isEditMode"
v-model="formData.address_line1"
class="form-control"
type="text"
placeholder="Adresse ligne 1"
/>
<p v-else class="form-control-static">
{{ warehouse.address_line1 || "-" }}
</p>
</div>
<div class="col-md-6">
<label>Adresse 2</label>
<input
v-if="isEditMode"
v-model="formData.address_line2"
class="form-control"
type="text"
placeholder="Adresse ligne 2"
/>
<p v-else class="form-control-static">
{{ warehouse.address_line2 || "-" }}
</p>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<label>Code postal</label>
<input
v-if="isEditMode"
v-model="formData.postal_code"
class="form-control"
type="text"
placeholder="Code postal"
/>
<p v-else class="form-control-static">
{{ warehouse.postal_code || "-" }}
</p>
</div>
<div class="col-md-6">
<label>Ville</label>
<input
v-if="isEditMode"
v-model="formData.city"
class="form-control"
type="text"
placeholder="Ville"
/>
<p v-else class="form-control-static">
{{ warehouse.city || "-" }}
</p>
</div>
</div>
</div>
</div>
</div>
<div v-show="activeTab === 'products'">
<div class="card">
<div class="card-body">
<h5 class="font-weight-bolder mb-4">
Produits dans l'entrepôt
</h5>
<div v-if="warehouseProducts.length === 0" class="text-muted">
Aucun produit trouvé dans cet entrepôt.
</div>
<div v-else class="table-responsive">
<table class="table align-items-center mb-0">
<thead>
<tr>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Produit
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Quantité
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Stock de sécurité
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder text-end"
>
Actions
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in warehouseProducts" :key="item.id">
<td>
<div class="d-flex flex-column">
<span class="text-sm fw-bold">{{
item.product?.nom || `Produit #${item.product_id}`
}}</span>
<span class="text-xs text-secondary"
>Réf: {{ item.product?.reference || "-" }}</span
>
</div>
</td>
<td class="text-sm">{{ item.qty_on_hand_base }}</td>
<td class="text-sm">{{ item.safety_stock_base }}</td>
<td class="text-end">
<button
type="button"
class="btn btn-link text-info btn-sm mb-0"
@click="goToProduct(item.product_id)"
>
Voir produit
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div v-show="activeTab === 'movements'">
<div class="card">
<div class="card-body">
<div
class="d-flex justify-content-between align-items-center mb-4"
>
<h5 class="font-weight-bolder mb-0">
Mouvements de stock de l'entrepôt
</h5>
<button
type="button"
class="btn btn-sm bg-gradient-primary mb-0"
@click="goToStockPage"
>
<i class="fas fa-external-link-alt me-1"></i>
Menu stock
</button>
</div>
<div v-if="warehouseMovements.length === 0" class="text-muted">
Aucun mouvement pour cet entrepôt.
</div>
<div v-else class="table-responsive">
<table class="table align-items-center mb-0">
<thead>
<tr>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Date
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Type
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Produit
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Entrée / Sortie
</th>
<th
class="text-uppercase text-secondary text-xxs font-weight-bolder"
>
Quantité
</th>
</tr>
</thead>
<tbody>
<tr
v-for="movement in warehouseMovements"
:key="movement.id"
>
<td class="text-sm">
{{
formatDate(movement.moved_at || movement.created_at)
}}
</td>
<td class="text-sm">{{ movement.move_type || "-" }}</td>
<td class="text-sm">
{{
movement.product?.nom ||
`Produit #${movement.product_id}`
}}
</td>
<td class="text-sm">
<span
class="badge"
:class="
isIncoming(movement)
? 'bg-success'
: 'bg-warning text-dark'
"
>
{{ isIncoming(movement) ? "Entrée" : "Sortie" }}
</span>
</td>
<td class="text-sm">{{ movement.qty_base }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, defineProps, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { useStockStore } from "@/stores/stockStore";
import { useWarehouseStore } from "@/stores/warehouseStore";
const props = defineProps({
warehouseId: {
type: [String, Number],
required: true,
},
});
const router = useRouter();
const warehouseStore = useWarehouseStore();
const stockStore = useStockStore();
const warehouse = ref(null);
const loading = ref(true);
const saving = ref(false);
const deleting = ref(false);
const error = ref(null);
const activeTab = ref("details");
const isEditMode = ref(false);
const formData = ref({
name: "",
address_line1: "",
address_line2: "",
postal_code: "",
city: "",
country_code: "FR",
});
const numericWarehouseId = computed(() => Number(props.warehouseId));
const warehouseProducts = computed(() => {
return stockStore.stockItems.filter(
(item) => Number(item.warehouse_id) === numericWarehouseId.value
);
});
const warehouseMovements = computed(() => {
return stockStore.stockMoves.filter(
(move) =>
Number(move.from_warehouse_id) === numericWarehouseId.value ||
Number(move.to_warehouse_id) === numericWarehouseId.value
);
});
const initializeFormData = (value) => {
formData.value = {
name: value?.name || "",
address_line1: value?.address_line1 || "",
address_line2: value?.address_line2 || "",
postal_code: value?.postal_code || "",
city: value?.city || "",
country_code: value?.country_code || "FR",
};
};
const loadWarehouseData = async () => {
loading.value = true;
error.value = null;
try {
const [fetchedWarehouse] = await Promise.all([
warehouseStore.fetchWarehouse(numericWarehouseId.value),
stockStore.fetchStockItems(),
stockStore.fetchStockMoves(),
]);
warehouse.value = fetchedWarehouse;
initializeFormData(fetchedWarehouse);
} catch (e) {
error.value = "Impossible de charger les informations de l'entrepôt.";
console.error(e);
} finally {
loading.value = false;
}
};
const toggleEditMode = () => {
isEditMode.value = true;
initializeFormData(warehouse.value);
};
const cancelEdit = () => {
isEditMode.value = false;
initializeFormData(warehouse.value);
};
const saveWarehouse = async () => {
saving.value = true;
try {
const updated = await warehouseStore.updateWarehouse(
numericWarehouseId.value,
{
...formData.value,
country_code: (formData.value.country_code || "FR").toUpperCase(),
}
);
initializeFormData(updated);
isEditMode.value = false;
await loadWarehouseData();
} catch (e) {
console.error("Failed to update warehouse", e);
error.value =
e?.response?.data?.message ||
"Erreur lors de la mise à jour de l'entrepôt.";
} finally {
saving.value = false;
}
};
const handleBack = () => {
router.push("/stock/warehouses");
};
const goToProduct = (productId) => {
router.push(`/stock/produits/details/${productId}`);
};
const goToStockPage = () => {
router.push("/stock");
};
const formatDate = (value) => {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString("fr-FR", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};
const isIncoming = (movement) => {
return Number(movement.to_warehouse_id) === numericWarehouseId.value;
};
const handleDelete = async () => {
if (!confirm("Êtes-vous sûr de vouloir supprimer cet entrepôt ?")) {
return;
}
deleting.value = true;
try {
await warehouseStore.deleteWarehouse(numericWarehouseId.value);
router.push("/stock/warehouses");
} catch (e) {
console.error("Failed to delete warehouse", e);
error.value =
e?.response?.data?.message || "Erreur lors de la suppression.";
} finally {
deleting.value = false;
}
};
onMounted(() => {
loadWarehouseData();
});
</script>
<style scoped>
.loading-container,
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
background-color: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin: 2rem 0;
}
.loading-spinner,
.error-message {
text-align: center;
padding: 2rem;
}
.bg-gradient-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.form-control-static {
padding: 0.375rem 0;
margin-bottom: 0;
border: none;
background-color: transparent;
color: #495057;
}
.warehouse-side-nav-card {
position: sticky;
top: 1rem;
border: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
}
.warehouse-sidebar-profile {
display: flex;
flex-direction: column;
align-items: center;
}
.warehouse-sidebar-icon {
width: 96px;
height: 96px;
border-radius: 12px;
border: 1px solid #e5e7eb;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
color: #667eea;
font-size: 2rem;
background: #f8f9ff;
}
.warehouse-sidebar-title {
font-size: 1rem;
font-weight: 700;
color: #1f2937;
}
.warehouse-sidebar-reference {
color: #6b7280;
font-size: 0.85rem;
}
.warehouse-sidebar-badges {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.warehouse-nav .nav-link {
color: #6b7280;
border-radius: 0.5rem;
}
.warehouse-nav .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
box-shadow: 0 0.25rem 0.75rem rgba(102, 126, 234, 0.35);
}
</style>