Afficher les patients suggérés et ajouter un sélecteur urgent/non urgent

This commit is contained in:
Boris Waaub 2025-07-04 07:45:33 +00:00 committed by Julien Fastré
parent 06e8264dde
commit 3df4043eb9
24 changed files with 1539 additions and 1121 deletions

View File

@ -10,6 +10,11 @@ export interface Civility {
// TODO // TODO
} }
export interface Household {
type: "household";
id: number;
}
export interface Job { export interface Job {
id: number; id: number;
type: "user_job"; type: "user_job";
@ -48,16 +53,10 @@ export interface User {
label: string; label: string;
// todo: mainCenter; mainJob; etc.. // todo: mainCenter; mainJob; etc..
} }
// TODO : Add missing household properties
export interface Household {
type: "household";
id: number;
}
export interface ThirdParty { export interface ThirdParty {
type: "thirdparty"; type: "thirdparty";
id: number; id: number;
text: string;
firstname: string; firstname: string;
name: string; name: string;
email: string; email: string;
@ -228,3 +227,61 @@ export interface WorkflowAttachment {
export interface PrivateCommentEmbeddable { export interface PrivateCommentEmbeddable {
comments: Record<number, string>; comments: Record<number, string>;
} }
// API Exception types
export interface TransportExceptionInterface {
name: string;
}
export interface ValidationExceptionInterface
extends TransportExceptionInterface {
name: "ValidationException";
error: object;
violations: string[];
titles: string[];
propertyPaths: string[];
}
export interface AccessExceptionInterface extends TransportExceptionInterface {
name: "AccessException";
violations: string[];
}
export interface NotFoundExceptionInterface
extends TransportExceptionInterface {
name: "NotFoundException";
}
export interface ServerExceptionInterface extends TransportExceptionInterface {
name: "ServerException";
message: string;
code: number;
body: string;
}
export interface ConflictHttpExceptionInterface
extends TransportExceptionInterface {
name: "ConflictHttpException";
violations: string[];
}
export type ApiException =
| ValidationExceptionInterface
| AccessExceptionInterface
| NotFoundExceptionInterface
| ServerExceptionInterface
| ConflictHttpExceptionInterface;
export interface Modal {
showModal: boolean;
modalDialogClass: string;
}
export interface Selected {
result: UserGroupOrUser;
}
export interface addNewEntities {
selected: Selected[];
modal: Modal;
}

View File

@ -1,7 +1,18 @@
<template> <template>
<div class="grey-card">
<ul :class="listClasses" v-if="picked.length && displayPicked"> <ul :class="listClasses" v-if="picked.length && displayPicked">
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id"> <li
<span class="chill_denomination">{{ p.text }}</span> v-for="p in picked"
@click="removeEntity(p)"
:key="p.type + p.id"
>
<span
:class="getBadgeClass(p)"
class="chill_denomination"
:style="getBadgeStyle(p)"
>
{{ p.text }}
</span>
</li> </li>
</ul> </ul>
<ul class="record_actions"> <ul class="record_actions">
@ -11,132 +22,251 @@
:key="uniqid" :key="uniqid"
:buttonTitle="translatedListOfTypes" :buttonTitle="translatedListOfTypes"
:modalTitle="translatedListOfTypes" :modalTitle="translatedListOfTypes"
ref="addPersons"
@addNewPersons="addNewEntity" @addNewPersons="addNewEntity"
> >
</add-persons> </add-persons>
</li> </li>
</ul> </ul>
<ul class="list-suggest add-items inline">
<ul class="badge-suggest add-items inline" style="float: right">
<li v-for="s in suggested" :key="s.id" @click="addNewSuggested(s)"> <li v-for="s in suggested" :key="s.id" @click="addNewSuggested(s)">
<span>{{ s.text }}</span> <span :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
{{ s.text }}
</span>
</li> </li>
</ul> </ul>
</div>
</template> </template>
<script> <script lang="ts" setup>
import { ref, computed, defineProps, defineEmits, defineComponent } from "vue";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import { appMessages } from "./i18n"; import { Entities, EntityType, SearchOptions } from "ChillPersonAssets/types";
import {
PICK_ENTITY_MODAL_TITLE,
PICK_ENTITY_USER,
PICK_ENTITY_USER_GROUP,
PICK_ENTITY_PERSON,
PICK_ENTITY_THIRDPARTY,
trans,
} from "translator";
import { addNewEntities } from "ChillMainAssets/types";
export default { defineComponent({
name: "PickEntity",
props: {
multiple: {
type: Boolean,
required: true,
},
types: {
type: Array,
required: true,
},
picked: {
required: true,
},
uniqid: {
type: String,
required: true,
},
removableIfSet: {
type: Boolean,
default: true,
},
displayPicked: {
// display picked entities.
type: Boolean,
default: true,
},
suggested: {
type: Array,
default: [],
},
label: {
type: String,
required: false,
},
},
emits: ["addNewEntity", "removeEntity", "addNewEntityProcessEnded"],
components: { components: {
AddPersons, AddPersons,
}, },
data() { });
return { const props = defineProps<{
key: "", multiple: boolean;
}; types: EntityType[];
}, picked: Entities[];
computed: { uniqid: string;
addPersonsOptions() { removableIfSet?: boolean;
return { displayPicked?: boolean;
uniq: !this.multiple, suggested?: Entities[];
type: this.types, label?: string;
}>();
const emits = defineEmits<{
(e: "addNewEntity", payload: { entity: Entities }): void;
(e: "removeEntity", payload: { entity: Entities }): void;
(e: "addNewEntityProcessEnded"): void;
}>();
const addPersons = ref();
const addPersonsOptions = computed(
() =>
({
uniq: !props.multiple,
type: props.types,
priority: null, priority: null,
button: { button: {
size: "btn-sm", size: "btn-sm",
class: "btn-submit", class: "btn-submit",
}, },
}; }) as SearchOptions,
}, );
translatedListOfTypes() {
if (this.label !== "") { const translatedListOfTypes = computed(() => {
return this.label; if (props.label !== undefined && props.label !== "") {
return props.label;
} }
let trans = []; const translatedTypes = props.types.map((type: EntityType) => {
this.types.forEach((t) => { switch (type) {
if (this.$props.multiple) { case "user":
trans.push(appMessages.fr.pick_entity[t].toLowerCase()); return trans(PICK_ENTITY_USER, {
} else { count: props.multiple ? 2 : 1,
trans.push( });
appMessages.fr.pick_entity[t + "_one"].toLowerCase(), case "person":
); return trans(PICK_ENTITY_PERSON, {
count: props.multiple ? 2 : 1,
});
case "third_party":
return trans(PICK_ENTITY_THIRDPARTY, {
count: props.multiple ? 2 : 1,
});
case "user_group":
return trans(PICK_ENTITY_USER_GROUP, {
count: props.multiple ? 2 : 1,
});
} }
}); });
if (this.$props.multiple) { return `${trans(PICK_ENTITY_MODAL_TITLE, {
return ( count: props.multiple ? 2 : 1,
appMessages.fr.pick_entity.modal_title + trans.join(", ") })} ${translatedTypes.join(", ")}`;
); });
} else {
return ( const listClasses = computed(() => ({
appMessages.fr.pick_entity.modal_title_one + "badge-suggest": true,
trans.join(", ") "remove-items": props.removableIfSet !== false,
); inline: true,
} }));
},
listClasses() { function addNewSuggested(entity: Entities) {
return { emits("addNewEntity", { entity });
"list-suggest": true, }
"remove-items": this.$props.removableIfSet,
}; function addNewEntity({ selected }: addNewEntities) {
}, Object.values(selected).forEach((item) => {
}, emits("addNewEntity", { entity: item.result });
methods: { });
addNewSuggested(entity) { addPersons.value?.resetSearch();
this.$emit("addNewEntity", { entity: entity });
}, emits("addNewEntityProcessEnded");
addNewEntity({ selected, modal }) { }
selected.forEach((item) => {
this.$emit("addNewEntity", { entity: item.result }); function removeEntity(entity: Entities) {
}, this); if (props.removableIfSet === false) {
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.$emit("addNewEntityProcessEnded");
},
removeEntity(entity) {
if (!this.$props.removableIfSet) {
return; return;
} }
this.$emit("removeEntity", { entity: entity }); emits("removeEntity", { entity });
}, }
},
}; function getBadgeClass(entities: Entities) {
if (entities.type !== "user_group") {
return entities.type;
}
return "";
}
function getBadgeStyle(entities: Entities) {
if (entities.type === "user_group") {
return [
`ul.badge-suggest li > span {
color: ${entities.foregroundColor}!important;
border-bottom-color: ${entities.backgroundColor};
}`,
];
}
return [];
}
</script> </script>
<style lang="scss" scoped>
.grey-card {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
min-height: 160px;
}
.btn-check:checked + .btn,
:not(.btn-check) + .btn:active,
.btn:first-child:active,
.btn.active,
.btn.show {
color: white;
box-shadow: 0 0 0 0.2rem var(--bs-chill-green);
outline: 0;
}
.as-user-group {
display: inline-block;
}
ul.badge-suggest {
list-style-type: none;
padding-left: 0;
margin-bottom: 0px;
}
ul.badge-suggest li > span {
white-space: normal;
text-align: start;
margin-bottom: 3px;
}
ul.badge-suggest.inline li {
display: inline-block;
margin-right: 0.2em;
}
ul.badge-suggest.add-items li {
position: relative;
}
ul.badge-suggest.add-items li span {
cursor: pointer;
padding-left: 2rem;
}
ul.badge-suggest.add-items li span:hover {
color: #ced4da;
}
ul.badge-suggest.add-items li > span:before {
font: normal normal normal 13px ForkAwesome;
margin-right: 1.8em;
content: "\f067";
color: var(--bs-success);
position: absolute;
display: block;
top: 50%;
left: 0.75rem;
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
ul.badge-suggest.remove-items li {
position: relative;
}
ul.badge-suggest.remove-items li span {
cursor: pointer;
padding-left: 2rem;
}
ul.badge-suggest.remove-items li span:hover {
color: #ced4da;
}
ul.badge-suggest.remove-items li > span:before {
font: normal normal normal 13px ForkAwesome;
margin-right: 1.8em;
content: "\f1f8";
color: var(--bs-danger);
position: absolute;
display: block;
top: 50%;
left: 0.75rem;
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
ul.badge-suggest li > span {
margin: 0.2rem 0.1rem;
display: inline-block;
padding: 0 1em 0 2.2em !important;
background-color: #fff;
border: 1px solid #dee2e6;
border-bottom-width: 3px;
border-bottom-style: solid;
border-radius: 6px;
font-size: 0.75em;
font-weight: 700;
}
ul.badge-suggest li > span.person {
border-bottom-color: #43b29d;
}
ul.badge-suggest li > span.thirdparty {
border-bottom-color: rgb(198.9, 72, 98.1);
}
</style>

View File

@ -156,3 +156,31 @@ renderbox:
no_current_address: "Sans adresse actuellement" no_current_address: "Sans adresse actuellement"
new_household: "Nouveau ménage" new_household: "Nouveau ménage"
no_members_yet: "Aucun membre actuellement" no_members_yet: "Aucun membre actuellement"
pick_entity:
add: "Ajouter"
modal_title: >-
{count, plural,
one {Indiquer un}
other {Ajouter des}
}
user: >-
{count, plural,
one {Utilisateur}
other {Utilisateurs}
}
user_group: >-
{count, plural,
one {Groupe d'utilisateur}
other {Groupes d'utilisateurs}
}
person: >-
{count, plural,
one {Usager}
other {Usagers}
}
thirdparty: >-
{count, plural,
one {Tiers}
other {Tiers}
}

View File

@ -1,21 +1,18 @@
import { StoredObject } from "ChillDocStoreAssets/types"; import { StoredObject } from "ChillDocStoreAssets/types";
import { import {
Address, Address,
Scope,
Center, Center,
Civility, Civility,
DateTime, DateTime,
User, User,
WorkflowAvailable,
Job,
Household,
User,
UserGroup, UserGroup,
WorkflowAvailable, Household,
ThirdParty, ThirdParty,
WorkflowAvailable,
Scope,
Job,
PrivateCommentEmbeddable, PrivateCommentEmbeddable,
} from "ChillMainAssets/types"; } from "../../../ChillMainBundle/Resources/public/types";
import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types"; import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types"; import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
@ -76,19 +73,6 @@ export interface AccompanyingPeriod {
| "DRAFT"; | "DRAFT";
} }
export interface AccompanyingPeriodWorkEvaluationDocument {
id: number;
type: "accompanying_period_work_evaluation_document";
storedObject: StoredObject;
title: string;
createdAt: DateTime | null;
createdBy: User | null;
updatedAt: DateTime | null;
updatedBy: User | null;
workflows_availables: WorkflowAvailable[];
workflows: object[];
}
export interface AccompanyingPeriodWork { export interface AccompanyingPeriodWork {
id: number; id: number;
accompanyingPeriod?: AccompanyingPeriod; accompanyingPeriod?: AccompanyingPeriod;
@ -113,7 +97,7 @@ export interface AccompanyingPeriodWork {
version: number; version: number;
} }
interface SocialAction { export interface SocialAction {
id: number; id: number;
parent?: SocialAction | null; parent?: SocialAction | null;
children: SocialAction[]; children: SocialAction[];
@ -195,18 +179,6 @@ export interface Goal {
}; };
} }
export interface Result {
id: number;
accompanyingPeriodWorks: AccompanyingPeriodWork[];
accompanyingPeriodWorkGoals: AccompanyingPeriodWorkGoal[];
goals: Goal[];
socialActions: SocialAction[];
title: {
fr: string;
};
desactivationDate?: string | null;
}
export interface AccompanyingPeriodWorkGoal { export interface AccompanyingPeriodWorkGoal {
id: number; id: number;
accompanyingPeriodWork: AccompanyingPeriodWork; accompanyingPeriodWork: AccompanyingPeriodWork;
@ -258,6 +230,26 @@ export interface AccompanyingPeriodWorkReferrerHistory {
updatedBy: User | null; updatedBy: User | null;
} }
export interface AccompanyingPeriodWorkEvaluationDocument {
id: number;
type: "accompanying_period_work_evaluation_document";
storedObject: StoredObject;
title: string;
createdAt: DateTime | null;
createdBy: User | null;
updatedAt: DateTime | null;
updatedBy: User | null;
workflows_availables: WorkflowAvailable[];
workflows: object[];
}
export type EntityType =
| "user_group"
| "user"
| "person"
| "third_party"
| "household";
export type Entities = (UserGroup | User | Person | ThirdParty | Household) & { export type Entities = (UserGroup | User | Person | ThirdParty | Household) & {
address?: Address | null; address?: Address | null;
kind?: string; kind?: string;

View File

@ -37,7 +37,9 @@
id="search-persons" id="search-persons"
name="query" name="query"
v-model="query" v-model="query"
:placeholder="trans(ADD_PERSONS_SEARCH_SOME_PERSONS)" :placeholder="
trans(ADD_PERSONS_SEARCH_SOME_PERSONS)
"
ref="searchRef" ref="searchRef"
/> />
<i class="fa fa-search fa-lg" /> <i class="fa fa-search fa-lg" />
@ -54,7 +56,10 @@
<a v-if="suggestedCounter > 2" @click="selectAll"> <a v-if="suggestedCounter > 2" @click="selectAll">
{{ trans(ACTION_CHECK_ALL) }} {{ trans(ACTION_CHECK_ALL) }}
</a> </a>
<a v-if="selectedCounter > 0" @click="resetSelection"> <a
v-if="selectedCounter > 0"
@click="resetSelection"
>
<i v-if="suggestedCounter > 2"> </i> <i v-if="suggestedCounter > 2"> </i>
{{ trans(ACTION_RESET) }} {{ trans(ACTION_RESET) }}
</a> </a>
@ -90,7 +95,9 @@
(options.type.includes('person') || (options.type.includes('person') ||
options.type.includes('thirdparty')) options.type.includes('thirdparty'))
" "
:button-text="trans(ONTHEFLY_CREATE_BUTTON, { q: query })" :button-text="
trans(ONTHEFLY_CREATE_BUTTON, { q: query })
"
:allowed-types="options.type" :allowed-types="options.type"
:query="query" :query="query"
action="create" action="create"
@ -106,7 +113,9 @@
class="btn btn-create" class="btn btn-create"
@click.prevent=" @click.prevent="
() => { () => {
$emit('addNewPersons', { selected: selectedComputed }); $emit('addNewPersons', {
selected: selectedComputed,
});
query = ''; query = '';
closeModal(); closeModal();
} }
@ -272,7 +281,10 @@ function setQuery(q: string) {
loadSuggestions(suggested.results); loadSuggestions(suggested.results);
}) })
.catch((error: DOMException) => { .catch((error: DOMException) => {
if (error instanceof DOMException && error.name === "AbortError") { if (
error instanceof DOMException &&
error.name === "AbortError"
) {
// Request was aborted, ignore // Request was aborted, ignore
return; return;
} }

View File

@ -15,7 +15,7 @@ export interface Motive {
export type TicketState = "open" | "closed"; export type TicketState = "open" | "closed";
export type TicketEmergencyState = "yes"|"no"; export type TicketEmergencyState = "yes" | "no";
interface TicketHistory<T extends string, D extends object> { interface TicketHistory<T extends string, D extends object> {
event_type: T; event_type: T;
@ -75,42 +75,52 @@ export interface AddresseeState {
export interface PersonsState { export interface PersonsState {
persons: Person[]; persons: Person[];
} }
export interface CallerState {
new_caller: Person | null;
}
export interface StateChange { export interface StateChange {
new_state: TicketState; new_state: TicketState;
} }
export interface StateChange { export interface EmergencyChange {
new_state: TicketState; new_emergency: TicketEmergencyState;
} }
export interface CreateTicketState { export interface CreateTicketState {
by: User; by: User;
} }
interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {}
export type AddCommentEvent = TicketHistory<"add_comment", Comment>; export type AddCommentEvent = TicketHistory<"add_comment", Comment>;
export type SetMotiveEvent = TicketHistory<"set_motive", MotiveHistory>; export type SetMotiveEvent = TicketHistory<"set_motive", MotiveHistory>;
//interface AddAddressee extends TicketHistory<"add_addressee", AddresseeHistory> {}; export type AddPersonEvent = TicketHistory<"add_person", PersonHistory>;
//interface RemoveAddressee extends TicketHistory<"remove_addressee", AddresseeHistory> {}; export type AddresseesStateEvent = TicketHistory<
export interface AddresseesStateEvent "addressees_state",
extends TicketHistory<"addressees_state", AddresseeState> {} AddresseeState
export interface CreateTicketEvent >;
extends TicketHistory<"create_ticket", CreateTicketState> {} export type CreateTicketEvent = TicketHistory<
export interface PersonStateEvent "create_ticket",
extends TicketHistory<"persons_state", PersonsState> {} CreateTicketState
export interface ChangeStateEvent >;
extends TicketHistory<"state_change", StateChange> {} export type PersonStateEvent = TicketHistory<"persons_state", PersonsState>;
export type ChangeStateEvent = TicketHistory<"state_change", StateChange>;
export type EmergencyStateEvent = TicketHistory<
"emergency_change",
EmergencyChange
>;
export type CallerStateEvent = TicketHistory<"set_caller", CallerState>;
// TODO : Remove add_persont event from TicketHistoryLine // TODO : Remove add_person event from TicketHistoryLine
export type TicketHistoryLine = export type TicketHistoryLine =
| AddPersonEvent | AddPersonEvent
| CreateTicketEvent | CreateTicketEvent
| AddCommentEvent | AddCommentEvent
| SetMotiveEvent /*AddAddressee | RemoveAddressee | */ | SetMotiveEvent
| AddresseesStateEvent | AddresseesStateEvent
| PersonStateEvent | PersonStateEvent
| ChangeStateEvent; | ChangeStateEvent
| EmergencyStateEvent
| CallerStateEvent;
export interface Ticket { export interface Ticket {
type: "ticket_ticket"; type: "ticket_ticket";
@ -124,16 +134,5 @@ export interface Ticket {
updatedBy: User | null; updatedBy: User | null;
currentState: TicketState | null; currentState: TicketState | null;
emergency: TicketEmergencyState | null; emergency: TicketEmergencyState | null;
} caller: Person | null;
export interface addNewPersons {
selected: Selected[];
modal: Modal;
}
export interface Modal {
showModal: boolean;
modalDialogClass: string;
}
export interface Selected {
result: User;
} }

View File

@ -34,6 +34,7 @@ onMounted(async () => {
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");
} catch (error) { } catch (error) {
toast.error(error as string); toast.error(error as string);
} }

View File

@ -2,25 +2,38 @@
<div class="fixed-bottom"> <div class="fixed-bottom">
<div class="footer-ticket-details" v-if="activeTab"> <div class="footer-ticket-details" v-if="activeTab">
<div class="tab-content p-2"> <div class="tab-content p-2">
<div> <button
type="button"
class="btn btn-link p-0"
style="
position: absolute;
top: 0.5rem;
right: 0.5rem;
font-size: 2rem;
line-height: 1;
color: #888;
text-decoration: none;
"
@click="closeAllActions"
aria-label="Fermer"
title="Fermer"
>
<span aria-hidden="true">&times;</span>
</button>
<div v-if="activeTabTitle">
<label class="col-form-label"> <label class="col-form-label">
{{ activeTabTitle }} {{ activeTabTitle }}
</label> </label>
</div> </div>
<form <form @submit.prevent="submitAction">
v-if="activeTab !== 'persons_state'"
@submit.prevent="submitAction"
>
<add-comment-component <add-comment-component
v-model="content" v-model="content"
v-if="activeTab === 'add_comment'" v-if="activeTab === 'add_comment'"
/> />
<addressee-selector-component <addressee-selector-component
v-model="addressees" v-model="addressees"
:user-groups="userGroups" :suggested="userGroups"
:users="users"
v-if="activeTab === 'addressees_state'" v-if="activeTab === 'addressees_state'"
/> />
@ -29,20 +42,40 @@
:motives="motives" :motives="motives"
v-if="activeTab === 'set_motive'" v-if="activeTab === 'set_motive'"
/> />
<div v-if="activeTab === 'persons_state'">
<ul class="record_actions sticky-form-buttons"> <div class="row">
<li class="cancel"> <label class="col col-form-label">
<button
@click="activeTab = ''"
class="btn btn-cancel"
>
{{ {{
trans( trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL, CHILL_TICKET_TICKET_SET_PERSONS_TITLE_CALLER,
) )
}} }}
</button> </label>
</li> <label class="col col-form-label">
{{
trans(
CHILL_TICKET_TICKET_SET_PERSONS_TITLE_PERSON,
)
}}
</label>
</div>
<div class="row">
<div class="col">
<caller-selector-component
v-model="caller"
:suggested="[]"
/>
</div>
<div class="col">
<persons-selector-component
v-model="persons"
:suggested="suggestedPersons"
/>
</div>
</div>
</div>
<ul class="record_actions sticky-form-buttons">
<li> <li>
<button class="btn btn-save" type="submit"> <button class="btn btn-save" type="submit">
{{ {{
@ -54,11 +87,6 @@
</li> </li>
</ul> </ul>
</form> </form>
<template v-else>
<persons-selector-component
@closeRequested="closeAllActions()"
/>
</template>
</div> </div>
</div> </div>
<div class="footer-ticket-main"> <div class="footer-ticket-main">
@ -67,8 +95,16 @@
<a :href="returnPath" class="btn btn-cancel">Annuler</a> <a :href="returnPath" class="btn btn-cancel">Annuler</a>
</li> </li>
<li v-else class="nav-item p-2 go-back"> <li v-else class="nav-item p-2 go-back">
<a href="/fr/ticket/ticket/list" class="btn btn-cancel" <button
>Annuler</a type="button"
class="btn btn-link p-0"
style="font-size: 1.5rem; line-height: 1; color: #888"
@click="closeAllActions"
aria-label="Fermer"
title="Fermer"
>
<span aria-hidden="true">&times;</span>
</button>
> >
</li> </li>
<li <li
@ -114,7 +150,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"; import { computed, ref, watch } from "vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
@ -123,6 +159,7 @@ import MotiveSelectorComponent from "./MotiveSelectorComponent.vue";
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue"; import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
import AddCommentComponent from "./AddCommentComponent.vue"; import AddCommentComponent from "./AddCommentComponent.vue";
import PersonsSelectorComponent from "./PersonsSelectorComponent.vue"; import PersonsSelectorComponent from "./PersonsSelectorComponent.vue";
import CallerSelectorComponent from "./CallerSelectorComponent.vue";
// Translations // Translations
import { import {
@ -136,8 +173,10 @@ import {
CHILL_TICKET_TICKET_SET_MOTIVE_TITLE, CHILL_TICKET_TICKET_SET_MOTIVE_TITLE,
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_PERSON,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE_CALLER,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE, CHILL_TICKET_TICKET_SET_PERSONS_TITLE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL, CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS, CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE_SUCCESS,
@ -149,11 +188,11 @@ import {
// Types // Types
import { import {
User,
UserGroup, UserGroup,
UserGroupOrUser, UserGroupOrUser,
} from "../../../../../../../ChillMainBundle/Resources/public/types"; } from "../../../../../../../ChillMainBundle/Resources/public/types";
import { Comment, Motive, Ticket } from "../../../types"; import { Comment, Motive, Ticket } from "../../../types";
import { Person } from "ChillPersonAssets/types";
const store = useStore(); const store = useStore();
const toast = useToast(); const toast = useToast();
@ -169,8 +208,6 @@ const activeTabTitle = computed((): string => {
return trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE); return trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE);
case "addressees_state": case "addressees_state":
return trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE); return trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE);
case "persons_state":
return trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE);
default: default:
return ""; return "";
} }
@ -207,8 +244,8 @@ const ticket = computed(() => store.getters.getTicket as Ticket);
const isOpen = computed(() => store.getters.isOpen); 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 suggestedPersons = computed(() => store.getters.getPersons as Person[]);
console.log("suggestedPersons", suggestedPersons.value);
const hasReturnPath = computed((): boolean => { const hasReturnPath = computed((): boolean => {
const params = new URL(document.location.toString()).searchParams; const params = new URL(document.location.toString()).searchParams;
return params.has("returnPath"); return params.has("returnPath");
@ -232,6 +269,8 @@ const motive = ref(
); );
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 caller = ref(ticket.value.caller as Person);
async function submitAction() { async function submitAction() {
try { try {
@ -279,6 +318,13 @@ async function submitAction() {
); );
} }
break; break;
case "persons_state":
await store.dispatch("setPersons", {
persons: persons.value,
});
activeTab.value = "";
toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS));
break;
} }
} catch (error) { } catch (error) {
toast.error(error as string); toast.error(error as string);
@ -311,6 +357,11 @@ async function reopenTicket() {
function closeAllActions() { function closeAllActions() {
activeTab.value = ""; activeTab.value = "";
} }
watch(caller, async (newCaller) => {
await store.dispatch("setCaller", { caller: newCaller });
await store.dispatch("getSuggestedPersons");
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -20,12 +20,8 @@
</span> </span>
</div> </div>
<div v-if="users.length > 0" class="col-12"> <div v-if="users.length > 0" class="col-12">
<span class="badge-user"> <span class="badge-user" v-for="user in users" :key="user.id">
<user-render-box-badge <user-render-box-badge :user="user" />
v-for="user in users"
:key="user.id"
:user="user"
></user-render-box-badge>
</span> </span>
</div> </div>
</template> </template>

View File

@ -1,90 +1,18 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 col-md-6 text-center"> <div class="col-12">
<div class="mb-5 level-line"> <pick-entity
<span uniqid="ticket-addressee-selector"
v-for="userGroupItem in userGroups.filter( :types="['user', 'user_group', 'third_party']"
(userGroup) => userGroup.excludeKey == 'level', :picked="selectedEntities"
)"
:key="userGroupItem.id"
class="m-2 as-user-group"
>
<input
type="radio"
class="btn-check"
name="options-outlined"
:id="`level-${userGroupItem.id}`"
autocomplete="off"
:value="userGroupItem"
v-model="userGroupLevel"
@click="
Object.values(userGroupLevel).includes(
userGroupItem.id,
)
? (userGroupLevel = {})
: (userGroupLevel = userGroupItem)
"
/>
<label
:class="`btn btn-${userGroupItem.id}`"
:for="`level-${userGroupItem.id}`"
:style="getUserGroupBtnColor(userGroupItem)"
>
{{ userGroupItem.label.fr }}
</label>
</span>
</div>
<div class="mb-2 level-line">
<span
v-for="userGroupItem in userGroups.filter(
(userGroup) => userGroup.excludeKey == '',
)"
:key="userGroupItem.id"
class="m-2"
>
<input
type="checkbox"
class="btn-check"
name="options-outlined"
:id="`user-group-${userGroupItem.id}`"
autocomplete="off"
:value="userGroupItem"
v-model="userGroup"
/>
<label
:class="`btn btn-${userGroupItem.id}`"
:for="`user-group-${userGroupItem.id}`"
:style="getUserGroupBtnColor(userGroupItem)"
>
{{ userGroupItem.label.fr }}
</label>
</span>
</div>
</div>
<div class="col-12 col-lg-6 col-md-6 mb-2 mb-2 text-center">
<add-persons
:options="addPersonsOptions"
key="add-person-ticket"
:buttonTitle="
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)
"
:modalTitle="
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)
"
:selected="selectedValues"
:suggested="suggestedValues" :suggested="suggestedValues"
@addNewPersons="addNewEntity" :multiple="true"
:removable-if-set="true"
:display-picked="true"
:label="trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)"
@add-new-entity="addNewEntity"
@remove-entity="removeEntity"
/> />
<div class="p-2">
<ul class="list-suggest inline remove-items">
<li v-for="user in users" :key="user.id">
<span :title="user.username" @click="removeUser(user)">
{{ user.username }}
</span>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -93,12 +21,10 @@
import { ref, watch, defineProps, defineEmits } from "vue"; import { ref, watch, defineProps, defineEmits } from "vue";
// Components // Components
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
// Types // Types
import type { User, UserGroup, UserGroupOrUser } from "ChillMainAssets/types"; import { Entities } from "ChillPersonAssets/types";
import { SearchOptions, Suggestion } from "ChillPersonAssets/types";
import type { addNewPersons } from "../../../types";
// Translations // Translations
import { import {
@ -107,117 +33,62 @@ import {
} from "translator"; } from "translator";
const props = defineProps<{ const props = defineProps<{
modelValue?: UserGroupOrUser[]; modelValue: Entities[];
userGroups: UserGroup[]; suggested: Entities[];
users: User[];
}>(); }>();
const selectedValues = ref<Suggestion[]>([]); const emit = defineEmits<(e: "update:modelValue", value: Entities[]) => void>();
const suggestedValues = ref<Suggestion[]>([]);
const emit = const selectedEntities = ref<Entities[]>([...props.modelValue]);
defineEmits<(e: "update:modelValue", value: UserGroupOrUser[]) => void>(); const suggestedValues = ref<Entities[]>([...props.suggested]);
watch(
() => [props.suggested, props.modelValue],
() => {
const modelValue = props.modelValue ?? [];
const addressees = ref<UserGroupOrUser[]>([...(props.modelValue ?? [])]); suggestedValues.value = props.suggested.filter(
(suggested: Entities) => {
const userGroupsInit = [ return !modelValue.some((selected: Entities) => {
...(props.modelValue ?? []).filter( if (
(addressee: UserGroupOrUser) => addressee.type == "user_group", suggested.type == "user_group" &&
), selected.type == "user_group"
] as UserGroup[]; ) {
switch (selected.excludeKey) {
const userGroupLevel = ref<UserGroup | Record<string, never>>( case "level":
(userGroupsInit.filter( return suggested.excludeKey === "level";
(userGroup: UserGroup) => userGroup.excludeKey == "level", case "":
)[0] as UserGroup) ?? {}, return (
); suggested.excludeKey === "" &&
suggested.id === selected.id
const userGroup = ref<UserGroup[]>(
userGroupsInit.filter(
(userGroup: UserGroup) => userGroup.excludeKey == "",
) as UserGroup[],
);
const users = ref<User[]>([
...(props.modelValue ?? []).filter(
(addressee: UserGroupOrUser) => addressee.type == "user",
),
] as User[]);
const addPersonsOptions = {
uniq: false,
type: ["user"],
priority: null,
button: {
size: "btn-sm",
class: "btn-submit",
},
} as SearchOptions;
function getUserGroupBtnColor(userGroup: UserGroup) {
return [
`color: ${userGroup.foregroundColor};
.btn-check:checked + .btn-${userGroup.id} {
color: ${userGroup.foregroundColor};
background-color: ${userGroup.backgroundColor};
}`,
];
}
function addNewEntity(datas: addNewPersons) {
const { selected } = datas;
users.value = selected.map((selected) => selected.result);
addressees.value = addressees.value.filter(
(addressee) => addressee.type === "user_group",
); );
addressees.value = [...addressees.value, ...users.value]; default:
emit("update:modelValue", addressees.value); return false;
selectedValues.value = [];
suggestedValues.value = [];
}
function removeUser(user: User) {
users.value.splice(users.value.indexOf(user), 1);
addressees.value = addressees.value.filter(
(addressee) => addressee.id !== user.id,
);
emit("update:modelValue", addressees.value);
}
watch(userGroupLevel, (userGroupLevelAdd, userGroupLevelRem) => {
const index = addressees.value.indexOf(userGroupLevelRem as UserGroup);
if (index !== -1) {
addressees.value.splice(index, 1);
} }
addressees.value.push(userGroupLevelAdd as UserGroup); } else {
emit("update:modelValue", addressees.value); return (
}); suggested.type === selected.type &&
suggested.id === selected.id
);
}
});
},
);
},
{ immediate: true, deep: true },
);
watch(userGroup, (userGroupAdd) => { function addNewEntity({ entity }: { entity: Entities }) {
const userGroupLevelArr = addressees.value.filter( selectedEntities.value.push(entity);
(addressee) => emit("update:modelValue", selectedEntities.value);
addressee.type == "user_group" && addressee.excludeKey == "level", }
) as UserGroup[];
const usersArr = addressees.value.filter( function removeEntity({ entity }: { entity: Entities }) {
(addressee) => addressee.type == "user", const index = selectedEntities.value.findIndex(
) as User[]; (selectedEntity) => selectedEntity === entity,
addressees.value = [...usersArr, ...userGroupLevelArr, ...userGroupAdd]; );
emit("update:modelValue", addressees.value); if (index !== -1) {
}); selectedEntities.value.splice(index, 1);
}
emit("update:modelValue", selectedEntities.value);
}
</script> </script>
<style lang="scss" scoped>
.btn-check:checked + .btn,
:not(.btn-check) + .btn:active,
.btn:first-child:active,
.btn.active,
.btn.show {
color: white;
box-shadow: 0 0 0 0.2rem var(--bs-chill-green);
outline: 0;
}
.as-user-group {
display: inline-block;
}
</style>

View File

@ -3,17 +3,25 @@
<div class="container-xxl text-primary"> <div class="container-xxl text-primary">
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0"> <div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0">
<h2>#{{ ticket.id }}</h2>
<h1 v-if="ticket.currentMotive"> <h1 v-if="ticket.currentMotive">
{{ ticket.currentMotive.label.fr }} #{{ ticket.id }} {{ ticket.currentMotive.label.fr }}
</h1> </h1>
<p class="chill-no-data-statement" v-else> <p class="chill-no-data-statement" v-else>
{{ trans(CHILL_TICKET_TICKET_BANNER_NO_MOTIVE) }} {{ trans(CHILL_TICKET_TICKET_BANNER_NO_MOTIVE) }}
</p> </p>
<h2 v-if="ticket.currentPersons.length">
{{
ticket.currentPersons
.map((person) => person.text)
.join(", ")
}}
</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">
<toggle-flags />
<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"
@ -45,7 +53,25 @@
<Teleport to="#header-ticket-details"> <Teleport to="#header-ticket-details">
<div class="container-xxl"> <div class="container-xxl">
<div class="row justify-content-between"> <div class="row justify-content-between">
<div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0"> <div
class="col-md-4 col-sm-12 d-flex flex-column align-items-start"
>
<h3 class="text-primary">
{{ trans(CHILL_TICKET_TICKET_BANNER_CALLER) }}
</h3>
<on-the-fly
v-if="ticket.caller"
:key="ticket.caller.id"
:type="ticket.caller.type"
:id="ticket.caller.id"
:buttonText="ticket.caller.textAge"
:displayBadge="'true' === 'true'"
action="show"
></on-the-fly>
</div>
<div
class="col-md-4 col-sm-12 d-flex flex-column align-items-start"
>
<h3 class="text-primary"> <h3 class="text-primary">
{{ trans(CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER) }} {{ trans(CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER) }}
</h3> </h3>
@ -59,7 +85,9 @@
action="show" action="show"
></on-the-fly> ></on-the-fly>
</div> </div>
<div class="col-md-6 col-sm-12"> <div
class="col-md-4 col-sm-12 d-flex flex-column align-items-start"
>
<h3 class="text-primary"> <h3 class="text-primary">
{{ trans(CHILL_TICKET_TICKET_BANNER_SPEAKER) }} {{ trans(CHILL_TICKET_TICKET_BANNER_SPEAKER) }}
</h3> </h3>
@ -84,6 +112,7 @@ import { ref, computed } from "vue";
// Components // Components
import AddresseeComponent from "./AddresseeComponent.vue"; import AddresseeComponent from "./AddresseeComponent.vue";
import ToggleFlags from "./ToggleFlags.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
// Types // Types
@ -104,6 +133,7 @@ import {
CHILL_TICKET_TICKET_BANNER_MINUTES, CHILL_TICKET_TICKET_BANNER_MINUTES,
CHILL_TICKET_TICKET_BANNER_SECONDS, CHILL_TICKET_TICKET_BANNER_SECONDS,
CHILL_TICKET_TICKET_BANNER_AND, CHILL_TICKET_TICKET_BANNER_AND,
CHILL_TICKET_TICKET_BANNER_CALLER,
} from "translator"; } from "translator";
// Store // Store

View File

@ -0,0 +1,85 @@
<template>
<pick-entity
uniqid="ticket-person-selector"
:types="['person', 'third_party']"
:picked="selectedEntity ? [selectedEntity] : []"
:suggested="suggestedValues"
:multiple="false"
:removable-if-set="true"
:display-picked="true"
:label="trans(CHILL_TICKET_TICKET_SET_PERSONS_CALLER_LABEL)"
@add-new-entity="addNewEntity"
@remove-entity="removeEntity"
/>
</template>
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from "vue";
// Components
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
// Types
import { Entities } from "ChillPersonAssets/types";
// Translations
import {
trans,
CHILL_TICKET_TICKET_SET_PERSONS_CALLER_LABEL,
} from "translator";
const props = defineProps<{
modelValue: Entities | null;
suggested: Entities[];
}>();
const emit =
defineEmits<(event: "update:modelValue", value: Entities | null) => void>();
const selectedEntity = ref<Entities | null>(props.modelValue);
const suggestedValues = ref<Entities[]>([...props.suggested]);
watch(
() => [props.suggested, props.modelValue],
() => {
suggestedValues.value = props.suggested.filter(
(suggested: Entities) =>
suggested.id === selectedEntity.value?.id &&
suggested.type === selectedEntity.value?.type,
);
},
{ immediate: true, deep: true },
);
function addNewEntity({ entity }: { entity: Entities }) {
selectedEntity.value = entity;
emit("update:modelValue", selectedEntity.value);
}
function removeEntity() {
selectedEntity.value = null;
emit("update:modelValue", null);
}
</script>
<style scoped lang="scss">
ul.person-list {
list-style-type: none;
& > li {
display: inline-block;
border: 1px solid transparent;
border-radius: 6px;
button.remove-person {
opacity: 10%;
}
}
& > li:hover {
border: 1px solid white;
button.remove-person {
opacity: 100%;
}
}
}
</style>

View File

@ -1,135 +1,70 @@
<template> <template>
<div> <pick-entity
<div style="display: flex; flex-direction: column; align-items: center"> uniqid="ticket-person-selector"
<add-persons :types="['person']"
:options="addPersonsOptions" :picked="selectedEntities"
key="add-person-selector"
:buttonTitle="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
:modalTitle="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
:selected="selectedValues"
:suggested="suggestedValues" :suggested="suggestedValues"
@addNewPersons="addNewEntity" :multiple="false"
:removable-if-set="true"
:display-picked="true"
:label="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
@add-new-entity="addNewEntity"
@remove-entity="removeEntity"
/> />
<div class="p-2">
<ul class="list-suggest inline remove-items">
<li v-for="person in currentPersons" :key="person.id">
<span
:title="`${person.firstName} ${person.lastName}`"
@click="removePerson(person)"
>
{{ `${person.firstName} ${person.lastName}` }}
</span>
</li>
</ul>
</div>
</div>
</div>
<ul class="record_actions">
<li class="cancel">
<button
class="btn btn-cancel"
type="button"
@click="emit('closeRequested')"
>
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL) }}
</button>
</li>
<li>
<button class="btn btn-save" type="submit" @click.prevent="save">
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE) }}
</button>
</li>
</ul>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, reactive, ref } from "vue"; import { ref, watch, defineProps, defineEmits } from "vue";
import { useStore } from "vuex";
import { ToastPluginApi } from "vue-toast-notification";
// Components // Components
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
// Types // Types
import { SearchOptions, Suggestion, Person } from "ChillPersonAssets/types"; import { Entities } from "ChillPersonAssets/types";
import { Ticket } from "../../../types";
// Translations // Translations
import { import { trans, CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL } from "translator";
trans,
CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS,
} from "translator";
const emit = defineEmits<(e: "closeRequested") => void>(); const props = defineProps<{
modelValue: Entities[];
suggested: Entities[];
}>();
const emit = defineEmits<{
"update:modelValue": [value: Entities[]];
}>();
const store = useStore(); const selectedEntities = ref<Entities[]>([...props.modelValue]);
const toast = inject("toast") as ToastPluginApi; const suggestedValues = ref<Entities[]>([...props.suggested]);
const ticket = computed<Ticket>(() => store.getters.getTicket);
const persons = computed(() => ticket.value.currentPersons);
const addPersonsOptions = { watch(
uniq: false, () => [props.suggested, props.modelValue],
type: ["person"], () => {
priority: null, suggestedValues.value = props.suggested.filter(
button: { (suggested: Entities) =>
size: "btn-sm", !props.modelValue.some(
class: "btn-submit", (selected: Entities) =>
}, suggested.id === selected.id &&
} as SearchOptions; suggested.type === selected.type,
),
const selectedValues = ref<Suggestion[]>([]);
const suggestedValues = ref<Suggestion[]>([]);
const added: Person[] = reactive([]);
const removed: Person[] = reactive([]);
const computeCurrentPersons = (
initial: Person[],
added: Person[],
removed: Person[],
): Person[] => {
for (let p of added) {
if (initial.findIndex((element) => element.id === p.id) === -1) {
initial.push(p);
}
}
return initial.filter(
(p) => removed.findIndex((element) => element.id === p.id) === -1,
); );
}; },
{ immediate: true, deep: true },
);
const currentPersons = computed((): Person[] => { function addNewEntity({ entity }: { entity: Entities }) {
return computeCurrentPersons(persons.value, added, removed); selectedEntities.value.push(entity);
}); emit("update:modelValue", selectedEntities.value);
}
const removePerson = (p: Person) => { function removeEntity({ entity }: { entity: Entities }) {
removed.push(p); const index = selectedEntities.value.findIndex(
}; (selectedEntity) => selectedEntity === entity,
);
const addNewEntity = (n: { selected: { result: Person }[] }) => { if (index !== -1) {
for (let p of n.selected) { selectedEntities.value.splice(index, 1);
added.push(p.result);
} }
selectedValues.value = []; emit("update:modelValue", selectedEntities.value);
suggestedValues.value = []; }
};
const save = async function (): Promise<void> {
try {
await store.dispatch("setPersons", {
persons: computeCurrentPersons(persons.value, added, removed),
});
toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS));
} catch (error) {
toast.error((error as Error).message);
return Promise.resolve();
}
emit("closeRequested");
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
ul.person-list { ul.person-list {

View File

@ -0,0 +1,18 @@
<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>
</template>
<script setup lang="ts">
import { trans, CHILL_TICKET_TICKET_BANNER_EMERGENCY } from "translator";
defineProps<{ new_emergency: string }>();
</script>

View File

@ -19,6 +19,10 @@
: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'"
/> />
<TicketHistoryEmergencyComponent
v-if="history_line.event_type == 'emergency_change'"
:new_emergency="history_line.data.new_emergency"
/>
</div> </div>
<div> <div>
<span class="badge-user"> <span class="badge-user">
@ -34,12 +38,24 @@
</div> </div>
<div <div
class="card-body row" class="card-body row"
v-if="history_line.event_type != 'state_change'" v-if="
!['state_change', 'emergency_change'].includes(
history_line.event_type,
)
"
> >
<ticket-history-person-component <ticket-history-person-component
:personHistory="history_line.data" :persons="history_line.data.persons"
v-if="history_line.event_type == 'persons_state'" v-if="history_line.event_type == 'persons_state'"
/> />
<ticket-history-person-component
:persons="
history_line.data.new_caller
? [history_line.data.new_caller]
: []
"
v-if="history_line.event_type == 'set_caller'"
/>
<ticket-history-motive-component <ticket-history-motive-component
:motiveHistory="history_line.data" :motiveHistory="history_line.data"
v-else-if="history_line.event_type == 'set_motive'" v-else-if="history_line.event_type == 'set_motive'"
@ -74,11 +90,25 @@ 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 TicketHistoryStateComponent from "./TicketHistoryStateComponent.vue"; import TicketHistoryStateComponent from "./TicketHistoryStateComponent.vue";
import TicketHistoryEmergencyComponent from "./TicketHistoryEmergencyComponent.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.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";
// Translations
import {
trans,
CHILL_TICKET_TICKET_HISTORY_ADD_COMMENT,
CHILL_TICKET_TICKET_HISTORY_ADDRESSEES_STATE,
CHILL_TICKET_TICKET_HISTORY_PERSONS_STATE,
CHILL_TICKET_TICKET_HISTORY_SET_MOTIVE,
CHILL_TICKET_TICKET_HISTORY_CREATE_TICKET,
CHILL_TICKET_TICKET_HISTORY_STATE_CHANGE,
CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE,
CHILL_TICKET_TICKET_HISTORY_SET_CALLER,
} from "translator";
defineProps<{ history: TicketHistoryLine[] }>(); defineProps<{ history: TicketHistoryLine[] }>();
const store = useStore(); const store = useStore();
@ -88,17 +118,21 @@ const actionIcons = ref(store.getters.getActionIcons);
function explainSentence(history: TicketHistoryLine): string { function explainSentence(history: TicketHistoryLine): string {
switch (history.event_type) { switch (history.event_type) {
case "add_comment": case "add_comment":
return "Nouveau commentaire"; return trans(CHILL_TICKET_TICKET_HISTORY_ADD_COMMENT);
case "addressees_state": case "addressees_state":
return "Attributions"; return trans(CHILL_TICKET_TICKET_HISTORY_ADDRESSEES_STATE);
case "persons_state": case "persons_state":
return "Usagés concernés"; return trans(CHILL_TICKET_TICKET_HISTORY_PERSONS_STATE);
case "set_motive": case "set_motive":
return "Nouveau motifs"; return trans(CHILL_TICKET_TICKET_HISTORY_SET_MOTIVE);
case "create_ticket": case "create_ticket":
return "Ticket créé"; return trans(CHILL_TICKET_TICKET_HISTORY_CREATE_TICKET);
case "state_change": case "state_change":
return "Status du ticket modifié"; return trans(CHILL_TICKET_TICKET_HISTORY_STATE_CHANGE);
case "emergency_change":
return trans(CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE);
case "set_caller":
return trans(CHILL_TICKET_TICKET_HISTORY_SET_CALLER);
default: default:
return ""; return "";
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="col-12"> <div class="col-12">
<ul class="persons-list"> <ul class="persons-list" v-if="persons.length > 0">
<li v-for="person in personHistory.persons" :key="person.id"> <li v-for="person in persons" :key="person.id">
<on-the-fly <on-the-fly
:type="person.type" :type="person.type"
:id="person.id" :id="person.id"
@ -11,6 +11,7 @@
></on-the-fly> ></on-the-fly>
</li> </li>
</ul> </ul>
<div v-else class="text-muted">Aucune personne concernée</div>
</div> </div>
</template> </template>
@ -18,9 +19,9 @@
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
// Types // Types
import { PersonsState } from "../../../types"; import { Person } from "ChillPersonAssets/types";
defineProps<{ personHistory: PersonsState }>(); defineProps<{ persons: Person[] }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,4 +1,19 @@
<template> <template>
<span
class="badge rounded-pill me-1 mx-2"
:class="{
'bg-chill-red': props.new_state == 'closed',
'bg-chill-green': props.new_state == 'open',
}"
>
<template v-if="props.new_state == 'open'">
{{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }}
</template>
<template v-else-if="props.new_state == 'closed'">
{{ trans(CHILL_TICKET_TICKET_BANNER_CLOSED) }}
</template>
</span>
<!--
<span <span
class="text-chill-green mx-2" class="text-chill-green mx-2"
style=" style="
@ -22,7 +37,7 @@
v-else-if="props.new_state == 'closed'" v-else-if="props.new_state == 'closed'"
> >
{{ trans(CHILL_TICKET_TICKET_BANNER_CLOSED) }} {{ trans(CHILL_TICKET_TICKET_BANNER_CLOSED) }}
</span> </span> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -0,0 +1,67 @@
<template>
<span class="d-block d-sm-inline-block ms-sm-3 ms-md-0">
<button
class="badge rounded-pill me-1"
:class="{
'bg-danger': isEmergency,
'bg-secondary': !isEmergency,
}"
@click="toggleEmergency"
>
{{ trans(CHILL_TICKET_TICKET_BANNER_EMERGENCY) }}
</button>
</span>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
import { useToast } from "vue-toast-notification";
import { trans, CHILL_TICKET_TICKET_BANNER_EMERGENCY } from "translator";
const store = useStore();
const toast = useToast();
const isEmergency = computed(() => store.getters.isEmergency);
// Methods
function toggleEmergency() {
store
.dispatch("toggleEmergency", isEmergency.value ? "no" : "yes")
.catch(({ name, violations }) => {
if (name === "ValidationException" || name === "AccessException") {
violations.forEach((violation: string) =>
toast.open({ message: violation }),
);
} else {
toast.open({ message: "An error occurred" });
}
});
}
</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

@ -7,6 +7,7 @@ import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { import {
ApiException,
User, User,
UserGroup, UserGroup,
UserGroupOrUser, UserGroupOrUser,
@ -46,8 +47,9 @@ export const moduleAddressee: Module<State, RootState> = {
commit("setUserGroups", results); commit("setUserGroups", results);
}, },
); );
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
fetchUsers({ commit }) { fetchUsers({ commit }) {
@ -55,8 +57,9 @@ export const moduleAddressee: Module<State, RootState> = {
fetchResults("/api/1.0/main/user.json").then((results) => { fetchResults("/api/1.0/main/user.json").then((results) => {
commit("setUsers", results); commit("setUsers", results);
}); });
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
@ -76,8 +79,9 @@ export const moduleAddressee: Module<State, RootState> = {
}, },
); );
commit("setTicket", result); commit("setTicket", result);
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
}, },

View File

@ -1,12 +1,10 @@
import { import { makeFetch } from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
fetchResults,
makeFetch,
} from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { Module } from "vuex"; import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Comment } from "../../../../types"; import { Comment } from "../../../../types";
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
export interface State { export interface State {
comments: Comment[]; comments: Comment[];
@ -31,8 +29,9 @@ export const moduleComment: Module<State, RootState> = {
{ content }, { content },
); );
commit("setTicket", result); commit("setTicket", result);
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
}, },

View File

@ -7,6 +7,7 @@ import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Motive } from "../../../../types"; import { Motive } from "../../../../types";
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
export interface State { export interface State {
motives: Motive[]; motives: Motive[];
@ -33,8 +34,9 @@ export const moduleMotive: Module<State, RootState> = {
"/api/1.0/ticket/motive.json", "/api/1.0/ticket/motive.json",
)) as Motive[]; )) as Motive[];
commit("setMotives", results); commit("setMotives", results);
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
@ -55,8 +57,9 @@ export const moduleMotive: Module<State, RootState> = {
}, },
); );
commit("setTicket", result); commit("setTicket", result);
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
}, },

View File

@ -1,12 +1,28 @@
import { makeFetch } from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods"; import { makeFetch } from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { Person } from "../../../../../../../../ChillPersonBundle/Resources/public/types"; import { Person } from "../../../../../../../../ChillPersonBundle/Resources/public/types";
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
import { Module } from "vuex"; import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Ticket } from "../../../../types"; import { Ticket } from "../../../../types";
export interface State {} export interface State {
persons: Person[];
}
export const modulePersons: Module<State, RootState> = { export const modulePersons: Module<State, RootState> = {
state: () => ({
persons: [] as Person[],
}),
getters: {
getPersons(state) {
return state.persons;
},
},
mutations: {
setPersons(state, persons: Person[]) {
state.persons = persons;
},
},
actions: { actions: {
async setPersons( async setPersons(
{ commit, rootState: RootState }, { commit, rootState: RootState },
@ -25,8 +41,45 @@ export const modulePersons: Module<State, RootState> = {
commit("setTicket", result); commit("setTicket", result);
return Promise.resolve(); return Promise.resolve();
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
}
},
async setCaller(
{ commit, rootState: RootState },
payload: { caller: Person | null },
) {
try {
const caller = payload.caller
? {
id: payload.caller.id,
type: payload.caller.type,
}
: null;
const result: Ticket = await makeFetch(
"POST",
`/api/1.0/ticket/ticket/${RootState.ticket.ticket.id}/set-caller`,
{ caller },
);
commit("setTicket", result as Ticket);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
}
},
async getSuggestedPersons({ commit, rootState: RootState }) {
try {
const ticketId = RootState.ticket.ticket.id;
const result: Person[] = await makeFetch(
"GET",
`/api/1.0/ticket/ticket/${ticketId}/suggest-person`,
);
commit("setPersons", result);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
} }
}, },
}, },

View File

@ -1,8 +1,9 @@
import { Module } from "vuex"; import { Module } from "vuex";
import { RootState } from ".."; import { RootState } from "..";
import { Ticket } from "../../../../types"; import { Ticket, TicketEmergencyState } from "../../../../types";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
export interface State { export interface State {
ticket: Ticket; ticket: Ticket;
@ -13,18 +14,23 @@ export const moduleTicket: Module<State, RootState> = {
state: () => ({ state: () => ({
ticket: {} as Ticket, ticket: {} as Ticket,
action_icons: { action_icons: {
add_person: "fa fa-eyedropper", add_person: "fa fa-user-plus",
add_comment: "fa fa-comment", add_comment: "fa fa-comment",
set_motive: "fa fa-paint-brush", set_motive: "fa fa-paint-brush",
addressees_state: "fa fa-paper-plane", addressees_state: "fa fa-paper-plane",
persons_state: "fa fa-eyedropper", persons_state: "fa fa-user",
state_change: "fa fa-bolt", set_caller: "fa fa-phone",
state_change: "",
emergency_change: "",
}, },
}), }),
getters: { getters: {
isOpen(state) { isOpen(state) {
return state.ticket.currentState === "open"; return state.ticket.currentState === "open";
}, },
isEmergency(state) {
return state.ticket.emergency == "yes" ? true : false;
},
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),
@ -52,8 +58,9 @@ export const moduleTicket: Module<State, RootState> = {
`/api/1.0/ticket/ticket/${state.ticket.id}/close`, `/api/1.0/ticket/ticket/${state.ticket.id}/close`,
); );
commit("setTicket", result as Ticket); commit("setTicket", result as Ticket);
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
} }
}, },
async reopenTicket({ commit, state }) { async reopenTicket({ commit, state }) {
@ -63,8 +70,24 @@ export const moduleTicket: Module<State, RootState> = {
`/api/1.0/ticket/ticket/${state.ticket.id}/open`, `/api/1.0/ticket/ticket/${state.ticket.id}/open`,
); );
commit("setTicket", result as Ticket); commit("setTicket", result as Ticket);
} catch (e: any) { } catch (e: unknown) {
throw e.name; const error = e as ApiException;
throw error.name;
}
},
async toggleEmergency(
{ commit, state },
emergency: TicketEmergencyState,
) {
try {
const result: Ticket = await makeFetch(
"POST",
`/api/1.0/ticket/ticket/${state.ticket.id}/emergency/${emergency}`,
);
commit("setTicket", result as Ticket);
} catch (e: unknown) {
const error = e as ApiException;
throw error.name;
} }
}, },
}, },

View File

@ -6,6 +6,15 @@ chill_ticket:
in_alert: Tickets en alerte (délai de résolution dépassé) in_alert: Tickets en alerte (délai de résolution dépassé)
created_between: Créés entre created_between: Créés entre
ticket: ticket:
history:
add_comment: "Nouveau commentaire"
addressees_state: "Attributions"
persons_state: "Usagé(s) concerné(s)"
set_caller: "Appelant Concerné"
set_motive: "Nouveau motifs"
create_ticket: "Ticket créé"
state_change: ""
emergency_change: ""
previous_tickets: "Précédents tickets" previous_tickets: "Précédents tickets"
actions_toolbar: actions_toolbar:
cancel: "Annuler" cancel: "Annuler"
@ -34,13 +43,17 @@ chill_ticket:
success: "Attribution effectuée" success: "Attribution effectuée"
error: "Aucun destinataire sélectionné" error: "Aucun destinataire sélectionné"
set_persons: set_persons:
title: "Usagers concernés" title: "Appelant et usager(s)"
title_person: "Usager(s)"
title_caller: "Appelant"
user_label: "Ajouter un usager" user_label: "Ajouter un usager"
success: "Usager ajouté" caller_label: "Ajouter un appelant"
success: "Appelants et usagers mis à jour"
error: "Aucun usager sélectionné" error: "Aucun usager sélectionné"
banner: banner:
concerned_usager: "Usagers concernés" concerned_usager: "Usagers concernés"
speaker: "Attribué à" speaker: "Attribué à"
caller: "Appelant"
open: "Ouvert" open: "Ouvert"
closed: "Fermé" closed: "Fermé"
since: "Depuis {time}" since: "Depuis {time}"
@ -70,3 +83,4 @@ chill_ticket:
other {# secondes} other {# secondes}
} }
no_motive: "Pas de motif" no_motive: "Pas de motif"
emergency: "URGENT"