Merge branch 'multiple-tasks-from-board-78' into 'ticket-app-master'

Merge request contenant différentes tâches provenant du board 78

See merge request Chill-Projet/chill-bundles!864
This commit is contained in:
2025-08-18 15:32:00 +00:00
27 changed files with 1397 additions and 301 deletions

View File

@@ -6,15 +6,19 @@ import {
} from "ChillMainAssets/types"; } from "ChillMainAssets/types";
import { Person } from "ChillPersonAssets/types"; import { Person } from "ChillPersonAssets/types";
import { Thirdparty } from "../../../../ChillThirdPartyBundle/Resources/public/types"; import { Thirdparty } from "../../../../ChillThirdPartyBundle/Resources/public/types";
import { StoredObject } from "ChillDocStoreAssets/types";
export interface Motive { export interface Motive {
type: "ticket_motive"; type: "ticket_motive";
id: number; id: number;
active: boolean; active: boolean;
label: TranslatableString; label: TranslatableString;
makeTicketEmergency: TicketEmergencyState;
storedObjects: StoredObject[];
supplementaryComments: { label: string }[];
} }
export type TicketState = "open" | "closed"; export type TicketState = "open" | "closed" | "close";
export type TicketEmergencyState = "yes" | "no"; export type TicketEmergencyState = "yes" | "no";
@@ -55,9 +59,7 @@ export interface Comment {
updatedBy: User | null; updatedBy: User | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
deleted: boolean; deleted: boolean;
supplementaryComments: { supplementaryComments: { label: string }[];
label: string;
};
} }
export interface AddresseeHistory { export interface AddresseeHistory {
@@ -162,7 +164,7 @@ export interface TicketFilters {
byCreatedAfter: string; byCreatedAfter: string;
byCreatedBefore: string; byCreatedBefore: string;
byResponseTimeExceeded: boolean; byResponseTimeExceeded: boolean;
byMyTickets: boolean; byAddresseeToMe: boolean;
} }
export interface TicketFilterParams { export interface TicketFilterParams {
@@ -173,5 +175,12 @@ export interface TicketFilterParams {
byCreatedAfter?: string; byCreatedAfter?: string;
byCreatedBefore?: string; byCreatedBefore?: string;
byResponseTimeExceeded?: string; byResponseTimeExceeded?: string;
byMyTickets?: boolean; byAddresseeToMe?: boolean;
}
export interface TicketInitForm {
content: string;
motive?: Motive;
persons: Person[];
caller: Person | null;
} }

View File

@@ -1,25 +1,68 @@
<template> <template>
<banner-component :ticket="ticket" /> <banner-component :ticket="ticket" />
<div class="container-xxl pt-1" style="padding-bottom: 55px"> <div class="container-xxl pt-1" style="padding-bottom: 55px" v-if="!loading">
<previous-tickets-component /> <previous-tickets-component :key="refreshKey" />
<ticket-history-list-component :history="ticketHistory" /> <ticket-history-list-component
:history="ticketHistory"
:key="ticketHistory.length"
/>
</div> </div>
<action-toolbar-component /> <div v-else class="text-center p-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">
{{ trans(CHILL_TICKET_LIST_LOADING_TICKET_DETAILS) }}
</span>
</div>
<div>{{ trans(CHILL_TICKET_LIST_LOADING_TICKET_DETAILS) }}</div>
</div>
<action-toolbar-component :key="refreshKey" v-if="!loading" />
<Modal
v-if="showTicketInitFormModal"
:show="showTicketInitFormModal"
modal-dialog-class="modal-lg"
@close="closeModal"
>
<template #header>
<h3 class="modal-title">
{{ trans(CHILL_TICKET_TICKET_INIT_FORM_TITLE) }}
</h3>
</template>
<template #body>
<ticket-init-form-component
:motives="motives"
:suggested-persons="suggestedPersons"
@submit="handleFormSubmit"
/>
</template>
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
import { computed, onMounted } from "vue"; import { computed, onMounted, ref } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
// Types // Types
import { Ticket } from "../../types"; import { Ticket, Motive, TicketInitForm } from "../../types";
import { Person } from "ChillPersonAssets/types";
// Components // Components
import Modal from "../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
import PreviousTicketsComponent from "./components/PreviousTicketsComponent.vue"; import PreviousTicketsComponent from "./components/PreviousTicketsComponent.vue";
import TicketHistoryListComponent from "../TicketList/components/TicketHistoryListComponent.vue"; import TicketHistoryListComponent from "../TicketList/components/TicketHistoryListComponent.vue";
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue"; import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
import BannerComponent from "./components/BannerComponent.vue"; import BannerComponent from "./components/BannerComponent.vue";
import TicketInitFormComponent from "./components/TicketInitFormComponent.vue";
// Translations
import {
trans,
CHILL_TICKET_TICKET_INIT_FORM_TITLE,
CHILL_TICKET_TICKET_INIT_FORM_SUCCESS,
CHILL_TICKET_TICKET_INIT_FORM_ERROR,
CHILL_TICKET_LIST_LOADING_TICKET_DETAILS,
} from "translator";
const store = useStore(); const store = useStore();
const toast = useToast(); const toast = useToast();
@@ -27,19 +70,72 @@ const toast = useToast();
store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket); store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket);
const ticket = computed(() => store.getters.getTicket as Ticket); const ticket = computed(() => store.getters.getTicket as Ticket);
const ticketHistory = computed(() => store.getters.getTicketHistory); const ticketHistory = computed(() => store.getters.getTicketHistory);
const motives = computed(() => store.getters.getMotives as Motive[]);
const suggestedPersons = computed(() => store.getters.getPersons as Person[]);
const showTicketInitFormModal = ref(false);
const loading = ref(true);
const refreshKey = ref(0);
async function handleFormSubmit(ticketForm: TicketInitForm) {
try {
if (ticketForm.motive) {
await store.dispatch("createMotive", ticketForm.motive);
}
if (ticketForm.content && ticketForm.content.trim() !== "") {
await store.dispatch("createComment", ticketForm.content);
}
await store.dispatch("setPersons", ticketForm.persons);
// Rafraîchir les données nécessaires
await store.dispatch("fetchTicketHistory");
await store.dispatch("fetchPreviousTickets");
// Forcer le rafraîchissement des composants
refreshKey.value++;
toast.success(trans(CHILL_TICKET_TICKET_INIT_FORM_SUCCESS));
closeModal();
} catch (error) {
toast.error(
(error as string) || trans(CHILL_TICKET_TICKET_INIT_FORM_ERROR),
);
}
}
function closeModal() {
showTicketInitFormModal.value = false;
}
onMounted(async () => { onMounted(async () => {
try { try {
await store.dispatch("getCurrentUser");
await store.dispatch("fetchMotives"); await store.dispatch("fetchMotives");
await store.dispatch("fetchUserGroups"); await store.dispatch("fetchUserGroups");
await store.dispatch("fetchUsers"); await store.dispatch("fetchUsers");
await store.dispatch("getSuggestedPersons"); await store.dispatch("getSuggestedPersons");
showTicketInitFormModal.value = store.getters.isNewTicket;
} catch (error) { } catch (error) {
toast.error(error as string); toast.error(error as string);
} finally {
loading.value = false;
} }
}); });
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
font-size: 2rem;
color: #333;
}
</style>

View File

@@ -29,6 +29,7 @@
<form @submit.prevent="submitAction"> <form @submit.prevent="submitAction">
<comment-editor-component <comment-editor-component
v-model="content" v-model="content"
:motive="motive"
v-if="activeTab === 'add_comment'" v-if="activeTab === 'add_comment'"
/> />
<addressee-selector-component <addressee-selector-component
@@ -169,6 +170,7 @@ import {
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_ERROR, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_ERROR,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_SET_PERSONS_ERROR,
} from "translator"; } from "translator";
// Types // Types
@@ -249,71 +251,75 @@ const returnPath = computed((): string => {
return returnPath; return returnPath;
}); });
const motive = ref( const motive = ref(ticket.value.currentMotive as Motive);
ticket.value.currentMotive ? ticket.value.currentMotive : ({} as Motive),
);
const content = ref("" as Comment["content"]); const content = ref("" as Comment["content"]);
const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]); const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]);
const persons = ref(ticket.value.currentPersons as Person[]); const persons = ref(ticket.value.currentPersons as Person[]);
const caller = ref(ticket.value.caller as Person); const caller = ref(ticket.value.caller as Person);
async function submitAction() { async function submitAction() {
try { switch (activeTab.value) {
switch (activeTab.value) { case "add_comment":
case "add_comment": if (!content.value) {
if (!content.value) { toast.error(trans(CHILL_TICKET_TICKET_ADD_COMMENT_ERROR));
toast.error(trans(CHILL_TICKET_TICKET_ADD_COMMENT_ERROR)); } else {
} else { await store.dispatch("createComment", content.value);
await store.dispatch("createComment", { content.value = "";
ticketId: ticket.value.id, activeTab.value = "";
content: content.value, toast.success(trans(CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS));
}); }
content.value = ""; break;
activeTab.value = ""; case "set_motive":
toast.success(trans(CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS)); if (!motive.value) {
} toast.error(trans(CHILL_TICKET_TICKET_SET_MOTIVE_ERROR));
break; } else {
case "set_motive": await store.dispatch("createMotive", motive.value);
if (!motive.value.id) { activeTab.value = "";
toast.error(trans(CHILL_TICKET_TICKET_SET_MOTIVE_ERROR)); toast.success(trans(CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS));
} else { }
await store.dispatch("createMotive", { break;
ticketId: ticket.value.id, case "addressees_state":
motive: motive.value, if (addressees.value.length) {
}); try {
activeTab.value = ""; await store.dispatch(
toast.success(trans(CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS)); "setAddressees",
} addressees.value as UserGroupOrUser[],
break; );
case "addressees_state":
if (!addressees.value.length) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR));
} else {
await store.dispatch("setAdressees", {
ticketId: ticket.value.id,
addressees: addressees.value,
});
activeTab.value = ""; activeTab.value = "";
toast.success(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS)); toast.success(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS));
} catch (error) {
console.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR), error);
toast.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR));
} }
break; } else {
case "persons_state": await store.dispatch("setAddressees", [] as UserGroupOrUser[]);
await store.dispatch("setPersons", { toast.success(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS));
persons: persons.value, }
}); break;
await store.dispatch("fetchTicketsByPerson"); case "persons_state":
activeTab.value = ""; if (persons.value.length) {
await store.dispatch("setPersons", persons.value);
try {
await store.dispatch("fetchTicketList", {
byPerson: persons.value.map((person) => person.id),
});
activeTab.value = "";
toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS));
} catch (error) {
console.error(trans(CHILL_TICKET_TICKET_SET_PERSONS_ERROR), error);
toast.error(trans(CHILL_TICKET_TICKET_SET_PERSONS_ERROR));
}
} else {
store.dispatch("setPersons", [] as Person[]);
toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS)); toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS));
break; }
} break;
} catch (error) {
toast.error(error as string);
} }
} }
async function closeTicket() { async function closeTicket() {
try { try {
await store.dispatch("closeTicket"); await store.dispatch("setTicketState", "close");
closeAllActions(); closeAllActions();
toast.success(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS)); toast.success(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS));
} catch (error) { } catch (error) {
@@ -324,7 +330,7 @@ async function closeTicket() {
async function reopenTicket() { async function reopenTicket() {
try { try {
await store.dispatch("reopenTicket"); await store.dispatch("setTicketState", "open");
toast.success(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS)); toast.success(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -337,7 +343,7 @@ function closeAllActions() {
} }
watch(caller, async (newCaller) => { watch(caller, async (newCaller) => {
await store.dispatch("setCaller", { caller: newCaller }); await store.dispatch("setCaller", newCaller);
await store.dispatch("getSuggestedPersons"); await store.dispatch("getSuggestedPersons");
}); });
</script> </script>

View File

@@ -8,31 +8,21 @@
</h1> </h1>
<h2 v-if="ticket.currentPersons.length"> <h2 v-if="ticket.currentPersons.length">
{{ ticket.currentPersons.map((person) => person.text).join(", ") }} {{
ticket.currentPersons
.map((person: Person) => person.text)
.join(", ")
}}
</h2> </h2>
</div> </div>
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<emergency-toggle-component <emergency-toggle-component
v-model="isEmergencyLocal" v-model="isEmergency"
@toggle-emergency="handleEmergencyToggle" :disabled="!isOpen"
/> />
<state-toggle-component v-model="isOpen" />
<span
class="badge text-bg-chill-green text-white"
style="font-size: 1rem"
v-if="isOpen"
>
{{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }}
</span>
<span
class="badge text-bg-chill-red text-white"
style="font-size: 1rem"
v-else
>
{{ trans(CHILL_TICKET_TICKET_BANNER_CLOSED) }}
</span>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<p class="created-at-timespan" v-if="ticket.createdAt"> <p class="created-at-timespan" v-if="ticket.createdAt">
@@ -102,26 +92,33 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { computed, ref } from "vue";
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
// Components // Components
import AddresseeComponent from "./Addressee/AddresseeComponent.vue"; import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
import EmergencyToggleComponent from "./Emergency/EmergencyToggleComponent.vue"; import EmergencyToggleComponent from "./Emergency/EmergencyToggleComponent.vue";
import StateToggleComponent from "./State/StateToggleComponent.vue";
import PersonComponent from "./Person/PersonComponent.vue"; import PersonComponent from "./Person/PersonComponent.vue";
// Types // Types
import { Ticket, TicketEmergencyState } from "../../../types"; import { Ticket } from "../../../types";
import { Person } from "ChillPersonAssets/types"; import { Person } from "ChillPersonAssets/types";
import { import {
trans, trans,
CHILL_TICKET_TICKET_BANNER_OPEN,
CHILL_TICKET_TICKET_BANNER_CLOSED,
CHILL_TICKET_TICKET_BANNER_SINCE, CHILL_TICKET_TICKET_BANNER_SINCE,
CHILL_TICKET_TICKET_BANNER_SPEAKER, CHILL_TICKET_TICKET_BANNER_SPEAKER,
CHILL_TICKET_TICKET_BANNER_CALLER, CHILL_TICKET_TICKET_BANNER_CALLER,
CHILL_TICKET_TICKET_BANNER_PERSON, CHILL_TICKET_TICKET_BANNER_PERSON,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_ERROR,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_ERROR,
CHILL_TICKET_TICKET_BANNER_EMERGENCY_SUCCESS,
CHILL_TICKET_TICKET_BANNER_EMERGENCY_ERROR,
CHILL_TICKET_TICKET_BANNER_NO_EMERGENCY_ERROR,
CHILL_TICKET_TICKET_BANNER_NO_EMERGENCY_SUCCESS,
} from "translator"; } from "translator";
// Store // Store
@@ -142,22 +139,56 @@ setInterval(() => {
today.value = new Date(); today.value = new Date();
}, 5000); }, 5000);
const isOpen = computed(() => store.getters.isOpen); const isOpen = computed({
const isEmergencyLocal = computed(() => store.getters.isEmergency); get: () => store.getters.isOpen as boolean,
set: async (value: boolean) => {
try {
await store.dispatch("setTicketState", value ? "open" : "close");
toast.success(
trans(
value
? CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS
: CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS,
),
);
} catch (error) {
console.error(error);
toast.error(
trans(
value
? CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_ERROR
: CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_ERROR,
),
);
}
},
});
const isEmergency = computed({
get: () => store.getters.isEmergency as boolean,
set: async (value: boolean) => {
try {
await store.dispatch("setEmergency", value ? "yes" : "no");
toast.success(
trans(
value
? CHILL_TICKET_TICKET_BANNER_EMERGENCY_SUCCESS
: CHILL_TICKET_TICKET_BANNER_NO_EMERGENCY_SUCCESS,
),
);
} catch (error) {
console.error(error);
toast.error(
trans(
value
? CHILL_TICKET_TICKET_BANNER_EMERGENCY_ERROR
: CHILL_TICKET_TICKET_BANNER_NO_EMERGENCY_ERROR,
),
);
}
},
});
const since = computed(() => { const since = computed(() => {
return store.getters.getSinceCreated(today.value); return store.getters.getSinceCreated(today.value);
}); });
// Methods
function handleEmergencyToggle(emergency: TicketEmergencyState) {
store.dispatch("toggleEmergency", emergency).catch(({ name, violations }) => {
if (name === "ValidationException" || name === "AccessException") {
violations.forEach((violation: string) =>
toast.open({ message: violation }),
);
} else {
toast.open({ message: "An error occurred" });
}
});
}
</script> </script>

View File

@@ -1,19 +1,103 @@
<template> <template>
<div class="col-12"> <div class="col-12">
<blockquote class="chill-user-quote"> <blockquote class="chill-user-quote">
<button
class="btn btn-sm btn-edit float-end"
title="Edit"
@click="editCommentModal = true"
style="top: 0.5rem; right: 0.5rem"
v-if="canBeEdited && isOpen"
/>
<p v-html="convertMarkdownToHtml(commentHistory.content)"></p> <p v-html="convertMarkdownToHtml(commentHistory.content)"></p>
</blockquote> </blockquote>
</div> </div>
<Modal
v-if="editCommentModal"
:show="editCommentModal"
modal-dialog-class="modal-xl"
@close="editCommentModal = false"
>
<template #header>
<h5 class="modal-title">
{{ trans(CHILL_TICKET_TICKET_EDIT_COMMENT_TITLE) }}
</h5>
</template>
<template #body>
<comment-editor v-model="editedComment" />
</template>
<template #footer>
<button
class="btn btn-primary"
@click="restoreComment"
v-if="commentHistory.deleted"
>
{{ trans(RESTORE) }}
</button>
<button class="btn btn-delete" @click="deleteComment" v-else>
{{ trans(DELETE) }}
</button>
<button class="btn btn-save" @click="saveComment">
{{ trans(SAVE) }}
</button>
</template>
</Modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue";
import { useStore } from "vuex";
import { marked } from "marked"; import { marked } from "marked";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
// Types // Types
import { Comment } from "../../../../types"; import { Comment } from "../../../../types";
defineProps<{ commentHistory: Comment }>(); // Components
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
import Modal from "../../../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
import {
trans,
SAVE,
DELETE,
RESTORE,
CHILL_TICKET_TICKET_EDIT_COMMENT_TITLE,
CHILL_TICKET_TICKET_EDIT_COMMENT_SUCCESS,
CHILL_TICKET_TICKET_DELETE_COMMENT_SUCCESS,
CHILL_TICKET_TICKET_RESTORE_COMMENT_SUCCESS,
} from "translator";
import { useToast } from "vue-toast-notification";
const props = defineProps<{ commentHistory: Comment }>();
const toast = useToast();
const store = useStore();
const isOpen = computed(() => store.getters.isOpen as boolean);
const canBeEdited = computed(() =>
store.getters["canBeEdited"](props.commentHistory),
);
const editCommentModal = ref<boolean>(false);
const editedComment = ref<string>(props.commentHistory.content);
const saveComment = () => {
store.dispatch("editComment", {
id: props.commentHistory.id,
content: editedComment.value,
});
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_EDIT_COMMENT_SUCCESS));
};
const deleteComment = () => {
store.dispatch("deleteComment", props.commentHistory.id);
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_DELETE_COMMENT_SUCCESS));
};
const restoreComment = () => {
store.dispatch("restoreComment", props.commentHistory.id);
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_RESTORE_COMMENT_SUCCESS));
};
const preprocess = (markdown: string): string => { const preprocess = (markdown: string): string => {
return markdown; return markdown;

View File

@@ -1,24 +1,92 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-12" v-if="motive">
<div class="input-group mb-2">
<input
type="text"
:value="localizeTranslatableString(motive.label)"
readonly
class="form-control"
/>
<div class="input-group-append">
<peloton-component :stored-objects="motive.storedObjects" />
</div>
</div>
</div>
<div class="col-12"> <div class="col-12">
<comment-editor v-model="content" /> <comment-editor v-model="content" />
</div> </div>
<div class="col-12" v-if="motive">
<div
class="input-group mb-2"
v-for="(supplementaryComments, index) in motive.supplementaryComments"
:key="index"
>
<div class="input-group-prepend d-flex align-items-center px-1">
<span class="badge rounded-pill bg-chill-red">
{{ supplementaryComments.label }}
</span>
</div>
<input
type="text"
v-model="supplementaryCommentsInput[index]"
class="form-control"
@keyup.enter="addSupplementaryComments(index)"
@keydown.enter.prevent
/>
<div class="input-group-append">
<button
class="input-group-text btn btn-submit"
type="button"
@click="addSupplementaryComments(index)"
>
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue"; import { reactive, ref, watch } from "vue";
// Components
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue"; import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
import PelotonComponent from "../PelotonComponent.vue";
// Types
import { Motive } from "../../../../types";
// Utils
import { localizeTranslatableString } from "../../utils/utils";
import { StoredObject } from "ChillDocStoreAssets/types";
const props = defineProps<{ const props = defineProps<{
modelValue?: string; modelValue?: string;
motive?: Motive;
}>(); }>();
const emit = const supplementaryCommentsInput = reactive<string[]>([]);
defineEmits<(e: "update:modelValue", value: string | undefined) => void>();
const emit = defineEmits<{
"update:modelValue": [value: string | undefined];
"show-peloton-modal": [storedObjects: StoredObject[]];
}>();
const content = ref(props.modelValue); const content = ref(props.modelValue);
function addSupplementaryComments(index: number) {
if (supplementaryCommentsInput[index]) {
const supplementaryText = `**${props.motive?.supplementaryComments[index].label}**: ${supplementaryCommentsInput[index]}`;
content.value = content.value
? content.value + "\n" + supplementaryText
: supplementaryText;
supplementaryCommentsInput[index] = "";
}
}
watch(content, (value) => { watch(content, (value) => {
emit("update:modelValue", value); emit("update:modelValue", value);
}); });

View File

@@ -3,36 +3,41 @@
<button <button
class="badge rounded-pill me-1" class="badge rounded-pill me-1"
:class="{ :class="{
'bg-danger': modelValue, 'bg-chill-red': modelValue,
'bg-secondary': !modelValue, 'bg-secondary': !modelValue,
'no-pointer': props.disabled,
}" }"
@click="toggleEmergency" @click="toggleEmergency"
:disabled="props.disabled"
> >
{{ trans(CHILL_TICKET_TICKET_BANNER_EMERGENCY) }} {{ trans(CHILL_TICKET_TICKET_BANNER_EMERGENCY) }}
</button> </button>
</span> </span>
</template> </template>
<style scoped>
.no-pointer {
cursor: not-allowed !important;
}
</style>
<script lang="ts" setup> <script lang="ts" setup>
import { trans, CHILL_TICKET_TICKET_BANNER_EMERGENCY } from "translator"; import { trans, CHILL_TICKET_TICKET_BANNER_EMERGENCY } from "translator";
import { TicketEmergencyState } from "../../../../types";
// Props // Props
const props = defineProps<{ const props = defineProps<{
modelValue: boolean; modelValue: boolean;
disabled?: boolean;
}>(); }>();
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
"update:modelValue": [value: boolean]; "update:modelValue": [value: boolean];
"toggle-emergency": [emergency: TicketEmergencyState];
}>(); }>();
// Methods // Methods
function toggleEmergency() { function toggleEmergency() {
const newValue = !props.modelValue; const newValue = !props.modelValue;
emit("update:modelValue", newValue); emit("update:modelValue", newValue);
emit("toggle-emergency", newValue ? "yes" : "no");
} }
</script> </script>

View File

@@ -0,0 +1,262 @@
<template>
<button
class="input-group-text btn btn-primary"
type="button"
@click="handleClick"
:disabled="!storedObjects?.length"
>
<i class="fa fa-sitemap"></i>
</button>
<Modal
v-if="showPelotonsModal"
:show="showPelotonsModal"
modal-dialog-class="modal-xl"
@close="closeModal"
>
<template #header>
<div class="dropdown-container">
<select
v-model="selectedStoredObject"
class="form-select"
@change="onSelectionChange"
style="max-width: 400px"
>
<option
v-for="storedObject in storedObjects"
:key="storedObject.id"
:value="storedObject"
>
{{ storedObject.title }}
</option>
</select>
</div>
</template>
<template #body>
<div v-if="selectedStoredObject && documentUrl" class="document-viewer">
<div v-if="documentType === 'image'" class="image-container">
<img
:src="documentUrl"
class="img-fluid"
style="max-width: 100%; height: auto"
/>
</div>
<div v-else-if="documentType === 'pdf'" class="pdf-container">
<iframe
:src="documentUrl"
width="100%"
height="100%"
style="border: 1px solid #ccc"
></iframe>
<noscript>
<p>
{{ trans(CHILL_TICKET_PELOTON_IFRAME_NOT_SUPPORTED) }}
<a :href="documentUrl" target="_blank">
{{ trans(CHILL_TICKET_PELOTON_CLICK_TO_OPEN_PDF) }}
</a>
</p>
</noscript>
</div>
<div v-else class="unsupported-document">
<p>{{ trans(CHILL_TICKET_PELOTON_UNSUPPORTED_TYPE) }}</p>
<a :href="documentUrl" target="_blank" class="btn btn-primary">
{{ trans(CHILL_TICKET_PELOTON_OPEN_NEW_TAB) }}
</a>
</div>
</div>
<div v-if="isLoading" class="loading-indicator">
<div class="spinner-border" role="status">
<span class="visually-hidden">{{
trans(CHILL_TICKET_PELOTON_LOADING)
}}</span>
</div>
<p>{{ trans(CHILL_TICKET_PELOTON_LOADING_DOCUMENT) }}</p>
</div>
<div v-if="error" class="alert alert-danger">
{{ error }}
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
// Component
import Modal from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
// Translations
import {
CHILL_TICKET_PELOTON_LOADING_DOCUMENT,
CHILL_TICKET_PELOTON_LOADING,
CHILL_TICKET_PELOTON_ERROR_LOADING,
CHILL_TICKET_PELOTON_ERROR_NOT_READY,
CHILL_TICKET_PELOTON_UNSUPPORTED_TYPE,
CHILL_TICKET_PELOTON_OPEN_NEW_TAB,
CHILL_TICKET_PELOTON_IFRAME_NOT_SUPPORTED,
CHILL_TICKET_PELOTON_CLICK_TO_OPEN_PDF,
trans,
} from "translator";
// Type
import { StoredObject } from "ChillDocStoreAssets/types";
// Utils
import {
download_and_decrypt_doc,
is_object_ready,
} from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpers";
const props = defineProps<{ storedObjects: StoredObject[] }>();
const selectedStoredObject = ref<StoredObject | null>(null);
const documentUrl = ref<string>("");
const documentType = ref<string>("");
const isLoading = ref<boolean>(false);
const error = ref<string>("");
const showPelotonsModal = ref<boolean>(false);
watch(
() => props.storedObjects,
(newStoredObjects) => {
if (
newStoredObjects &&
newStoredObjects.length > 0 &&
!selectedStoredObject.value
) {
selectedStoredObject.value = newStoredObjects[0];
onSelectionChange();
}
},
{ immediate: true },
);
async function onSelectionChange() {
if (!selectedStoredObject.value) {
cleanupPrevious();
documentType.value = "";
error.value = "";
return;
}
isLoading.value = true;
error.value = "";
cleanupPrevious();
try {
const document = await is_object_ready(selectedStoredObject.value);
if (!document) {
error.value = trans(CHILL_TICKET_PELOTON_ERROR_NOT_READY);
return;
}
const doc = await download_and_decrypt_doc(
selectedStoredObject.value,
selectedStoredObject.value.currentVersion,
);
const blob = new Blob([doc], { type: document.type });
documentUrl.value = URL.createObjectURL(blob);
if (document.type.startsWith("image/")) {
documentType.value = "image";
} else if (document.type === "application/pdf") {
documentType.value = "pdf";
} else {
documentType.value = "other";
}
} catch (err) {
console.error(trans(CHILL_TICKET_PELOTON_ERROR_LOADING), err);
error.value = trans(CHILL_TICKET_PELOTON_ERROR_LOADING);
} finally {
isLoading.value = false;
}
}
// Nettoyer les URLs blob précédentes
function cleanupPrevious() {
if (documentUrl.value) {
URL.revokeObjectURL(documentUrl.value);
documentUrl.value = "";
}
}
function handleClick() {
showPelotonsModal.value = true;
if (
!selectedStoredObject.value &&
props.storedObjects &&
props.storedObjects.length > 0
) {
selectedStoredObject.value = props.storedObjects[0];
onSelectionChange();
}
}
function closeModal() {
showPelotonsModal.value = false;
}
</script>
<style lang="scss" scoped>
.dropdown-container {
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
}
.document-viewer {
height: 80vh;
.image-container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
img {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 0.25rem;
}
}
.pdf-container {
height: 100%;
iframe {
border-radius: 0.25rem;
height: 100% !important;
}
}
.unsupported-document {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
p {
margin-bottom: 1rem;
color: #6c757d;
}
}
}
.loading-indicator {
text-align: center;
padding: 2rem;
.spinner-border {
margin-bottom: 1rem;
}
p {
color: #6c757d;
margin: 0;
}
}
</style>

View File

@@ -37,8 +37,10 @@
<template #body> <template #body>
<ticket-list-component <ticket-list-component
:hasMoreTickets="pagination.next !== null"
:tickets="previousTickets" :tickets="previousTickets"
:title="trans(CHILL_TICKET_LIST_NO_TICKETS)" :title="trans(CHILL_TICKET_LIST_NO_TICKETS)"
@fetchNextPage="fetchNextPage"
@view-ticket="handleViewTicket" @view-ticket="handleViewTicket"
@edit-ticket="handleEditTicket" @edit-ticket="handleEditTicket"
/> />
@@ -59,6 +61,7 @@ import TicketListComponent from "../../TicketList/components/TicketListComponent
import { import {
trans, trans,
CHILL_TICKET_LIST_NO_TICKETS, CHILL_TICKET_LIST_NO_TICKETS,
CHILL_TICKET_LIST_ERROR_LOADING_TICKET,
CHILL_TICKET_TICKET_PREVIOUS_TICKETS, CHILL_TICKET_TICKET_PREVIOUS_TICKETS,
CHILL_TICKET_LIST_TITLE_PREVIOUS_TICKETS, CHILL_TICKET_LIST_TITLE_PREVIOUS_TICKETS,
} from "translator"; } from "translator";
@@ -69,12 +72,14 @@ import { TicketSimple } from "../../../types";
// Utils // Utils
import { localizedUrl } from "ChillMainAssets/lib/localizationHelper/localizationHelper"; import { localizedUrl } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import { Pagination } from "ChillMainAssets/lib/api/apiMethods";
const store = useStore(); const store = useStore();
const showPreviousTicketModal = ref(false); const showPreviousTicketModal = ref(false);
const showTicketHistoryModal = ref(false); const showTicketHistoryModal = ref(false);
const selectedTicketId = ref<number | null>(null); const selectedTicketId = ref<number | null>(null);
const pagination = computed(() => store.getters.getPagination as Pagination);
const currentPersons = computed( const currentPersons = computed(
() => store.getters.getCurrentPersons as Person[], () => store.getters.getCurrentPersons as Person[],
); );
@@ -84,11 +89,13 @@ const previousTickets = computed(
onMounted(async () => { onMounted(async () => {
try { try {
await store.dispatch("fetchTicketList", { if (currentPersons.value.length) {
byPerson: currentPersons.value.map((person) => person.id), await store.dispatch("fetchTicketList", {
}); byPerson: currentPersons.value.map((person) => person.id),
});
}
} catch (error) { } catch (error) {
console.error("Erreur lors du chargement des tickets:", error); console.error(trans(CHILL_TICKET_LIST_ERROR_LOADING_TICKET), error);
} }
}); });
@@ -107,10 +114,16 @@ async function handleViewTicket(ticketId: number) {
try { try {
await store.dispatch("fetchTicket", ticketId); await store.dispatch("fetchTicket", ticketId);
} catch (error) { } catch (error) {
console.error("Erreur lors du chargement du ticket:", error); console.error(trans(CHILL_TICKET_LIST_ERROR_LOADING_TICKET), error);
} }
} }
const fetchNextPage = async () => {
if (pagination.value.next) {
await store.dispatch("fetchTicketListByUrl", pagination.value.next);
}
};
function handleEditTicket(ticketId: number) { function handleEditTicket(ticketId: number) {
const returnPath = localizedUrl(`/ticket/ticket/list`); const returnPath = localizedUrl(`/ticket/ticket/list`);
window.location.href = localizedUrl( window.location.href = localizedUrl(

View File

@@ -0,0 +1,68 @@
<template>
<span class="d-block d-sm-inline-block ms-sm-3 ms-md-0">
<button
class="badge rounded-pill me-1"
:class="{
'bg-chill-green': modelValue,
'bg-chill-red': !modelValue,
}"
@click="toggleEmergency"
>
{{
modelValue
? trans(CHILL_TICKET_TICKET_BANNER_OPEN)
: trans(CHILL_TICKET_TICKET_BANNER_CLOSED)
}}
</button>
</span>
</template>
<script lang="ts" setup>
import {
trans,
CHILL_TICKET_TICKET_BANNER_OPEN,
CHILL_TICKET_TICKET_BANNER_CLOSED,
} from "translator";
// Props
const props = defineProps<{
modelValue: boolean;
}>();
// Emits
const emit = defineEmits<{
"update:modelValue": [value: boolean];
}>();
// Methods
function toggleEmergency() {
const newValue = !props.modelValue;
emit("update:modelValue", newValue);
}
</script>
<style lang="scss" scoped>
a.flag-toggle {
color: white;
cursor: pointer;
&:hover {
color: white;
text-decoration: underline;
border-radius: 20px;
}
i {
margin: auto 0.4em;
}
span.on {
font-weight: bolder;
}
}
button.badge {
&.bg-secondary {
opacity: 0.5;
&:hover {
opacity: 0.7;
}
}
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<div class="card mb-4">
<div class="card-body">
<form @submit.prevent="submitForm">
<!-- Sélection du motif -->
<div class="mb-3">
<label class="form-label">{{
trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE)
}}</label>
<motive-selector-component
v-model="ticketForm.motive"
:motives="motives"
/>
</div>
<!-- Sélection des personnes -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">
{{ trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE_CALLER) }}
</label>
<persons-selector-component
v-model="ticketForm.caller"
:suggested="[]"
:multiple="false"
:types="['person', 'thirdparty']"
:label="trans(CHILL_TICKET_TICKET_SET_PERSONS_CALLER_LABEL)"
/>
</div>
<div class="col-md-6">
<label class="form-label">
{{ trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE_PERSON) }}
</label>
<persons-selector-component
v-model="ticketForm.persons"
:suggested="suggestedPersons"
:multiple="true"
:types="['person']"
:label="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
/>
</div>
</div>
<!-- Éditeur de commentaire -->
<div class="mb-3">
<label class="form-label">{{
trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE)
}}</label>
<comment-editor-component
v-model="ticketForm.content"
:motive="ticketForm.motive"
/>
</div>
<!-- Boutons d'action -->
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-secondary" @click="resetForm">
{{ trans(CHILL_TICKET_TICKET_INIT_FORM_RESET) }}
</button>
<button type="submit" class="btn btn-primary">
{{ trans(CHILL_TICKET_TICKET_INIT_FORM_SUBMIT) }}
</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, watch } from "vue";
import { useStore } from "vuex";
// Components
import MotiveSelectorComponent from "./Motive/MotiveSelectorComponent.vue";
import CommentEditorComponent from "./Comment/CommentEditorComponent.vue";
import PersonsSelectorComponent from "./Person/PersonsSelectorComponent.vue";
// Types
import { Motive, TicketInitForm } from "../../../types";
import { Person } from "ChillPersonAssets/types";
// Translations
import {
trans,
CHILL_TICKET_TICKET_INIT_FORM_SUBMIT,
CHILL_TICKET_TICKET_INIT_FORM_RESET,
CHILL_TICKET_TICKET_SET_MOTIVE_TITLE,
CHILL_TICKET_TICKET_ADD_COMMENT_TITLE,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE_CALLER,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE_PERSON,
CHILL_TICKET_TICKET_SET_PERSONS_CALLER_LABEL,
CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL,
} from "translator";
defineProps<{
motives: Motive[];
suggestedPersons: Person[];
}>();
const emit = defineEmits<{
"update:modelValue": [value: TicketInitForm];
submit: [formData: TicketInitForm];
}>();
const store = useStore();
const ticketForm = reactive({
content: "",
motive: undefined as Motive | undefined,
persons: [] as Person[],
caller: null as Person | null,
} as TicketInitForm);
watch(
() => ticketForm.caller,
async (newCaller) => {
await store.dispatch("setCaller", newCaller);
await store.dispatch("getSuggestedPersons");
},
);
function submitForm() {
emit("submit", {
content: ticketForm.content,
motive: ticketForm.motive,
persons: [...ticketForm.persons],
caller: ticketForm.caller,
});
}
function resetForm() {
ticketForm.content = "";
ticketForm.motive = undefined;
ticketForm.persons = [];
ticketForm.caller = null;
}
</script>
<style lang="scss" scoped>
.card {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.gap-2 {
gap: 0.5rem;
}
</style>

View File

@@ -8,6 +8,7 @@ import {
State as TicketListState, State as TicketListState,
moduleTicketList, moduleTicketList,
} from "./modules/ticket_list"; } from "./modules/ticket_list";
import { State as UserState, moduleUser } from "./modules/user";
export interface RootState { export interface RootState {
motive: MotiveStates; motive: MotiveStates;
@@ -16,6 +17,7 @@ export interface RootState {
addressee: AddresseeStates; addressee: AddresseeStates;
persons: PersonsState; persons: PersonsState;
ticketList: TicketListState; ticketList: TicketListState;
user: UserState;
} }
export const store = createStore<RootState>({ export const store = createStore<RootState>({
@@ -26,5 +28,6 @@ export const store = createStore<RootState>({
addressee: moduleAddressee, addressee: moduleAddressee,
persons: modulePersons, persons: modulePersons,
ticketList: moduleTicketList, ticketList: moduleTicketList,
user: moduleUser,
}, },
}); });

View File

@@ -61,15 +61,11 @@ export const moduleAddressee: Module<State, RootState> = {
} }
}, },
async setAdressees( async setAddressees({ commit, rootState }, addressees: UserGroupOrUser[]) {
{ commit },
datas: { ticketId: number; addressees: UserGroupOrUser[] },
) {
const { ticketId, addressees } = datas;
try { try {
const result = await makeFetch( const result = await makeFetch(
"POST", "POST",
`/api/1.0/ticket/${ticketId}/addressees/set`, `/api/1.0/ticket/${rootState.ticket.ticket.id}/addressees/set`,
{ {
addressees: addressees.map((addressee) => { addressees: addressees.map((addressee) => {
return { id: addressee.id, type: addressee.type }; return { id: addressee.id, type: addressee.type };

View File

@@ -3,7 +3,7 @@ import { makeFetch } from "../../../../../../../../ChillMainBundle/Resources/pub
import { Module } from "vuex"; import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Comment } from "../../../../types"; import { Comment, Ticket } from "../../../../types";
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types"; import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
export interface State { export interface State {
@@ -14,18 +14,32 @@ export const moduleComment: Module<State, RootState> = {
state: () => ({ state: () => ({
comments: [] as Comment[], comments: [] as Comment[],
}), }),
getters: {}, getters: {
canBeDisplayed:
(state: State, getters: unknown, rootState: RootState) =>
(comment: Comment) => {
return (
(comment.deleted &&
comment.createdBy?.username ===
rootState.user.currentUser?.username) ||
!comment.deleted
);
},
canBeEdited:
(state: State, getters: unknown, rootState: RootState) =>
(comment: Comment) => {
return (
comment.createdBy?.username === rootState.user.currentUser?.username
);
},
},
mutations: {}, mutations: {},
actions: { actions: {
async createComment( async createComment({ commit, rootState }, content: Comment["content"]) {
{ commit },
datas: { ticketId: number; content: Comment["content"] },
) {
const { ticketId, content } = datas;
try { try {
const result = await makeFetch( const result: Ticket = await makeFetch(
"POST", "POST",
`/api/1.0/ticket/${ticketId}/comment/add`, `/api/1.0/ticket/${rootState.ticket.ticket.id}/comment/add`,
{ content }, { content },
); );
commit("setTicket", result); commit("setTicket", result);
@@ -34,5 +48,46 @@ export const moduleComment: Module<State, RootState> = {
throw error.name; throw error.name;
} }
}, },
async editComment(
{ commit },
{ id, content }: { id: Comment["id"]; content: Comment["content"] },
) {
try {
const result: Comment = await makeFetch(
"POST",
`/api/1.0/ticket/comment/${id}/edit`,
{ content },
);
commit("setTicketHistoryComment", result);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
}
},
async deleteComment({ commit }, id: Comment["id"]) {
try {
const result: Comment = await makeFetch(
"POST",
`/api/1.0/ticket/comment/${id}/delete`,
);
commit("setTicketHistoryComment", result);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
}
},
async restoreComment({ commit }, id: Comment["id"]) {
try {
const result: Comment = await makeFetch(
"POST",
`/api/1.0/ticket/comment/${id}/restore`,
);
commit("setTicketHistoryComment", result);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
}
},
}, },
}; };

View File

@@ -40,15 +40,11 @@ export const moduleMotive: Module<State, RootState> = {
} }
}, },
async createMotive( async createMotive({ commit, rootState }, motive: Motive) {
{ commit },
datas: { ticketId: number; motive: Motive },
) {
const { ticketId, motive } = datas;
try { try {
const result = await makeFetch( const result = await makeFetch(
"POST", "POST",
`/api/1.0/ticket/${ticketId}/motive/set`, `/api/1.0/ticket/${rootState.ticket.ticket.id}/motive/set`,
{ {
motive: { motive: {
id: motive.id, id: motive.id,

View File

@@ -24,19 +24,17 @@ export const modulePersons: Module<State, RootState> = {
}, },
}, },
actions: { actions: {
async setPersons( async setPersons({ commit, rootState }, persons: Person[]) {
{ commit, rootState: RootState }, const personData = persons.map((person: Person) => ({
payload: { persons: Person[] },
) {
const persons = payload.persons.map((person: Person) => ({
id: person.id, id: person.id,
type: person.type, type: person.type,
})); }));
console.log("Setting persons:", personData);
try { try {
const result: Ticket = await makeFetch( const result: Ticket = await makeFetch(
"POST", "POST",
`/api/1.0/ticket/${RootState.ticket.ticket.id}/persons/set`, `/api/1.0/ticket/${rootState.ticket.ticket.id}/persons/set`,
{ persons }, { persons: personData },
); );
commit("setTicket", result); commit("setTicket", result);
@@ -46,21 +44,18 @@ export const modulePersons: Module<State, RootState> = {
throw error.name; throw error.name;
} }
}, },
async setCaller( async setCaller({ commit, rootState }, caller: Person | null) {
{ commit, rootState: RootState },
payload: { caller: Person | null },
) {
try { try {
const caller = payload.caller const callerData = caller
? { ? {
id: payload.caller.id, id: caller.id,
type: payload.caller.type, type: caller.type,
} }
: null; : null;
const result: Ticket = await makeFetch( const result: Ticket = await makeFetch(
"POST", "POST",
`/api/1.0/ticket/ticket/${RootState.ticket.ticket.id}/set-caller`, `/api/1.0/ticket/ticket/${rootState.ticket.ticket.id}/set-caller`,
{ caller }, { caller: callerData },
); );
commit("setTicket", result as Ticket); commit("setTicket", result as Ticket);
} catch (e: unknown) { } catch (e: unknown) {
@@ -69,9 +64,9 @@ export const modulePersons: Module<State, RootState> = {
} }
}, },
async getSuggestedPersons({ commit, rootState: RootState }) { async getSuggestedPersons({ commit, rootState }) {
try { try {
const ticketId = RootState.ticket.ticket.id; const ticketId = rootState.ticket.ticket.id;
const result: Person[] = await makeFetch( const result: Person[] = await makeFetch(
"GET", "GET",
`/api/1.0/ticket/ticket/${ticketId}/suggest-person`, `/api/1.0/ticket/ticket/${ticketId}/suggest-person`,

View File

@@ -1,7 +1,13 @@
import { Module } from "vuex"; import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Ticket, TicketEmergencyState } from "../../../../types"; import {
Comment,
Ticket,
TicketEmergencyState,
TicketHistoryLine,
TicketState,
} from "../../../../types";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types"; import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
import { getSinceCreated } from "../../utils/utils"; import { getSinceCreated } from "../../utils/utils";
@@ -30,7 +36,10 @@ export const moduleTicket: Module<State, RootState> = {
return state.ticket.currentState === "open"; return state.ticket.currentState === "open";
}, },
isEmergency(state) { isEmergency(state) {
return state.ticket.emergency == "yes" ? true : false; return state.ticket.emergency == "yes";
},
isNewTicket(state) {
return state.ticket.history.length == 3;
}, },
getTicket(state) { getTicket(state) {
state.ticket.history = state.ticket.history.sort((a, b) => state.ticket.history = state.ticket.history.sort((a, b) =>
@@ -59,13 +68,24 @@ export const moduleTicket: Module<State, RootState> = {
setTicket(state, ticket: Ticket) { setTicket(state, ticket: Ticket) {
state.ticket = ticket; state.ticket = ticket;
}, },
setTicketHistoryComment(state, ticketHistoryData: Comment) {
const index = state.ticket.history.findIndex(
(item: TicketHistoryLine) =>
item.event_type == "add_comment" &&
item.data.id === ticketHistoryData.id,
);
if (index !== -1) {
state.ticket.history[index].data = ticketHistoryData;
}
},
}, },
actions: { actions: {
async closeTicket({ commit, state }) { async setTicketState({ commit, state }, ticketState: TicketState) {
try { try {
const result: Ticket = await makeFetch( const result: Ticket = await makeFetch(
"POST", "POST",
`/api/1.0/ticket/ticket/${state.ticket.id}/close`, `/api/1.0/ticket/ticket/${state.ticket.id}/${ticketState}`,
); );
commit("setTicket", result as Ticket); commit("setTicket", result as Ticket);
} catch (e: unknown) { } catch (e: unknown) {
@@ -73,19 +93,23 @@ export const moduleTicket: Module<State, RootState> = {
throw error.name; throw error.name;
} }
}, },
async reopenTicket({ commit, state }) {
async fetchTicket({ commit }, ticketId: number) {
try { try {
const result: Ticket = await makeFetch( const ticket: Ticket = await makeFetch(
"POST", "GET",
`/api/1.0/ticket/ticket/${state.ticket.id}/open`, `/api/1.0/ticket/ticket/${ticketId}`,
); );
commit("setTicket", result as Ticket); commit("setTicket", ticket);
} catch (e: unknown) { } catch (error) {
const error = e as ApiException; console.error(
throw error.name; "Erreur lors du chargement du ticket:",
error as ApiException,
);
throw error;
} }
}, },
async toggleEmergency({ commit, state }, emergency: TicketEmergencyState) { async setEmergency({ commit, state }, emergency: TicketEmergencyState) {
try { try {
const result: Ticket = await makeFetch( const result: Ticket = await makeFetch(
"POST", "POST",

View File

@@ -1,7 +1,7 @@
import { Module } from "vuex"; import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Ticket, TicketFilterParams, TicketSimple } from "../../../../types"; import { TicketFilterParams, TicketSimple } from "../../../../types";
import { import {
makeFetch, makeFetch,
Pagination, Pagination,
@@ -14,7 +14,6 @@ import {
export interface State { export interface State {
ticket_list: TicketSimple[]; ticket_list: TicketSimple[];
ticket_details: Ticket | null;
pagination: Pagination; pagination: Pagination;
count: number; count: number;
user: User; user: User;
@@ -23,7 +22,6 @@ export interface State {
export const moduleTicketList: Module<State, RootState> = { export const moduleTicketList: Module<State, RootState> = {
state: () => ({ state: () => ({
ticket_list: [], ticket_list: [],
ticket_details: null,
pagination: { pagination: {
first: 0, first: 0,
items_per_page: 50, items_per_page: 50,
@@ -38,9 +36,6 @@ export const moduleTicketList: Module<State, RootState> = {
getTicketList(state): TicketSimple[] { getTicketList(state): TicketSimple[] {
return state.ticket_list; return state.ticket_list;
}, },
getTicketDetails(state): Ticket | null {
return state.ticket_details;
},
getPagination(state) { getPagination(state) {
return state.pagination; return state.pagination;
}, },
@@ -55,9 +50,6 @@ export const moduleTicketList: Module<State, RootState> = {
setTicketList(state, ticketList: TicketSimple[]) { setTicketList(state, ticketList: TicketSimple[]) {
state.ticket_list = ticketList; state.ticket_list = ticketList;
}, },
setTicketDetails(state, ticket: Ticket | null) {
state.ticket_details = ticket;
},
setPagination(state, pagination: State["pagination"]) { setPagination(state, pagination: State["pagination"]) {
state.pagination = pagination; state.pagination = pagination;
}, },
@@ -69,11 +61,26 @@ export const moduleTicketList: Module<State, RootState> = {
}, },
}, },
actions: { actions: {
async fetchTicketList({ commit }, ticketFilterParams: TicketFilterParams) { async fetchTicketList(
{ commit },
ticketFilterParams: TicketFilterParams | null,
) {
try { try {
const params = new URLSearchParams( let params = "";
ticketFilterParams as Record<string, string>, if (ticketFilterParams) {
).toString(); const filteredParams = Object.fromEntries(
Object.entries(ticketFilterParams).filter(
([, value]) =>
value !== undefined &&
value !== null &&
value !== "" &&
value.length > 0,
),
);
params = new URLSearchParams(
filteredParams as Record<string, string>,
).toString();
}
const { results, pagination, count } = (await makeFetch( const { results, pagination, count } = (await makeFetch(
"GET", "GET",
`/api/1.0/ticket/ticket/list/?${params}`, `/api/1.0/ticket/ticket/list/?${params}`,
@@ -94,7 +101,7 @@ export const moduleTicketList: Module<State, RootState> = {
try { try {
const user = await makeFetch( const user = await makeFetch(
"GET", "GET",
"http://localhost:8000/api/1.0/main/whoami.json", "/api/1.0/main/whoami.json",
); );
commit("setUser", user); commit("setUser", user);
return user; return user;
@@ -106,21 +113,6 @@ export const moduleTicketList: Module<State, RootState> = {
throw error; throw error;
} }
}, },
async fetchTicketDetails({ commit }, ticketId: number) {
try {
const ticket: Ticket = await makeFetch(
"GET",
`/api/1.0/ticket/ticket/${ticketId}`,
);
commit("setTicketDetails", ticket);
} catch (error) {
console.error(
"Erreur lors du chargement du ticket:",
error as ApiException,
);
throw error;
}
},
async fetchTicketListByUrl({ commit, state }, url: string) { async fetchTicketListByUrl({ commit, state }, url: string) {
try { try {
const { results, pagination, count } = (await makeFetch( const { results, pagination, count } = (await makeFetch(

View File

@@ -0,0 +1,41 @@
import { Module } from "vuex";
import { RootState } from "../index";
import { ApiException, User } from "ChillMainAssets/types";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export interface State {
currentUser: User | null;
}
export const moduleUser: Module<State, RootState> = {
state: () => ({
currentUser: null,
}),
getters: {
currentUser(state) {
return state.currentUser;
},
},
mutations: {
setCurrentUser(state, user: User) {
state.currentUser = user;
},
},
actions: {
async getCurrentUser({ commit }) {
try {
const userData: User = await makeFetch(
"GET",
"/api/1.0/main/whoami.json",
);
commit("setCurrentUser", userData);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
}
},
},
};

View File

@@ -49,7 +49,7 @@ import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
// Types // Types
import { TicketSimple, Motive } from "../../types"; import { TicketSimple, Motive, TicketFilterParams } from "../../types";
import type { Person } from "ChillPersonAssets/types"; import type { Person } from "ChillPersonAssets/types";
// Components // Components
@@ -60,15 +60,6 @@ import { Pagination } from "ChillMainAssets/lib/api/apiMethods";
// Translations // Translations
import { trans, CHILL_TICKET_LIST_LOADING_TICKET } from "translator"; import { trans, CHILL_TICKET_LIST_LOADING_TICKET } from "translator";
interface TicketFilterParams {
byPerson?: number[];
byCurrentState?: string[];
byCurrentStateEmergency?: string[];
byMotives?: number[];
byCreatedAfter?: string;
byCreatedBefore?: string;
byResponseTimeExceeded?: string;
}
const store = useStore(); const store = useStore();
const title = window.title; const title = window.title;
@@ -91,7 +82,6 @@ const handleFiltersChanged = async (filters: TicketFilterParams) => {
}; };
const fetchNextPage = async () => { const fetchNextPage = async () => {
console.log("Fetching next page...");
if (pagination.value.next) { if (pagination.value.next) {
await store.dispatch("fetchTicketListByUrl", pagination.value.next); await store.dispatch("fetchTicketListByUrl", pagination.value.next);
} }
@@ -99,11 +89,18 @@ const fetchNextPage = async () => {
onMounted(async () => { onMounted(async () => {
isLoading.value = true; isLoading.value = true;
const filters: TicketFilterParams = {
byCurrentState: ["open"],
byCurrentStateEmergency: [],
byCreatedAfter: "",
byCreatedBefore: "",
byResponseTimeExceeded: "",
byAddresseeToMe: false,
};
try { try {
await Promise.all([ await store.dispatch("getCurrentUser");
store.dispatch("fetchTicketList"), await store.dispatch("fetchTicketList", filters);
store.dispatch("fetchMotives"), await store.dispatch("fetchMotives");
]);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@@ -10,7 +10,7 @@
<div class="row"> <div class="row">
<!-- Filtre par usagé --> <!-- Filtre par usagé -->
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label class="form-label">{{ <label class="form-label" for="personSelector">{{
trans(CHILL_TICKET_LIST_FILTER_PERSONS_CONCERNED) trans(CHILL_TICKET_LIST_FILTER_PERSONS_CONCERNED)
}}</label> }}</label>
<persons-selector <persons-selector
@@ -18,6 +18,7 @@
:suggested="availablePersons" :suggested="availablePersons"
:multiple="true" :multiple="true"
:types="['person']" :types="['person']"
id="personSelector"
:label="trans(CHILL_TICKET_LIST_FILTER_BY_PERSON)" :label="trans(CHILL_TICKET_LIST_FILTER_BY_PERSON)"
/> />
</div> </div>
@@ -25,12 +26,13 @@
<div class="col-md-6"> <div class="col-md-6">
<!-- Filtre par motifs --> <!-- Filtre par motifs -->
<div class="row"> <div class="row">
<label class="form-label">{{ <label class="form-label" for="motiveSelector">{{
trans(CHILL_TICKET_LIST_FILTER_BY_MOTIVES) trans(CHILL_TICKET_LIST_FILTER_BY_MOTIVES)
}}</label> }}</label>
<motive-selector <motive-selector
v-model="selectedMotive" v-model="selectedMotive"
:motives="availableMotives" :motives="availableMotives"
id="motiveSelector"
/> />
<div class="mt-1" style="min-height: 2.2em"> <div class="mt-1" style="min-height: 2.2em">
@@ -54,56 +56,40 @@
<!-- Filtre par état actuel --> <!-- Filtre par état actuel -->
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<div class="form-check"> <div class="mb-2">
<input <label class="form-label pe-2" for="currentState">
class="form-check-input" {{ trans(CHILL_TICKET_LIST_FILTER_CURRENT_STATE) }}
type="checkbox"
value="open"
v-model="filters.byCurrentState"
id="stateOpen"
/>
<label class="form-check-label" for="stateOpen">{{
trans(CHILL_TICKET_LIST_FILTER_OPEN)
}}</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
value="closed"
v-model="filters.byCurrentState"
id="stateClosed"
/>
<label class="form-check-label" for="stateClosed">
{{ trans(CHILL_TICKET_LIST_FILTER_CLOSED) }}
</label> </label>
<toggle-component
v-model="isClosedToggled"
:on-label="trans(CHILL_TICKET_LIST_FILTER_CLOSED)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_OPEN)"
:classColor="{
on: 'bg-chill-red',
off: 'bg-chill-green',
}"
@update:model-value="handleStateToggle"
id="currentState"
/>
</div> </div>
</div> </div>
<!-- Filtre par état d'urgence --> <!-- Filtre par état d'urgence -->
<div class="col-6"> <div class="col-6">
<div class="form-check"> <div class="mb-2">
<input <label class="form-label pe-2" for="emergency">
class="form-check-input" {{ trans(CHILL_TICKET_LIST_FILTER_EMERGENCY) }}
type="checkbox" </label>
value="yes" <toggle-component
v-model="filters.byCurrentStateEmergency" v-model="isEmergencyToggled"
id="emergencyYes" :on-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:classColor="{
on: 'bg-chill-red',
off: 'bg-secondary',
}"
@update:model-value="handleEmergencyToggle"
id="emergency"
/> />
<label class="form-check-label" for="emergencyYes">{{
trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)
}}</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
value="no"
v-model="filters.byCurrentStateEmergency"
id="emergencyNo"
/>
<label class="form-check-label" for="emergencyNo">{{
trans(CHILL_TICKET_LIST_FILTER_NO_EMERGENCY)
}}</label>
</div> </div>
</div> </div>
</div> </div>
@@ -135,7 +121,7 @@
<div class="d-flex gap-3"> <div class="d-flex gap-3">
<div class="form-check"> <div class="form-check">
<input <input
v-model="filters.byMyTickets" v-model="filters.byAddresseeToMe"
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="stateMe" id="stateMe"
@@ -159,6 +145,7 @@
id="byCreatedAfter" id="byCreatedAfter"
v-model="filters.byCreatedAfter" v-model="filters.byCreatedAfter"
class="form-control" class="form-control"
:disabled="filters.byResponseTimeExceeded"
/> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
@@ -205,7 +192,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue"; import { ref, computed, watch } from "vue";
import type { Person } from "ChillPersonAssets/types"; import type { Person } from "ChillPersonAssets/types";
import type { Motive, TicketFilterParams, TicketFilters } from "../../../types"; import type { Motive, TicketFilterParams, TicketFilters } from "../../../types";
@@ -221,7 +208,6 @@ import {
CHILL_TICKET_LIST_FILTER_CLOSED, CHILL_TICKET_LIST_FILTER_CLOSED,
CHILL_TICKET_LIST_FILTER_TO_ME, CHILL_TICKET_LIST_FILTER_TO_ME,
CHILL_TICKET_LIST_FILTER_EMERGENCY, CHILL_TICKET_LIST_FILTER_EMERGENCY,
CHILL_TICKET_LIST_FILTER_NO_EMERGENCY,
CHILL_TICKET_LIST_FILTER_CREATED_AFTER, CHILL_TICKET_LIST_FILTER_CREATED_AFTER,
CHILL_TICKET_LIST_FILTER_CREATED_BEFORE, CHILL_TICKET_LIST_FILTER_CREATED_BEFORE,
CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_EXCEEDED, CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_EXCEEDED,
@@ -229,11 +215,13 @@ import {
CHILL_TICKET_LIST_FILTER_RESET, CHILL_TICKET_LIST_FILTER_RESET,
CHILL_TICKET_LIST_FILTER_APPLY, CHILL_TICKET_LIST_FILTER_APPLY,
CHILL_TICKET_LIST_FILTER_RESULT, CHILL_TICKET_LIST_FILTER_RESULT,
CHILL_TICKET_LIST_FILTER_CURRENT_STATE,
} from "translator"; } from "translator";
// Components // Components
import PersonsSelector from "../../TicketApp/components/Person/PersonsSelectorComponent.vue"; import PersonsSelector from "../../TicketApp/components/Person/PersonsSelectorComponent.vue";
import MotiveSelector from "../../TicketApp/components/Motive/MotiveSelectorComponent.vue"; import MotiveSelector from "../../TicketApp/components/Motive/MotiveSelectorComponent.vue";
import ToggleComponent from "./ToggleComponent.vue";
// Props // Props
const props = defineProps<{ const props = defineProps<{
@@ -249,12 +237,12 @@ const emit = defineEmits<{
// État réactif // État réactif
const filters = ref<TicketFilters>({ const filters = ref<TicketFilters>({
byCurrentState: [], byCurrentState: ["open"],
byCurrentStateEmergency: [], byCurrentStateEmergency: [],
byCreatedAfter: "", byCreatedAfter: "",
byCreatedBefore: "", byCreatedBefore: "",
byResponseTimeExceeded: false, byResponseTimeExceeded: false,
byMyTickets: false, byAddresseeToMe: false,
}); });
// Sélection des personnes // Sélection des personnes
@@ -283,6 +271,27 @@ const selectedMotiveIds = computed(() =>
selectedMotives.value.map((motive) => motive.id), selectedMotives.value.map((motive) => motive.id),
); );
// Nouveaux états pour les toggles
const isClosedToggled = ref(false);
const isEmergencyToggled = ref(false);
// Méthodes pour gérer les toggles
const handleStateToggle = (value: boolean) => {
if (value) {
filters.value.byCurrentState = ["closed"];
} else {
filters.value.byCurrentState = ["open"];
}
};
const handleEmergencyToggle = (value: boolean) => {
if (value) {
filters.value.byCurrentStateEmergency = ["yes"];
} else {
filters.value.byCurrentStateEmergency = ["no"];
}
};
// Méthodes // Méthodes
const formatDateToISO = (dateString: string): string => { const formatDateToISO = (dateString: string): string => {
if (!dateString) return dateString; if (!dateString) return dateString;
@@ -340,8 +349,8 @@ const applyFilters = (): void => {
if (filters.value.byResponseTimeExceeded) { if (filters.value.byResponseTimeExceeded) {
apiFilters.byResponseTimeExceeded = "true"; apiFilters.byResponseTimeExceeded = "true";
} }
if (filters.value.byMyTickets) { if (filters.value.byAddresseeToMe) {
apiFilters.byMyTickets = true; apiFilters.byAddresseeToMe = true;
} }
emit("filters-changed", apiFilters); emit("filters-changed", apiFilters);
@@ -349,31 +358,28 @@ const applyFilters = (): void => {
const resetFilters = (): void => { const resetFilters = (): void => {
filters.value = { filters.value = {
byCurrentState: [], byCurrentState: ["open"],
byCurrentStateEmergency: [], byCurrentStateEmergency: [],
byCreatedAfter: "", byCreatedAfter: "",
byCreatedBefore: "", byCreatedBefore: "",
byResponseTimeExceeded: false, byResponseTimeExceeded: false,
byMyTickets: false, byAddresseeToMe: false,
}; };
selectedPersons.value = []; selectedPersons.value = [];
selectedMotives.value = []; selectedMotives.value = [];
selectedMotive.value = undefined; selectedMotive.value = undefined;
isClosedToggled.value = true;
isEmergencyToggled.value = false;
applyFilters(); applyFilters();
}; };
const handleResponseTimeExceededChange = (): void => { const handleResponseTimeExceededChange = (): void => {
if (filters.value.byResponseTimeExceeded) { if (filters.value.byResponseTimeExceeded) {
filters.value.byCurrentState = [];
filters.value.byCreatedBefore = ""; filters.value.byCreatedBefore = "";
filters.value.byCreatedAfter = "";
isClosedToggled.value = true;
} }
}; };
// Charger les données disponibles si nécessaire
onMounted(() => {
// Ici vous pourriez faire des appels API pour charger les personnes et motifs disponibles
// si ils ne sont pas fournis en props
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -13,6 +13,15 @@
<div class="d-flex align-items-center fw-bold"> <div class="d-flex align-items-center fw-bold">
<i :class="`${actionIcons[history_line.event_type]} me-1`"></i> <i :class="`${actionIcons[history_line.event_type]} me-1`"></i>
<span>{{ explainSentence(history_line) }}</span> <span>{{ explainSentence(history_line) }}</span>
<span
v-if="
history_line.event_type === 'add_comment' &&
history_line.data.deleted
"
class="badge bg-danger ms-2"
>
{{ trans(CHILL_TICKET_TICKET_HISTORY_DELETED) }}
</span>
<state-component <state-component
:new_state="history_line.data.new_state" :new_state="history_line.data.new_state"
v-if="history_line.event_type == 'state_change'" v-if="history_line.event_type == 'state_change'"
@@ -24,9 +33,7 @@
</div> </div>
<div> <div>
<span class="badge-user"> <span class="badge-user">
<user-render-box-badge <user-render-box-badge :user="history_line.by" />
:user="history_line.by"
></user-render-box-badge>
</span> </span>
<span class="fst-italic mx-2"> <span class="fst-italic mx-2">
{{ formatDate(history_line.at) }} {{ formatDate(history_line.at) }}
@@ -60,7 +67,10 @@
/> />
<comment-component <comment-component
:commentHistory="history_line.data" :commentHistory="history_line.data"
v-else-if="history_line.event_type == 'add_comment'" v-else-if="
history_line.event_type == 'add_comment' &&
canBeDisplayed(history_line.data)
"
/> />
<addressee-component <addressee-component
:addressees="history_line.data.addressees" :addressees="history_line.data.addressees"
@@ -103,6 +113,7 @@ import {
CHILL_TICKET_TICKET_HISTORY_STATE_CHANGE, CHILL_TICKET_TICKET_HISTORY_STATE_CHANGE,
CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE, CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE,
CHILL_TICKET_TICKET_HISTORY_SET_CALLER, CHILL_TICKET_TICKET_HISTORY_SET_CALLER,
CHILL_TICKET_TICKET_HISTORY_DELETED,
} from "translator"; } from "translator";
const props = defineProps<{ history?: TicketHistoryLine[] }>(); const props = defineProps<{ history?: TicketHistoryLine[] }>();
@@ -110,6 +121,7 @@ const history = props.history ?? [];
const store = useStore(); const store = useStore();
const actionIcons = ref(store.getters.getActionIcons); const actionIcons = ref(store.getters.getActionIcons);
const canBeDisplayed = ref(store.getters["canBeDisplayed"]);
function explainSentence(history: TicketHistoryLine): string { function explainSentence(history: TicketHistoryLine): string {
switch (history.event_type) { switch (history.event_type) {

View File

@@ -107,9 +107,7 @@ const emit = defineEmits<{
const store = useStore(); const store = useStore();
const selectedTicketId = ref<number | null>(null); const selectedTicketId = ref<number | null>(null);
const showTicketHistoryModal = ref(false); const showTicketHistoryModal = ref(false);
const ticketDetails = computed( const ticketDetails = computed(() => store.getters.getTicket as Ticket | null);
() => store.getters.getTicketDetails as Ticket | null,
);
function closeHistoryModal() { function closeHistoryModal() {
showTicketHistoryModal.value = false; showTicketHistoryModal.value = false;
@@ -117,7 +115,7 @@ function closeHistoryModal() {
} }
async function handleViewTicket(ticketId: number) { async function handleViewTicket(ticketId: number) {
await store.dispatch("fetchTicketDetails", ticketId); await store.dispatch("fetchTicket", ticketId);
selectedTicketId.value = ticketId; selectedTicketId.value = ticketId;
showTicketHistoryModal.value = true; showTicketHistoryModal.value = true;

View File

@@ -25,7 +25,6 @@
</div> </div>
</div> </div>
<div class="wh-row"> <div class="wh-row">
<div class="wh-col">#{{ ticket.id }}</div>
<div class="wh-col" v-if="ticket.createdAt"> <div class="wh-col" v-if="ticket.createdAt">
<span <span
v-if="ticket" v-if="ticket"

View File

@@ -0,0 +1,156 @@
<template>
<div class="toggle-wrapper">
<input
:id="inputId"
type="checkbox"
class="toggle-input"
:checked="modelValue"
@change="handleChange"
:disabled="disabled"
/>
<label
:for="inputId"
class="toggle-label"
:class="[
modelValue
? classColor?.on || 'bg-chill-green'
: classColor?.off || 'bg-chill-red',
]"
:style="{
backgroundColor: classColor
? undefined
: !modelValue
? colorOff
: colorOn,
height: '28px',
width: toggleWidth + 'px',
}"
>
<span
class="toggle-slider"
:style="{
transform: modelValue ? `translateX(${toggleWidth - 26}px)` : 'none',
width: '24px',
height: '24px',
}"
>
</span>
<span
class="toggle-text"
:style="{
color: 'white',
marginLeft: !modelValue ? '28px' : '0',
paddingLeft: modelValue ? '6px' : '0',
}"
>
{{ modelValue ? onLabel : offLabel }}
</span>
</label>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
interface Props {
modelValue: boolean;
onLabel?: string;
offLabel?: string;
disabled?: boolean;
id?: string;
colorOn?: string;
colorOff?: string;
classColor?: {
on: string;
off: string;
};
}
type Emits = (e: "update:modelValue", value: boolean) => void;
const props = withDefaults(defineProps<Props>(), {
onLabel: "ON",
offLabel: "OFF",
disabled: false,
id: undefined,
colorOn: "#4caf50",
colorOff: "#ccc",
classColor: () => ({
on: "bg-chill-green",
off: "bg-chill-red",
}),
});
const emit = defineEmits<Emits>();
const inputId = computed(
() => props.id || `toggle-${Math.random().toString(36).substr(2, 9)}`,
);
// Calcule la largeur du toggle basée sur le label le plus long
const toggleWidth = computed(() => {
const onLength = props.onLabel.length;
const offLength = props.offLabel.length;
const maxLength = Math.max(onLength, offLength);
// Largeur minimale: 56px, puis 7px par caractère supplémentaire au-delà de 3 caractères
const baseWidth = 56;
const extraWidth = Math.max(0, maxLength - 3) * 7;
return baseWidth + extraWidth;
});
const handleChange = (event: Event) => {
const target = event.target as HTMLInputElement;
emit("update:modelValue", target.checked);
};
</script>
<style scoped>
.toggle-wrapper {
display: inline-block;
position: relative;
}
.toggle-input {
display: none;
}
.toggle-label {
display: block;
border-radius: 14px;
position: relative;
cursor: pointer;
transition: background-color 0.3s ease;
user-select: none;
}
.toggle-label:hover {
filter: brightness(0.95);
}
.toggle-input:disabled + .toggle-label {
opacity: 0.6;
cursor: not-allowed;
}
.toggle-slider {
position: absolute;
top: 2px;
left: 2px;
background-color: white;
border-radius: 50%;
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-text {
font-size: 9px;
font-weight: bold;
letter-spacing: 0.5px;
transition: opacity 0.3s ease;
}
</style>

View File

@@ -1,6 +1,8 @@
import App from "./App.vue"; import App from "./App.vue";
import { createApp } from "vue"; import { createApp } from "vue";
import { store } from "../TicketApp/store"; import { store } from "../TicketApp/store";
import VueToast from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
declare global { declare global {
interface Window { interface Window {
@@ -12,4 +14,4 @@ const _app = createApp({
template: "<app></app>", template: "<app></app>",
}); });
_app.component("app", App).use(store).mount("#ticketList"); _app.component("app", App).use(store).use(VueToast).mount("#ticketList");

View File

@@ -1,3 +1,4 @@
restore: Restaurer
chill_ticket: chill_ticket:
list: list:
title: Tickets title: Tickets
@@ -5,6 +6,7 @@ chill_ticket:
no_tickets: "Aucun ticket" no_tickets: "Aucun ticket"
loading_ticket: "Chargement des tickets..." loading_ticket: "Chargement des tickets..."
loading_ticket_details: "Chargement de l'historique du ticket..." loading_ticket_details: "Chargement de l'historique du ticket..."
error_loading_ticket: "Erreur lors du chargement des ticket"
load_more: "Voir plus..." load_more: "Voir plus..."
addressees: "Attribué à" addressees: "Attribué à"
persons: "Usager concernés" persons: "Usager concernés"
@@ -15,7 +17,7 @@ chill_ticket:
created_between: Créés entre created_between: Créés entre
state_change: État actuel state_change: État actuel
title: "Filtres des tickets" title: "Filtres des tickets"
persons_concerned: "Usager concernés" persons_concerned: "Usagers concernés"
by_person: "Par personne" by_person: "Par personne"
by_motives: "Par motifs" by_motives: "Par motifs"
current_state: "État actuel" current_state: "État actuel"
@@ -26,13 +28,19 @@ chill_ticket:
created_after: "Créé après" created_after: "Créé après"
created_before: "Créé avant" created_before: "Créé avant"
response_time_exceeded: "Temps de réponse dépassé" response_time_exceeded: "Temps de réponse dépassé"
response_time_warning: 'Attention : Ce filtre supprime automatiquement les filtres "État actuel" et "Créé avant"' response_time_warning: 'Attention : ce filtre désactive les filtres "par dates" et affiche uniquement les tickets ouverts'
reset: "Réinitialiser" reset: "Réinitialiser"
apply: "Appliquer les filtres" apply: "Appliquer les filtres"
remove: "Supprimer" remove: "Supprimer"
result: "{count, plural, =0 {Aucun résultat} =1 {resultat} other {resultats}}" result: "{count, plural, =0 {Aucun résultat} =1 {resultat} other {resultats}}"
ticket: ticket:
init_form:
title: "Ajouter des informations sur le ticket"
submit: "Mettre à jour le ticket"
reset: "Réinitialiser"
success: "Ticket mis à jour avec succès"
error: "Veuillez remplir tous les champs obligatoires"
history: history:
add_comment: "Nouveau commentaire" add_comment: "Nouveau commentaire"
addressees_state: "Attributions" addressees_state: "Attributions"
@@ -42,6 +50,7 @@ chill_ticket:
create_ticket: "Ticket créé" create_ticket: "Ticket créé"
state_change: "" state_change: ""
emergency_change: "" emergency_change: ""
deleted: "Supprimé"
previous_tickets: "Précédents tickets" previous_tickets: "Précédents tickets"
actions_toolbar: actions_toolbar:
cancel: "Annuler" cancel: "Annuler"
@@ -52,6 +61,13 @@ chill_ticket:
reopen: "Rouvrir" reopen: "Rouvrir"
reopen_success: "Rouverture du ticket réussie" reopen_success: "Rouverture du ticket réussie"
reopen_error: "Erreur lors de la rouverture du ticket" reopen_error: "Erreur lors de la rouverture du ticket"
restore_comment:
success: "Commentaire restauré"
delete_comment:
success: "Commentaire supprimé"
edit_comment:
title: "Éditer le commentaire"
success: "Commentaire modifié"
add_comment: add_comment:
title: "Commentaire" title: "Commentaire"
label: "Ajouter un commentaire" label: "Ajouter un commentaire"
@@ -78,8 +94,8 @@ chill_ticket:
success: "Appelants et usagers mis à jour" success: "Appelants et usagers mis à jour"
error: "Aucun usager sélectionné" error: "Aucun usager sélectionné"
banner: banner:
person: "{count, plural, =0 {Aucun usager concerné} =1 {Usager concerné} other {Usagers concernés}}" person: "{count, plural, =0 {Aucun usager impliqué} =1 {Usager impliqué} other {Usagers impliqués}}"
speaker: "{count, plural, =0 {Aucun intervenant} =1 {Attribué à} other {Attribués à}}" speaker: "{count, plural, =0 {Aucun intervenant} =1 {Intervenant attribué} other {Intervenants attribués}}"
caller: "{count, plural, =0 {Aucun appelant} =1 {Appelant} other {Appelants}}" caller: "{count, plural, =0 {Aucun appelant} =1 {Appelant} other {Appelants}}"
open: "Ouvert" open: "Ouvert"
closed: "Fermé" closed: "Fermé"
@@ -87,27 +103,40 @@ chill_ticket:
and: "et" and: "et"
days: >- days: >-
{count, plural, {count, plural,
=0 {aucun jour} =0 {aucun jour}
=1 {1 jour} =1 {1 jour}
other {# jours} other {# jours}
} }
hours: >- hours: >-
{count, plural, {count, plural,
=0 {aucune heure} =0 {aucune heure}
=1 {1 heure} =1 {1 heure}
other {# heures} other {# heures}
} }
minutes: >- minutes: >-
{count, plural, {count, plural,
=0 {aucune minute} =0 {aucune minute}
=1 {1 minute} =1 {1 minute}
other {# minutes} other {# minutes}
} }
seconds: >- seconds: >-
{count, plural, {count, plural,
=0 {aucune seconde} =0 {aucune seconde}
=1 {1 seconde} =1 {1 seconde}
other {# secondes} other {# secondes}
} }
no_motive: "Pas de motif" no_motive: "Aucun motif"
emergency: "URGENT" emergency: "URGENT"
emergency_success: "Ticket marqué comme urgent"
emergency_error: "Erreur lors de la tentative de marquage du ticket comme urgent"
no_emergency_success: "Ticket marqué comme non urgent"
no_emergency_error: "Erreur lors de la tentative de marquage du ticket comme non urgent"
peloton:
loading: "Chargement..."
loading_document: "Chargement du document..."
error_loading: "Erreur lors du chargement du document."
error_not_ready: "Le document n'est pas prêt ou accessible."
unsupported_type: "Type de document non supporté pour l'affichage."
open_new_tab: "Ouvrir dans un nouvel onglet"
iframe_not_supported: "Votre navigateur ne supporte pas les iframes."
click_to_open_pdf: "Cliquez ici pour ouvrir le PDF"