Merge branch '1241-add-feature-close-open-ticket' into 'ticket-app-master'

Add feature open and close ticket

See merge request Chill-Projet/chill-bundles!835
This commit is contained in:
Julien Fastré 2025-06-24 10:44:04 +00:00
commit 0566ab0910
8 changed files with 220 additions and 140 deletions

View File

@ -13,7 +13,7 @@ export interface Motive {
label: TranslatableString; label: TranslatableString;
} }
export type TicketState = "open"|"closed"; export type TicketState = "open" | "closed";
export type TicketEmergencyState = "yes"|"no"; export type TicketEmergencyState = "yes"|"no";
@ -77,16 +77,20 @@ export interface PersonsState {
} }
export interface StateChange { export interface StateChange {
new_state: TicketState new_state: TicketState;
} }
export interface CreateTicketState {} export interface StateChange {
new_state: TicketState;
}
//interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {}; export interface CreateTicketState {
export interface AddCommentEvent by: User;
extends TicketHistory<"add_comment", Comment> {} }
export interface SetMotiveEvent
extends TicketHistory<"set_motive", MotiveHistory> {} interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {}
export type AddCommentEvent = TicketHistory<"add_comment", Comment>;
export type SetMotiveEvent = TicketHistory<"set_motive", MotiveHistory>;
//interface AddAddressee extends TicketHistory<"add_addressee", AddresseeHistory> {}; //interface AddAddressee extends TicketHistory<"add_addressee", AddresseeHistory> {};
//interface RemoveAddressee extends TicketHistory<"remove_addressee", AddresseeHistory> {}; //interface RemoveAddressee extends TicketHistory<"remove_addressee", AddresseeHistory> {};
export interface AddresseesStateEvent export interface AddresseesStateEvent
@ -98,8 +102,9 @@ export interface PersonStateEvent
export interface ChangeStateEvent export interface ChangeStateEvent
extends TicketHistory<"state_change", StateChange> {} extends TicketHistory<"state_change", StateChange> {}
// TODO : Remove add_persont event from TicketHistoryLine
export type TicketHistoryLine = export type TicketHistoryLine =
/* AddPersonEvent */ | AddPersonEvent
| CreateTicketEvent | CreateTicketEvent
| AddCommentEvent | AddCommentEvent
| SetMotiveEvent /*AddAddressee | RemoveAddressee | */ | SetMotiveEvent /*AddAddressee | RemoveAddressee | */

View File

@ -9,7 +9,7 @@
</div> </div>
<form <form
v-if="activeTab !== 'set_persons'" v-if="activeTab !== 'persons_state'"
@submit.prevent="submitAction" @submit.prevent="submitAction"
> >
<add-comment-component <add-comment-component
@ -21,7 +21,7 @@
v-model="addressees" v-model="addressees"
:user-groups="userGroups" :user-groups="userGroups"
:users="users" :users="users"
v-if="activeTab === 'add_addressee'" v-if="activeTab === 'addressees_state'"
/> />
<motive-selector-component <motive-selector-component
@ -71,87 +71,41 @@
>Annuler</a >Annuler</a
> >
</li> </li>
<li class="nav-item p-2"> <li
v-for="btn in actionButtons"
:key="btn.key"
class="nav-item p-2"
>
<button <button
type="button" type="button"
:class="`btn ${ :class="`btn ${activeTab === btn.key ? 'btn-primary' : 'btn-light'}`"
activeTab === 'set_motive'
? 'btn-primary'
: 'btn-light'
}`"
@click=" @click="
activeTab === 'set_motive' activeTab === btn.key
? (activeTab = '') ? (activeTab = '')
: (activeTab = 'set_motive') : (activeTab = btn.key)
" "
:disabled="btn.disabled.value"
> >
<i :class="actionIcons['set_motive']"></i> <i :class="actionIcons[btn.key]" />
{{ trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE) }} {{ trans(btn.label) }}
</button> </button>
</li> </li>
<li class="nav-item p-2">
<button
type="button"
:class="`btn ${
activeTab === 'add_comment'
? 'btn-primary'
: 'btn-light'
}`"
@click="
activeTab === 'add_comment'
? (activeTab = '')
: (activeTab = 'add_comment')
"
>
<i :class="actionIcons['add_comment']"></i>
{{ trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE) }}
</button>
</li>
<li class="nav-item p-2">
<button
type="button"
:class="`btn ${
activeTab === 'add_addressee'
? 'btn-primary'
: 'btn-light'
}`"
@click="
activeTab === 'add_addressee'
? (activeTab = '')
: (activeTab = 'add_addressee')
"
>
<i :class="actionIcons['addressees_state']"></i>
{{ trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE) }}
</button>
</li>
<li class="nav-item p-2">
<button
type="button"
:class="`btn ${
activeTab === 'set_persons'
? 'btn-primary'
: 'btn-light'
}`"
@click="
activeTab === 'set_persons'
? (activeTab = '')
: (activeTab = 'set_persons')
"
>
<i :class="actionIcons['set_persons']"></i>
{{ trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE) }}
</button>
</li>
<li class="nav-item p-2"> <li class="nav-item p-2">
<button <button
type="button" type="button"
class="btn btn-light" class="btn btn-light"
@click="handleClick()" @click="isOpen ? closeTicket() : reopenTicket()"
> >
<i class="fa fa-bolt"></i> <i :class="actionIcons['state_change']"></i>
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE) }} {{
isOpen
? trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE,
)
: trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN,
)
}}
</button> </button>
</li> </li>
</ul> </ul>
@ -183,9 +137,14 @@ import {
CHILL_TICKET_TICKET_SET_MOTIVE_ERROR, CHILL_TICKET_TICKET_SET_MOTIVE_ERROR,
CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS, CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE, CHILL_TICKET_TICKET_SET_PERSONS_TITLE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_ERROR,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_ERROR,
} from "translator"; } from "translator";
// Types // Types
@ -199,9 +158,8 @@ import { Comment, Motive, Ticket } from "../../../types";
const store = useStore(); const store = useStore();
const toast = useToast(); const toast = useToast();
const activeTab = ref( const activeTab = ref("" as string);
"" as "" | "add_comment" | "set_motive" | "add_addressee" | "set_persons", const actionIcons = ref(store.getters.getActionIcons);
);
const activeTabTitle = computed((): string => { const activeTabTitle = computed((): string => {
switch (activeTab.value) { switch (activeTab.value) {
@ -209,16 +167,44 @@ const activeTabTitle = computed((): string => {
return trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE); return trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE);
case "set_motive": case "set_motive":
return trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE); return trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE);
case "add_addressee": case "addressees_state":
return trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE); return trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE);
case "set_persons": case "persons_state":
return trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE); return trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE);
default: default:
return ""; return "";
} }
}); });
const actionButtons = [
{
key: "set_motive",
label: CHILL_TICKET_TICKET_SET_MOTIVE_TITLE,
icon: computed(() => actionIcons.value["set_motive"]),
disabled: computed(() => !isOpen.value),
},
{
key: "add_comment",
label: CHILL_TICKET_TICKET_ADD_COMMENT_TITLE,
icon: computed(() => actionIcons.value["add_comment"]),
disabled: computed(() => !isOpen.value),
},
{
key: "addressees_state",
label: CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE,
icon: computed(() => actionIcons.value["addressees_state"]),
disabled: computed(() => !isOpen.value),
},
{
key: "persons_state",
label: CHILL_TICKET_TICKET_SET_PERSONS_TITLE,
icon: computed(() => actionIcons.value["persons_state"]),
disabled: computed(() => !isOpen.value),
},
];
const ticket = computed(() => store.getters.getTicket as Ticket); const ticket = computed(() => store.getters.getTicket as Ticket);
const isOpen = computed(() => store.getters.isOpen);
const motives = computed(() => store.getters.getMotives as Motive[]); const motives = computed(() => store.getters.getMotives as Motive[]);
const userGroups = computed(() => store.getters.getUserGroups as UserGroup[]); const userGroups = computed(() => store.getters.getUserGroups as UserGroup[]);
const users = computed(() => store.getters.getUsers as User[]); const users = computed(() => store.getters.getUsers as User[]);
@ -279,7 +265,7 @@ async function submitAction() {
); );
} }
break; break;
case "add_addressee": case "addressees_state":
if (!addressees.value.length) { if (!addressees.value.length) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR)); toast.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR));
} else { } else {
@ -299,15 +285,32 @@ async function submitAction() {
} }
} }
function handleClick() { async function closeTicket() {
alert("Sera disponible plus tard"); try {
await store.dispatch("closeTicket");
closeAllActions();
toast.success(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS));
} catch (error) {
console.error(error);
toast.success(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_ERROR));
}
}
async function reopenTicket() {
try {
await store.dispatch("reopenTicket");
toast.success(
trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_SUCCESS),
);
} catch (error) {
console.error(error);
toast.error(trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN_ERROR));
}
} }
function closeAllActions() { function closeAllActions() {
activeTab.value = ""; activeTab.value = "";
} }
const actionIcons = ref(store.getters.getActionIcons);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -328,7 +331,8 @@ div.footer-ticket-details {
} }
.fixed-bottom { .fixed-bottom {
max-width: 1272px; position: sticky;
margin: 0 auto; top: 0;
overflow: hidden;
} }
</style> </style>

View File

@ -17,9 +17,17 @@
<span <span
class="badge text-bg-chill-green text-white" class="badge text-bg-chill-green text-white"
style="font-size: 1rem" style="font-size: 1rem"
v-if="isOpen"
> >
{{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }} {{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }}
</span> </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">
@ -49,7 +57,6 @@
:buttonText="person.textAge" :buttonText="person.textAge"
:displayBadge="'true' === 'true'" :displayBadge="'true' === 'true'"
action="show" action="show"
CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER
></on-the-fly> ></on-the-fly>
</div> </div>
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
@ -88,6 +95,7 @@ import {
trans, trans,
CHILL_TICKET_TICKET_BANNER_NO_MOTIVE, CHILL_TICKET_TICKET_BANNER_NO_MOTIVE,
CHILL_TICKET_TICKET_BANNER_OPEN, CHILL_TICKET_TICKET_BANNER_OPEN,
CHILL_TICKET_TICKET_BANNER_CLOSED,
CHILL_TICKET_TICKET_BANNER_SINCE, CHILL_TICKET_TICKET_BANNER_SINCE,
CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER, CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER,
CHILL_TICKET_TICKET_BANNER_SPEAKER, CHILL_TICKET_TICKET_BANNER_SPEAKER,
@ -98,10 +106,14 @@ import {
CHILL_TICKET_TICKET_BANNER_AND, CHILL_TICKET_TICKET_BANNER_AND,
} from "translator"; } from "translator";
// Store
import { useStore } from "vuex";
const props = defineProps<{ const props = defineProps<{
ticket: Ticket; ticket: Ticket;
}>(); }>();
const store = useStore();
const today = ref(new Date()); const today = ref(new Date());
const createdAt = ref(props.ticket.createdAt); const createdAt = ref(props.ticket.createdAt);
@ -109,6 +121,8 @@ setInterval(() => {
today.value = new Date(); today.value = new Date();
}, 5000); }, 5000);
const isOpen = computed(() => store.getters.isOpen);
const since = computed(() => { const since = computed(() => {
if (createdAt.value == null) { if (createdAt.value == null) {
return ""; return "";

View File

@ -1,17 +1,17 @@
<template> <template>
<div class="col-12"> <div class="col-12">
<addressee-component :addressees="addressees" /> <addressee-component :addressees="addresseeState.addressees" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// Types // Types
import { UserGroupOrUser } from "../../../../../../../ChillMainBundle/Resources/public/types"; import { AddresseeState } from "../../../types";
// Components // Components
import AddresseeComponent from "./AddresseeComponent.vue"; import AddresseeComponent from "./AddresseeComponent.vue";
defineProps<{ defineProps<{
addressees: UserGroupOrUser[]; addresseeState: AddresseeState;
}>(); }>();
</script> </script>

View File

@ -1,21 +1,24 @@
<template> <template>
<div <div
class="card my-2 bg-light" class="card my-2 bg-light"
v-for="history_line in history" v-for="history_line in history.filter(
(line) => line.event_type != 'add_person',
)"
:key="history.indexOf(history_line)" :key="history.indexOf(history_line)"
> >
<div class="card-header"> <div class="card-header">
<div class="history-header"> <div
<div class="description"> class="history-header d-flex align-items-center justify-content-between"
>
<div class="d-flex align-items-center fw-bold">
<i <i
:class="`${actionIcons[history_line.event_type]} me-1`" :class="`${actionIcons[history_line.event_type]} me-1`"
></i> ></i>
<span>{{ explainSentence(history_line) }}</span> <span>{{ explainSentence(history_line) }}</span>
</div> <TicketHistoryStateComponent
<div> :new_state="history_line.data.new_state"
<span class="fw-bold fst-italic mx-1"> v-if="history_line.event_type == 'state_change'"
{{ formatDate(history_line.at) }} />
</span>
</div> </div>
<div> <div>
<span class="badge-user"> <span class="badge-user">
@ -23,10 +26,16 @@
:user="history_line.by" :user="history_line.by"
></user-render-box-badge> ></user-render-box-badge>
</span> </span>
<span class="fst-italic mx-2">
{{ formatDate(history_line.at) }}
</span>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body row"> <div
class="card-body row"
v-if="history_line.event_type != 'state_change'"
>
<ticket-history-person-component <ticket-history-person-component
:personHistory="history_line.data" :personHistory="history_line.data"
v-if="history_line.event_type == 'persons_state'" v-if="history_line.event_type == 'persons_state'"
@ -40,7 +49,7 @@
v-else-if="history_line.event_type == 'add_comment'" v-else-if="history_line.event_type == 'add_comment'"
/> />
<ticket-history-addressee-component <ticket-history-addressee-component
:addressees="history_line.data.addressees" :addresseeState="history_line.data"
v-else-if="history_line.event_type == 'addressees_state'" v-else-if="history_line.event_type == 'addressees_state'"
/> />
<ticket-history-create-component <ticket-history-create-component
@ -64,8 +73,9 @@ import TicketHistoryMotiveComponent from "./TicketHistoryMotiveComponent.vue";
import TicketHistoryCommentComponent from "./TicketHistoryCommentComponent.vue"; import TicketHistoryCommentComponent from "./TicketHistoryCommentComponent.vue";
import TicketHistoryAddresseeComponent from "./TicketHistoryAddresseeComponent.vue"; import TicketHistoryAddresseeComponent from "./TicketHistoryAddresseeComponent.vue";
import TicketHistoryCreateComponent from "./TicketHistoryCreateComponent.vue"; import TicketHistoryCreateComponent from "./TicketHistoryCreateComponent.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; import TicketHistoryStateComponent from "./TicketHistoryStateComponent.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
// Utils // Utils
import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date"; import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
@ -87,6 +97,8 @@ function explainSentence(history: TicketHistoryLine): string {
return "Nouveau motifs"; return "Nouveau motifs";
case "create_ticket": case "create_ticket":
return "Ticket créé"; return "Ticket créé";
case "state_change":
return "Status du ticket modifié";
default: default:
return ""; return "";
} }

View File

@ -0,0 +1,41 @@
<template>
<span
class="text-chill-green mx-2"
style="
font-size: 1rem;
max-width: 80px;
white-space: normal;
word-break: break-word;
"
v-if="props.new_state == 'open'"
>
{{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }}
</span>
<span
class="text-chill-red mx-2"
style="
font-size: 1rem;
max-width: 80px;
white-space: normal;
word-break: break-word;
"
v-else-if="props.new_state == 'closed'"
>
{{ trans(CHILL_TICKET_TICKET_BANNER_CLOSED) }}
</span>
</template>
<script setup lang="ts">
// Types
import { StateChange } from "../../../types";
// Translations
import {
trans,
CHILL_TICKET_TICKET_BANNER_OPEN,
CHILL_TICKET_TICKET_BANNER_CLOSED,
} from "translator";
const props = defineProps<StateChange>();
</script>
<style scoped lang="scss"></style>

View File

@ -2,6 +2,7 @@ import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Ticket } from "../../../../types"; import { Ticket } from "../../../../types";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export interface State { export interface State {
ticket: Ticket; ticket: Ticket;
@ -12,18 +13,18 @@ export const moduleTicket: Module<State, RootState> = {
state: () => ({ state: () => ({
ticket: {} as Ticket, ticket: {} as Ticket,
action_icons: { action_icons: {
// TODO cleanup those keys
add_person: "fa fa-eyedropper", add_person: "fa fa-eyedropper",
add_comment: "fa fa-comment", add_comment: "fa fa-comment",
set_motive: "fa fa-paint-brush", set_motive: "fa fa-paint-brush",
//add_addressee: "fa fa-paper-plane",
addressees_state: "fa fa-paper-plane", addressees_state: "fa fa-paper-plane",
set_persons: "fa fa-eyedropper",
persons_state: "fa fa-eyedropper", persons_state: "fa fa-eyedropper",
state_change: "fa fa-bolt",
}, },
toto: "toto",
}), }),
getters: { getters: {
isOpen(state) {
return state.ticket.currentState === "open";
},
getTicket(state) { getTicket(state) {
state.ticket.history = state.ticket.history.sort((a, b) => state.ticket.history = state.ticket.history.sort((a, b) =>
b.at.datetime.localeCompare(a.at.datetime), b.at.datetime.localeCompare(a.at.datetime),
@ -35,39 +36,36 @@ export const moduleTicket: Module<State, RootState> = {
return state.action_icons; return state.action_icons;
}, },
getDistinctAddressesHistory(state) { getDistinctAddressesHistory(state) {
const addresseeHistory = state.ticket.history.reduce( return state.ticket.history;
(result, item) => {
const { datetime } = item.at;
if (
![
"add_addressee",
"remove_addressee",
"add_person",
].includes(item.event_type)
) {
result[datetime] = item;
return result;
}
if (!result[datetime]) {
result[datetime] = [];
}
/*
if (item.event_type === "add_addressee") {
result[datetime].push(item);
}
*/
return result;
},
{} as any,
);
return Object.values(addresseeHistory) as Ticket["history"][];
}, },
}, },
mutations: { mutations: {
setTicket(state, ticket) { setTicket(state, ticket: Ticket) {
state.ticket = ticket; state.ticket = ticket;
}, },
}, },
actions: {}, actions: {
async closeTicket({ commit, state }) {
try {
const result: Ticket = await makeFetch(
"POST",
`/api/1.0/ticket/ticket/${state.ticket.id}/close`,
);
commit("setTicket", result as Ticket);
} catch (e: any) {
throw e.name;
}
},
async reopenTicket({ commit, state }) {
try {
const result: Ticket = await makeFetch(
"POST",
`/api/1.0/ticket/ticket/${state.ticket.id}/open`,
);
commit("setTicket", result as Ticket);
} catch (e: any) {
throw e.name;
}
},
},
}; };

View File

@ -11,6 +11,11 @@ chill_ticket:
cancel: "Annuler" cancel: "Annuler"
save: "Enregistrer" save: "Enregistrer"
close: "Fermer" close: "Fermer"
close_success: "Ticket fermé"
close_error: "Erreur lors de la fermeture du ticket"
reopen: "Rouvrir"
reopen_success: "Rouverture du ticket réussie"
reopen_error: "Erreur lors de la rouverture du ticket"
add_comment: add_comment:
title: "Commentaire" title: "Commentaire"
label: "Ajouter un commentaire" label: "Ajouter un commentaire"
@ -37,6 +42,7 @@ chill_ticket:
concerned_usager: "Usagers concernés" concerned_usager: "Usagers concernés"
speaker: "Attribué à" speaker: "Attribué à"
open: "Ouvert" open: "Ouvert"
closed: "Fermé"
since: "Depuis {time}" since: "Depuis {time}"
and: "et" and: "et"
days: >- days: >-