Merge branch '1826-1891-1858-mobile-layout-fix-peloton-display-change-comment-display' into 'ticket-app-master'

Fix mobile layout, update Peloton display, and change comment display

See merge request Chill-Projet/chill-bundles!933
This commit is contained in:
2025-12-05 16:27:12 +00:00
13 changed files with 230 additions and 151 deletions

View File

@@ -267,6 +267,7 @@ async function is_object_ready(
export {
build_convert_link,
build_wopi_editor_link,
download_info_link,
download_and_decrypt_doc,
download_doc,
download_doc_as_pdf,

View File

@@ -3,17 +3,10 @@
<div class="container-xxl pt-1" style="padding-bottom: 55px" v-if="!loading">
<div class="row">
<div class="col">
<div class="form-check">
<input
v-model="showOnlyHistoryComments"
class="form-check-input"
type="checkbox"
id="showOnlyCommentsCheckbox"
/>
<label class="form-check-label" for="showOnlyCommentsCheckbox">
{{ trans(CHILL_TICKET_LIST_SHOW_ONLY_HISTORY_COMMENTS) }}
</label>
</div>
<label class="form-label pe-2" for="showAllHistory">
{{ trans(CHILL_TICKET_LIST_SHOW_ALL_HISTORY) }}
</label>
<toggle-component v-model="showAllHistory" id="showAllHistory" />
</div>
<div class="col d-flex justify-content-end">
<previous-tickets-component :key="refreshKey" />
@@ -24,6 +17,7 @@
:key="ticketHistory.length"
/>
</div>
<div v-else class="text-center p-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">
@@ -72,6 +66,7 @@ import TicketHistoryListComponent from "../TicketList/components/TicketHistoryLi
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
import BannerComponent from "./components/BannerComponent.vue";
import TicketInitFormComponent from "./components/TicketInitFormComponent.vue";
import ToggleComponent from "../TicketList/components/ToggleComponent.vue";
// Translations
import {
@@ -81,19 +76,19 @@ import {
CHILL_TICKET_TICKET_INIT_FORM_ERROR,
CHILL_TICKET_TICKET_INIT_FORM_WARNING,
CHILL_TICKET_LIST_LOADING_TICKET_DETAILS,
CHILL_TICKET_LIST_SHOW_ONLY_HISTORY_COMMENTS,
CHILL_TICKET_LIST_SHOW_ALL_HISTORY,
} from "translator";
const store = useStore();
const toast = useToast();
store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket);
const showOnlyHistoryComments = ref(false);
const showAllHistory = ref(false);
const ticket = computed(() => store.getters.getTicket as Ticket);
const ticketHistory = computed(() =>
showOnlyHistoryComments.value
? store.getters.getTicketHistoryComments
: store.getters.getTicketHistory,
showAllHistory.value
? store.getters.getTicketHistory
: store.getters.getTicketHistoryComments,
);
const motives = computed(() => store.getters.getMotives as Motive[]);
const suggestedPersons = computed(() => store.getters.getPersons as Person[]);
@@ -169,4 +164,7 @@ onMounted(() => {
font-size: 2rem;
color: #333;
}
.form-label {
font-weight: bold;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="fixed-bottom">
<teleport to="#actionToolbar">
<div class="footer-ticket-details" v-if="activeTab">
<div class="tab-content p-2">
<button
@@ -86,52 +86,56 @@
</div>
</div>
<div class="footer-ticket-main">
<ul class="nav nav-tabs justify-content-end">
<li v-if="hasReturnPath" class="nav-item p-2 go-back">
<a :href="returnPath" class="btn btn-cancel">Annuler</a>
</li>
<li v-else class="nav-item p-2 go-back">
<button
type="button"
class="btn btn-light"
@click="closeAllActions"
aria-label="Fermer"
title="Fermer"
>
<i class="fa fa-arrow-left"></i>
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL) }}
</button>
</li>
<li v-for="btn in actionButtons" :key="btn.key" class="nav-item p-2">
<button
type="button"
:class="`btn ${activeTab === btn.key ? 'btn-primary' : 'btn-light'}`"
@click="
activeTab === btn.key ? (activeTab = '') : (activeTab = btn.key)
"
:disabled="btn.disabled.value"
>
<i :class="actionIcons[btn.key]" />
{{ trans(btn.label) }}
</button>
</li>
<li class="nav-item p-2">
<button
type="button"
class="btn btn-light"
@click="isOpen ? closeTicket() : reopenTicket()"
>
<i :class="actionIcons['state_change']"></i>
{{
isOpen
? trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE)
: trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN)
}}
</button>
</li>
</ul>
<div class="d-flex w-100">
<ul class="nav nav-tabs flex-grow-1 justify-content-start">
<li v-if="hasReturnPath" class="nav-item p-2 go-back">
<a :href="returnPath" class="btn btn-cancel">
<span class="hide-on-sm">
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL) }}
</span>
</a>
</li>
</ul>
<ul class="nav nav-tabs flex-grow-1 justify-content-center">
<template v-for="btn in actionButtons" :key="btn.key">
<li class="nav-item p-2">
<button
type="button"
:class="`btn ${activeTab === btn.key ? 'btn-primary' : 'btn-light'}`"
@click="
activeTab === btn.key
? (activeTab = '')
: (activeTab = btn.key)
"
:disabled="btn.disabled.value"
>
<i :class="actionIcons[btn.key]" />
<span class="hide-on-sm ms-2">{{ trans(btn.label) }}</span>
</button>
</li>
</template>
</ul>
<ul class="nav nav-tabs flex-grow-1 justify-content-end">
<li class="nav-item p-2">
<button
type="button"
class="btn bg-chill-green text-white"
@click="isOpen ? closeTicket() : reopenTicket()"
>
<i :class="isOpen ? 'fa fa-lock' : 'fa fa-unlock'"></i>
<span class="hide-on-sm ms-2">
{{
isOpen
? trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE)
: trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_REOPEN)
}}
</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</teleport>
</template>
<script setup lang="ts">
@@ -365,10 +369,4 @@ div.footer-ticket-main {
div.footer-ticket-details {
background: none repeat scroll 0 0 #efe2ca;
}
.fixed-bottom {
position: sticky;
top: 0;
overflow: visible;
}
</style>

View File

@@ -2,11 +2,11 @@
<Teleport to="#header-ticket-main">
<div class="container-xxl text-primary">
<div class="row">
<div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0">
<div class="col-md-12 col-sm-12 ps-md-5 ps-xxl-0">
<div class="small text-muted">
{{ motiveHierarchyLabel(ticket.currentMotive) }}
</div>
<h1>
<h1 class="responsive-title-h1">
{{ getTicketTitle(ticket) }}
<peloton-component
:stored-objects="
@@ -15,12 +15,12 @@
/>
</h1>
<h2 v-if="ticket.caller">
<h2 v-if="ticket.caller" class="responsive-title-h2">
{{ ticket.caller.text }}
</h2>
</div>
<div class="col-md-6 col-sm-12">
<div class="col-md-12 col-sm-12">
<div class="d-flex justify-content-end">
<emergency-toggle-component
v-model="isEmergency"
@@ -45,10 +45,10 @@
<div class="container-xxl">
<div class="row justify-content-between">
<div
class="col-md-4 col-sm-12 d-flex flex-column align-items-start"
class="col-4 d-flex flex-column align-items-start"
v-if="ticket.caller"
>
<h3 class="text-primary">
<h3 class="text-primary responsive-title-h3">
{{
trans(CHILL_TICKET_TICKET_BANNER_CALLER, {
count: ticket.caller ? 1 : 0,
@@ -58,10 +58,10 @@
<person-component :entities="[ticket.caller] as Person[]" />
</div>
<div
class="col-md-4 col-sm-12 d-flex flex-column align-items-start"
class="col-4 d-flex flex-column align-items-start"
v-if="ticket.currentPersons.length"
>
<h3 class="text-primary">
<h3 class="text-primary responsive-title-h3">
{{
trans(CHILL_TICKET_TICKET_BANNER_PERSON, {
count: ticket.currentPersons.length,
@@ -71,10 +71,10 @@
<person-component :entities="ticket.currentPersons" />
</div>
<div
class="col-md-4 col-sm-12 d-flex flex-column align-items-start"
class="col-4 d-flex flex-column align-items-start"
v-if="ticket.currentAddressees.length"
>
<h3 class="text-primary">
<h3 class="text-primary responsive-title-h3">
{{
trans(CHILL_TICKET_TICKET_BANNER_SPEAKER, {
count: ticket.currentAddressees.length,

View File

@@ -1,6 +1,6 @@
<template>
<span
class="badge rounded-pill me-1 mx-2"
class="badge rounded-pill me-1"
:class="{
'bg-warning': new_emergency === 'yes',
'bg-secondary': new_emergency === 'no',

View File

@@ -93,7 +93,6 @@ import {
CHILL_TICKET_PELOTON_LOADING_DOCUMENT,
CHILL_TICKET_PELOTON_LOADING,
CHILL_TICKET_PELOTON_ERROR_LOADING,
CHILL_TICKET_PELOTON_ERROR_NOT_READY,
CHILL_TICKET_PELOTON_UNSUPPORTED_TYPE,
CHILL_TICKET_PELOTON_OPEN_NEW_TAB,
CHILL_TICKET_PELOTON_IFRAME_NOT_SUPPORTED,
@@ -101,11 +100,11 @@ import {
trans,
} from "translator";
// Type
import { StoredObject } from "ChillDocStoreAssets/types";
import { StoredObject, StoredObjectVersion } from "ChillDocStoreAssets/types";
// Utils
import {
download_and_decrypt_doc,
download_info_link,
is_object_ready,
} from "ChillDocStoreAssets/vuejs/StoredObjectButton/helpers";
@@ -156,22 +155,61 @@ async function onSelectionChange() {
try {
const document = await is_object_ready(selectedStoredObject.value);
if (!document) {
error.value = trans(CHILL_TICKET_PELOTON_ERROR_NOT_READY);
return;
}
const doc = await download_and_decrypt_doc(
const algo = "AES-CBC";
const atVersionToDownload = selectedStoredObject.value
.currentVersion as StoredObjectVersion;
const downloadInfo = await download_info_link(
selectedStoredObject.value,
selectedStoredObject.value.currentVersion,
);
console.log("downloadInfo", downloadInfo);
const rawResponse = await window.fetch(downloadInfo.url);
if (!rawResponse.ok) {
throw new Error(
"error while downloading raw file " +
rawResponse.status +
" " +
rawResponse.statusText,
);
}
let blob: Blob;
if (atVersionToDownload.iv.length === 0) {
blob = await rawResponse.blob();
} else {
const rawBuffer = await rawResponse.arrayBuffer();
try {
const key = await window.crypto.subtle.importKey(
"jwk",
atVersionToDownload.keyInfos,
{ name: algo },
false,
["decrypt"],
);
const iv = Uint8Array.from(atVersionToDownload.iv);
const decrypted = await window.crypto.subtle.decrypt(
{ name: algo, iv: iv },
key,
rawBuffer,
);
blob = new Blob([decrypted], { type: document.type });
documentUrl.value = URL.createObjectURL(blob);
} catch (e) {
console.error("encounter error while keys and decrypt operations");
console.error(e);
throw e;
}
}
const blob = new Blob([doc], { type: document.type });
documentUrl.value = URL.createObjectURL(blob);
if (document.type.startsWith("image/")) {
const mimeType = blob.type;
if (mimeType.startsWith("image/")) {
documentType.value = "image";
} else if (document.type === "application/pdf") {
} else if (mimeType === "application/pdf") {
documentType.value = "pdf";
} else {
documentType.value = "other";
@@ -183,7 +221,6 @@ async function onSelectionChange() {
isLoading.value = false;
}
}
// Nettoyer les URLs blob précédentes
function cleanupPrevious() {
if (documentUrl.value) {

View File

@@ -1,6 +1,6 @@
<template>
<span
class="badge rounded-pill me-1 mx-2"
class="badge rounded-pill me-1"
:class="{
'bg-chill-red': props.new_state == 'closed',
'bg-chill-green': props.new_state == 'open',

View File

@@ -3,6 +3,7 @@ import { createApp } from "vue";
import VueToast from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
import "./scss/reactive.scss";
import { store } from "./store";

View File

@@ -0,0 +1,42 @@
.responsive-title-h1 {
font-size: 2.5rem;
@media (max-width: 1200px) {
font-size: 2rem;
}
@media (max-width: 900px) {
font-size: 1.7rem;
}
@media (max-width: 768px) {
font-size: 1.3rem;
}
}
.responsive-title-h2 {
font-size: 2rem;
@media (max-width: 1200px) {
font-size: 1.5rem;
}
@media (max-width: 900px) {
font-size: 1.2rem;
}
@media (max-width: 768px) {
font-size: 1rem;
}
}
.responsive-title-h3 {
font-size: 1.5rem;
@media (max-width: 1200px) {
font-size: 1.2rem;
}
@media (max-width: 900px) {
font-size: 1rem;
}
@media (max-width: 768px) {
font-size: 1rem;
}
}
.hide-on-sm {
@media (max-width: 900px) {
display: none !important;
}
}

View File

@@ -5,12 +5,10 @@
:key="history.indexOf(history_line)"
>
<div class="card-header">
<div
class="history-header d-flex align-items-center justify-content-between"
>
<div class="d-flex align-items-center fw-bold">
<div class="history-header row align-items-center gx-0">
<div class="col d-flex align-items-center">
<i :class="`${actionIcons[history_line.event_type]} me-1`"></i>
<span>{{ explainSentence(history_line) }}</span>
<span class="fw-bold">{{ explainSentence(history_line) }}</span>
<state-component
:new_state="history_line.data.new_state"
v-if="history_line.event_type == 'state_change'"
@@ -20,11 +18,11 @@
:new_emergency="history_line.data.new_emergency"
/>
</div>
<div>
<span class="badge-user">
<div class="col-auto d-flex justify-content-end align-items-center">
<span class="badge-user d-flex">
<user-render-box-badge :user="history_line.by" />
</span>
<span class="fst-italic mx-2">
<span class="fst-italic mx-2 d-flex">
{{ formatDate(history_line.at) }}
</span>
</div>

View File

@@ -2,13 +2,19 @@
<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-top">
<div v-if="null !== ticket.currentMotive && null !== ticket.currentMotive.parent" class="wh-col">
<div class="row align-items-top">
<div
v-if="
null !== ticket.currentMotive &&
null !== ticket.currentMotive.parent
"
class="col-6"
>
<div class="small text-muted">
{{ motiveHierarchyLabel(ticket.currentMotive) }}
</div>
</div>
<div class="wh-col">
<div class="col-6 text-end">
<emergency-component
:new_emergency="ticket.emergency"
v-if="ticket.emergency == 'yes'"
@@ -19,59 +25,55 @@
/>
</div>
</div>
<div class="wh-row">
<div class="wh-col">
<span
class="h2"
style="color: var(--bs-chill-blue); font-variant: all-small-caps"
>
{{ getTicketTitle(ticket) }}
</span>
</div>
<div class="wh-col">
<span
v-if="ticket.createdAt"
:title="formatDateTime(ticket.createdAt.datetime, 'long', 'long')"
style="font-style: italic"
>
{{ getSinceCreated(ticket.createdAt.datetime, new Date()) }}
</span>
</div>
</div>
<div class="row align-items-top">
<div class="col-md-6 col-12">
<span
class="h2"
style="color: var(--bs-chill-blue); font-variant: all-small-caps"
>
{{ getTicketTitle(ticket) }}
</span>
</div>
<div class="col-md-6 col-12 text-md-end text-start">
<span
v-if="ticket.createdAt"
:title="formatDateTime(ticket.createdAt.datetime, 'long', 'long')"
style="font-style: italic"
>
{{ getSinceCreated(ticket.createdAt.datetime, new Date()) }}
</span>
</div>
</div>
</div>
<div class="card-body pt-0">
<div class="wrap-list">
<div class="wl-row" v-if="ticket.currentAddressees.length">
<div class="wl-col title text-end">
<h3>{{ trans(CHILL_TICKET_LIST_ADDRESSEES) }}</h3>
</div>
<div class="wl-col list">
<addressee-component :addressees="ticket.currentAddressees" />
</div>
<div class="row" v-if="ticket.currentAddressees.length">
<div class="col-12 title text-start">
<h3>{{ trans(CHILL_TICKET_LIST_ADDRESSEES) }}</h3>
</div>
<div class="wl-row" v-if="ticket.currentPersons.length">
<div class="wl-col title text-end">
<h3>{{ trans(CHILL_TICKET_LIST_PERSONS) }}</h3>
</div>
<div class="wl-col list">
<person-component :entities="ticket.currentPersons" />
</div>
<div class="col-12 list">
<addressee-component :addressees="ticket.currentAddressees" />
</div>
<div class="wl-row" v-if="ticket.caller">
<div class="wl-col title text-end">
<h3>{{ trans(CHILL_TICKET_LIST_CALLERS) }}</h3>
</div>
<div class="wl-col list">
<person-component
:entities="
ticket.caller
? ([ticket.caller] as Person[] | Thirdparty[])
: []
"
/>
</div>
</div>
<div class="row" v-if="ticket.currentPersons.length">
<div class="col-12 title text-start">
<h3>{{ trans(CHILL_TICKET_LIST_PERSONS) }}</h3>
</div>
<div class="col-12 list">
<person-component :entities="ticket.currentPersons" />
</div>
</div>
<div class="row" v-if="ticket.caller">
<div class="col-12 title text-start">
<h3>{{ trans(CHILL_TICKET_LIST_CALLERS) }}</h3>
</div>
<div class="col-12 list">
<person-component
:entities="
ticket.caller ? ([ticket.caller] as Person[] | Thirdparty[]) : []
"
/>
</div>
</div>
</div>

View File

@@ -16,4 +16,6 @@
{% block content %}
<div id="ticketRoot"></div>
<div class="sticky-form-buttons" id="actionToolbar" style="display: block;text-align: start;padding: inherit;">
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@ chill_ticket:
title: "Tickets"
title_with_name: "{name, select, null {Tickets} undefined {Tickets} other {Tickets de {name}}}"
title_menu: "Tickets de l'usager"
show_only_history_comments: "Afficher uniquement les commentaires"
show_all_history: "Afficher tout l'historique"
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..."