mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-08-01 13:37:46 +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 interface Pagination {
|
||||
first: number;
|
||||
items_per_page: number;
|
||||
more: boolean;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
}
|
||||
|
||||
export interface PaginationResponse<T> {
|
||||
pagination: {
|
||||
more: boolean;
|
||||
items_per_page: number;
|
||||
};
|
||||
pagination: Pagination;
|
||||
results: T[];
|
||||
count: number;
|
||||
}
|
||||
|
@ -8,6 +8,26 @@ import { TranslatableString } from "ChillMainAssets/types";
|
||||
* @param locale defaults to browser locale
|
||||
* @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(
|
||||
translatableString: TranslatableString | null | undefined,
|
||||
locale?: string,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="grey-card">
|
||||
<ul :class="listClasses" v-if="picked.length > 0 && displayPicked">
|
||||
<div class="grey-card p-2">
|
||||
<ul :class="listClasses" v-if="displayPicked">
|
||||
<li
|
||||
v-for="p in picked"
|
||||
@click="removeEntity(p)"
|
||||
@ -21,14 +21,15 @@
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="record_actions">
|
||||
<ul class="record_actions mb-0">
|
||||
<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
|
||||
:checked="isMePicked"
|
||||
ref="itsMeCheckbox"
|
||||
:type="multiple ? 'checkbox' : 'radio'"
|
||||
@change="selectItsMe($event as InputEvent)"
|
||||
style="margin: 0"
|
||||
/>
|
||||
{{ trans(USER_CURRENT_USER) }}
|
||||
</label>
|
||||
@ -45,11 +46,15 @@
|
||||
</li>
|
||||
</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
|
||||
v-for="s in suggested"
|
||||
:key="s.type + s.id"
|
||||
@click="addNewSuggested(s)"
|
||||
style="margin: 0"
|
||||
>
|
||||
<span :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
|
||||
{{ s.text }}
|
||||
@ -221,8 +226,6 @@ function getBadgeStyle(entities: Entities) {
|
||||
.grey-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.btn-check:checked + .btn,
|
||||
@ -242,6 +245,7 @@ ul.badge-suggest {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0px;
|
||||
min-height: 30px;
|
||||
}
|
||||
ul.badge-suggest li > span {
|
||||
white-space: normal;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<html lang="{{ app.request.locale }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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) {
|
||||
encore.addEntry('page_ticket', __dirname + '/src/Resources/public/page/ticket/index.ts');
|
||||
encore.addEntry('vue_ticket_app', __dirname + '/src/Resources/public/vuejs/TicketApp/index.ts');
|
||||
module.exports = function (encore, entries) {
|
||||
encore.addEntry(
|
||||
"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;
|
||||
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
|
||||
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 BannerComponent from "./components/BannerComponent.vue";
|
||||
|
||||
|
@ -1,68 +1,39 @@
|
||||
<template>
|
||||
<div class="col-12">
|
||||
<span
|
||||
v-for="userGroup in userGroupLevels"
|
||||
:key="userGroup.id"
|
||||
class="badge-user-group"
|
||||
:style="`background-color: ${userGroup.backgroundColor}; color: ${userGroup.foregroundColor};`"
|
||||
>
|
||||
{{ userGroup.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>
|
||||
<ul class="addressees-list" v-if="addressees.length > 0">
|
||||
<li v-for="addressee in addressees" :key="addressee.id">
|
||||
<span
|
||||
v-if="addressee.type === 'user_group'"
|
||||
class="badge-user-group"
|
||||
:style="`background-color: ${addressee.backgroundColor}; color: ${addressee.foregroundColor};`"
|
||||
>
|
||||
{{ addressee.label.fr }}
|
||||
</span>
|
||||
<span v-else-if="addressee.type === 'user'" class="badge-user">
|
||||
<user-render-box-badge :user="addressee"
|
||||
/></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
// Components
|
||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||
|
||||
// Types
|
||||
import {
|
||||
User,
|
||||
UserGroup,
|
||||
UserGroupOrUser,
|
||||
} from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import { UserGroupOrUser } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||
|
||||
const props = 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[],
|
||||
);
|
||||
defineProps<{ addressees: UserGroupOrUser[] }>();
|
||||
</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,20 +1,16 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<pick-entity
|
||||
uniqid="ticket-addressee-selector"
|
||||
:types="['user', 'user_group', 'thirdparty']"
|
||||
:picked="selectedEntities"
|
||||
:suggested="suggestedValues"
|
||||
: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>
|
||||
</div>
|
||||
<pick-entity
|
||||
uniqid="ticket-addressee-selector"
|
||||
:types="['user', 'user_group']"
|
||||
:picked="selectedEntities"
|
||||
:suggested="suggestedValues"
|
||||
: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"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -24,7 +20,7 @@ import { ref, watch, defineProps, defineEmits } from "vue";
|
||||
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
||||
|
||||
// Types
|
||||
import { Entities } from "ChillPersonAssets/types";
|
||||
import { Entities, EntitiesOrMe } from "ChillPersonAssets/types";
|
||||
|
||||
// Translations
|
||||
import {
|
||||
@ -70,12 +66,12 @@ watch(
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
function addNewEntity({ entity }: { entity: Entities }) {
|
||||
selectedEntities.value.push(entity);
|
||||
function addNewEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||
selectedEntities.value.push(entity as Entities);
|
||||
emit("update:modelValue", selectedEntities.value);
|
||||
}
|
||||
|
||||
function removeEntity({ entity }: { entity: Entities }) {
|
||||
function removeEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||
const index = selectedEntities.value.findIndex(
|
||||
(selectedEntity) => selectedEntity === entity,
|
||||
);
|
||||
|
@ -14,7 +14,10 @@
|
||||
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="d-flex justify-content-end">
|
||||
<emergency-toggle-component />
|
||||
<emergency-toggle-component
|
||||
v-model="isEmergencyLocal"
|
||||
@toggle-emergency="handleEmergencyToggle"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="badge text-bg-chill-green text-white"
|
||||
@ -100,6 +103,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
|
||||
// Components
|
||||
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
||||
@ -107,7 +111,8 @@ import EmergencyToggleComponent from "./Emergency/EmergencyToggleComponent.vue";
|
||||
import PersonComponent from "./Person/PersonComponent.vue";
|
||||
|
||||
// Types
|
||||
import { Ticket } from "../../../types";
|
||||
import { Ticket, TicketEmergencyState } from "../../../types";
|
||||
import { Person } from "ChillPersonAssets/types";
|
||||
|
||||
import {
|
||||
trans,
|
||||
@ -121,7 +126,8 @@ import {
|
||||
|
||||
// Store
|
||||
import { useStore } from "vuex";
|
||||
import { Person } from "ChillPersonAssets/types";
|
||||
|
||||
// Utils
|
||||
import { getTicketTitle } from "../utils/utils";
|
||||
|
||||
defineProps<{
|
||||
@ -129,6 +135,7 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const store = useStore();
|
||||
const toast = useToast();
|
||||
const today = ref(new Date());
|
||||
|
||||
setInterval(() => {
|
||||
@ -136,7 +143,21 @@ setInterval(() => {
|
||||
}, 5000);
|
||||
|
||||
const isOpen = computed(() => store.getters.isOpen);
|
||||
const isEmergencyLocal = computed(() => store.getters.isEmergency);
|
||||
const since = computed(() => {
|
||||
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>
|
||||
|
@ -3,8 +3,8 @@
|
||||
<button
|
||||
class="badge rounded-pill me-1"
|
||||
:class="{
|
||||
'bg-danger': isEmergency,
|
||||
'bg-secondary': !isEmergency,
|
||||
'bg-danger': modelValue,
|
||||
'bg-secondary': !modelValue,
|
||||
}"
|
||||
@click="toggleEmergency"
|
||||
>
|
||||
@ -14,29 +14,25 @@
|
||||
</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";
|
||||
import { TicketEmergencyState } from "../../../../types";
|
||||
|
||||
const store = useStore();
|
||||
const toast = useToast();
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const isEmergency = computed(() => store.getters.isEmergency);
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
"toggle-emergency": [emergency: TicketEmergencyState];
|
||||
}>();
|
||||
|
||||
// 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" });
|
||||
}
|
||||
});
|
||||
const newValue = !props.modelValue;
|
||||
emit("update:modelValue", newValue);
|
||||
emit("toggle-emergency", newValue ? "yes" : "no");
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
||||
:options="motives"
|
||||
v-model="motive"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,7 +11,6 @@
|
||||
></on-the-fly>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="text-muted">Aucune personne concernée</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -20,10 +20,10 @@ import { ref, watch, defineProps, defineEmits, computed } from "vue";
|
||||
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
||||
|
||||
// Types
|
||||
import { Entities, EntityType } from "ChillPersonAssets/types";
|
||||
import { Entities, EntitiesOrMe, EntityType } from "ChillPersonAssets/types";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Entities[] | Entities | null;
|
||||
modelValue: EntitiesOrMe[] | EntitiesOrMe | null;
|
||||
suggested: Entities[];
|
||||
multiple: boolean;
|
||||
types: EntityType[];
|
||||
@ -31,15 +31,13 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: Entities[] | Entities | null];
|
||||
"update:modelValue": [value: EntitiesOrMe[] | EntitiesOrMe | null];
|
||||
}>();
|
||||
|
||||
// Valeurs par défaut
|
||||
const multiple = props.multiple;
|
||||
const types = props.types;
|
||||
const label = props.label;
|
||||
|
||||
// État local
|
||||
const selectedEntities = ref<Entities[]>(
|
||||
multiple
|
||||
? [...((props.modelValue as Entities[]) || [])]
|
||||
@ -88,17 +86,17 @@ watch(
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
function addNewEntity({ entity }: { entity: Entities }) {
|
||||
function addNewEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||
if (multiple) {
|
||||
selectedEntities.value.push(entity);
|
||||
selectedEntities.value.push(entity as Entities);
|
||||
emit("update:modelValue", selectedEntities.value);
|
||||
} else {
|
||||
selectedEntities.value = [entity];
|
||||
selectedEntities.value = [entity as Entities];
|
||||
emit("update:modelValue", entity);
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntity({ entity }: { entity: Entities }) {
|
||||
function removeEntity({ entity }: { entity: EntitiesOrMe }) {
|
||||
if (multiple) {
|
||||
const index = selectedEntities.value.findIndex(
|
||||
(selectedEntity) => selectedEntity === entity,
|
||||
|
@ -36,51 +36,14 @@
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<TicketListComponent
|
||||
<ticket-list-component
|
||||
:tickets="previousTickets"
|
||||
:title="trans(CHILL_TICKET_LIST_NO_TICKETS)"
|
||||
@view-ticket="handleViewTicket"
|
||||
@edit-ticket="handleEditTicket"
|
||||
/>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -90,23 +53,22 @@ import { useStore } from "vuex";
|
||||
|
||||
// Components
|
||||
import Modal from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_components/Modal.vue";
|
||||
import TicketListComponent from "./TicketListComponent.vue";
|
||||
import TicketHistoryListComponent from "./TicketHistoryListComponent.vue";
|
||||
import TicketListComponent from "../../TicketList/components/TicketListComponent.vue";
|
||||
|
||||
// Translations
|
||||
import {
|
||||
trans,
|
||||
CHILL_TICKET_LIST_NO_TICKETS,
|
||||
CHILL_TICKET_TICKET_PREVIOUS_TICKETS,
|
||||
CHILL_TICKET_LIST_TITLE_PREVIOUS_TICKETS,
|
||||
EDIT,
|
||||
} from "translator";
|
||||
|
||||
// Types
|
||||
import { Person } from "ChillPersonAssets/types";
|
||||
import { TicketHistoryLine, TicketSimple } from "../../../types";
|
||||
import { TicketSimple } from "../../../types";
|
||||
|
||||
// Utils
|
||||
import { getTicketTitle } from "../utils/utils";
|
||||
import { localizedUrl } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
|
||||
const store = useStore();
|
||||
const showPreviousTicketModal = ref(false);
|
||||
@ -117,18 +79,14 @@ const currentPersons = computed(
|
||||
() => store.getters.getCurrentPersons as Person[],
|
||||
);
|
||||
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 () => {
|
||||
try {
|
||||
await store.dispatch("fetchTicketsByPerson");
|
||||
await store.dispatch("fetchTicketList", {
|
||||
byPerson: currentPersons.value.map((person) => person.id),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des tickets:", error);
|
||||
}
|
||||
@ -147,19 +105,17 @@ async function handleViewTicket(ticketId: number) {
|
||||
showTicketHistoryModal.value = true;
|
||||
|
||||
try {
|
||||
await store.dispatch("fetchPreviousTicketDetails", ticketId);
|
||||
await store.dispatch("fetchTicket", ticketId);
|
||||
} 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) {
|
||||
window.location.href = `/fr/ticket/ticket/${ticketId}/edit`;
|
||||
}
|
||||
|
||||
function closeHistoryModal() {
|
||||
showTicketHistoryModal.value = false;
|
||||
selectedTicketId.value = null;
|
||||
const returnPath = localizedUrl(`/ticket/ticket/list`);
|
||||
window.location.href = localizedUrl(
|
||||
`/ticket/ticket/${ticketId}/edit?returnPath=${returnPath}`,
|
||||
);
|
||||
}
|
||||
</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 AddresseeStates, moduleAddressee } from "./modules/addressee";
|
||||
import { State as PersonsState, modulePersons } from "./modules/persons";
|
||||
import {
|
||||
State as TicketListState,
|
||||
moduleTicketList,
|
||||
} from "./modules/ticket_list";
|
||||
|
||||
export interface RootState {
|
||||
motive: MotiveStates;
|
||||
@ -11,6 +15,7 @@ export interface RootState {
|
||||
comment: CommentStates;
|
||||
addressee: AddresseeStates;
|
||||
persons: PersonsState;
|
||||
ticketList: TicketListState;
|
||||
}
|
||||
|
||||
export const store = createStore<RootState>({
|
||||
@ -20,5 +25,6 @@ export const store = createStore<RootState>({
|
||||
comment: moduleComment,
|
||||
addressee: moduleAddressee,
|
||||
persons: modulePersons,
|
||||
ticketList: moduleTicketList,
|
||||
},
|
||||
});
|
||||
|
@ -1,23 +1,19 @@
|
||||
import { Module } from "vuex";
|
||||
import { RootState } from "..";
|
||||
|
||||
import { Ticket, TicketEmergencyState, TicketSimple } from "../../../../types";
|
||||
import { Ticket, TicketEmergencyState } from "../../../../types";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||
import { getSinceCreated } from "../../utils/utils";
|
||||
|
||||
export interface State {
|
||||
ticket: Ticket;
|
||||
previous_tickets: TicketSimple[];
|
||||
previous_ticket_details: Ticket;
|
||||
action_icons: object;
|
||||
}
|
||||
|
||||
export const moduleTicket: Module<State, RootState> = {
|
||||
state: () => ({
|
||||
ticket: {} as Ticket,
|
||||
previous_tickets: [] as TicketSimple[],
|
||||
previous_ticket_details: {} as Ticket,
|
||||
action_icons: {
|
||||
add_person: "fa fa-user-plus",
|
||||
add_comment: "fa fa-comment",
|
||||
@ -42,21 +38,6 @@ export const moduleTicket: Module<State, RootState> = {
|
||||
);
|
||||
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) {
|
||||
return state.action_icons;
|
||||
},
|
||||
@ -66,18 +47,18 @@ export const moduleTicket: Module<State, RootState> = {
|
||||
}
|
||||
return getSinceCreated(state.ticket.createdAt.datetime, currentTime);
|
||||
},
|
||||
getTicketHistory: (state) => {
|
||||
return state.ticket.history;
|
||||
},
|
||||
getCurrentPersons(state) {
|
||||
return state.ticket.currentPersons;
|
||||
},
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setTicket(state, ticket: Ticket) {
|
||||
state.ticket = ticket;
|
||||
},
|
||||
setPreviousTickets(state, previousTickets: TicketSimple[]) {
|
||||
state.previous_tickets = previousTickets;
|
||||
},
|
||||
setPreviousTicketDetails(state, ticket: Ticket) {
|
||||
state.previous_ticket_details = ticket;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async closeTicket({ commit, state }) {
|
||||
@ -116,39 +97,5 @@ export const moduleTicket: Module<State, RootState> = {
|
||||
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";
|
||||
|
||||
// Components
|
||||
import PersonComponent from "./Person/PersonComponent.vue";
|
||||
import MotiveComponent from "./Motive/MotiveComponent.vue";
|
||||
import CommentComponent from "./Comment/CommentComponent.vue";
|
||||
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
||||
import StateComponent from "./State/StateComponent.vue";
|
||||
import EmergencyComponent from "./Emergency/EmergencyComponent.vue";
|
||||
import PersonComponent from "../../TicketApp/components/Person/PersonComponent.vue";
|
||||
import MotiveComponent from "../../TicketApp/components/Motive/MotiveComponent.vue";
|
||||
import CommentComponent from "../../TicketApp/components/Comment/CommentComponent.vue";
|
||||
import AddresseeComponent from "../../TicketApp/components/Addressee/AddresseeComponent.vue";
|
||||
import StateComponent from "../../TicketApp/components/State/StateComponent.vue";
|
||||
import EmergencyComponent from "../../TicketApp/components/Emergency/EmergencyComponent.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_SET_CALLER,
|
||||
} from "translator";
|
||||
defineProps<{ history: TicketHistoryLine[] }>();
|
||||
|
||||
const props = defineProps<{ history?: TicketHistoryLine[] }>();
|
||||
const history = props.history ?? [];
|
||||
const store = useStore();
|
||||
|
||||
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>
|
||||
<div class="card rounded-4 mb-3">
|
||||
<div class="card mb-3 text-primary border-primary">
|
||||
<div class="card-body">
|
||||
<div class="wrap-header">
|
||||
<div class="wh-row d-flex justify-content-between align-items-center">
|
||||
<div class="wh-col">
|
||||
<span
|
||||
v-if="ticket.currentMotive"
|
||||
class="h2"
|
||||
style="color: var(--bs-chill-blue); font-variant: all-small-caps"
|
||||
>
|
||||
@ -44,7 +43,7 @@
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title text-end">
|
||||
<h3>Attribué à</h3>
|
||||
<h3>{{ trans(CHILL_TICKET_LIST_ADDRESSEES) }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<addressee-component :addressees="ticket.currentAddressees" />
|
||||
@ -52,7 +51,7 @@
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title text-end">
|
||||
<h3>Patients concernés</h3>
|
||||
<h3>{{ trans(CHILL_TICKET_LIST_PERSONS) }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<person-component :entities="ticket.currentPersons" />
|
||||
@ -60,7 +59,7 @@
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title text-end">
|
||||
<h3>Appelants</h3>
|
||||
<h3>{{ trans(CHILL_TICKET_LIST_CALLERS) }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<person-component
|
||||
@ -109,13 +108,21 @@ import {
|
||||
getSinceCreated,
|
||||
formatDateTime,
|
||||
getTicketTitle,
|
||||
} from "../utils/utils";
|
||||
} from "../../TicketApp/utils/utils";
|
||||
|
||||
// Components
|
||||
import EmergencyComponent from "./Emergency/EmergencyComponent.vue";
|
||||
import StateComponent from "./State/StateComponent.vue";
|
||||
import AddresseeComponent from "./Addressee/AddresseeComponent.vue";
|
||||
import PersonComponent from "./Person/PersonComponent.vue";
|
||||
import EmergencyComponent from "../../TicketApp/components/Emergency/EmergencyComponent.vue";
|
||||
import StateComponent from "../../TicketApp/components/State/StateComponent.vue";
|
||||
import AddresseeComponent from "../../TicketApp/components/Addressee/AddresseeComponent.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<{
|
||||
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' %}
|
||||
|
||||
{% 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 %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{{ 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 %}
|
||||
<div id="ticketList"></div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li>
|
||||
|
@ -3,10 +3,35 @@ chill_ticket:
|
||||
title: Tickets
|
||||
title_previous_tickets: "{name, select, other {Précédent ticket de {name}} undefined {Précédent 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:
|
||||
to_me: Tickets qui me sont attribués
|
||||
in_alert: Tickets en alerte (délai de résolution dépassé)
|
||||
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:
|
||||
history:
|
||||
add_comment: "Nouveau commentaire"
|
||||
|
Loading…
x
Reference in New Issue
Block a user