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:
Julien Fastré 2025-07-18 16:06:17 +00:00
commit fe2eba3b29
28 changed files with 1068 additions and 388 deletions

View File

@ -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;
}

View File

@ -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,

View File

@ -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;

View File

@ -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">

View File

@ -0,0 +1,3 @@
{
"idf.pythonInstallPath": "/usr/bin/python3"
}

View File

@ -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",
);
};

View File

@ -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;
}

View File

@ -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";

View File

@ -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>

View File

@ -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,
);

View File

@ -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>

View File

@ -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>

View File

@ -16,7 +16,6 @@
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
:options="motives"
v-model="motive"
class="mb-4"
/>
</div>
</div>

View File

@ -11,7 +11,6 @@
></on-the-fly>
</li>
</ul>
<div v-else class="text-muted">Aucune personne concernée</div>
</div>
</template>

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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,
},
});

View File

@ -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;
}
},
},
};

View File

@ -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,
);
}
},
},
};

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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;

View File

@ -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");

View File

@ -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>

View File

@ -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"