feat(convoys): add convoys feature with UI, routes, store, services

Introduces convoys management pages, including list and add forms,
along with corresponding Vuex store and API service.
This commit is contained in:
kevin 2026-04-16 16:37:13 +03:00
parent 284d228dc5
commit 3e6ac4055c
13 changed files with 905 additions and 0 deletions

128
.opencode/package-lock.json generated Normal file
View File

@ -0,0 +1,128 @@
{
"name": ".opencode",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.10",
"@opencode-ai/plugin": "1.1.31"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.10",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.10.tgz",
"integrity": "sha512-VJPhJC+E5WWu7XgEJzrVOxKJlwJ+OATwxEzgjqEPj8KN5N38YxUPBY/rzUTjv90x7nkzyk1rFGfCVqXdA/Koug==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.10",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.10",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.10.tgz",
"integrity": "sha512-H6jGXYAhN/yjOGX3MRZ0OxyEAuRGY3VOwDbLTh4O6ljpgutFHaLvomDZ82qNVy7gl7AjJgi3SAQAt9UQpeGl/w==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/@opencode-ai/plugin": {
"version": "1.1.31",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.1.31",
"zod": "4.1.8"
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.1.31",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@ -0,0 +1,20 @@
<template>
<new-convoy-template>
<template #multi-step></template>
<template #convoy-form>
<new-convoy-form :loading="loading" @create-convoy="$emit('create-convoy', $event)" />
</template>
</new-convoy-template>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
import NewConvoyTemplate from "@/components/templates/Convoys/NewConvoyTemplate.vue";
import NewConvoyForm from "@/components/molecules/form/NewConvoyForm.vue";
defineProps({
loading: { type: Boolean, default: false },
});
defineEmits(["createConvoy"]);
</script>

View File

@ -0,0 +1,107 @@
<template>
<convoy-template>
<template #convoy-new-action>
<soft-button color="success" variant="gradient" @click="goToCreate">
<i class="fas fa-plus me-2"></i> Ajouter convoi
</soft-button>
</template>
<template #header-pagination>
<div v-if="pagination && pagination.last_page > 1" class="d-flex justify-content-center">
<soft-pagination color="success" size="sm">
<soft-pagination-item prev :disabled="pagination.current_page <= 1" @click="changePage(pagination.current_page - 1)" />
<soft-pagination-item
v-for="page in visiblePages"
:key="page"
:label="page.toString()"
:active="pagination.current_page === page"
@click="typeof page === 'number' && changePage(page)"
/>
<soft-pagination-item next :disabled="pagination.current_page >= pagination.last_page" @click="changePage(pagination.current_page + 1)" />
</soft-pagination>
</div>
</template>
<template #select-filter>
<soft-button color="dark" variant="outline" class="dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-filter me-2"></i> Filtrer
</soft-button>
<ul class="dropdown-menu dropdown-menu-end px-2 py-3">
<li><button class="dropdown-item border-radius-md" @click="$emit('filter-status', 'planned')">Planifiés</button></li>
<li><button class="dropdown-item border-radius-md" @click="$emit('filter-status', 'in_progress')">En cours</button></li>
<li><button class="dropdown-item border-radius-md" @click="$emit('filter-status', 'completed')">Terminés</button></li>
</ul>
</template>
<template #convoy-list>
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Chargement...</span></div>
<p class="mt-2">Chargement des convois...</p>
</div>
<div v-else-if="error" class="alert alert-danger text-center py-4">
<p>{{ error }}</p>
<button class="btn btn-outline-danger" @click="$emit('retry')">Réessayer</button>
</div>
<div v-else-if="!convoys.length" class="card border-0 shadow-sm text-center py-5">
<div class="card-body">
<i class="fas fa-road fa-3x text-secondary mb-3"></i>
<h5>Aucun convoi trouvé</h5>
<p class="text-sm text-secondary mb-0">Créez votre premier convoi pour commencer.</p>
</div>
</div>
<div v-else class="row g-4">
<div v-for="convoy in convoys" :key="convoy.id" class="col-12 col-md-6 col-xl-4">
<convoy-event-card :convoy="convoy" @view="$emit('view', $event)" />
</div>
</div>
</template>
</convoy-template>
</template>
<script setup>
import { computed, defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router";
import ConvoyTemplate from "@/components/templates/Convoys/ConvoyTemplate.vue";
import ConvoyEventCard from "@/components/molecules/Convoy/ConvoyEventCard.vue";
import SoftButton from "@/components/SoftButton.vue";
import SoftPagination from "@/components/SoftPagination.vue";
import SoftPaginationItem from "@/components/SoftPaginationItem.vue";
const props = defineProps({
convoys: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
error: { type: String, default: null },
pagination: { type: Object, default: null },
});
const router = useRouter();
const goToCreate = () => router.push({ name: "Ajouter convoi" });
const changePage = (page) => {
if (typeof page !== "number") return;
if (page < 1 || (props.pagination && page > props.pagination.last_page)) return;
if (page === props.pagination.current_page) return;
emit("page-change", page);
};
const emit = defineEmits(["retry", "page-change", "view", "filter-status"]);
const visiblePages = computed(() => {
if (!props.pagination) return [];
const total = props.pagination.last_page;
const current = props.pagination.current_page;
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages = [1];
if (current > 3) pages.push("...");
const start = Math.max(2, current - 1);
const end = Math.min(total - 1, current + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (current < total - 2) pages.push("...");
pages.push(total);
return pages;
});
</script>

View File

@ -0,0 +1,105 @@
<template>
<div class="card h-100 convoy-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-3 gap-3">
<div>
<p class="text-xs text-uppercase text-muted mb-1">Convoi</p>
<h5 class="mb-1">{{ convoy.mission_title || defaultTitle }}</h5>
<p class="text-sm text-secondary mb-0">
{{ deceasedName }}
</p>
</div>
<span class="badge" :class="statusClass">{{ statusLabel }}</span>
</div>
<div class="convoy-card__meta">
<div class="convoy-card__meta-item">
<i class="fas fa-calendar-alt text-info"></i>
<span>{{ formattedDate }}</span>
</div>
<div class="convoy-card__meta-item">
<i class="fas fa-route text-info"></i>
<span>{{ transportLabel }}</span>
</div>
<div class="convoy-card__meta-item">
<i class="fas fa-map-marker-alt text-info"></i>
<span>{{ departureLabel }}</span>
</div>
<div class="convoy-card__meta-item">
<i class="fas fa-truck text-info"></i>
<span>{{ vehicleLabel }}</span>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 pt-2 border-top">
<div class="d-flex gap-2 text-xs text-secondary">
<span>{{ convoyTypeLabel }}</span>
<span></span>
<span>{{ notificationLabel }}</span>
</div>
<soft-button color="info" variant="outline" size="sm" @click="$emit('view', convoy.id)">
Voir
</soft-button>
</div>
</div>
</div>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
import { computed } from "vue";
import SoftButton from "@/components/SoftButton.vue";
const props = defineProps({
convoy: {
type: Object,
required: true,
},
});
defineEmits(["view"]);
const deceasedName = computed(() => {
const deceased = props.convoy.deceased;
if (!deceased) return "Défunt non renseigné";
return deceased.full_name || [deceased.first_name, deceased.last_name].filter(Boolean).join(" ") || "Défunt";
});
const defaultTitle = computed(() => `Convoi #${props.convoy.id}`);
const formattedDate = computed(() => {
return new Date(props.convoy.planned_start_at).toLocaleDateString("fr-FR", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
});
const transportLabel = computed(() => ({ road: "Route", air: "Aérien", sea: "Maritime", rail: "Ferroviaire" }[props.convoy.transport_mode] || props.convoy.transport_mode));
const convoyTypeLabel = computed(() => ({ local: "Local", national: "National", international: "International" }[props.convoy.convoy_type] || props.convoy.convoy_type));
const statusLabel = computed(() => ({ planned: "Planifié", in_progress: "En cours", completed: "Terminé", cancelled: "Annulé" }[props.convoy.status] || props.convoy.status));
const statusClass = computed(() => ({ planned: "bg-gradient-secondary", in_progress: "bg-gradient-info", completed: "bg-gradient-success", cancelled: "bg-gradient-danger" }[props.convoy.status] || "bg-gradient-secondary"));
const departureLabel = computed(() => props.convoy.departure?.city || props.convoy.departure?.name || "Départ non défini");
const vehicleLabel = computed(() => props.convoy.vehicle ? `${props.convoy.vehicle.brand} ${props.convoy.vehicle.model}` : "Aucun véhicule");
const notificationLabel = computed(() => props.convoy.automatic_notifications ? "Notifications actives" : "Notifications inactives");
</script>
<style scoped>
.convoy-card {
border-radius: 1rem;
background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
}
.convoy-card__meta {
display: grid;
gap: 0.75rem;
}
.convoy-card__meta-item {
display: flex;
align-items: center;
gap: 0.65rem;
font-size: 0.875rem;
color: #67748e;
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
<h5 class="font-weight-bolder mb-0">Nouveau convoi</h5>
<p class="mb-0 text-sm">Informations générales de la mission</p>
<div class="multisteps-form__content">
<div class="row mt-3">
<div class="col-12 col-sm-6">
<label class="form-label">ID défunt <span class="text-danger">*</span></label>
<soft-input v-model="form.deceased_id" type="number" min="1" />
</div>
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Titre de mission</label>
<soft-input v-model="form.mission_title" type="text" placeholder="Ex. Convoi Mme DUPONT" />
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-sm-6">
<label class="form-label">Type de convoi</label>
<select v-model="form.convoy_type" class="form-control">
<option value="local">Local</option>
<option value="national">National</option>
<option value="international">International</option>
</select>
</div>
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Mode de transport</label>
<select v-model="form.transport_mode" class="form-control">
<option value="road">Route</option>
<option value="air">Aérien</option>
<option value="sea">Maritime</option>
<option value="rail">Ferroviaire</option>
</select>
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-sm-6">
<label class="form-label">Début prévu <span class="text-danger">*</span></label>
<soft-input v-model="form.planned_start_at" type="datetime-local" />
</div>
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Fin estimée</label>
<soft-input v-model="form.estimated_end_at" type="datetime-local" />
</div>
</div>
<div class="row mt-3">
<div class="col-12 col-sm-6">
<label class="form-label">Ville de départ</label>
<soft-input v-model="form.departure_city" type="text" placeholder="Ex. Paris" />
</div>
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Email famille</label>
<soft-input v-model="form.family_email" type="email" placeholder="famille@email.fr" />
</div>
</div>
<div class="button-row d-flex mt-4">
<soft-button type="button" color="secondary" variant="outline" class="me-2 mb-0" @click="resetForm">
Réinitialiser
</soft-button>
<soft-button type="button" color="dark" variant="gradient" class="ms-auto mb-0" :disabled="loading" @click="submitForm">
{{ loading ? "Création..." : "Créer le convoi" }}
</soft-button>
</div>
</div>
</div>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
import { ref } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
const props = defineProps({
loading: { type: Boolean, default: false },
});
const emit = defineEmits(["createConvoy"]);
const defaultForm = () => ({
deceased_id: "",
mission_title: "",
convoy_type: "local",
transport_mode: "road",
planned_start_at: "",
estimated_end_at: "",
departure_city: "",
family_email: "",
});
const form = ref(defaultForm());
const submitForm = () => {
emit("createConvoy", {
deceased_id: Number(form.value.deceased_id),
mission_title: form.value.mission_title || null,
convoy_type: form.value.convoy_type,
transport_mode: form.value.transport_mode,
planned_start_at: form.value.planned_start_at,
estimated_end_at: form.value.estimated_end_at || null,
departure_city: form.value.departure_city || null,
family_email: form.value.family_email || null,
});
};
const resetForm = () => {
form.value = defaultForm();
};
</script>

View File

@ -0,0 +1,21 @@
<template>
<div class="container-fluid py-4">
<div class="d-sm-flex justify-content-between">
<div>
<slot name="convoy-new-action"></slot>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<slot name="header-pagination"></slot>
<div class="dropdown d-inline">
<slot name="select-filter"></slot>
</div>
<slot name="convoy-other-action"></slot>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<slot name="convoy-list"></slot>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,20 @@
<template>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="multisteps-form mb-5">
<div class="row">
<div class="col-12 col-lg-8 mx-auto my-5">
<slot name="multi-step" />
</div>
</div>
<div class="row">
<div class="col-12 col-lg-8 m-auto">
<slot name="convoy-form" />
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -381,6 +381,28 @@ export default {
},
],
},
{
id: "convois",
type: "collapse",
text: "Convois",
icon: "Office",
collapseRef: "convoisMenu",
routeKey: "convois",
children: [
{
id: "convois-list",
route: { name: "Liste convois" },
miniIcon: "L",
text: "Liste convois",
},
{
id: "convois-add",
route: { name: "Ajouter convoi" },
miniIcon: "A",
text: "Ajouter convoi",
},
],
},
{
id: "parametrage",
type: "collapse",

View File

@ -406,6 +406,16 @@ const routes = [
name: "Agenda",
component: () => import("@/views/pages/Agenda.vue"),
},
{
path: "/convois",
name: "Liste convois",
component: () => import("@/views/pages/Convoys/Convoys.vue"),
},
{
path: "/convois/new",
name: "Ajouter convoi",
component: () => import("@/views/pages/Convoys/AddConvoy.vue"),
},
// Planning
{
path: "/planning",

View File

@ -0,0 +1,148 @@
import { request } from "./http";
export interface ConvoyDepartureLocation {
id: number;
client_id: number;
name: string;
address_line1: string | null;
address_line2: string | null;
postal_code: string | null;
city: string | null;
country_code: string | null;
gps_lat: string | null;
gps_lng: string | null;
}
export interface ConvoyDeparture {
location_selection_mode: "place" | "manual";
location_id: number | null;
location?: ConvoyDepartureLocation | null;
name: string | null;
address: string | null;
city: string | null;
postal_code: string | null;
country_code: string | null;
latitude: string | null;
longitude: string | null;
additional_details: string | null;
}
export interface Convoy {
id: number;
deceased_id: number;
client_id: number | null;
vehicle_id: number | null;
mission_title: string | null;
convoy_type: "local" | "national" | "international";
transport_mode: "road" | "air" | "sea" | "rail";
status: "planned" | "in_progress" | "completed" | "cancelled";
planned_start_at: string;
estimated_end_at: string | null;
family_email: string | null;
automatic_notifications: boolean;
departure: ConvoyDeparture;
tabs: Record<string, boolean>;
deceased?: any;
client?: any;
vehicle?: any;
created_at: string;
updated_at: string;
}
export interface ConvoyListResponse {
data: Convoy[];
meta: {
current_page: number;
last_page: number;
per_page: number;
total: number;
};
status: string;
}
export interface ConvoyResponse {
data: Convoy;
message?: string;
status?: string;
}
export interface CreateConvoyPayload {
deceased_id: number;
client_id?: number | null;
vehicle_id?: number | null;
mission_title?: string | null;
convoy_type?: "local" | "national" | "international";
transport_mode?: "road" | "air" | "sea" | "rail";
status?: "planned" | "in_progress" | "completed" | "cancelled";
planned_start_at: string;
estimated_end_at?: string | null;
family_email?: string | null;
automatic_notifications?: boolean;
departure_location_selection_mode?: "place" | "manual";
departure_location_id?: number | null;
departure_name?: string | null;
departure_address?: string | null;
departure_city?: string | null;
departure_postal_code?: string | null;
departure_country_code?: string | null;
departure_latitude?: number | null;
departure_longitude?: number | null;
departure_additional_details?: string | null;
}
export interface UpdateConvoyPayload extends Partial<CreateConvoyPayload> {
id: number;
}
export const ConvoyService = {
async getAllConvoys(params?: {
page?: number;
per_page?: number;
search?: string;
status?: string;
convoy_type?: string;
vehicle_id?: number;
deceased_id?: number;
sort_by?: string;
sort_direction?: string;
}): Promise<ConvoyListResponse> {
return await request<ConvoyListResponse>({
url: "/api/convoys",
method: "get",
params,
});
},
async getConvoy(id: number): Promise<ConvoyResponse> {
return await request<ConvoyResponse>({
url: `/api/convoys/${id}`,
method: "get",
});
},
async createConvoy(payload: CreateConvoyPayload): Promise<ConvoyResponse> {
return await request<ConvoyResponse>({
url: "/api/convoys",
method: "post",
data: payload,
});
},
async updateConvoy(payload: UpdateConvoyPayload): Promise<ConvoyResponse> {
const { id, ...updateData } = payload;
return await request<ConvoyResponse>({
url: `/api/convoys/${id}`,
method: "put",
data: updateData,
});
},
async deleteConvoy(id: number): Promise<{ message: string; status: string }> {
return await request<{ message: string; status: string }>({
url: `/api/convoys/${id}`,
method: "delete",
});
},
};
export default ConvoyService;

View File

@ -0,0 +1,145 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import ConvoyService from "@/services/convoy";
import type {
Convoy,
CreateConvoyPayload,
UpdateConvoyPayload,
} from "@/services/convoy";
export const useConvoyStore = defineStore("convoy", () => {
const convoys = ref<Convoy[]>([]);
const currentConvoy = ref<Convoy | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const pagination = ref({
current_page: 1,
last_page: 1,
per_page: 10,
total: 0,
});
const allConvoys = computed(() => convoys.value);
const isLoading = computed(() => loading.value);
const hasError = computed(() => error.value !== null);
const getError = computed(() => error.value);
const getPagination = computed(() => pagination.value);
const setPagination = (meta: any) => {
if (!meta) return;
pagination.value = {
current_page: Number(meta.current_page) || 1,
last_page: Number(meta.last_page) || 1,
per_page: Number(meta.per_page) || 10,
total: Number(meta.total) || 0,
};
};
const fetchConvoys = async (params?: {
page?: number;
per_page?: number;
search?: string;
status?: string;
convoy_type?: string;
vehicle_id?: number;
deceased_id?: number;
sort_by?: string;
sort_direction?: string;
}) => {
loading.value = true;
error.value = null;
try {
const response = await ConvoyService.getAllConvoys(params);
convoys.value = response.data;
setPagination(response.meta);
return response;
} catch (err: any) {
error.value = err.response?.data?.message || err.message || "Failed to fetch convoys";
throw err;
} finally {
loading.value = false;
}
};
const fetchConvoy = async (id: number) => {
loading.value = true;
error.value = null;
try {
const response = await ConvoyService.getConvoy(id);
currentConvoy.value = response.data;
return response.data;
} catch (err: any) {
error.value = err.response?.data?.message || err.message || "Failed to fetch convoy";
throw err;
} finally {
loading.value = false;
}
};
const createConvoy = async (payload: CreateConvoyPayload) => {
loading.value = true;
error.value = null;
try {
const response = await ConvoyService.createConvoy(payload);
convoys.value.unshift(response.data);
currentConvoy.value = response.data;
return response.data;
} catch (err: any) {
error.value = err.response?.data?.message || err.message || "Failed to create convoy";
throw err;
} finally {
loading.value = false;
}
};
const updateConvoy = async (payload: UpdateConvoyPayload) => {
loading.value = true;
error.value = null;
try {
const response = await ConvoyService.updateConvoy(payload);
const index = convoys.value.findIndex((convoy) => convoy.id === response.data.id);
if (index !== -1) convoys.value[index] = response.data;
if (currentConvoy.value?.id === response.data.id) currentConvoy.value = response.data;
return response.data;
} catch (err: any) {
error.value = err.response?.data?.message || err.message || "Failed to update convoy";
throw err;
} finally {
loading.value = false;
}
};
const deleteConvoy = async (id: number) => {
loading.value = true;
error.value = null;
try {
const response = await ConvoyService.deleteConvoy(id);
convoys.value = convoys.value.filter((convoy) => convoy.id !== id);
if (currentConvoy.value?.id === id) currentConvoy.value = null;
return response;
} catch (err: any) {
error.value = err.response?.data?.message || err.message || "Failed to delete convoy";
throw err;
} finally {
loading.value = false;
}
};
return {
convoys,
currentConvoy,
loading,
error,
allConvoys,
isLoading,
hasError,
getError,
getPagination,
fetchConvoys,
fetchConvoy,
createConvoy,
updateConvoy,
deleteConvoy,
};
});

View File

@ -0,0 +1,25 @@
<template>
<add-convoy-presentation :loading="convoyStore.isLoading" @create-convoy="handleCreateConvoy" />
</template>
<script setup>
import { useRouter } from "vue-router";
import AddConvoyPresentation from "@/components/Organism/Convoys/AddConvoyPresentation.vue";
import { useConvoyStore } from "@/stores/convoyStore";
import { useNotificationStore } from "@/stores/notification";
const router = useRouter();
const convoyStore = useConvoyStore();
const notificationStore = useNotificationStore();
const handleCreateConvoy = async (payload) => {
try {
await convoyStore.createConvoy(payload);
notificationStore.created("Convoi");
router.push({ name: "Liste convois" });
} catch (error) {
console.error("Error creating convoy:", error);
notificationStore.error("Erreur", "Impossible de créer le convoi");
}
};
</script>

View File

@ -0,0 +1,41 @@
<template>
<convoy-list-presentation
:convoys="convoyStore.convoys"
:loading="convoyStore.loading"
:error="convoyStore.error"
:pagination="convoyStore.getPagination"
@retry="loadConvoys"
@page-change="changePage"
@filter-status="applyStatusFilter"
/>
</template>
<script setup>
import { onMounted, ref } from "vue";
import ConvoyListPresentation from "@/components/Organism/Convoys/ConvoyListPresentation.vue";
import { useConvoyStore } from "@/stores/convoyStore";
const convoyStore = useConvoyStore();
const currentStatus = ref(undefined);
const loadConvoys = async (page = 1) => {
await convoyStore.fetchConvoys({
page,
per_page: 9,
status: currentStatus.value,
sort_by: "planned_start_at",
sort_direction: "desc",
});
};
const changePage = async (page) => {
await loadConvoys(page);
};
const applyStatusFilter = async (status) => {
currentStatus.value = status;
await loadConvoys(1);
};
onMounted(loadConvoys);
</script>