Merge branch 'fix-and-change-from-board-78' into 'ticket-app-master'

Améliorations liées au board 78

See merge request Chill-Projet/chill-bundles!873
This commit is contained in:
2025-09-08 12:19:49 +00:00
committed by GitLab
18 changed files with 159 additions and 126 deletions

View File

@@ -30,6 +30,7 @@
<template #body>
<ticket-init-form-component
:ticket="ticket"
:motives="motives"
:suggested-persons="suggestedPersons"
@submit="handleFormSubmit"
@@ -61,6 +62,7 @@ import {
CHILL_TICKET_TICKET_INIT_FORM_TITLE,
CHILL_TICKET_TICKET_INIT_FORM_SUCCESS,
CHILL_TICKET_TICKET_INIT_FORM_ERROR,
CHILL_TICKET_TICKET_INIT_FORM_WARNING,
CHILL_TICKET_LIST_LOADING_TICKET_DETAILS,
} from "translator";
@@ -81,16 +83,18 @@ async function handleFormSubmit(ticketForm: TicketInitForm) {
try {
if (ticketForm.motive) {
await store.dispatch("createMotive", ticketForm.motive);
} else {
toast.warning(trans(CHILL_TICKET_TICKET_INIT_FORM_WARNING));
return;
}
if (ticketForm.content && ticketForm.content.trim() !== "") {
await store.dispatch("createComment", ticketForm.content);
} else {
toast.warning(trans(CHILL_TICKET_TICKET_INIT_FORM_WARNING));
return;
}
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++;
@@ -114,7 +118,7 @@ onMounted(async () => {
await store.dispatch("fetchUserGroups");
await store.dispatch("fetchUsers");
await store.dispatch("getSuggestedPersons");
showTicketInitFormModal.value = store.getters.isNewTicket;
showTicketInitFormModal.value = store.getters.isIncompleteTicket;
} catch (error) {
toast.error(error as string);
} finally {

View File

@@ -29,7 +29,6 @@
<form @submit.prevent="submitAction">
<comment-editor-component
v-model="content"
@supplementary-text="(value) => (supplementaryText = value)"
:motive="motive"
v-if="activeTab === 'add_comment'"
/>
@@ -254,7 +253,6 @@ const returnPath = computed((): string => {
const motive = ref(ticket.value.currentMotive as Motive);
const content = ref("" as Comment["content"]);
const supplementaryText = ref("" as string);
const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]);
const persons = ref(ticket.value.currentPersons as Person[]);
const caller = ref(ticket.value.caller as Person);
@@ -265,12 +263,8 @@ async function submitAction() {
if (!content.value) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_COMMENT_ERROR));
} else {
await store.dispatch(
"createComment",
content.value + supplementaryText.value,
);
await store.dispatch("createComment", content.value);
content.value = "";
supplementaryText.value = "";
activeTab.value = "";
toast.success(trans(CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS));
}

View File

@@ -10,12 +10,8 @@
/>
</h1>
<h2 v-if="ticket.currentPersons.length">
{{
ticket.currentPersons
.map((person: Person) => person.text)
.join(", ")
}}
<h2 v-if="ticket.caller">
{{ ticket.caller.text }}
</h2>
</div>

View File

@@ -2,13 +2,34 @@
<div class="col-12">
<blockquote class="chill-user-quote">
<button
class="btn btn-sm btn-edit float-end"
title="Edit"
class="btn btn-sm btn-primary float-end"
title="Visible"
@click="visibleComment"
v-if="commentHistory.deleted"
>
<i class="bi bi-eye"></i>
</button>
<button
class="btn btn-sm bg-chill-red float-end text-white"
@click="maskComment"
v-else
>
<i class="bi bi-eye-slash"></i>
</button>
<button
class="btn btn-sm btn-edit mx-2 float-end text-white"
@click="editCommentModal = true"
style="top: 0.5rem; right: 0.5rem"
v-if="canBeEdited && isOpen"
/>
<p v-html="convertMarkdownToHtml(commentHistory.content)"></p>
<span
v-if="commentHistory.deleted"
class="ms-2 d-block text-center fst-italic text-muted"
>
{{ trans(CHILL_TICKET_TICKET_MASK_COMMENT_HINT) }}
</span>
</blockquote>
</div>
<Modal
@@ -26,16 +47,6 @@
<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>
@@ -59,12 +70,11 @@ import Modal from "../../../../../../../../ChillMainBundle/Resources/public/vuej
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,
CHILL_TICKET_TICKET_MASK_COMMENT_SUCCESS,
CHILL_TICKET_TICKET_MASK_COMMENT_HINT,
CHILL_TICKET_TICKET_VISIBLE_COMMENT_SUCCESS,
} from "translator";
import { useToast } from "vue-toast-notification";
@@ -87,16 +97,16 @@ const saveComment = () => {
toast.success(trans(CHILL_TICKET_TICKET_EDIT_COMMENT_SUCCESS));
};
const deleteComment = () => {
store.dispatch("deleteComment", props.commentHistory.id);
const maskComment = () => {
store.dispatch("maskComment", props.commentHistory.id);
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_DELETE_COMMENT_SUCCESS));
toast.success(trans(CHILL_TICKET_TICKET_MASK_COMMENT_SUCCESS));
};
const restoreComment = () => {
store.dispatch("restoreComment", props.commentHistory.id);
const visibleComment = () => {
store.dispatch("visibleComment", props.commentHistory.id);
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_RESTORE_COMMENT_SUCCESS));
toast.success(trans(CHILL_TICKET_TICKET_VISIBLE_COMMENT_SUCCESS));
};
const preprocess = (markdown: string): string => {

View File

@@ -26,7 +26,7 @@
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
import { reactive, ref, watch, computed } from "vue";
// Components
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
@@ -45,31 +45,38 @@ const props = defineProps<{
const supplementaryCommentsInput = reactive<string[]>([]);
const emit = defineEmits<{
"update:modelValue": [value: string | undefined];
"update:modelValue": [value: string];
"show-peloton-modal": [storedObjects: StoredObject[]];
"supplementary-text": [value: string];
}>();
const content = ref(props.modelValue);
const aggregateSupplementaryComments = computed(() => {
let supplementaryText = " \n\n ";
if (props.motive && props.motive.supplementaryComments) {
props.motive.supplementaryComments.forEach(
(item: { label: string }, index: number) => {
if (supplementaryCommentsInput[index]) {
supplementaryText +=
`**${item.label}**: ${supplementaryCommentsInput[index]}` +
" \n\n ";
}
},
);
}
return (content.value || "") + supplementaryText;
});
watch(
supplementaryCommentsInput,
(value) => {
let supplementaryText = " \n\n ";
for (const index in value) {
if (value[index]) {
supplementaryText +=
`**${props.motive?.supplementaryComments[index].label}**: ${value[index]}` +
" \n\n ";
}
}
emit("supplementary-text", supplementaryText);
() => {
emit("update:modelValue", aggregateSupplementaryComments.value);
},
{ deep: true },
);
watch(content, (value) => {
emit("update:modelValue", value);
watch(content, () => {
emit("update:modelValue", aggregateSupplementaryComments.value);
});
</script>

View File

@@ -1,15 +1,13 @@
<template>
<div>
<span
class="badge rounded-pill me-1 mx-2"
:class="{
'bg-danger': new_emergency === 'yes',
'bg-secondary': new_emergency === 'no',
}"
>
{{ trans(CHILL_TICKET_TICKET_BANNER_EMERGENCY) }}
</span>
</div>
<span
class="badge rounded-pill me-1 mx-2"
:class="{
'bg-warning': new_emergency === 'yes',
'bg-secondary': new_emergency === 'no',
}"
>
{{ trans(CHILL_TICKET_TICKET_BANNER_EMERGENCY) }}
</span>
</template>
<script setup lang="ts">
import { trans, CHILL_TICKET_TICKET_BANNER_EMERGENCY } from "translator";

View File

@@ -3,7 +3,7 @@
<button
class="badge rounded-pill me-1"
:class="{
'bg-chill-red': modelValue,
'bg-warning': modelValue,
'bg-secondary': !modelValue,
'no-pointer': props.disabled,
}"

View File

@@ -25,7 +25,6 @@ import {
CHILL_TICKET_TICKET_BANNER_OPEN,
CHILL_TICKET_TICKET_BANNER_CLOSED,
} from "translator";
const props = defineProps<StateChange>();
</script>

View File

@@ -76,7 +76,7 @@ import CommentEditorComponent from "./Comment/CommentEditorComponent.vue";
import PersonsSelectorComponent from "./Person/PersonsSelectorComponent.vue";
// Types
import { Motive, TicketInitForm } from "../../../types";
import { Motive, Ticket, TicketInitForm } from "../../../types";
import { Person } from "ChillPersonAssets/types";
// Translations
@@ -92,7 +92,8 @@ import {
CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL,
} from "translator";
defineProps<{
const props = defineProps<{
ticket: Ticket;
motives: Motive[];
suggestedPersons: Person[];
}>();
@@ -106,9 +107,9 @@ const store = useStore();
const ticketForm = reactive({
content: "",
motive: undefined as Motive | undefined,
persons: [] as Person[],
caller: null as Person | null,
motive: props.ticket.currentMotive as Motive | null,
persons: props.ticket.currentPersons as Person[],
caller: props.ticket.caller as Person | null,
} as TicketInitForm);
watch(

View File

@@ -64,7 +64,7 @@ export const moduleComment: Module<State, RootState> = {
throw error.name;
}
},
async deleteComment({ commit }, id: Comment["id"]) {
async maskComment({ commit }, id: Comment["id"]) {
try {
const result: Comment = await makeFetch(
"POST",
@@ -77,7 +77,7 @@ export const moduleComment: Module<State, RootState> = {
}
},
async restoreComment({ commit }, id: Comment["id"]) {
async visibleComment({ commit }, id: Comment["id"]) {
try {
const result: Comment = await makeFetch(
"POST",

View File

@@ -4,6 +4,7 @@ import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/
import { Module } from "vuex";
import { RootState } from "..";
import { Ticket } from "../../../../types";
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
export interface State {
persons: Person[];
@@ -29,7 +30,6 @@ export const modulePersons: Module<State, RootState> = {
id: person.id,
type: person.type,
}));
console.log("Setting persons:", personData);
try {
const result: Ticket = await makeFetch(
"POST",
@@ -67,10 +67,14 @@ export const modulePersons: Module<State, RootState> = {
async getSuggestedPersons({ commit, rootState }) {
try {
const ticketId = rootState.ticket.ticket.id;
const result: Person[] = await makeFetch(
const caller = rootState.ticket.ticket.caller;
const result: (Person | Thirdparty)[] = await makeFetch(
"GET",
`/api/1.0/ticket/ticket/${ticketId}/suggest-person`,
);
if (caller) {
result.push(caller);
}
commit("setPersons", result);
} catch (e: unknown) {
const error = e as ApiException;

View File

@@ -38,8 +38,11 @@ export const moduleTicket: Module<State, RootState> = {
isEmergency(state) {
return state.ticket.emergency == "yes";
},
isNewTicket(state) {
return state.ticket.history.length == 3;
isIncompleteTicket(state) {
return (
state.ticket.currentMotive === null ||
!state.ticket.history.some((item) => item.event_type == "add_comment")
);
},
getTicket(state) {
state.ticket.history = state.ticket.history.sort((a, b) =>
@@ -62,6 +65,9 @@ export const moduleTicket: Module<State, RootState> = {
getCurrentPersons(state) {
return state.ticket.currentPersons;
},
getCaller: (state) => {
return state.ticket.caller;
},
},
mutations: {

View File

@@ -73,14 +73,14 @@ export const moduleTicketList: Module<State, RootState> = {
([, value]) =>
value !== undefined &&
value !== null &&
value !== "" &&
value.length > 0,
(value === true || (value !== "" && value.length > 0)),
),
);
params = new URLSearchParams(
filteredParams as Record<string, string>,
).toString();
}
const { results, pagination, count } = (await makeFetch(
"GET",
`/api/1.0/ticket/ticket/list/?${params}`,
@@ -99,10 +99,7 @@ export const moduleTicketList: Module<State, RootState> = {
},
async fetchConnectedUser({ commit }) {
try {
const user = await makeFetch(
"GET",
"/api/1.0/main/whoami.json",
);
const user = await makeFetch("GET", "/api/1.0/main/whoami.json");
commit("setUser", user);
return user;
} catch (error) {

View File

@@ -84,7 +84,7 @@
:on-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:classColor="{
on: 'bg-chill-red',
on: 'bg-warning',
off: 'bg-secondary',
}"
@update:model-value="handleEmergencyToggle"
@@ -288,7 +288,7 @@ const handleEmergencyToggle = (value: boolean) => {
if (value) {
filters.value.byCurrentStateEmergency = ["yes"];
} else {
filters.value.byCurrentStateEmergency = ["no"];
filters.value.byCurrentStateEmergency = [];
}
};

View File

@@ -1,9 +1,7 @@
<template>
<div
class="card my-2 bg-light"
v-for="history_line in history.filter(
(line) => line.event_type != 'add_person',
)"
v-for="history_line in filteredHistoryLines"
:key="history.indexOf(history_line)"
>
<div class="card-header">
@@ -20,8 +18,9 @@
"
class="badge bg-danger ms-2"
>
{{ trans(CHILL_TICKET_TICKET_HISTORY_DELETED) }}
{{ trans(CHILL_TICKET_TICKET_HISTORY_MASK_COMMENT) }}
</span>
<state-component
:new_state="history_line.data.new_state"
v-if="history_line.event_type == 'state_change'"
@@ -41,14 +40,7 @@
</div>
</div>
</div>
<div
class="card-body row"
v-if="
!['state_change', 'emergency_change', 'create_ticket'].includes(
history_line.event_type,
)
"
>
<div class="card-body row" v-if="displayBody(history_line)">
<person-component
:entities="history_line.data.persons"
v-if="history_line.event_type == 'persons_state'"
@@ -80,7 +72,7 @@
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import { useStore } from "vuex";
// Types
@@ -113,7 +105,7 @@ import {
CHILL_TICKET_TICKET_HISTORY_STATE_CHANGE,
CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE,
CHILL_TICKET_TICKET_HISTORY_SET_CALLER,
CHILL_TICKET_TICKET_HISTORY_DELETED,
CHILL_TICKET_TICKET_HISTORY_MASK_COMMENT,
} from "translator";
const props = defineProps<{ history?: TicketHistoryLine[] }>();
@@ -121,14 +113,22 @@ const history = props.history ?? [];
const store = useStore();
const actionIcons = ref(store.getters.getActionIcons);
const canBeDisplayed = ref(store.getters["canBeDisplayed"]);
const canBeDisplayed = ref(store.getters.canBeDisplayed);
const filteredHistoryLines = computed(() =>
history.filter(
(line: TicketHistoryLine) => !(line.event_type === "add_person"),
),
);
function explainSentence(history: TicketHistoryLine): string {
switch (history.event_type) {
case "add_comment":
return trans(CHILL_TICKET_TICKET_HISTORY_ADD_COMMENT);
case "addressees_state":
return trans(CHILL_TICKET_TICKET_HISTORY_ADDRESSEES_STATE);
return trans(CHILL_TICKET_TICKET_HISTORY_ADDRESSEES_STATE, {
count: history.data.addressees.length,
});
case "persons_state":
return trans(CHILL_TICKET_TICKET_HISTORY_PERSONS_STATE, {
count: history.data.persons.length,
@@ -142,12 +142,29 @@ function explainSentence(history: TicketHistoryLine): string {
case "emergency_change":
return trans(CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE);
case "set_caller":
return trans(CHILL_TICKET_TICKET_HISTORY_SET_CALLER);
return trans(CHILL_TICKET_TICKET_HISTORY_SET_CALLER, {
id: history.data.new_caller?.id ?? "null",
});
default:
return "";
}
}
function displayBody(history_line: TicketHistoryLine): boolean {
// Dont display body if no entities or if is change of state, emergency
const data = Object.values(history_line.data)[0];
if (data == null) {
return false;
} else if (Array.isArray(data) && data.length === 0) {
return false;
} else if (
["state_change", "emergency_change"].includes(history_line.event_type)
) {
return false;
}
return true;
}
function formatDate(d: DateTime): string {
const date = ISOToDatetime(d.datetime);

View File

@@ -14,10 +14,8 @@
<div class="wh-col">
<emergency-component
:new_emergency="ticket.emergency"
v-if="ticket.emergency"
v-if="ticket.emergency == 'yes'"
/>
</div>
<div class="wh-col">
<state-component
v-if="ticket.currentState"
:new_state="ticket.currentState"
@@ -40,7 +38,7 @@
<div class="card-body pt-0">
<div class="wrap-list">
<div class="wl-row">
<div class="wl-row" v-if="ticket.currentAddressees.length">
<div class="wl-col title text-end">
<h3>{{ trans(CHILL_TICKET_LIST_ADDRESSEES) }}</h3>
</div>
@@ -48,7 +46,7 @@
<addressee-component :addressees="ticket.currentAddressees" />
</div>
</div>
<div class="wl-row">
<div class="wl-row" v-if="ticket.currentPersons.length">
<div class="wl-col title text-end">
<h3>{{ trans(CHILL_TICKET_LIST_PERSONS) }}</h3>
</div>
@@ -56,7 +54,7 @@
<person-component :entities="ticket.currentPersons" />
</div>
</div>
<div class="wl-row">
<div class="wl-row" v-if="ticket.caller">
<div class="wl-col title text-end">
<h3>{{ trans(CHILL_TICKET_LIST_CALLERS) }}</h3>
</div>

View File

@@ -14,7 +14,7 @@
:class="[
modelValue
? classColor?.on || 'bg-chill-green'
: classColor?.off || 'bg-chill-red',
: classColor?.off || 'bg-danger',
]"
:style="{
backgroundColor: classColor

View File

@@ -22,7 +22,7 @@ chill_ticket:
by_motives: "Par motifs"
current_state: "État actuel"
open: "Ouvert"
closed: "Fermé"
closed: "Clôturé"
emergency: "Urgent"
no_emergency: "Non urgent"
created_after: "Créé après"
@@ -41,30 +41,32 @@ chill_ticket:
reset: "Réinitialiser"
success: "Ticket mis à jour avec succès"
error: "Veuillez remplir tous les champs obligatoires"
warning: "Veuillez remplir au minimum le motif et le commentaire"
history:
add_comment: "Nouveau commentaire"
addressees_state: "Attributions"
persons_state: "{count, plural, =0 {Aucun usager concerné} =1 {Usager concerné} other {Usagers concernés}}"
set_caller: "Appelant"
set_motive: "Nouveau motifs"
add_comment: "Commentaire ajouté"
addressees_state: "{count, plural, =0 {Attributions supprimées} =1 {Attribution ajoutée} other {Attributions ajoutées}}"
persons_state: "{count, plural, =0 {Usagers supprimés} =1 {Usager concerné ajouté} other {Usagers concernés ajoutés}}"
set_caller: "{id, select, null {Appelant supprimé} other {Appelant ajouté}}"
set_motive: "Motif ajouté"
create_ticket: "Ticket créé"
state_change: ""
emergency_change: ""
deleted: "Supprimé"
mask_comment: "Masqué"
previous_tickets: "Précédents tickets"
actions_toolbar:
cancel: "Annuler"
save: "Enregistrer"
close: "Fermer"
close_success: "Ticket fermé"
close_error: "Erreur lors de la fermeture du ticket"
close: "Clôturer"
close_success: "Ticket clôturé"
close_error: "Erreur lors de la clotûre du ticket"
reopen: "Rouvrir"
reopen_success: "Rouverture du ticket réussie"
reopen_error: "Erreur lors de la rouverture du ticket"
restore_comment:
success: "Commentaire restauré"
delete_comment:
success: "Commentaire supprimé"
visible_comment:
success: "Commentaire visible"
mask_comment:
success: "Commentaire masqué"
hint: "Ce commentaire est masqué; il n'est visible que par vous."
edit_comment:
title: "Éditer le commentaire"
success: "Commentaire modifié"
@@ -98,7 +100,7 @@ chill_ticket:
speaker: "{count, plural, =0 {Aucun intervenant} =1 {Intervenant attribué} other {Intervenants attribués}}"
caller: "{count, plural, =0 {Aucun appelant} =1 {Appelant} other {Appelants}}"
open: "Ouvert"
closed: "Fermé"
closed: "Clôturé"
since: "Depuis {time}"
and: "et"
days: >-