mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-02 14:07:43 +00:00
Merge branch '1249-implement-app-vue-with-tickets-list' into 'ticket-app-master'
Implémenter une app vue avec la liste des tickets attribués See merge request Chill-Projet/chill-bundles!858
This commit is contained in:
commit
fe2eba3b29
@ -5,11 +5,16 @@ export type fetchOption = Record<string, boolean | string | number | null>;
|
|||||||
|
|
||||||
export type Params = Record<string, number | string>;
|
export type Params = Record<string, number | string>;
|
||||||
|
|
||||||
export interface PaginationResponse<T> {
|
export interface Pagination {
|
||||||
pagination: {
|
first: number;
|
||||||
more: boolean;
|
|
||||||
items_per_page: number;
|
items_per_page: number;
|
||||||
};
|
more: boolean;
|
||||||
|
next: string | null;
|
||||||
|
previous: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationResponse<T> {
|
||||||
|
pagination: Pagination;
|
||||||
results: T[];
|
results: T[];
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,26 @@ import { TranslatableString } from "ChillMainAssets/types";
|
|||||||
* @param locale defaults to browser locale
|
* @param locale defaults to browser locale
|
||||||
* @returns The localized string or null if no translation is available
|
* @returns The localized string or null if no translation is available
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepends the current HTML lang code to the given URL.
|
||||||
|
* Example: If lang="fr" and url="/about", returns "/fr/about"
|
||||||
|
*
|
||||||
|
* @param url The URL to localize
|
||||||
|
* @returns The localized URL
|
||||||
|
*/
|
||||||
|
export function localizedUrl(url: string): string {
|
||||||
|
const lang =
|
||||||
|
document.documentElement.lang || navigator.language.split("-")[0] || "fr";
|
||||||
|
// Ensure url starts with a slash and does not already start with /{lang}/
|
||||||
|
const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
|
||||||
|
const langPrefix = `/${lang}`;
|
||||||
|
if (normalizedUrl.startsWith(langPrefix + "/")) {
|
||||||
|
return normalizedUrl;
|
||||||
|
}
|
||||||
|
return `${langPrefix}${normalizedUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function localizeString(
|
export function localizeString(
|
||||||
translatableString: TranslatableString | null | undefined,
|
translatableString: TranslatableString | null | undefined,
|
||||||
locale?: string,
|
locale?: string,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grey-card">
|
<div class="grey-card p-2">
|
||||||
<ul :class="listClasses" v-if="picked.length > 0 && displayPicked">
|
<ul :class="listClasses" v-if="displayPicked">
|
||||||
<li
|
<li
|
||||||
v-for="p in picked"
|
v-for="p in picked"
|
||||||
@click="removeEntity(p)"
|
@click="removeEntity(p)"
|
||||||
@ -21,14 +21,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="record_actions">
|
<ul class="record_actions mb-0">
|
||||||
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
|
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
|
||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
:checked="isMePicked"
|
:checked="isMePicked"
|
||||||
ref="itsMeCheckbox"
|
ref="itsMeCheckbox"
|
||||||
:type="multiple ? 'checkbox' : 'radio'"
|
:type="multiple ? 'checkbox' : 'radio'"
|
||||||
@change="selectItsMe($event as InputEvent)"
|
@change="selectItsMe($event as InputEvent)"
|
||||||
|
style="margin: 0"
|
||||||
/>
|
/>
|
||||||
{{ trans(USER_CURRENT_USER) }}
|
{{ trans(USER_CURRENT_USER) }}
|
||||||
</label>
|
</label>
|
||||||
@ -45,11 +46,15 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<ul class="badge-suggest add-items inline" style="float: right">
|
<ul
|
||||||
|
class="badge-suggest add-items inline"
|
||||||
|
style="justify-content: flex-end; display: flex"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="s in suggested"
|
v-for="s in suggested"
|
||||||
:key="s.type + s.id"
|
:key="s.type + s.id"
|
||||||
@click="addNewSuggested(s)"
|
@click="addNewSuggested(s)"
|
||||||
|
style="margin: 0"
|
||||||
>
|
>
|
||||||
<span :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
|
<span :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
|
||||||
{{ s.text }}
|
{{ s.text }}
|
||||||
@ -221,8 +226,6 @@ function getBadgeStyle(entities: Entities) {
|
|||||||
.grey-card {
|
.grey-card {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
|
||||||
min-height: 160px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-check:checked + .btn,
|
.btn-check:checked + .btn,
|
||||||
@ -242,6 +245,7 @@ ul.badge-suggest {
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
ul.badge-suggest li > span {
|
ul.badge-suggest li > span {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="{{ app.request.locale }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
3
src/Bundle/ChillTicketBundle/.vscode/settings.json
vendored
Normal file
3
src/Bundle/ChillTicketBundle/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"idf.pythonInstallPath": "/usr/bin/python3"
|
||||||
|
}
|
@ -1,4 +1,14 @@
|
|||||||
module.exports = function(encore, entries) {
|
module.exports = function (encore, entries) {
|
||||||
encore.addEntry('page_ticket', __dirname + '/src/Resources/public/page/ticket/index.ts');
|
encore.addEntry(
|
||||||
encore.addEntry('vue_ticket_app', __dirname + '/src/Resources/public/vuejs/TicketApp/index.ts');
|
"page_ticket",
|
||||||
|
__dirname + "/src/Resources/public/page/ticket/index.ts",
|
||||||
|
);
|
||||||
|
encore.addEntry(
|
||||||
|
"vue_ticket_app",
|
||||||
|
__dirname + "/src/Resources/public/vuejs/TicketApp/index.ts",
|
||||||
|
);
|
||||||
|
encore.addEntry(
|
||||||
|
"vue_ticket_list",
|
||||||
|
__dirname + "/src/Resources/public/vuejs/TicketList/index.ts",
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -155,3 +155,23 @@ export interface Ticket extends BaseTicket<"ticket_ticket:extended"> {
|
|||||||
updatedBy: User | null;
|
updatedBy: User | null;
|
||||||
history: TicketHistoryLine[];
|
history: TicketHistoryLine[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TicketFilters {
|
||||||
|
byCurrentState: TicketState[];
|
||||||
|
byCurrentStateEmergency: TicketEmergencyState[];
|
||||||
|
byCreatedAfter: string;
|
||||||
|
byCreatedBefore: string;
|
||||||
|
byResponseTimeExceeded: boolean;
|
||||||
|
byMyTickets: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketFilterParams {
|
||||||
|
byPerson?: number[];
|
||||||
|
byCurrentState?: TicketState[];
|
||||||
|
byCurrentStateEmergency?: TicketEmergencyState[];
|
||||||
|
byMotives?: number[];
|
||||||
|
byCreatedAfter?: string;
|
||||||
|
byCreatedBefore?: string;
|
||||||
|
byResponseTimeExceeded?: string;
|
||||||
|
byMyTickets?: boolean;
|
||||||
|
}
|
||||||
|
@ -17,7 +17,7 @@ import { Ticket } from "../../types";
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import PreviousTicketsComponent from "./components/PreviousTicketsComponent.vue";
|
import PreviousTicketsComponent from "./components/PreviousTicketsComponent.vue";
|
||||||
import TicketHistoryListComponent from "./components/TicketHistoryListComponent.vue";
|
import TicketHistoryListComponent from "../TicketList/components/TicketHistoryListComponent.vue";
|
||||||
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
|
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
|
||||||
import BannerComponent from "./components/BannerComponent.vue";
|
import BannerComponent from "./components/BannerComponent.vue";
|
||||||
|
|
||||||
|
@ -1,68 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
<ul class="addressees-list" v-if="addressees.length > 0">
|
||||||
|
<li v-for="addressee in addressees" :key="addressee.id">
|
||||||
<span
|
<span
|
||||||
v-for="userGroup in userGroupLevels"
|
v-if="addressee.type === 'user_group'"
|
||||||
:key="userGroup.id"
|
|
||||||
class="badge-user-group"
|
class="badge-user-group"
|
||||||
:style="`background-color: ${userGroup.backgroundColor}; color: ${userGroup.foregroundColor};`"
|
:style="`background-color: ${addressee.backgroundColor}; color: ${addressee.foregroundColor};`"
|
||||||
>
|
>
|
||||||
{{ userGroup.label.fr }}
|
{{ addressee.label.fr }}
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<span
|
|
||||||
v-for="userGroup in userGroups"
|
|
||||||
:key="userGroup.id"
|
|
||||||
class="badge-user-group"
|
|
||||||
:style="`background-color: ${userGroup.backgroundColor}; color: ${userGroup.foregroundColor};`"
|
|
||||||
>
|
|
||||||
{{ userGroup.label.fr }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="users.length > 0" class="col-12">
|
|
||||||
<span class="badge-user" v-for="user in users" :key="user.id">
|
|
||||||
<user-render-box-badge :user="user" />
|
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="addressee.type === 'user'" class="badge-user">
|
||||||
|
<user-render-box-badge :user="addressee"
|
||||||
|
/></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {
|
import { UserGroupOrUser } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||||
User,
|
|
||||||
UserGroup,
|
|
||||||
UserGroupOrUser,
|
|
||||||
} from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
|
||||||
|
|
||||||
const props = defineProps<{ addressees: UserGroupOrUser[] }>();
|
defineProps<{ addressees: UserGroupOrUser[] }>();
|
||||||
|
|
||||||
const userGroups = computed(
|
|
||||||
() =>
|
|
||||||
props.addressees.filter(
|
|
||||||
(addressee: UserGroupOrUser) =>
|
|
||||||
addressee.type == "user_group" && addressee.excludeKey == "",
|
|
||||||
) as UserGroup[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const userGroupLevels = computed(
|
|
||||||
() =>
|
|
||||||
props.addressees.filter(
|
|
||||||
(addressee: UserGroupOrUser) =>
|
|
||||||
addressee.type == "user_group" && addressee.excludeKey == "level",
|
|
||||||
) as UserGroup[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = computed(
|
|
||||||
() =>
|
|
||||||
props.addressees.filter(
|
|
||||||
(addressee: UserGroupOrUser) => addressee.type == "user",
|
|
||||||
) as User[],
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
ul.addressees-list {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
& > li {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<pick-entity
|
<pick-entity
|
||||||
uniqid="ticket-addressee-selector"
|
uniqid="ticket-addressee-selector"
|
||||||
:types="['user', 'user_group', 'thirdparty']"
|
:types="['user', 'user_group']"
|
||||||
:picked="selectedEntities"
|
:picked="selectedEntities"
|
||||||
:suggested="suggestedValues"
|
:suggested="suggestedValues"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
@ -13,8 +11,6 @@
|
|||||||
@add-new-entity="addNewEntity"
|
@add-new-entity="addNewEntity"
|
||||||
@remove-entity="removeEntity"
|
@remove-entity="removeEntity"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -24,7 +20,7 @@ import { ref, watch, defineProps, defineEmits } from "vue";
|
|||||||
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Entities } from "ChillPersonAssets/types";
|
import { Entities, EntitiesOrMe } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
import {
|
import {
|
||||||
@ -70,12 +66,12 @@ watch(
|
|||||||
{ immediate: true, deep: true },
|
{ immediate: true, deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
function addNewEntity({ entity }: { entity: Entities }) {
|
function addNewEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||||
selectedEntities.value.push(entity);
|
selectedEntities.value.push(entity as Entities);
|
||||||
emit("update:modelValue", selectedEntities.value);
|
emit("update:modelValue", selectedEntities.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntity({ entity }: { entity: Entities }) {
|
function removeEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||||
const index = selectedEntities.value.findIndex(
|
const index = selectedEntities.value.findIndex(
|
||||||
(selectedEntity) => selectedEntity === entity,
|
(selectedEntity) => selectedEntity === entity,
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
|
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-6 col-sm-12">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<emergency-toggle-component />
|
<emergency-toggle-component
|
||||||
|
v-model="isEmergencyLocal"
|
||||||
|
@toggle-emergency="handleEmergencyToggle"
|
||||||
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="badge text-bg-chill-green text-white"
|
class="badge text-bg-chill-green text-white"
|
||||||
@ -100,6 +103,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { useToast } from "vue-toast-notification";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
||||||
@ -107,7 +111,8 @@ import EmergencyToggleComponent from "./Emergency/EmergencyToggleComponent.vue";
|
|||||||
import PersonComponent from "./Person/PersonComponent.vue";
|
import PersonComponent from "./Person/PersonComponent.vue";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Ticket } from "../../../types";
|
import { Ticket, TicketEmergencyState } from "../../../types";
|
||||||
|
import { Person } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
trans,
|
trans,
|
||||||
@ -121,7 +126,8 @@ import {
|
|||||||
|
|
||||||
// Store
|
// Store
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { Person } from "ChillPersonAssets/types";
|
|
||||||
|
// Utils
|
||||||
import { getTicketTitle } from "../utils/utils";
|
import { getTicketTitle } from "../utils/utils";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -129,6 +135,7 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const toast = useToast();
|
||||||
const today = ref(new Date());
|
const today = ref(new Date());
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@ -136,7 +143,21 @@ setInterval(() => {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
const isOpen = computed(() => store.getters.isOpen);
|
const isOpen = computed(() => store.getters.isOpen);
|
||||||
|
const isEmergencyLocal = computed(() => store.getters.isEmergency);
|
||||||
const since = computed(() => {
|
const since = computed(() => {
|
||||||
return store.getters.getSinceCreated(today.value);
|
return store.getters.getSinceCreated(today.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function handleEmergencyToggle(emergency: TicketEmergencyState) {
|
||||||
|
store.dispatch("toggleEmergency", emergency).catch(({ name, violations }) => {
|
||||||
|
if (name === "ValidationException" || name === "AccessException") {
|
||||||
|
violations.forEach((violation: string) =>
|
||||||
|
toast.open({ message: violation }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.open({ message: "An error occurred" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
<button
|
<button
|
||||||
class="badge rounded-pill me-1"
|
class="badge rounded-pill me-1"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-danger': isEmergency,
|
'bg-danger': modelValue,
|
||||||
'bg-secondary': !isEmergency,
|
'bg-secondary': !modelValue,
|
||||||
}"
|
}"
|
||||||
@click="toggleEmergency"
|
@click="toggleEmergency"
|
||||||
>
|
>
|
||||||
@ -14,29 +14,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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";
|
import { trans, CHILL_TICKET_TICKET_BANNER_EMERGENCY } from "translator";
|
||||||
|
import { TicketEmergencyState } from "../../../../types";
|
||||||
|
|
||||||
const store = useStore();
|
// Props
|
||||||
const toast = useToast();
|
const props = defineProps<{
|
||||||
|
modelValue: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const isEmergency = computed(() => store.getters.isEmergency);
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: boolean];
|
||||||
|
"toggle-emergency": [emergency: TicketEmergencyState];
|
||||||
|
}>();
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
function toggleEmergency() {
|
function toggleEmergency() {
|
||||||
store
|
const newValue = !props.modelValue;
|
||||||
.dispatch("toggleEmergency", isEmergency.value ? "no" : "yes")
|
emit("update:modelValue", newValue);
|
||||||
.catch(({ name, violations }) => {
|
emit("toggle-emergency", newValue ? "yes" : "no");
|
||||||
if (name === "ValidationException" || name === "AccessException") {
|
|
||||||
violations.forEach((violation: string) =>
|
|
||||||
toast.open({ message: violation }),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toast.open({ message: "An error occurred" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
||||||
:options="motives"
|
:options="motives"
|
||||||
v-model="motive"
|
v-model="motive"
|
||||||
class="mb-4"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
></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>
|
||||||
|
|
||||||
|
@ -20,10 +20,10 @@ import { ref, watch, defineProps, defineEmits, computed } from "vue";
|
|||||||
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Entities, EntityType } from "ChillPersonAssets/types";
|
import { Entities, EntitiesOrMe, EntityType } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Entities[] | Entities | null;
|
modelValue: EntitiesOrMe[] | EntitiesOrMe | null;
|
||||||
suggested: Entities[];
|
suggested: Entities[];
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
types: EntityType[];
|
types: EntityType[];
|
||||||
@ -31,15 +31,13 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: Entities[] | Entities | null];
|
"update:modelValue": [value: EntitiesOrMe[] | EntitiesOrMe | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Valeurs par défaut
|
|
||||||
const multiple = props.multiple;
|
const multiple = props.multiple;
|
||||||
const types = props.types;
|
const types = props.types;
|
||||||
const label = props.label;
|
const label = props.label;
|
||||||
|
|
||||||
// État local
|
|
||||||
const selectedEntities = ref<Entities[]>(
|
const selectedEntities = ref<Entities[]>(
|
||||||
multiple
|
multiple
|
||||||
? [...((props.modelValue as Entities[]) || [])]
|
? [...((props.modelValue as Entities[]) || [])]
|
||||||
@ -88,17 +86,17 @@ watch(
|
|||||||
{ immediate: true, deep: true },
|
{ immediate: true, deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
function addNewEntity({ entity }: { entity: Entities }) {
|
function addNewEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
selectedEntities.value.push(entity);
|
selectedEntities.value.push(entity as Entities);
|
||||||
emit("update:modelValue", selectedEntities.value);
|
emit("update:modelValue", selectedEntities.value);
|
||||||
} else {
|
} else {
|
||||||
selectedEntities.value = [entity];
|
selectedEntities.value = [entity as Entities];
|
||||||
emit("update:modelValue", entity);
|
emit("update:modelValue", entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntity({ entity }: { entity: Entities }) {
|
function removeEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const index = selectedEntities.value.findIndex(
|
const index = selectedEntities.value.findIndex(
|
||||||
(selectedEntity) => selectedEntity === entity,
|
(selectedEntity) => selectedEntity === entity,
|
||||||
|
@ -36,51 +36,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #body>
|
<template #body>
|
||||||
<TicketListComponent
|
<ticket-list-component
|
||||||
:tickets="previousTickets"
|
:tickets="previousTickets"
|
||||||
|
:title="trans(CHILL_TICKET_LIST_NO_TICKETS)"
|
||||||
@view-ticket="handleViewTicket"
|
@view-ticket="handleViewTicket"
|
||||||
@edit-ticket="handleEditTicket"
|
@edit-ticket="handleEditTicket"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<!-- Modal for ticket history -->
|
|
||||||
<Modal
|
|
||||||
v-if="
|
|
||||||
showTicketHistoryModal &&
|
|
||||||
selectedTicketId !== null &&
|
|
||||||
previousTicketHistory
|
|
||||||
"
|
|
||||||
:show="showTicketHistoryModal"
|
|
||||||
modal-dialog-class="modal-xl"
|
|
||||||
@close="closeHistoryModal"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<h3 class="modal-title">
|
|
||||||
{{ getTicketTitle(previousTicketDetails) }}
|
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<TicketHistoryListComponent
|
|
||||||
v-if="previousTicketHistory.length > 0"
|
|
||||||
:history="previousTicketHistory"
|
|
||||||
/>
|
|
||||||
<div v-else class="text-center p-4">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<button
|
|
||||||
class="btn btn-edit"
|
|
||||||
@click="handleEditTicket(selectedTicketId)"
|
|
||||||
>
|
|
||||||
{{ trans(EDIT) }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -90,23 +53,22 @@ import { useStore } from "vuex";
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Modal from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
|
import Modal from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
|
||||||
import TicketListComponent from "./TicketListComponent.vue";
|
import TicketListComponent from "../../TicketList/components/TicketListComponent.vue";
|
||||||
import TicketHistoryListComponent from "./TicketHistoryListComponent.vue";
|
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
import {
|
import {
|
||||||
trans,
|
trans,
|
||||||
|
CHILL_TICKET_LIST_NO_TICKETS,
|
||||||
CHILL_TICKET_TICKET_PREVIOUS_TICKETS,
|
CHILL_TICKET_TICKET_PREVIOUS_TICKETS,
|
||||||
CHILL_TICKET_LIST_TITLE_PREVIOUS_TICKETS,
|
CHILL_TICKET_LIST_TITLE_PREVIOUS_TICKETS,
|
||||||
EDIT,
|
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Person } from "ChillPersonAssets/types";
|
import { Person } from "ChillPersonAssets/types";
|
||||||
import { TicketHistoryLine, TicketSimple } from "../../../types";
|
import { TicketSimple } from "../../../types";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { getTicketTitle } from "../utils/utils";
|
import { localizedUrl } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const showPreviousTicketModal = ref(false);
|
const showPreviousTicketModal = ref(false);
|
||||||
@ -117,18 +79,14 @@ const currentPersons = computed(
|
|||||||
() => store.getters.getCurrentPersons as Person[],
|
() => store.getters.getCurrentPersons as Person[],
|
||||||
);
|
);
|
||||||
const previousTickets = computed(
|
const previousTickets = computed(
|
||||||
() => store.getters.getPreviousTickets as TicketSimple[],
|
() => store.getters.getTicketList as TicketSimple[],
|
||||||
);
|
);
|
||||||
const previousTicketHistory = computed(
|
|
||||||
() => store.getters.getPreviousTicketHistory as TicketHistoryLine[],
|
|
||||||
);
|
|
||||||
const previousTicketDetails = computed(() => {
|
|
||||||
return store.getters.getPreviousTicketDetails as TicketSimple;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await store.dispatch("fetchTicketsByPerson");
|
await store.dispatch("fetchTicketList", {
|
||||||
|
byPerson: currentPersons.value.map((person) => person.id),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du chargement des tickets:", error);
|
console.error("Erreur lors du chargement des tickets:", error);
|
||||||
}
|
}
|
||||||
@ -147,19 +105,17 @@ async function handleViewTicket(ticketId: number) {
|
|||||||
showTicketHistoryModal.value = true;
|
showTicketHistoryModal.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.dispatch("fetchPreviousTicketDetails", ticketId);
|
await store.dispatch("fetchTicket", ticketId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du chargement de l'historique:", error);
|
console.error("Erreur lors du chargement du ticket:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditTicket(ticketId: number) {
|
function handleEditTicket(ticketId: number) {
|
||||||
window.location.href = `/fr/ticket/ticket/${ticketId}/edit`;
|
const returnPath = localizedUrl(`/ticket/ticket/list`);
|
||||||
}
|
window.location.href = localizedUrl(
|
||||||
|
`/ticket/ticket/${ticketId}/edit?returnPath=${returnPath}`,
|
||||||
function closeHistoryModal() {
|
);
|
||||||
showTicketHistoryModal.value = false;
|
|
||||||
selectedTicketId.value = null;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="ticket-list-container">
|
|
||||||
<div v-if="tickets.length === 0" class="chill-no-data-statement">
|
|
||||||
{{ trans(CHILL_TICKET_LIST_NO_TICKETS) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="flex-table">
|
|
||||||
<TicketListItemComponent
|
|
||||||
v-for="ticket in tickets"
|
|
||||||
:key="ticket.id"
|
|
||||||
:ticket="ticket"
|
|
||||||
@view-ticket="$emit('view-ticket', $event)"
|
|
||||||
@edit-ticket="$emit('edit-ticket', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { defineProps, defineEmits } from "vue";
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { TicketSimple } from "../../../types";
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import TicketListItemComponent from "./TicketListItemComponent.vue";
|
|
||||||
|
|
||||||
// Translations
|
|
||||||
import { trans, CHILL_TICKET_LIST_NO_TICKETS } from "translator";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
tickets: TicketSimple[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
"view-ticket": [ticketId: number];
|
|
||||||
"edit-ticket": [ticketId: number];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
@ -4,6 +4,10 @@ import { State as TicketStates, moduleTicket } from "./modules/ticket";
|
|||||||
import { State as CommentStates, moduleComment } from "./modules/comment";
|
import { State as CommentStates, moduleComment } from "./modules/comment";
|
||||||
import { State as AddresseeStates, moduleAddressee } from "./modules/addressee";
|
import { State as AddresseeStates, moduleAddressee } from "./modules/addressee";
|
||||||
import { State as PersonsState, modulePersons } from "./modules/persons";
|
import { State as PersonsState, modulePersons } from "./modules/persons";
|
||||||
|
import {
|
||||||
|
State as TicketListState,
|
||||||
|
moduleTicketList,
|
||||||
|
} from "./modules/ticket_list";
|
||||||
|
|
||||||
export interface RootState {
|
export interface RootState {
|
||||||
motive: MotiveStates;
|
motive: MotiveStates;
|
||||||
@ -11,6 +15,7 @@ export interface RootState {
|
|||||||
comment: CommentStates;
|
comment: CommentStates;
|
||||||
addressee: AddresseeStates;
|
addressee: AddresseeStates;
|
||||||
persons: PersonsState;
|
persons: PersonsState;
|
||||||
|
ticketList: TicketListState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const store = createStore<RootState>({
|
export const store = createStore<RootState>({
|
||||||
@ -20,5 +25,6 @@ export const store = createStore<RootState>({
|
|||||||
comment: moduleComment,
|
comment: moduleComment,
|
||||||
addressee: moduleAddressee,
|
addressee: moduleAddressee,
|
||||||
persons: modulePersons,
|
persons: modulePersons,
|
||||||
|
ticketList: moduleTicketList,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
import { Module } from "vuex";
|
import { Module } from "vuex";
|
||||||
import { RootState } from "..";
|
import { RootState } from "..";
|
||||||
|
|
||||||
import { Ticket, TicketEmergencyState, TicketSimple } 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";
|
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||||
import { getSinceCreated } from "../../utils/utils";
|
import { getSinceCreated } from "../../utils/utils";
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
ticket: Ticket;
|
ticket: Ticket;
|
||||||
previous_tickets: TicketSimple[];
|
|
||||||
previous_ticket_details: Ticket;
|
|
||||||
action_icons: object;
|
action_icons: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const moduleTicket: Module<State, RootState> = {
|
export const moduleTicket: Module<State, RootState> = {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
ticket: {} as Ticket,
|
ticket: {} as Ticket,
|
||||||
previous_tickets: [] as TicketSimple[],
|
|
||||||
previous_ticket_details: {} as Ticket,
|
|
||||||
action_icons: {
|
action_icons: {
|
||||||
add_person: "fa fa-user-plus",
|
add_person: "fa fa-user-plus",
|
||||||
add_comment: "fa fa-comment",
|
add_comment: "fa fa-comment",
|
||||||
@ -42,21 +38,6 @@ export const moduleTicket: Module<State, RootState> = {
|
|||||||
);
|
);
|
||||||
return state.ticket;
|
return state.ticket;
|
||||||
},
|
},
|
||||||
getTicketHistory(state) {
|
|
||||||
return state.ticket.history;
|
|
||||||
},
|
|
||||||
getPreviousTickets(state) {
|
|
||||||
return state.previous_tickets;
|
|
||||||
},
|
|
||||||
getPreviousTicketHistory(state) {
|
|
||||||
return state.previous_ticket_details.history;
|
|
||||||
},
|
|
||||||
getPreviousTicketDetails(state) {
|
|
||||||
return state.previous_ticket_details;
|
|
||||||
},
|
|
||||||
getCurrentPersons(state) {
|
|
||||||
return state.ticket.currentPersons ?? [];
|
|
||||||
},
|
|
||||||
getActionIcons(state) {
|
getActionIcons(state) {
|
||||||
return state.action_icons;
|
return state.action_icons;
|
||||||
},
|
},
|
||||||
@ -66,18 +47,18 @@ export const moduleTicket: Module<State, RootState> = {
|
|||||||
}
|
}
|
||||||
return getSinceCreated(state.ticket.createdAt.datetime, currentTime);
|
return getSinceCreated(state.ticket.createdAt.datetime, currentTime);
|
||||||
},
|
},
|
||||||
|
getTicketHistory: (state) => {
|
||||||
|
return state.ticket.history;
|
||||||
|
},
|
||||||
|
getCurrentPersons(state) {
|
||||||
|
return state.ticket.currentPersons;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
setTicket(state, ticket: Ticket) {
|
setTicket(state, ticket: Ticket) {
|
||||||
state.ticket = ticket;
|
state.ticket = ticket;
|
||||||
},
|
},
|
||||||
setPreviousTickets(state, previousTickets: TicketSimple[]) {
|
|
||||||
state.previous_tickets = previousTickets;
|
|
||||||
},
|
|
||||||
setPreviousTicketDetails(state, ticket: Ticket) {
|
|
||||||
state.previous_ticket_details = ticket;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async closeTicket({ commit, state }) {
|
async closeTicket({ commit, state }) {
|
||||||
@ -116,39 +97,5 @@ export const moduleTicket: Module<State, RootState> = {
|
|||||||
throw error.name;
|
throw error.name;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchTicketsByPerson({ commit, state }) {
|
|
||||||
try {
|
|
||||||
if (state.ticket.currentPersons.length === 0) {
|
|
||||||
commit("setPreviousTickets", []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { results }: { results: TicketSimple[] } = await makeFetch(
|
|
||||||
"GET",
|
|
||||||
`/api/1.0/ticket/ticket/list?byPerson=${state.ticket.currentPersons.map((person) => person.id).join(",")}`,
|
|
||||||
);
|
|
||||||
const excludeCurrentTicket = results.filter(
|
|
||||||
(ticket) => ticket.id !== state.ticket.id,
|
|
||||||
);
|
|
||||||
commit("setPreviousTickets", excludeCurrentTicket);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const error = e as ApiException;
|
|
||||||
throw error.name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchPreviousTicketDetails({ commit }, ticketId: number) {
|
|
||||||
try {
|
|
||||||
const ticket: Ticket = await makeFetch(
|
|
||||||
"GET",
|
|
||||||
`/api/1.0/ticket/ticket/${ticketId}`,
|
|
||||||
);
|
|
||||||
commit("setPreviousTicketDetails", ticket);
|
|
||||||
return ticket;
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const error = e as ApiException;
|
|
||||||
throw error.name;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,142 @@
|
|||||||
|
import { Module } from "vuex";
|
||||||
|
import { RootState } from "..";
|
||||||
|
|
||||||
|
import { Ticket, TicketFilterParams, TicketSimple } from "../../../../types";
|
||||||
|
import {
|
||||||
|
makeFetch,
|
||||||
|
Pagination,
|
||||||
|
PaginationResponse,
|
||||||
|
} from "ChillMainAssets/lib/api/apiMethods";
|
||||||
|
import {
|
||||||
|
ApiException,
|
||||||
|
User,
|
||||||
|
} from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
ticket_list: TicketSimple[];
|
||||||
|
ticket_details: Ticket | null;
|
||||||
|
pagination: Pagination;
|
||||||
|
count: number;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moduleTicketList: Module<State, RootState> = {
|
||||||
|
state: () => ({
|
||||||
|
ticket_list: [],
|
||||||
|
ticket_details: null,
|
||||||
|
pagination: {
|
||||||
|
first: 0,
|
||||||
|
items_per_page: 50,
|
||||||
|
more: false,
|
||||||
|
next: null,
|
||||||
|
previous: null,
|
||||||
|
},
|
||||||
|
count: 0,
|
||||||
|
user: {} as User,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getTicketList(state): TicketSimple[] {
|
||||||
|
return state.ticket_list;
|
||||||
|
},
|
||||||
|
getTicketDetails(state): Ticket | null {
|
||||||
|
return state.ticket_details;
|
||||||
|
},
|
||||||
|
getPagination(state) {
|
||||||
|
return state.pagination;
|
||||||
|
},
|
||||||
|
getUser(state): User {
|
||||||
|
return state.user;
|
||||||
|
},
|
||||||
|
getCount(state): number {
|
||||||
|
return state.count;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setTicketList(state, ticketList: TicketSimple[]) {
|
||||||
|
state.ticket_list = ticketList;
|
||||||
|
},
|
||||||
|
setTicketDetails(state, ticket: Ticket | null) {
|
||||||
|
state.ticket_details = ticket;
|
||||||
|
},
|
||||||
|
setPagination(state, pagination: State["pagination"]) {
|
||||||
|
state.pagination = pagination;
|
||||||
|
},
|
||||||
|
setUser(state, user: User) {
|
||||||
|
state.user = user;
|
||||||
|
},
|
||||||
|
setCount(state, count: number) {
|
||||||
|
state.count = count;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async fetchTicketList({ commit }, ticketFilterParams: TicketFilterParams) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams(
|
||||||
|
ticketFilterParams as Record<string, string>,
|
||||||
|
).toString();
|
||||||
|
const { results, pagination, count } = (await makeFetch(
|
||||||
|
"GET",
|
||||||
|
`/api/1.0/ticket/ticket/list/?${params}`,
|
||||||
|
)) as PaginationResponse<TicketSimple>;
|
||||||
|
|
||||||
|
commit("setTicketList", results);
|
||||||
|
commit("setCount", count);
|
||||||
|
commit("setPagination", pagination);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const error = e as ApiException;
|
||||||
|
console.error(
|
||||||
|
"Erreur lors du chargement de la liste des tickets:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchConnectedUser({ commit }) {
|
||||||
|
try {
|
||||||
|
const user = await makeFetch(
|
||||||
|
"GET",
|
||||||
|
"http://localhost:8000/api/1.0/main/whoami.json",
|
||||||
|
);
|
||||||
|
commit("setUser", user);
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Erreur lors de la récupération de l'utilisateur connecté:",
|
||||||
|
error as ApiException,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchTicketDetails({ commit }, ticketId: number) {
|
||||||
|
try {
|
||||||
|
const ticket: Ticket = await makeFetch(
|
||||||
|
"GET",
|
||||||
|
`/api/1.0/ticket/ticket/${ticketId}`,
|
||||||
|
);
|
||||||
|
commit("setTicketDetails", ticket);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Erreur lors du chargement du ticket:",
|
||||||
|
error as ApiException,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchTicketListByUrl({ commit, state }, url: string) {
|
||||||
|
try {
|
||||||
|
const { results, pagination, count } = (await makeFetch(
|
||||||
|
"GET",
|
||||||
|
url,
|
||||||
|
)) as PaginationResponse<TicketSimple>;
|
||||||
|
commit("setTicketList", [...state.ticket_list, ...results]);
|
||||||
|
commit("setCount", count);
|
||||||
|
commit("setPagination", pagination);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const error = e as ApiException;
|
||||||
|
console.error(
|
||||||
|
"Erreur lors du chargement de la liste des tickets par URL:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<h1 class="text-primary">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<ticket-filter-list-component
|
||||||
|
:resultCount="resultCount"
|
||||||
|
:available-persons="availablePersons"
|
||||||
|
:available-motives="availableMotives"
|
||||||
|
@filters-changed="handleFiltersChanged"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="d-flex justify-content-center align-items-center"
|
||||||
|
style="height: 200px"
|
||||||
|
>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border mb-3" role="status">
|
||||||
|
<span class="visually-hidden">{{
|
||||||
|
trans(CHILL_TICKET_LIST_LOADING_TICKET)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_LOADING_TICKET) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticket list -->
|
||||||
|
<ticket-list-component
|
||||||
|
v-else
|
||||||
|
:tickets="ticketList"
|
||||||
|
:title="title"
|
||||||
|
:hasMoreTickets="pagination.next !== null"
|
||||||
|
@fetchNextPage="fetchNextPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { TicketSimple, Motive } from "../../types";
|
||||||
|
import type { Person } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import TicketListComponent from "./components/TicketListComponent.vue";
|
||||||
|
import TicketFilterListComponent from "./components/TicketFilterListComponent.vue";
|
||||||
|
import { Pagination } from "ChillMainAssets/lib/api/apiMethods";
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
import { trans, CHILL_TICKET_LIST_LOADING_TICKET } from "translator";
|
||||||
|
|
||||||
|
interface TicketFilterParams {
|
||||||
|
byPerson?: number[];
|
||||||
|
byCurrentState?: string[];
|
||||||
|
byCurrentStateEmergency?: string[];
|
||||||
|
byMotives?: number[];
|
||||||
|
byCreatedAfter?: string;
|
||||||
|
byCreatedBefore?: string;
|
||||||
|
byResponseTimeExceeded?: string;
|
||||||
|
}
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const title = window.title;
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const ticketList = computed(
|
||||||
|
() => store.getters.getTicketList as TicketSimple[],
|
||||||
|
);
|
||||||
|
const resultCount = computed(() => store.getters.getCount as number);
|
||||||
|
const pagination = computed(() => store.getters.getPagination as Pagination);
|
||||||
|
const availablePersons = ref<Person[]>([]);
|
||||||
|
const availableMotives = computed(() => store.getters.getMotives as Motive[]);
|
||||||
|
|
||||||
|
const handleFiltersChanged = async (filters: TicketFilterParams) => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
await store.dispatch("fetchTicketList", filters);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNextPage = async () => {
|
||||||
|
console.log("Fetching next page...");
|
||||||
|
if (pagination.value.next) {
|
||||||
|
await store.dispatch("fetchTicketListByUrl", pagination.value.next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
store.dispatch("fetchTicketList"),
|
||||||
|
store.dispatch("fetchMotives"),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container-fluid {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,396 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_TITLE) }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form @submit.prevent="applyFilters">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Filtre par usagé -->
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_PERSONS_CONCERNED)
|
||||||
|
}}</label>
|
||||||
|
<persons-selector
|
||||||
|
v-model="selectedPersons"
|
||||||
|
:suggested="availablePersons"
|
||||||
|
:multiple="true"
|
||||||
|
:types="['person']"
|
||||||
|
:label="trans(CHILL_TICKET_LIST_FILTER_BY_PERSON)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<!-- Filtre par motifs -->
|
||||||
|
<div class="row">
|
||||||
|
<label class="form-label">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_BY_MOTIVES)
|
||||||
|
}}</label>
|
||||||
|
<motive-selector
|
||||||
|
v-model="selectedMotive"
|
||||||
|
:motives="availableMotives"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-1" style="min-height: 2.2em">
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="motive in selectedMotives"
|
||||||
|
:key="motive.id"
|
||||||
|
class="badge bg-secondary d-flex align-items-center gap-1"
|
||||||
|
>
|
||||||
|
{{ getMotiveDisplayName(motive) }}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close btn-close-white"
|
||||||
|
:aria-label="trans(CHILL_TICKET_LIST_FILTER_REMOVE)"
|
||||||
|
@click="removeMotive(motive)"
|
||||||
|
></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Filtre par état actuel -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value="open"
|
||||||
|
v-model="filters.byCurrentState"
|
||||||
|
id="stateOpen"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="stateOpen">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_OPEN)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value="closed"
|
||||||
|
v-model="filters.byCurrentState"
|
||||||
|
id="stateClosed"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="stateClosed">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_CLOSED) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Filtre par état d'urgence -->
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value="yes"
|
||||||
|
v-model="filters.byCurrentStateEmergency"
|
||||||
|
id="emergencyYes"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="emergencyYes">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
value="no"
|
||||||
|
v-model="filters.byCurrentStateEmergency"
|
||||||
|
id="emergencyNo"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="emergencyNo">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_NO_EMERGENCY)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Filtre pour temps de réponse dépassé -->
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="filters.byResponseTimeExceeded"
|
||||||
|
@change="handleResponseTimeExceededChange"
|
||||||
|
id="responseTimeExceeded"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="responseTimeExceeded">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_EXCEEDED) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_WARNING) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<!-- Filtre par mes tickets -->
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<div class="d-flex gap-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
v-model="filters.byMyTickets"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id="stateMe"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="stateMe">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_TO_ME) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtre par date de création -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="byCreatedAfter" class="form-label">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_CREATED_AFTER)
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="byCreatedAfter"
|
||||||
|
v-model="filters.byCreatedAfter"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="byCreatedBefore" class="form-label">{{
|
||||||
|
trans(CHILL_TICKET_LIST_FILTER_CREATED_BEFORE)
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="byCreatedBefore"
|
||||||
|
v-model="filters.byCreatedBefore"
|
||||||
|
class="form-control"
|
||||||
|
:disabled="filters.byResponseTimeExceeded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 d-flex align-items-end justify-content-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="resetFilters"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_RESET) }}
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-funnel"></i>
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_APPLY) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span
|
||||||
|
class="col-12 d-flex align-items-end justify-content-end form-text text-muted mt-1"
|
||||||
|
>
|
||||||
|
{{ resultCount !== 0 ? `${resultCount} ` : "" }}
|
||||||
|
{{ trans(CHILL_TICKET_LIST_FILTER_RESULT, { count: resultCount }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
|
import type { Person } from "ChillPersonAssets/types";
|
||||||
|
import type { Motive, TicketFilterParams, TicketFilters } from "../../../types";
|
||||||
|
|
||||||
|
// Translation
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
CHILL_TICKET_LIST_FILTER_TITLE,
|
||||||
|
CHILL_TICKET_LIST_FILTER_PERSONS_CONCERNED,
|
||||||
|
CHILL_TICKET_LIST_FILTER_BY_PERSON,
|
||||||
|
CHILL_TICKET_LIST_FILTER_BY_MOTIVES,
|
||||||
|
CHILL_TICKET_LIST_FILTER_REMOVE,
|
||||||
|
CHILL_TICKET_LIST_FILTER_OPEN,
|
||||||
|
CHILL_TICKET_LIST_FILTER_CLOSED,
|
||||||
|
CHILL_TICKET_LIST_FILTER_TO_ME,
|
||||||
|
CHILL_TICKET_LIST_FILTER_EMERGENCY,
|
||||||
|
CHILL_TICKET_LIST_FILTER_NO_EMERGENCY,
|
||||||
|
CHILL_TICKET_LIST_FILTER_CREATED_AFTER,
|
||||||
|
CHILL_TICKET_LIST_FILTER_CREATED_BEFORE,
|
||||||
|
CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_EXCEEDED,
|
||||||
|
CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_WARNING,
|
||||||
|
CHILL_TICKET_LIST_FILTER_RESET,
|
||||||
|
CHILL_TICKET_LIST_FILTER_APPLY,
|
||||||
|
CHILL_TICKET_LIST_FILTER_RESULT,
|
||||||
|
} from "translator";
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import PersonsSelector from "../../TicketApp/components/Person/PersonsSelectorComponent.vue";
|
||||||
|
import MotiveSelector from "../../TicketApp/components/Motive/MotiveSelectorComponent.vue";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
availablePersons?: Person[];
|
||||||
|
availableMotives: Motive[];
|
||||||
|
resultCount: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"filters-changed": [filters: TicketFilterParams];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// État réactif
|
||||||
|
const filters = ref<TicketFilters>({
|
||||||
|
byCurrentState: [],
|
||||||
|
byCurrentStateEmergency: [],
|
||||||
|
byCreatedAfter: "",
|
||||||
|
byCreatedBefore: "",
|
||||||
|
byResponseTimeExceeded: false,
|
||||||
|
byMyTickets: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sélection des personnes
|
||||||
|
const selectedPersons = ref<Person[]>([]);
|
||||||
|
const availablePersons = ref<Person[]>(props.availablePersons || []);
|
||||||
|
|
||||||
|
// Sélection des motifs
|
||||||
|
const selectedMotive = ref<Motive | undefined>();
|
||||||
|
const selectedMotives = ref<Motive[]>([]);
|
||||||
|
|
||||||
|
// Watchers pour les sélecteurs
|
||||||
|
watch(selectedMotive, (newMotive) => {
|
||||||
|
if (newMotive && !selectedMotives.value.find((m) => m.id === newMotive.id)) {
|
||||||
|
selectedMotives.value.push(newMotive);
|
||||||
|
selectedMotive.value = undefined; // Reset pour permettre une nouvelle sélection
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed pour les IDs des personnes sélectionnées
|
||||||
|
const selectedPersonIds = computed(() =>
|
||||||
|
selectedPersons.value.map((person) => person.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Computed pour les IDs des motifs sélectionnés
|
||||||
|
const selectedMotiveIds = computed(() =>
|
||||||
|
selectedMotives.value.map((motive) => motive.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Méthodes
|
||||||
|
const formatDateToISO = (dateString: string): string => {
|
||||||
|
if (!dateString) return dateString;
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMotiveDisplayName = (motive: Motive): string => {
|
||||||
|
if (typeof motive.label === "string") {
|
||||||
|
return motive.label;
|
||||||
|
}
|
||||||
|
if (typeof motive.label === "object" && motive.label !== null) {
|
||||||
|
const labels = Object.values(motive.label);
|
||||||
|
return labels[0] || `Motive ${motive.id}`;
|
||||||
|
}
|
||||||
|
return `Motive ${motive.id}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeMotive = (motiveToRemove: Motive): void => {
|
||||||
|
const index = selectedMotives.value.findIndex(
|
||||||
|
(m) => m.id === motiveToRemove.id,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
selectedMotives.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilters = (): void => {
|
||||||
|
const apiFilters: TicketFilterParams = {};
|
||||||
|
|
||||||
|
if (selectedPersonIds.value.length > 0) {
|
||||||
|
apiFilters.byPerson = selectedPersonIds.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.byCurrentState.length > 0) {
|
||||||
|
apiFilters.byCurrentState = filters.value.byCurrentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.byCurrentStateEmergency.length > 0) {
|
||||||
|
apiFilters.byCurrentStateEmergency = filters.value.byCurrentStateEmergency;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMotiveIds.value.length > 0) {
|
||||||
|
apiFilters.byMotives = selectedMotiveIds.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.byCreatedAfter) {
|
||||||
|
apiFilters.byCreatedAfter = formatDateToISO(filters.value.byCreatedAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.byCreatedBefore) {
|
||||||
|
apiFilters.byCreatedBefore = formatDateToISO(filters.value.byCreatedBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.byResponseTimeExceeded) {
|
||||||
|
apiFilters.byResponseTimeExceeded = "true";
|
||||||
|
}
|
||||||
|
if (filters.value.byMyTickets) {
|
||||||
|
apiFilters.byMyTickets = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("filters-changed", apiFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFilters = (): void => {
|
||||||
|
filters.value = {
|
||||||
|
byCurrentState: [],
|
||||||
|
byCurrentStateEmergency: [],
|
||||||
|
byCreatedAfter: "",
|
||||||
|
byCreatedBefore: "",
|
||||||
|
byResponseTimeExceeded: false,
|
||||||
|
byMyTickets: false,
|
||||||
|
};
|
||||||
|
selectedPersons.value = [];
|
||||||
|
selectedMotives.value = [];
|
||||||
|
selectedMotive.value = undefined;
|
||||||
|
applyFilters();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResponseTimeExceededChange = (): void => {
|
||||||
|
if (filters.value.byResponseTimeExceeded) {
|
||||||
|
filters.value.byCurrentState = [];
|
||||||
|
filters.value.byCreatedBefore = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Charger les données disponibles si nécessaire
|
||||||
|
onMounted(() => {
|
||||||
|
// Ici vous pourriez faire des appels API pour charger les personnes et motifs disponibles
|
||||||
|
// si ils ne sont pas fournis en props
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge .btn-close {
|
||||||
|
font-size: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.form-check-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
@ -80,12 +80,12 @@ import { Person } from "ChillPersonAssets/types";
|
|||||||
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
|
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import PersonComponent from "./Person/PersonComponent.vue";
|
import PersonComponent from "../../TicketApp/components/Person/PersonComponent.vue";
|
||||||
import MotiveComponent from "./Motive/MotiveComponent.vue";
|
import MotiveComponent from "../../TicketApp/components/Motive/MotiveComponent.vue";
|
||||||
import CommentComponent from "./Comment/CommentComponent.vue";
|
import CommentComponent from "../../TicketApp/components/Comment/CommentComponent.vue";
|
||||||
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
import AddresseeComponent from "../../TicketApp/components/Addressee/AddresseeComponent.vue";
|
||||||
import StateComponent from "./State/StateComponent.vue";
|
import StateComponent from "../../TicketApp/components/State/StateComponent.vue";
|
||||||
import EmergencyComponent from "./Emergency/EmergencyComponent.vue";
|
import EmergencyComponent from "../../TicketApp/components/Emergency/EmergencyComponent.vue";
|
||||||
|
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||||
|
|
||||||
@ -104,8 +104,9 @@ import {
|
|||||||
CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE,
|
CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE,
|
||||||
CHILL_TICKET_TICKET_HISTORY_SET_CALLER,
|
CHILL_TICKET_TICKET_HISTORY_SET_CALLER,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
defineProps<{ history: TicketHistoryLine[] }>();
|
|
||||||
|
|
||||||
|
const props = defineProps<{ history?: TicketHistoryLine[] }>();
|
||||||
|
const history = props.history ?? [];
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const actionIcons = ref(store.getters.getActionIcons);
|
const actionIcons = ref(store.getters.getActionIcons);
|
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ticket-list-container">
|
||||||
|
<div
|
||||||
|
v-if="tickets.length === 0"
|
||||||
|
class="chill-no-data-statement d-flex justify-content-center align-items-center"
|
||||||
|
>
|
||||||
|
<h3 class="text-muted fst-italic display-4">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_NO_TICKETS) }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
ref="ticketList"
|
||||||
|
style="overflow-y: scroll; flex: 1; min-height: 0; max-height: 100%"
|
||||||
|
>
|
||||||
|
<TicketListItemComponent
|
||||||
|
v-for="ticket in tickets"
|
||||||
|
:key="ticket.id"
|
||||||
|
:ticket="ticket"
|
||||||
|
@view-ticket="handleViewTicket"
|
||||||
|
@edit-ticket="handleEditTicket"
|
||||||
|
/>
|
||||||
|
<!-- Bouton pour charger plus de tickets -->
|
||||||
|
<div
|
||||||
|
v-if="hasMoreTickets"
|
||||||
|
class="text-center py-3"
|
||||||
|
style="margin: 12px 0"
|
||||||
|
>
|
||||||
|
<button class="btn btn-outline-primary" @click="loadMoreTickets">
|
||||||
|
{{ trans(CHILL_TICKET_LIST_LOAD_MORE) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-if="selectedTicketId !== null && ticketDetails"
|
||||||
|
:show="showTicketHistoryModal"
|
||||||
|
modal-dialog-class="modal-xl"
|
||||||
|
@close="closeHistoryModal"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="modal-title">
|
||||||
|
{{ getTicketTitle(ticketDetails) }}
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<ticket-history-list-component
|
||||||
|
v-if="ticketDetails.history.length > 0"
|
||||||
|
:history="ticketDetails.history"
|
||||||
|
/>
|
||||||
|
<div v-else class="text-center p-4">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">{{
|
||||||
|
trans(CHILL_TICKET_LIST_LOADING_TICKET_DETAILS)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
class="btn btn-edit"
|
||||||
|
@click="handleEditTicket(selectedTicketId)"
|
||||||
|
>
|
||||||
|
{{ trans(EDIT) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { TicketSimple, Ticket } from "../../../types";
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import TicketListItemComponent from "./TicketListItemComponent.vue";
|
||||||
|
import TicketHistoryListComponent from "./TicketHistoryListComponent.vue";
|
||||||
|
import Modal from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { getTicketTitle } from "../../TicketApp/utils/utils";
|
||||||
|
import { localizedUrl } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
EDIT,
|
||||||
|
CHILL_TICKET_LIST_NO_TICKETS,
|
||||||
|
CHILL_TICKET_LIST_LOAD_MORE,
|
||||||
|
CHILL_TICKET_LIST_LOADING_TICKET_DETAILS,
|
||||||
|
} from "translator";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tickets: TicketSimple[];
|
||||||
|
hasMoreTickets: boolean;
|
||||||
|
title: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
fetchNextPage: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const selectedTicketId = ref<number | null>(null);
|
||||||
|
const showTicketHistoryModal = ref(false);
|
||||||
|
const ticketDetails = computed(
|
||||||
|
() => store.getters.getTicketDetails as Ticket | null,
|
||||||
|
);
|
||||||
|
|
||||||
|
function closeHistoryModal() {
|
||||||
|
showTicketHistoryModal.value = false;
|
||||||
|
selectedTicketId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleViewTicket(ticketId: number) {
|
||||||
|
await store.dispatch("fetchTicketDetails", ticketId);
|
||||||
|
|
||||||
|
selectedTicketId.value = ticketId;
|
||||||
|
showTicketHistoryModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditTicket(ticketId: number) {
|
||||||
|
const returnPath = localizedUrl(`/ticket/ticket/list`);
|
||||||
|
window.location.href = localizedUrl(
|
||||||
|
`/ticket/ticket/${ticketId}/edit?returnPath=${returnPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMoreTickets() {
|
||||||
|
emit("fetchNextPage");
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Nettoyage si nécessaire
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card rounded-4 mb-3">
|
<div class="card mb-3 text-primary border-primary">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="wrap-header">
|
<div class="wrap-header">
|
||||||
<div class="wh-row d-flex justify-content-between align-items-center">
|
<div class="wh-row d-flex justify-content-between align-items-center">
|
||||||
<div class="wh-col">
|
<div class="wh-col">
|
||||||
<span
|
<span
|
||||||
v-if="ticket.currentMotive"
|
|
||||||
class="h2"
|
class="h2"
|
||||||
style="color: var(--bs-chill-blue); font-variant: all-small-caps"
|
style="color: var(--bs-chill-blue); font-variant: all-small-caps"
|
||||||
>
|
>
|
||||||
@ -44,7 +43,7 @@
|
|||||||
<div class="wrap-list">
|
<div class="wrap-list">
|
||||||
<div class="wl-row">
|
<div class="wl-row">
|
||||||
<div class="wl-col title text-end">
|
<div class="wl-col title text-end">
|
||||||
<h3>Attribué à</h3>
|
<h3>{{ trans(CHILL_TICKET_LIST_ADDRESSEES) }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="wl-col list">
|
<div class="wl-col list">
|
||||||
<addressee-component :addressees="ticket.currentAddressees" />
|
<addressee-component :addressees="ticket.currentAddressees" />
|
||||||
@ -52,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="wl-row">
|
<div class="wl-row">
|
||||||
<div class="wl-col title text-end">
|
<div class="wl-col title text-end">
|
||||||
<h3>Patients concernés</h3>
|
<h3>{{ trans(CHILL_TICKET_LIST_PERSONS) }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="wl-col list">
|
<div class="wl-col list">
|
||||||
<person-component :entities="ticket.currentPersons" />
|
<person-component :entities="ticket.currentPersons" />
|
||||||
@ -60,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="wl-row">
|
<div class="wl-row">
|
||||||
<div class="wl-col title text-end">
|
<div class="wl-col title text-end">
|
||||||
<h3>Appelants</h3>
|
<h3>{{ trans(CHILL_TICKET_LIST_CALLERS) }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="wl-col list">
|
<div class="wl-col list">
|
||||||
<person-component
|
<person-component
|
||||||
@ -109,13 +108,21 @@ import {
|
|||||||
getSinceCreated,
|
getSinceCreated,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
getTicketTitle,
|
getTicketTitle,
|
||||||
} from "../utils/utils";
|
} from "../../TicketApp/utils/utils";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import EmergencyComponent from "./Emergency/EmergencyComponent.vue";
|
import EmergencyComponent from "../../TicketApp/components/Emergency/EmergencyComponent.vue";
|
||||||
import StateComponent from "./State/StateComponent.vue";
|
import StateComponent from "../../TicketApp/components/State/StateComponent.vue";
|
||||||
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
import AddresseeComponent from "../../TicketApp/components/Addressee/AddresseeComponent.vue";
|
||||||
import PersonComponent from "./Person/PersonComponent.vue";
|
import PersonComponent from "../../TicketApp/components/Person/PersonComponent.vue";
|
||||||
|
|
||||||
|
// Translation
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
CHILL_TICKET_LIST_ADDRESSEES,
|
||||||
|
CHILL_TICKET_LIST_PERSONS,
|
||||||
|
CHILL_TICKET_LIST_CALLERS,
|
||||||
|
} from "translator";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
ticket: TicketSimple;
|
ticket: TicketSimple;
|
@ -0,0 +1,15 @@
|
|||||||
|
import App from "./App.vue";
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import { store } from "../TicketApp/store";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _app = createApp({
|
||||||
|
template: "<app></app>",
|
||||||
|
});
|
||||||
|
|
||||||
|
_app.component("app", App).use(store).mount("#ticketList");
|
@ -1,95 +1,20 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
|
||||||
{% block title 'chill_ticket.list.title'|trans %}
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('vue_ticket_list') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.title = "{{ 'chill_ticket.list.title'|trans|escape('js') }}";
|
||||||
|
</script>
|
||||||
|
{{ encore_entry_script_tags('vue_ticket_list') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ block('title') }}</h1>
|
<div id="ticketList"></div>
|
||||||
|
|
||||||
{{ filter|chill_render_filter_order_helper }}
|
|
||||||
|
|
||||||
{% if tickets|length == 0 %}
|
|
||||||
<p class="chill-no-data-statement">No tickets</p>
|
|
||||||
{% else %}
|
|
||||||
<div class="flex-table">
|
|
||||||
{% for ticket in tickets %}
|
|
||||||
<div class="item-bloc">
|
|
||||||
<div class="item-row">
|
|
||||||
<div class="wrap-header">
|
|
||||||
<div class="wh-row">
|
|
||||||
<div class="wh-col">
|
|
||||||
{% if ticket.motive is not null %}
|
|
||||||
<span class="h2" style="color: var(--bs-chill-blue); font-variant: all-small-caps">
|
|
||||||
{{ ticket.motive.label|localize_translatable_string }}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="h3" style="color: var(--bs-chill-blue); font-style: italic">Sans motif</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="wh-col">
|
|
||||||
<p style="font-size: 1.5rem;"><span class="badge text-bg-chill-green text-white">Ouvert</span></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wh-row">
|
|
||||||
<div class="wh-col">
|
|
||||||
#{{ ticket.id }}
|
|
||||||
</div>
|
|
||||||
<div class="wh-col">
|
|
||||||
{% if ticket.createdAt is not null %}
|
|
||||||
<span title="{{ ticket.createdAt|format_datetime('long', 'long') }}" style="font-style: italic;">{{ ticket.createdAt|ago|capitalize }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if ticket.persons|length > 0 or ticket.currentAddressee|length > 0 %}
|
|
||||||
<div class="item-row separator">
|
|
||||||
<div class="wrap-list">
|
|
||||||
{% if ticket.persons|length > 0 %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3 class="mb-2">Patients concernés</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for p in ticket.persons %}
|
|
||||||
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
|
||||||
targetEntity: { name: 'person', id: p.id },
|
|
||||||
action: 'show',
|
|
||||||
displayBadge: true,
|
|
||||||
buttonText: p|chill_entity_render_string,
|
|
||||||
isDead: p.deathdate is not null
|
|
||||||
} %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if ticket.currentAddressee|length > 0 %}
|
|
||||||
<div class="wl-row">
|
|
||||||
<div class="wl-col title"><h3 class="mb-2">Attribué à</h3></div>
|
|
||||||
<div class="wl-col list">
|
|
||||||
{% for d in ticket.currentAddressee %}
|
|
||||||
{% if d.isUser is defined and d.isUser %}
|
|
||||||
<span class="badge-user">
|
|
||||||
{{ d|chill_entity_render_box }}
|
|
||||||
</span>
|
|
||||||
{% elseif d.isUserGroup is defined and d.isUserGroup %}
|
|
||||||
{{ d|chill_entity_render_box() }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="item-row separator">
|
|
||||||
<ul class="record_actions">
|
|
||||||
<li>
|
|
||||||
<a class="btn btn-update" href="{{ chill_path_add_return_path('chill_ticket_ticket_edit', {'id': ticket.id}) }}"></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li>
|
<li>
|
||||||
|
@ -3,10 +3,35 @@ chill_ticket:
|
|||||||
title: Tickets
|
title: Tickets
|
||||||
title_previous_tickets: "{name, select, other {Précédent ticket de {name}} undefined {Précédent ticket}}"
|
title_previous_tickets: "{name, select, other {Précédent ticket de {name}} undefined {Précédent ticket}}"
|
||||||
no_tickets: "Aucun ticket"
|
no_tickets: "Aucun ticket"
|
||||||
|
loading_ticket: "Chargement des tickets..."
|
||||||
|
loading_ticket_details: "Chargement de l'historique du ticket..."
|
||||||
|
load_more: "Voir plus..."
|
||||||
|
addressees: "Attribué à"
|
||||||
|
persons: "Usager concernés"
|
||||||
|
callers: "Appelants"
|
||||||
filter:
|
filter:
|
||||||
to_me: Tickets qui me sont attribués
|
to_me: Tickets qui me sont attribués
|
||||||
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
|
||||||
|
state_change: État actuel
|
||||||
|
title: "Filtres des tickets"
|
||||||
|
persons_concerned: "Usager concernés"
|
||||||
|
by_person: "Par personne"
|
||||||
|
by_motives: "Par motifs"
|
||||||
|
current_state: "État actuel"
|
||||||
|
open: "Ouvert"
|
||||||
|
closed: "Fermé"
|
||||||
|
emergency: "Urgent"
|
||||||
|
no_emergency: "Non urgent"
|
||||||
|
created_after: "Créé après"
|
||||||
|
created_before: "Créé avant"
|
||||||
|
response_time_exceeded: "Temps de réponse dépassé"
|
||||||
|
response_time_warning: 'Attention : Ce filtre supprime automatiquement les filtres "État actuel" et "Créé avant"'
|
||||||
|
reset: "Réinitialiser"
|
||||||
|
apply: "Appliquer les filtres"
|
||||||
|
remove: "Supprimer"
|
||||||
|
result: "{count, plural, =0 {Aucun résultat} =1 {resultat} other {resultats}}"
|
||||||
|
|
||||||
ticket:
|
ticket:
|
||||||
history:
|
history:
|
||||||
add_comment: "Nouveau commentaire"
|
add_comment: "Nouveau commentaire"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user