Misc: homepage widget with tickets, and improvements in ticket list

This commit is contained in:
Boris Waaub
2025-09-16 11:16:57 +00:00
committed by Julien Fastré
parent e87429933a
commit 0ba2cbc1e8
33 changed files with 1200 additions and 838 deletions

View File

@@ -1,15 +0,0 @@
import { createApp } from "vue";
import { _createI18n } from "ChillMainAssets/vuejs/_js/i18n";
import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n";
import { store } from "ChillMainAssets/vuejs/HomepageWidget/js/store";
import App from "ChillMainAssets/vuejs/HomepageWidget/App";
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component("app", App)
.mount("#homepage_widget");

View File

@@ -0,0 +1,6 @@
import App from "ChillMainAssets/vuejs/HomepageWidget/App.vue";
import { createApp } from "vue";
import { store } from "ChillMainAssets/vuejs/HomepageWidget/store";
const _app = createApp(App);
_app.use(store).mount("#homepage_widget");

View File

@@ -1,5 +1,5 @@
<template>
<h2>{{ $t("main_title") }}</h2>
<h2>{{ trans(MAIN_TITLE) }}</h2>
<ul class="nav nav-tabs">
<li class="nav-item">
@@ -11,14 +11,24 @@
<i class="fa fa-dashboard" />
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyTickets' }"
@click="selectTab('MyTickets')"
>
{{ trans(MY_TICKETS_TAB) }}
<tab-counter :count="ticketListState.value?.count || 0" />
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyNotifications' }"
@click="selectTab('MyNotifications')"
>
{{ $t("my_notifications.tab") }}
<tab-counter :count="state.notifications.count" />
{{ trans(MY_NOTIFICATIONS_TAB) }}
<tab-counter :count="state.value?.notifications?.count || 0" />
</a>
</li>
<li class="nav-item">
@@ -27,25 +37,17 @@
:class="{ active: activeTab === 'MyAccompanyingCourses' }"
@click="selectTab('MyAccompanyingCourses')"
>
{{ $t("my_accompanying_courses.tab") }}
{{ trans(MY_ACCOMPANYING_COURSES_TAB) }}
</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link"
:class="{'active': activeTab === 'MyWorks'}"
@click="selectTab('MyWorks')">
{{ $t('my_works.tab') }}
<tab-counter :count="state.works.count"></tab-counter>
</a>
</li> -->
<li class="nav-item">
<a
class="nav-link"
:class="{ active: activeTab === 'MyEvaluations' }"
@click="selectTab('MyEvaluations')"
>
{{ $t("my_evaluations.tab") }}
<tab-counter :count="state.evaluations.count" />
{{ trans(MY_EVALUATIONS_TAB) }}
<tab-counter :count="state.value?.evaluations?.count || 0" />
</a>
</li>
<li class="nav-item">
@@ -54,9 +56,12 @@
:class="{ active: activeTab === 'MyTasks' }"
@click="selectTab('MyTasks')"
>
{{ $t("my_tasks.tab") }}
{{ trans(MY_TASKS_TAB) }}
<tab-counter
:count="state.tasks.warning.count + state.tasks.alert.count"
:count="
(state.value?.tasks?.warning?.count || 0) +
(state.value?.tasks?.alert?.count || 0)
"
/>
</a>
</li>
@@ -66,19 +71,25 @@
:class="{ active: activeTab === 'MyWorkflows' }"
@click="selectTab('MyWorkflows')"
>
{{ $t("my_workflows.tab") }}
<tab-counter :count="state.workflows.count + state.workflowsCc.count" />
{{ trans(MY_WORKFLOWS_TAB) }}
<tab-counter
:count="
(state.value?.workflows?.count || 0) +
(state.value?.workflowsCc?.count || 0)
"
/>
</a>
</li>
<li class="nav-item loading ms-auto py-2" v-if="loading">
<i
class="fa fa-circle-o-notch fa-spin fa-lg text-chill-gray"
:title="$t('loading')"
:title="trans(LOADING)"
/>
</li>
</ul>
<div class="my-4">
<my-tickets v-if="activeTab == 'MyTickets'" />
<my-customs v-if="activeTab === 'MyCustoms'" />
<my-works v-else-if="activeTab === 'MyWorks'" />
<my-evaluations v-else-if="activeTab === 'MyEvaluations'" />
@@ -91,63 +102,58 @@
</div>
</template>
<script>
import MyCustoms from "./MyCustoms";
import MyWorks from "./MyWorks";
import MyEvaluations from "./MyEvaluations";
import MyTasks from "./MyTasks";
import MyAccompanyingCourses from "./MyAccompanyingCourses";
import MyNotifications from "./MyNotifications";
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import MyCustoms from "./MyCustoms.vue";
import MyWorks from "./MyWorks.vue";
import MyEvaluations from "./MyEvaluations.vue";
import MyTasks from "./MyTasks.vue";
import MyAccompanyingCourses from "./MyAccompanyingCourses.vue";
import MyNotifications from "./MyNotifications.vue";
import MyWorkflows from "./MyWorkflows.vue";
import TabCounter from "./TabCounter";
import { mapState } from "vuex";
import MyTickets from "./MyTickets.vue";
import TabCounter from "./TabCounter.vue";
import {
MAIN_TITLE,
MY_TICKETS_TAB,
MY_EVALUATIONS_TAB,
MY_TASKS_TAB,
MY_ACCOMPANYING_COURSES_TAB,
MY_NOTIFICATIONS_TAB,
MY_WORKFLOWS_TAB,
LOADING,
trans,
} from "translator";
const store = useStore();
export default {
name: "App",
components: {
MyCustoms,
MyWorks,
MyEvaluations,
MyTasks,
MyWorkflows,
MyAccompanyingCourses,
MyNotifications,
TabCounter,
},
data() {
return {
activeTab: "MyCustoms",
};
},
computed: {
...mapState(["loading"]),
// just to see all in devtool :
...mapState({
state: (state) => state,
}),
},
methods: {
selectTab(tab) {
if (tab !== "MyCustoms") {
this.$store.dispatch("getByTab", { tab: tab });
}
this.activeTab = tab;
console.log(this.activeTab);
},
},
mounted() {
for (const m of [
"MyNotifications",
"MyAccompanyingCourses",
// 'MyWorks',
"MyEvaluations",
"MyTasks",
"MyWorkflows",
]) {
this.$store.dispatch("getByTab", { tab: m, param: "countOnly=1" });
}
},
};
const activeTab = ref("MyCustoms");
const loading = computed(() => store.state.loading);
const state = computed(() => store.state.homepage);
const ticketListState = computed(() => store.state.ticketList);
function selectTab(tab: string) {
if (tab !== "MyCustoms") {
store.dispatch("getByTab", { tab: tab });
}
activeTab.value = tab;
}
onMounted(() => {
for (const m of [
"MyTickets",
"MyNotifications",
"MyAccompanyingCourses",
// 'MyWorks',
"MyEvaluations",
"MyTasks",
"MyWorkflows",
]) {
store.dispatch("getByTab", { tab: m, param: "countOnly=1" });
}
});
</script>
<style scoped>

View File

@@ -1,35 +1,35 @@
<template>
<div class="alert alert-light">
{{ $t("my_accompanying_courses.description") }}
{{ trans(MY_ACCOMPANYING_COURSES_DESCRIPTION) }}
</div>
<span v-if="noResults" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="noResults" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("opening_date") }}
{{ trans(OPENING_DATE) }}
</th>
<th scope="col">
{{ $t("social_issues") }}
{{ trans(SOCIAL_ISSUES) }}
</th>
<th scope="col">
{{ $t("concerned_persons") }}
{{ trans(CONCERNED_PERSONS) }}
</th>
<th scope="col" />
<th scope="col" />
</template>
<template #tbody>
<tr v-for="(c, i) in accompanyingCourses.results" :key="`course-${i}`">
<td>{{ $d(c.openingDate.datetime, "short") }}</td>
<td>{{ $d(new Date(c.openingDate.datetime), "short") }}</td>
<td>
<span
v-for="(i, index) in c.socialIssues"
v-for="(issue, index) in c.socialIssues"
:key="index"
class="chill-entity entity-social-issue"
>
<span class="badge bg-chill-l-gray text-dark">
{{ localizeString(i.title) }}
{{ localizeString(issue.title) }}
</span>
</span>
</td>
@@ -46,15 +46,15 @@
</td>
<td>
<span v-if="c.emergency" class="badge rounded-pill bg-danger me-1">{{
$t("emergency")
trans(EMERGENCY)
}}</span>
<span v-if="c.confidential" class="badge rounded-pill bg-danger">{{
$t("confidential")
trans(CONFIDENTIAL)
}}</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(c)">
{{ $t("show_entity", { entity: $t("the_course") }) }}
{{ trans(SHOW_ENTITY, { entity: trans(THE_COURSE) }) }}
</a>
</td>
</tr>
@@ -62,36 +62,45 @@
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
<script lang="ts" setup>
import { computed, ComputedRef } from "vue";
import { useStore } from "vuex";
import TabTable from "./TabTable.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import { AccompanyingCourse } from "ChillPersonAssets/types";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
import {
MY_ACCOMPANYING_COURSES_DESCRIPTION,
OPENING_DATE,
SOCIAL_ISSUES,
CONCERNED_PERSONS,
SHOW_ENTITY,
THE_COURSE,
NO_DATA,
EMERGENCY,
CONFIDENTIAL,
trans,
} from "translator";
const store = useStore();
export default {
name: "MyAccompanyingCourses",
components: {
TabTable,
OnTheFly,
},
computed: {
...mapState(["accompanyingCourses"]),
...mapGetters(["isAccompanyingCoursesLoaded"]),
noResults() {
if (!this.isAccompanyingCoursesLoaded) {
return false;
} else {
return this.accompanyingCourses.count === 0;
}
},
},
methods: {
localizeString,
getUrl(c) {
return `/fr/parcours/${c.id}`;
},
},
};
const accompanyingCourses: ComputedRef<PaginationResponse<AccompanyingCourse>> =
computed(() => store.state.homepage.accompanyingCourses);
const isAccompanyingCoursesLoaded = computed(
() => store.getters.isAccompanyingCoursesLoaded,
);
const noResults = computed(() => {
if (!isAccompanyingCoursesLoaded.value) {
return false;
} else {
return accompanyingCourses.value.count === 0;
}
});
function getUrl(c: { id: number }): string {
return `/fr/parcours/${c.id}`;
}
</script>
<style scoped>

View File

@@ -1,93 +1,72 @@
<template>
<span v-if="noResults" class="chill-no-data-statement">{{
$t("no_dashboard")
}}</span>
<span v-if="noResults" class="chill-no-data-statement">
{{ trans(NO_DASHBOARD) }}
</span>
<div v-else id="dashboards" class="container g-3">
<div class="row">
<div class="mbloc col-xs-12 col-sm-4">
<div class="custom1">
<ul class="list-unstyled">
<li v-if="counter.notifications > 0">
<i18n-t
keypath="counter.unread_notifications"
tag="span"
:class="counterClass"
:plural="counter.notifications"
>
<template #n>
<span>{{ counter.notifications }}</span>
</template>
</i18n-t>
<li v-if="(counter.value?.notifications || 0) > 0">
<span :class="counterClass">
{{
trans(COUNTER_UNREAD_NOTIFICATIONS, {
n: counter.value?.notifications || 0,
})
}}
</span>
</li>
<li v-if="counter.accompanyingCourses > 0">
<i18n-t
keypath="counter.assignated_courses"
tag="span"
:class="counterClass"
:plural="counter.accompanyingCourses"
>
<template #n>
<span>{{ counter.accompanyingCourses }}</span>
</template>
</i18n-t>
<li v-if="(counter.value?.accompanyingCourses || 0) > 0">
<span :class="counterClass">
{{
trans(COUNTER_ASSIGNATED_COURSES, {
n: counter.value?.accompanyingCourses || 0,
})
}}
</span>
</li>
<li v-if="counter.works > 0">
<i18n-t
keypath="counter.assignated_actions"
tag="span"
:class="counterClass"
:plural="counter.works"
>
<template #n>
<span>{{ counter.works }}</span>
</template>
</i18n-t>
<li v-if="(counter.value?.works || 0) > 0">
<span :class="counterClass">
{{
trans(COUNTER_ASSIGNATED_ACTIONS, {
n: counter.value?.works || 0,
})
}}
</span>
</li>
<li v-if="counter.evaluations > 0">
<i18n-t
keypath="counter.assignated_evaluations"
tag="span"
:class="counterClass"
:plural="counter.evaluations"
>
<template #n>
<span>{{ counter.evaluations }}</span>
</template>
</i18n-t>
<li v-if="(counter.value?.evaluations || 0) > 0">
<span :class="counterClass">
{{
trans(COUNTER_ASSIGNATED_EVALUATIONS, {
n: counter.value?.evaluations || 0,
})
}}
</span>
</li>
<li v-if="counter.tasksAlert > 0">
<i18n-t
keypath="counter.alert_tasks"
tag="span"
:class="counterClass"
:plural="counter.tasksAlert"
>
<template #n>
<span>{{ counter.tasksAlert }}</span>
</template>
</i18n-t>
<li v-if="(counter.value?.tasksAlert || 0) > 0">
<span :class="counterClass">
{{
trans(COUNTER_ALERT_TASKS, {
n: counter.value?.tasksAlert || 0,
})
}}
</span>
</li>
<li v-if="counter.tasksWarning > 0">
<i18n-t
keypath="counter.warning_tasks"
tag="span"
:class="counterClass"
:plural="counter.tasksWarning"
>
<template #n>
<span>{{ counter.tasksWarning }}</span>
</template>
</i18n-t>
<li v-if="(counter.value?.tasksWarning || 0) > 0">
<span :class="counterClass">
{{
trans(COUNTER_WARNING_TASKS, {
n: counter.value?.tasksWarning || 0,
})
}}
</span>
</li>
</ul>
</div>
</div>
<template v-if="this.hasDashboardItems">
<template
v-for="(dashboardItem, index) in this.dashboardItems"
:key="index"
>
<template v-if="hasDashboardItems">
<template v-for="(dashboardItem, index) in dashboardItems" :key="index">
<div
class="mbloc col-xs-12 col-sm-8 news"
v-if="dashboardItem.type === 'news'"
@@ -100,44 +79,53 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { useStore } from "vuex";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import News from "./DashboardWidgets/News.vue";
import {
NO_DASHBOARD,
COUNTER_UNREAD_NOTIFICATIONS,
COUNTER_ASSIGNATED_COURSES,
COUNTER_ASSIGNATED_ACTIONS,
COUNTER_ASSIGNATED_EVALUATIONS,
COUNTER_ALERT_TASKS,
COUNTER_WARNING_TASKS,
trans,
} from "translator";
export default {
name: "MyCustoms",
components: {
News,
},
data() {
return {
counterClass: {
counter: true, //hack to pass class 'counter' in i18n-t
},
dashboardItems: [],
masonry: null,
};
},
computed: {
...mapGetters(["counter"]),
noResults() {
return false;
},
hasDashboardItems() {
return this.dashboardItems.length > 0;
},
},
mounted() {
makeFetch("GET", "/api/1.0/main/dashboard-config-item.json")
.then((response) => {
this.dashboardItems = response;
})
.catch((error) => {
throw error;
});
},
};
interface MyCustom {
id: number;
type: string;
metadata: Record<string, unknown>;
userId: number;
position: string;
}
const store = useStore();
const counter = computed(() => store.getters.counter);
const counterClass = { counter: true };
const dashboardItems = ref<MyCustom[]>([]);
const noResults = computed(() => false);
const hasDashboardItems = computed(() => dashboardItems.value.length > 0);
onMounted(async () => {
try {
const response: MyCustom[] = await makeFetch(
"GET",
"/api/1.0/main/dashboard-config-item.json",
);
dashboardItems.value = response;
} catch (error) {
throw error;
}
});
</script>
<style lang="scss" scoped>

View File

@@ -1,44 +1,50 @@
<template>
<div class="accompanying-course-work">
<div class="alert alert-light">
{{ $t("my_evaluations.description") }}
{{ trans(MY_EVALUATIONS_DESCRIPTION) }}
</div>
<span v-if="noResults" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="noResults" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("max_date") }}
{{ trans(MAX_DATE) }}
</th>
<th scope="col">
{{ $t("evaluation") }}
{{ trans(EVALUATION) }}
</th>
<th scope="col">
{{ $t("SocialAction") }}
{{ trans(SOCIAL_ACTION) }}
</th>
<th scope="col" />
</template>
<template #tbody>
<tr v-for="(e, i) in evaluations.results" :key="`evaluation-${i}`">
<td>{{ $d(e.maxDate.datetime, "short") }}</td>
<td>
{{ localizeString(e.evaluation.title) }}
{{
e.maxDate?.datetime
? $d(new Date(e.maxDate.datetime), "short")
: ""
}}
</td>
<td>
{{ localizeString(e.evaluation?.title ?? null) }}
</td>
<td>
<span class="chill-entity entity-social-issue">
<span class="badge bg-chill-l-gray text-dark">
{{ e.accompanyingPeriodWork.socialAction.issue.text }}
{{ e.accompanyingPeriodWork?.socialAction?.issue?.text ?? "" }}
</span>
</span>
<h4 class="badge-title">
<span class="title_label" />
<span class="title_action">
{{ e.accompanyingPeriodWork.socialAction.text }}
{{ e.accompanyingPeriodWork?.socialAction?.text ?? "" }}
</span>
</h4>
<span
v-for="person in e.accompanyingPeriodWork.persons"
v-for="person in e.accompanyingPeriodWork?.persons ?? []"
class="me-1"
:key="person.id"
>
@@ -55,18 +61,22 @@
<div class="btn-group-vertical" role="group" aria-label="Actions">
<a class="btn btn-sm btn-show" :href="getUrl(e)">
{{
$t("show_entity", {
entity: $t("the_evaluation"),
trans(SHOW_ENTITY, {
entity: trans(THE_EVALUATION),
})
}}
</a>
<a
class="btn btn-sm btn-show"
:href="getUrl(e.accompanyingPeriodWork.accompanyingPeriod)"
:href="getUrl(e.accompanyingPeriodWork.accompanyingPeriod!)"
v-if="
e.accompanyingPeriodWork &&
e.accompanyingPeriodWork.accompanyingPeriod
"
>
{{
$t("show_entity", {
entity: $t("the_course"),
trans(SHOW_ENTITY, {
entity: trans(THE_COURSE),
})
}}
</a>
@@ -78,46 +88,70 @@
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
import TabTable from "./TabTable.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { localizeString } from "../../lib/localizationHelper/localizationHelper";
export default {
name: "MyEvaluations",
components: {
TabTable,
OnTheFly,
},
computed: {
...mapState(["evaluations"]),
...mapGetters(["isEvaluationsLoaded"]),
noResults() {
if (!this.isEvaluationsLoaded) {
return false;
} else {
return this.evaluations.count === 0;
}
},
},
methods: {
localizeString,
getUrl(e) {
switch (e.type) {
case "accompanying_period_work_evaluation":
let anchor = "#evaluations";
return `/fr/person/accompanying-period/work/${e.accompanyingPeriodWork.id}/edit${anchor}`;
case "accompanying_period_work":
return `/fr/person/accompanying-period/work/${e.id}/edit`;
case "accompanying_period":
return `/fr/parcours/${e.id}`;
default:
throw "entity type unknown";
}
},
},
};
const store = useStore();
import type { ComputedRef } from "vue";
import {
AccompanyingPeriod,
AccompanyingPeriodWorkEvaluation,
AccompanyingPeriodWork,
} from "ChillPersonAssets/types";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
import {
MY_EVALUATIONS_DESCRIPTION,
MAX_DATE,
EVALUATION,
SHOW_ENTITY,
THE_COURSE,
THE_EVALUATION,
SOCIAL_ACTION,
NO_DATA,
trans,
} from "translator";
const evaluations: ComputedRef<
PaginationResponse<AccompanyingPeriodWorkEvaluation>
> = computed(() => store.state.homepage.evaluations);
const isEvaluationsLoaded = computed(() => store.getters.isEvaluationsLoaded);
const noResults = computed(() => {
if (!isEvaluationsLoaded.value) {
return false;
} else {
return evaluations.value.count === 0;
}
});
function getUrl(
e:
| AccompanyingPeriodWorkEvaluation
| AccompanyingPeriod
| AccompanyingPeriodWork,
): string {
if (!e) {
throw "entity is undefined";
}
if ("type" in e && typeof e.type === "string") {
switch (e.type) {
case "accompanying_period_work_evaluation":
return `/fr/person/accompanying-period/work/${e.accompanyingPeriodWork?.id}/edit#evaluations`;
case "accompanying_period_work":
return `/fr/person/accompanying-period/work/${e.id}/edit`;
case "accompanying_period":
return `/fr/parcours/${e.id}`;
default:
throw "entity type unknown";
}
}
return "";
}
</script>
<style scoped></style>

View File

@@ -1,26 +1,26 @@
<template>
<div class="alert alert-light">
{{ $t("my_notifications.description") }}
{{ trans(MY_NOTIFICATIONS_DESCRIPTION) }}
</div>
<span v-if="noResults" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="noResults" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("Date") }}
{{ trans(DATE) }}
</th>
<th scope="col">
{{ $t("Subject") }}
{{ trans(SUBJECT) }}
</th>
<th scope="col">
{{ $t("From") }}
{{ trans(FROM) }}
</th>
<th scope="col" />
</template>
<template #tbody>
<tr v-for="(n, i) in notifications.results" :key="`notify-${i}`">
<td>{{ $d(n.date.datetime, "long") }}</td>
<td>{{ $d(new Date(n.date.datetime), "long") }}</td>
<td>
<span class="unread">
<i class="fa fa-envelope-o" />
@@ -31,11 +31,11 @@
{{ n.sender.text }}
</td>
<td v-else>
{{ $t("automatic_notification") }}
{{ trans(AUTOMATIC_NOTIFICATION) }}
</td>
<td>
<a class="btn btn-sm btn-show" :href="getEntityUrl(n)">
{{ $t("show_entity", { entity: getEntityName(n) }) }}
{{ trans(SHOW_ENTITY, { entity: getEntityName(n) }) }}
</a>
</td>
</tr>
@@ -43,65 +43,82 @@
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import { appMessages } from "ChillMainAssets/vuejs/HomepageWidget/js/i18n";
<script lang="ts" setup>
import { computed, ComputedRef } from "vue";
import { useStore } from "vuex";
import TabTable from "./TabTable.vue";
import { Notification } from "ChillPersonAssets/types";
export default {
name: "MyNotifications",
components: {
TabTable,
},
computed: {
...mapState(["notifications"]),
...mapGetters(["isNotificationsLoaded"]),
noResults() {
if (!this.isNotificationsLoaded) {
return false;
} else {
return this.notifications.count === 0;
}
},
},
methods: {
getNotificationUrl(n) {
return `/fr/notification/${n.id}/show`;
},
getEntityName(n) {
switch (n.relatedEntityClass) {
case "Chill\\ActivityBundle\\Entity\\Activity":
return appMessages.fr.the_activity;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod":
return appMessages.fr.the_course;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork":
return appMessages.fr.the_action;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument":
return appMessages.fr.the_evaluation_document;
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
return appMessages.fr.the_workflow;
default:
throw "notification type unknown";
}
},
getEntityUrl(n) {
switch (n.relatedEntityClass) {
case "Chill\\ActivityBundle\\Entity\\Activity":
return `/fr/activity/${n.relatedEntityId}/show`;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod":
return `/fr/parcours/${n.relatedEntityId}`;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork":
return `/fr/person/accompanying-period/work/${n.relatedEntityId}/show`;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument":
return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`;
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
return `/fr/main/workflow/${n.relatedEntityId}/show`;
default:
throw "notification type unknown";
}
},
},
};
import {
MY_NOTIFICATIONS_DESCRIPTION,
DATE,
FROM,
SUBJECT,
SHOW_ENTITY,
THE_ACTIVITY,
THE_COURSE,
THE_ACTION,
THE_EVALUATION_DOCUMENT,
THE_WORKFLOW,
NO_DATA,
AUTOMATIC_NOTIFICATION,
trans,
} from "translator";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
const store = useStore();
const notifications: ComputedRef<PaginationResponse<Notification>> = computed(
() => store.state.homepage.notifications,
);
const isNotificationsLoaded = computed(
() => store.getters.isNotificationsLoaded,
);
const noResults = computed(() => {
if (!isNotificationsLoaded.value) {
return false;
} else {
return notifications.value.count === 0;
}
});
function getNotificationUrl(n: Notification): string {
return `/fr/notification/${n.id}/show`;
}
function getEntityName(n: Notification): string {
switch (n.relatedEntityClass) {
case "Chill\\ActivityBundle\\Entity\\Activity":
return trans(THE_ACTIVITY);
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod":
return trans(THE_COURSE);
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork":
return trans(THE_ACTION);
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument":
return trans(THE_EVALUATION_DOCUMENT);
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
return trans(THE_WORKFLOW);
default:
throw "notification type unknown";
}
}
function getEntityUrl(n: Notification): string {
switch (n.relatedEntityClass) {
case "Chill\\ActivityBundle\\Entity\\Activity":
return `/fr/activity/${n.relatedEntityId}/show`;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod":
return `/fr/parcours/${n.relatedEntityId}`;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork":
return `/fr/person/accompanying-period/work/${n.relatedEntityId}/show`;
case "Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument":
return `/fr/person/accompanying-period/work/evaluation/document/${n.relatedEntityId}/show`;
case "Chill\\MainBundle\\Entity\\Workflow\\EntityWorkflow":
return `/fr/main/workflow/${n.relatedEntityId}/show`;
default:
throw "notification type unknown";
}
}
</script>
<style lang="scss" scoped>

View File

@@ -1,36 +1,38 @@
<template>
<div class="alert alert-light">
{{ $t("my_tasks.description_warning") }}
{{ trans(MY_TASKS_DESCRIPTION_WARNING) }}
</div>
<span v-if="noResultsAlert" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="noResultsAlert" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("warning_date") }}
{{ trans(WARNING_DATE) }}
</th>
<th scope="col">
{{ $t("max_date") }}
{{ trans(MAX_DATE) }}
</th>
<th scope="col">
{{ $t("task") }}
{{ trans(TASK) }}
</th>
<th scope="col" />
</template>
<template #tbody>
<tr v-for="(t, i) in tasks.alert.results" :key="`task-alert-${i}`">
<td v-if="null !== t.warningDate">
{{ $d(t.warningDate.datetime, "short") }}
<td v-if="t.warningDate !== null">
{{ $d(new Date(t.warningDate.datetime), "short") }}
</td>
<td v-else />
<td>
<span class="outdated">{{ $d(t.endDate.datetime, "short") }}</span>
<span class="outdated">{{
$d(new Date(t.endDate.datetime), "short")
}}</span>
</td>
<td>{{ t.title }}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t("show_entity", { entity: $t("the_task") }) }}
{{ trans(SHOW_ENTITY, { entity: trans(THE_TASK) }) }}
</a>
</td>
</tr>
@@ -38,21 +40,21 @@
</tab-table>
<div class="alert alert-light">
{{ $t("my_tasks.description_alert") }}
{{ trans(MY_TASKS_DESCRIPTION_ALERT) }}
</div>
<span v-if="noResultsWarning" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="noResultsWarning" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("warning_date") }}
{{ trans(WARNING_DATE) }}
</th>
<th scope="col">
{{ $t("max_date") }}
{{ trans(MAX_DATE) }}
</th>
<th scope="col">
{{ $t("task") }}
{{ trans(TASK) }}
</th>
<th scope="col" />
</template>
@@ -60,14 +62,14 @@
<tr v-for="(t, i) in tasks.warning.results" :key="`task-warning-${i}`">
<td>
<span class="outdated">{{
$d(t.warningDate.datetime, "short")
$d(new Date(t.warningDate.datetime), "short")
}}</span>
</td>
<td>{{ $d(t.endDate.datetime, "short") }}</td>
<td>{{ $d(new Date(t.endDate.datetime), "short") }}</td>
<td>{{ t.title }}</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(t)">
{{ $t("show_entity", { entity: $t("the_task") }) }}
{{ trans(SHOW_ENTITY, { entity: trans(THE_TASK) }) }}
</a>
</td>
</tr>
@@ -75,39 +77,51 @@
</tab-table>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
<script lang="ts" setup>
import { computed, ComputedRef } from "vue";
import { useStore } from "vuex";
import TabTable from "./TabTable.vue";
import {
MY_TASKS_DESCRIPTION_ALERT,
MY_TASKS_DESCRIPTION_WARNING,
MAX_DATE,
WARNING_DATE,
TASK,
SHOW_ENTITY,
THE_TASK,
NO_DATA,
trans,
} from "translator";
import { TasksState } from "./store/modules/homepage";
import { Alert, Warning } from "ChillPersonAssets/types";
export default {
name: "MyTasks",
components: {
TabTable,
},
computed: {
...mapState(["tasks"]),
...mapGetters(["isTasksWarningLoaded", "isTasksAlertLoaded"]),
noResultsAlert() {
if (!this.isTasksAlertLoaded) {
return false;
} else {
return this.tasks.alert.count === 0;
}
},
noResultsWarning() {
if (!this.isTasksWarningLoaded) {
return false;
} else {
return this.tasks.warning.count === 0;
}
},
},
methods: {
getUrl(t) {
return `/fr/task/single-task/${t.id}/show`;
},
},
};
const store = useStore();
const tasks: ComputedRef<TasksState> = computed(
() => store.state.homepage.tasks,
);
const isTasksWarningLoaded = computed(() => store.getters.isTasksWarningLoaded);
const isTasksAlertLoaded = computed(() => store.getters.isTasksAlertLoaded);
const noResultsAlert = computed(() => {
if (!isTasksAlertLoaded.value) {
return false;
} else {
return tasks.value.alert.count === 0;
}
});
const noResultsWarning = computed(() => {
if (!isTasksWarningLoaded.value) {
return false;
} else {
return tasks.value.warning.count === 0;
}
});
function getUrl(t: Warning | Alert): string {
return `/fr/task/single-task/${t.id}/show`;
}
</script>
<style scoped>

View File

@@ -0,0 +1,58 @@
<template>
<div class="col-12">
<!-- Loading state -->
<div
v-if="isTicketLoading"
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=""
:hasMoreTickets="pagination.next !== null"
@fetchNextPage="fetchNextPage"
/>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
// Components
import TicketListComponent from "../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketList/components/TicketListComponent.vue";
// Types
import { Pagination } from "ChillMainAssets/lib/api/apiMethods";
import { TicketSimple } from "src/Bundle/ChillTicketBundle/src/Resources/public/types";
// Translation
import { CHILL_TICKET_LIST_LOADING_TICKET, trans } from "translator";
const store = useStore();
const pagination = computed(() => store.getters.getPagination as Pagination);
const ticketList = computed(
() => store.getters.getTicketList as TicketSimple[],
);
const isTicketLoading = computed(() => store.getters.isTicketLoading);
const fetchNextPage = async () => {
if (pagination.value.next) {
await store.dispatch("fetchTicketListByUrl", pagination.value.next);
}
};
</script>

View File

@@ -1,26 +1,27 @@
<template>
<div class="alert alert-light">
{{ $t("my_workflows.description") }}
{{ trans(MY_WORKFLOWS_DESCRIPTION) }}
</div>
<my-workflows-table :workflows="workflows" />
<div class="alert alert-light">
{{ $t("my_workflows.description_cc") }}
{{ trans(MY_WORKFLOWS_DESCRIPTION_CC) }}
</div>
<my-workflows-table :workflows="workflowsCc" />
</template>
<script>
import { mapState } from "vuex";
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
import MyWorkflowsTable from "./MyWorkflowsTable.vue";
import {
MY_WORKFLOWS_DESCRIPTION,
MY_WORKFLOWS_DESCRIPTION_CC,
trans,
} from "translator";
export default {
name: "MyWorkflows",
components: {
MyWorkflowsTable,
},
computed: {
...mapState(["workflows", "workflowsCc"]),
},
};
const store = useStore();
const workflows = computed(() => store.state.homepage.workflows);
const workflowsCc = computed(() => store.state.homepage.workflowsCc);
</script>

View File

@@ -1,17 +1,17 @@
<template>
<span v-if="hasNoResults(workflows)" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="hasNoResults" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("Object_workflow") }}
{{ trans(OBJECT_WORKFLOW) }}
</th>
<th scope="col">
{{ $t("Step") }}
{{ trans(STEP) }}
</th>
<th scope="col">
{{ $t("concerned_users") }}
{{ trans(CONCERNED_USERS) }}
</th>
<th scope="col" />
</template>
@@ -29,7 +29,7 @@
<span
v-if="w.isOnHoldAtCurrentStep"
class="badge bg-success rounded-pill"
>{{ $t("on_hold") }}</span
>{{ trans(ON_HOLD) }}</span
>
</div>
</td>
@@ -46,7 +46,7 @@
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ $t("show_entity", { entity: $t("the_workflow") }) }}
{{ trans(SHOW_ENTITY, { entity: trans(THE_WORKFLOW) }) }}
</a>
</td>
</tr>
@@ -54,38 +54,48 @@
</tab-table>
</template>
<script>
import { mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
import TabTable from "./TabTable.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { Workflow } from "ChillPersonAssets/types";
import {
CONCERNED_USERS,
OBJECT_WORKFLOW,
ON_HOLD,
SHOW_ENTITY,
THE_WORKFLOW,
NO_DATA,
STEP,
trans,
} from "translator";
import { PaginationResponse } from "ChillMainAssets/lib/api/apiMethods";
export default {
name: "MyWorkflows",
components: {
TabTable,
OnTheFly,
},
props: ["workflows"],
computed: {
...mapGetters(["isWorkflowsLoaded"]),
},
methods: {
hasNoResults(workflows) {
if (!this.isWorkflowsLoaded) {
return false;
} else {
return workflows.count === 0;
}
},
getUrl(w) {
return `/fr/main/workflow/${w.id}/show`;
},
getStep(w) {
const lastStep = w.steps.length - 1;
return w.steps[lastStep].currentStep.text;
},
},
};
const props = defineProps<{
workflows: PaginationResponse<Workflow>;
}>();
const store = useStore();
const isWorkflowsLoaded = computed(() => store.getters.isWorkflowsLoaded);
const hasNoResults = computed(() => {
if (!isWorkflowsLoaded.value) {
return false;
} else {
return props.workflows.count === 0;
}
});
function getUrl(w: Workflow): string {
return `/fr/main/workflow/${w.id}/show`;
}
function getStep(w: Workflow): string {
const lastStep = w.steps.length - 1;
return w.steps[lastStep].currentStep.text;
}
</script>
<style scoped>

View File

@@ -1,27 +1,26 @@
// CURRENTLY NOT IN USE
<template>
<div class="accompanying-course-work">
<div class="alert alert-light">
{{ $t("my_works.description") }}
{{ trans(MY_WORKS_DESCRIPTION) }}
</div>
<span v-if="noResults" class="chill-no-data-statement">{{
$t("no_data")
}}</span>
<span v-if="noResults" class="chill-no-data-statement">
{{ trans(NO_DATA) }}
</span>
<tab-table v-else>
<template #thead>
<th scope="col">
{{ $t("StartDate") }}
{{ trans(START_DATE) }}
</th>
<th scope="col">
{{ $t("SocialAction") }}
{{ trans(SOCIAL_ACTION) }}
</th>
<th scope="col">
{{ $t("concerned_persons") }}
{{ trans(CONCERNED_PERSONS) }}
</th>
<th scope="col" />
</template>
<template #tbody>
<tr v-for="(w, i) in works.results" :key="`works-${i}`">
<tr v-for="(w, i) in works.value.results" :key="`works-${i}`">
<td>{{ $d(w.startDate.datetime, "short") }}</td>
<td>
<span class="chill-entity entity-social-issue">
@@ -51,8 +50,8 @@
<div class="btn-group-vertical" role="group" aria-label="Actions">
<a class="btn btn-sm btn-update" :href="getUrl(w)">
{{
$t("show_entity", {
entity: $t("the_action"),
trans(SHOW_ENTITY, {
entity: trans(THE_ACTION),
})
}}
</a>
@@ -61,8 +60,8 @@
:href="getUrl(w.accompanyingPeriod)"
>
{{
$t("show_entity", {
entity: $t("the_course"),
trans(SHOW_ENTITY, {
entity: trans(THE_COURSE),
})
}}
</a>
@@ -74,41 +73,47 @@
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly";
<script lang="ts" setup>
import { computed } from "vue";
import { useStore } from "vuex";
import TabTable from "./TabTable.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import {
MY_WORKS_DESCRIPTION,
CONCERNED_PERSONS,
SHOW_ENTITY,
THE_COURSE,
THE_ACTION,
SOCIAL_ACTION,
START_DATE,
NO_DATA,
trans,
} from "translator";
import { Workflow } from "ChillPersonAssets/types";
export default {
name: "MyWorks",
components: {
TabTable,
OnTheFly,
},
computed: {
...mapState(["works"]),
...mapGetters(["isWorksLoaded"]),
noResults() {
if (!this.isWorksLoaded) {
return false;
} else {
return this.works.count === 0;
}
},
},
methods: {
getUrl(e) {
switch (e.type) {
case "accompanying_period_work":
return `/fr/person/accompanying-period/work/${e.id}/edit`;
case "accompanying_period":
return `/fr/parcours/${e.id}`;
default:
throw "entity type unknown";
}
},
},
};
const store = useStore();
const works = computed(() => store.state.homepage.works);
const isWorksLoaded = computed(() => store.getters.isWorksLoaded);
const noResults = computed(() => {
if (!isWorksLoaded.value) {
return false;
} else {
return works.value.count === 0;
}
});
function getUrl(e: Workflow): string {
switch (e.type) {
case "accompanying_period_work":
return `/fr/person/accompanying-period/work/${e.id}/edit`;
case "accompanying_period":
return `/fr/parcours/${e.id}`;
default:
throw "entity type unknown";
}
}
</script>
<style scoped></style>

View File

@@ -4,14 +4,14 @@
</span>
</template>
<script>
export default {
name: "TabCounter",
props: ["count"],
computed: {
isCounterAvailable() {
return typeof this.count !== "undefined" && this.count > 0;
},
},
};
<script lang="ts" setup>
import { computed } from "vue";
const props = defineProps<{
count: number;
}>();
const isCounterAvailable = computed(
() => typeof props.count !== "undefined" && props.count > 0,
);
</script>

View File

@@ -11,11 +11,8 @@
</table>
</template>
<script>
export default {
name: "TabTable",
props: [],
};
<script lang="ts" setup>
// Pas de props à définir, composant slot simple
</script>
<style scoped></style>

View File

@@ -1,89 +0,0 @@
const appMessages = {
fr: {
main_title: "Vue d'ensemble",
my_works: {
tab: "Mes actions",
description:
"Liste des actions d'accompagnement dont je suis référent et qui arrivent à échéance.",
},
my_evaluations: {
tab: "Mes évaluations",
description:
"Liste des évaluations dont je suis référent et qui arrivent à échéance.",
},
my_tasks: {
tab: "Mes tâches",
description_alert:
"Liste des tâches auxquelles je suis assigné et dont la date de rappel est dépassée.",
description_warning:
"Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée.",
},
my_accompanying_courses: {
tab: "Mes nouveaux parcours",
description:
"Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours.",
},
my_notifications: {
tab: "Mes nouvelles notifications",
description: "Liste des notifications reçues et non lues.",
},
my_workflows: {
tab: "Mes workflows",
description: "Liste des workflows en attente d'une action.",
description_cc: "Liste des workflows dont je suis en copie.",
},
opening_date: "Date d'ouverture",
social_issues: "Problématiques sociales",
concerned_persons: "Usagers concernés",
max_date: "Date d'échéance",
warning_date: "Date de rappel",
evaluation: "Évaluation",
task: "Tâche",
Date: "Date",
From: "Expéditeur",
Subject: "Objet",
Entity: "Associé à",
Step: "Étape",
concerned_users: "Usagers concernés",
Object_workflow: "Objet du workflow",
on_hold: "En attente",
show_entity: "Voir {entity}",
the_activity: "l'échange",
the_course: "le parcours",
the_action: "l'action",
the_evaluation: "l'évaluation",
the_evaluation_document: "le document",
the_task: "la tâche",
the_workflow: "le workflow",
StartDate: "Date d'ouverture",
SocialAction: "Action d'accompagnement",
no_data: "Aucun résultats",
no_dashboard: "Pas de tableaux de bord",
counter: {
unread_notifications:
"{n} notification non lue | {n} notifications non lues",
assignated_courses:
"{n} parcours récent assigné | {n} parcours récents assignés",
assignated_actions: "{n} action assignée | {n} actions assignées",
assignated_evaluations:
"{n} évaluation assignée | {n} évaluations assignées",
alert_tasks: "{n} tâche en rappel | {n} tâches en rappel",
warning_tasks: "{n} tâche à échéance | {n} tâches à échéance",
},
emergency: "Urgent",
confidential: "Confidentiel",
automatic_notification: "Notification automatique",
widget: {
news: {
title: "Actualités",
readMore: "Lire la suite",
date: "Date",
none: "Aucune actualité",
},
},
},
};
Object.assign(appMessages.fr);
export { appMessages };

View File

@@ -0,0 +1,55 @@
import { createStore } from "vuex";
import { State as HomepageStates, moduleHomepage } from "./modules/homepage";
import {
State as TicketListStates,
moduleTicketList,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/ticket_list";
import {
State as TicketStates,
moduleTicket,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/ticket";
import {
State as CommentStates,
moduleComment,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/comment";
import {
moduleMotive,
State as MotiveStates,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/motive";
import {
State as AddresseeStates,
moduleAddressee,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/addressee";
import {
modulePersons,
State as PersonsState,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/persons";
import {
moduleUser,
State as UserState,
} from "../../../../../../ChillTicketBundle/src/Resources/public/vuejs/TicketApp/store/modules/user";
export interface RootState {
homepage: HomepageStates;
motive: MotiveStates;
ticket: TicketStates;
comment: CommentStates;
addressee: AddresseeStates;
persons: PersonsState;
ticketList: TicketListStates;
user: UserState;
}
export const store = createStore<RootState>({
modules: {
homepage: moduleHomepage,
motive: moduleMotive,
ticket: moduleTicket,
comment: moduleComment,
addressee: moduleAddressee,
person: modulePersons,
ticketList: moduleTicketList,
user: moduleUser,
},
});

View File

@@ -1,90 +1,113 @@
import "es6-promise/auto";
import { createStore } from "vuex";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { Module } from "vuex";
import {
makeFetch,
PaginationResponse,
} from "ChillMainAssets/lib/api/apiMethods";
import {
AccompanyingCourse,
Alert,
Evaluation,
Warning,
Workflow,
WorflowCc,
} from "ChillPersonAssets/types";
import { RootState } from "..";
const debug = process.env.NODE_ENV !== "production";
export interface TasksState {
warning: PaginationResponse<Warning>;
alert: PaginationResponse<Alert>;
}
const isEmpty = (obj) => {
return (
obj &&
Object.keys(obj).length <= 1 &&
Object.getPrototypeOf(obj) === Object.prototype
);
};
export interface State {
evaluations: PaginationResponse<Evaluation>;
tasks: TasksState;
accompanyingCourses: PaginationResponse<AccompanyingCourse>;
notifications: PaginationResponse<Notification>;
workflows: PaginationResponse<Workflow>;
workflowsCc: PaginationResponse<WorflowCc>;
errorMsg: unknown[];
loading: boolean;
ticketsLoading: boolean;
}
const store = createStore({
strict: debug,
export const moduleHomepage: Module<State, RootState> = {
state: {
// works: {},
evaluations: {},
evaluations: {
count: 0,
} as PaginationResponse<Evaluation>,
tasks: {
warning: {},
alert: {},
warning: {
count: 0,
} as PaginationResponse<Warning>,
alert: {
count: 0,
} as PaginationResponse<Alert>,
},
accompanyingCourses: {},
notifications: {},
workflows: {},
workflowsCc: {},
accompanyingCourses: {
count: 0,
} as PaginationResponse<AccompanyingCourse>,
notifications: {
count: 0,
} as PaginationResponse<Notification>,
workflows: {
count: 0,
} as PaginationResponse<Workflow>,
workflowsCc: {
count: 0,
} as PaginationResponse<WorflowCc>,
ticketsLoading: false,
errorMsg: [],
loading: false,
},
getters: {
// isWorksLoaded(state) {
// return !isEmpty(state.works);
// },
isTicketLoading: (state) => {
return state.ticketsLoading;
},
isEvaluationsLoaded(state) {
return !isEmpty(state.evaluations);
return Array.isArray(state.evaluations.results);
},
isTasksWarningLoaded(state) {
return !isEmpty(state.tasks.warning);
return Array.isArray(state.tasks.warning.results);
},
isTasksAlertLoaded(state) {
return !isEmpty(state.tasks.alert);
return Array.isArray(state.tasks.alert.results);
},
isAccompanyingCoursesLoaded(state) {
return !isEmpty(state.accompanyingCourses);
return Array.isArray(state.accompanyingCourses.results);
},
isNotificationsLoaded(state) {
return !isEmpty(state.notifications);
return Array.isArray(state.notifications.results);
},
isWorkflowsLoaded(state) {
return !isEmpty(state.workflows);
return Array.isArray(state.workflows.results);
},
counter(state) {
counter(state, getters, rootState, rootGetters) {
return {
// works: state.works.count,
evaluations: state.evaluations.count,
tasksWarning: state.tasks.warning.count,
tasksAlert: state.tasks.alert.count,
accompanyingCourses: state.accompanyingCourses.count,
notifications: state.notifications.count,
workflows: state.workflows.count,
tickets: rootGetters["ticketList/getCount"] || 0,
evaluations: state.evaluations.count || 0,
tasksWarning: state.tasks.warning.count || 0,
tasksAlert: state.tasks.alert.count || 0,
accompanyingCourses: state.accompanyingCourses.count || 0,
notifications: state.notifications.count || 0,
workflows: state.workflows.count || 0,
};
},
},
mutations: {
// addWorks(state, works) {
// //console.log('addWorks', works);
// state.works = works;
// },
addEvaluations(state, evaluations) {
//console.log('addEvaluations', evaluations);
state.evaluations = evaluations;
},
addTasksWarning(state, tasks) {
//console.log('addTasksWarning', tasks);
state.tasks.warning = tasks;
},
addTasksAlert(state, tasks) {
//console.log('addTasksAlert', tasks);
state.tasks.alert = tasks;
},
addCourses(state, courses) {
//console.log('addCourses', courses);
state.accompanyingCourses = courses;
},
addNotifications(state, notifications) {
//console.log('addNotifications', notifications);
state.notifications = notifications;
},
addWorkflows(state, workflows) {
@@ -96,29 +119,29 @@ const store = createStore({
setLoading(state, bool) {
state.loading = bool;
},
setTicketsLoading(state, bool) {
state.ticketsLoading = bool;
},
catchError(state, error) {
state.errorMsg.push(error);
},
},
actions: {
getByTab({ commit, getters }, { tab, param }) {
async getByTab({ commit, getters, dispatch }, { tab, param }) {
switch (tab) {
// case 'MyWorks':
// if (!getters.isWorksLoaded) {
// commit('setLoading', true);
// const url = `/api/1.0/person/accompanying-period/work/my-near-end${'?'+ param}`;
// makeFetch('GET', url)
// .then((response) => {
// commit('addWorks', response);
// commit('setLoading', false);
// })
// .catch((error) => {
// commit('catchError', error);
// throw error;
// })
// ;
// }
// break;
case "MyTickets":
if (!getters.isTicketsLoaded) {
commit("setTicketsLoading", true);
// Utilise l'action du module ticket_list
await dispatch(
"fetchTicketList",
{ byAddresseeToMe: true },
{ root: true },
);
commit("setTicketsLoading", false);
}
break;
case "MyEvaluations":
if (!getters.isEvaluationsLoaded) {
commit("setLoading", true);
@@ -180,7 +203,6 @@ const store = createStore({
const url = `/api/1.0/main/notification/my/unread${"?" + param}`;
makeFetch("GET", url)
.then((response) => {
console.log("notifications", response);
commit("addNotifications", response);
commit("setLoading", false);
})
@@ -217,6 +239,4 @@ const store = createStore({
}
},
},
});
export { store };
};

View File

@@ -46,10 +46,7 @@
</li>
</ul>
<ul
class="badge-suggest add-items inline"
style="justify-content: flex-end; display: flex"
>
<ul class="badge-suggest add-items inline text-end">
<li
v-for="s in suggested"
:key="s.type + s.id"

View File

@@ -44,16 +44,16 @@ class SearchUserGroupApiProvider implements SearchApiInterface, LocaleAwareInter
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
{
return $this->userGroupRepository->provideSearchApiQuery($pattern, $this->getLocale(), 'user-group');
return $this->userGroupRepository->provideSearchApiQuery($pattern, $this->getLocale(), 'user_group');
}
public function supportsResult(string $key, array $metadatas): bool
{
return 'user-group' === $key;
return 'user_group' === $key;
}
public function supportsTypes(string $pattern, array $types, array $parameters): bool
{
return in_array('user-group', $types, true);
return in_array('user_group', $types, true);
}
}

View File

@@ -305,7 +305,7 @@ paths:
- thirdparty
- user
- household
- user-group
- user_group
responses:
200:
description: "OK"

View File

@@ -26,7 +26,7 @@ module.exports = function (encore, entries) {
);
encore.addEntry(
"page_homepage_widget",
__dirname + "/Resources/public/page/homepage_widget/index.js",
__dirname + "/Resources/public/page/homepage_widget/index.ts",
);
encore.addEntry(
"page_export",

View File

@@ -191,3 +191,61 @@ pick_entity:
one {Tiers}
other {Tiers}
}
loading: "Chargement..."
main_title: "Vue d'ensemble"
my_tickets.tab: "Mes Tickets"
my_works.tab: "Mes actions"
my_works.description: "Liste des actions d'accompagnement dont je suis référent et qui arrivent à échéance."
my_evaluations.tab: "Mes évaluations"
my_evaluations.description: "Liste des évaluations dont je suis référent et qui arrivent à échéance."
my_tasks.tab: "Mes tâches"
my_tasks.description_alert: "Liste des tâches auxquelles je suis assigné et dont la date de rappel est dépassée."
my_tasks.description_warning: "Liste des tâches auxquelles je suis assigné et dont la date d'échéance est dépassée."
my_accompanying_courses.tab: "Mes nouveaux parcours"
my_accompanying_courses.description: "Liste des parcours d'accompagnement que l'on vient de m'attribuer depuis moins de 15 jours."
my_notifications.tab: "Mes nouvelles notifications"
my_notifications.description: "Liste des notifications reçues et non lues."
my_workflows.tab: "Mes workflows"
my_workflows.description: "Liste des workflows en attente d'une action."
my_workflows.description_cc: "Liste des workflows dont je suis en copie."
opening_date: "Date d'ouverture"
social_issues: "Problématiques sociales"
concerned_persons: "Usagers concernés"
max_date: "Date d'échéance"
warning_date: "Date de rappel"
evaluation: "Évaluation"
task: "Tâche"
Date: "Date"
From: "Expéditeur"
Subject: "Objet"
Entity: "Associé à"
Step: "Étape"
concerned_users: "Usagers concernés"
Object_workflow: "Objet du workflow"
on_hold: "En attente"
show_entity: "Voir {entity}"
the_activity: "l'échange"
the_course: "le parcours"
the_action: "l'action"
the_evaluation: "l'évaluation"
the_evaluation_document: "le document"
the_task: "la tâche"
the_workflow: "le workflow"
StartDate: "Date d'ouverture"
SocialAction: "Action d'accompagnement"
no_data: "Aucun résultats"
no_dashboard: "Pas de tableaux de bord"
counter.unread_notifications: "{n, plural, one {# notification non lue} other {# notifications non lues}}"
counter.assignated_courses: "{n, plural, one {# parcours récent assigné} other {# parcours récents assignés}}"
counter.assignated_actions: "{n, plural, one {# action assignée} other {# actions assignées}}"
counter.assignated_evaluations: "{n, plural, one {# évaluation assignée} other {# évaluations assignées}}"
counter.alert_tasks: "{n, plural, one {# tâche en rappel} other {# tâches en rappel}}"
counter.warning_tasks: "{n, plural, one {# tâche à échéance} other {# tâches à échéance}}"
emergency: "Urgent"
confidential: "Confidentiel"
automatic_notification: "Notification automatique"
widget.news.title: "Actualités"
widget.news.readMore: "Lire la suite"
widget.news.date: "Date"
widget.news.none: "Aucune actualité"

View File

@@ -14,12 +14,12 @@ import {
import { StoredObject } from "ChillDocStoreAssets/types";
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
import Person from "./vuejs/_components/OnTheFly/Person.vue";
export interface AltName {
label: string;
key: string;
}
export interface Person {
id: number;
type: "person";
@@ -45,6 +45,7 @@ export interface Person {
export interface AccompanyingPeriod {
id: number;
type: "accompanying_period";
addressLocation?: Address | null;
administrativeLocation?: Location | null;
calendars: Calendar[];
@@ -81,6 +82,7 @@ export interface AccompanyingPeriod {
export interface AccompanyingPeriodWork {
id: number;
type: "accompanying_period_work";
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;
@@ -105,6 +107,7 @@ export interface AccompanyingPeriodWork {
export interface SocialAction {
id: number;
text: string;
parent?: SocialAction | null;
children: SocialAction[];
issue?: SocialIssue | null;
@@ -166,6 +169,7 @@ export interface AccompanyingPeriodLocationHistory {
export interface SocialIssue {
id: number;
text: string;
parent?: SocialIssue | null;
children: SocialIssue[];
socialActions?: SocialAction[] | null;
@@ -206,6 +210,7 @@ export interface AccompanyingPeriodWorkGoal {
}
export interface AccompanyingPeriodWorkEvaluation {
type: "accompanying_period_work_evaluation";
accompanyingPeriodWork: AccompanyingPeriodWork | null;
comment: string;
createdAt: DateTime | null;
@@ -236,6 +241,68 @@ export interface Evaluation {
notificationDelay: string;
}
export interface Step {
currentStep: {
text: string;
};
}
export interface Workflow {
id: number;
title: string;
type: "accompanying_period_work" | "accompanying_period";
isOnHoldAtCurrentStep: boolean;
datas: {
persons: Person[];
};
steps: Step[];
}
export interface WorflowCc {
id: number;
title: string;
isOnHoldAtCurrentStep: boolean;
datas: {
persons: Person[];
};
steps: Step[];
}
export interface Warning {
id: number;
warningDate: DateTime;
endDate: DateTime;
title: string;
}
export interface Alert {
id: number;
warningDate: DateTime;
endDate: DateTime;
title: string;
}
export interface Notification {
id: number;
date: DateTime;
title: string;
sender: {
text: string;
};
relatedEntityClass: string;
relatedEntityId: number;
}
export interface Participation {
person: Person;
}
export interface AccompanyingCourse {
id: number;
openingDate: DateTime;
socialIssues: SocialIssue[];
participations: Participation[];
emergency: boolean;
confidential: boolean;
}
export interface AccompanyingPeriodWorkReferrerHistory {
id: number;
accompanyingPeriodWork: AccompanyingPeriodWork;

View File

@@ -165,6 +165,7 @@ export interface TicketFilters {
byCreatedBefore: string;
byResponseTimeExceeded: boolean;
byAddresseeToMe: boolean;
byTicketId: number | null;
}
export interface TicketFilterParams {
@@ -179,6 +180,7 @@ export interface TicketFilterParams {
byCreatedBefore?: string;
byResponseTimeExceeded?: string;
byAddresseeToMe?: boolean;
byTicketId?: number;
}
export interface TicketInitForm {

View File

@@ -1,20 +1,11 @@
<template>
<div class="col-12">
<div class="col-12" v-if="!commentHistory.deleted">
<blockquote class="chill-user-quote">
<button
class="btn btn-sm btn-primary float-end"
title="Visible"
@click="visibleComment"
v-if="commentHistory.deleted"
>
<i class="bi bi-eye"></i>
</button>
<button
class="btn btn-sm bg-chill-red float-end text-white"
@click="maskComment"
v-else
@click="deleteComment"
>
<i class="bi bi-eye-slash"></i>
<i class="bi bi-trash"></i>
</button>
<button
class="btn btn-sm btn-edit mx-2 float-end text-white"
@@ -24,14 +15,16 @@
/>
<p v-html="convertMarkdownToHtml(commentHistory.content)"></p>
<span
v-if="commentHistory.deleted"
class="ms-2 d-block text-center fst-italic text-muted"
>
{{ trans(CHILL_TICKET_TICKET_MASK_COMMENT_HINT) }}
</span>
</blockquote>
</div>
<div class="col-12" v-else>
<span class="ms-2 d-block text-center">
{{ trans(CHILL_TICKET_TICKET_MASK_COMMENT_HINT) }}
<button class="btn btn-primary btn-sm ms-2" @click="restoreComment">
{{ trans(CANCEL) }}
</button>
</span>
</div>
<Modal
v-if="editCommentModal"
:show="editCommentModal"
@@ -75,6 +68,7 @@ import {
CHILL_TICKET_TICKET_MASK_COMMENT_SUCCESS,
CHILL_TICKET_TICKET_MASK_COMMENT_HINT,
CHILL_TICKET_TICKET_VISIBLE_COMMENT_SUCCESS,
CANCEL,
} from "translator";
import { useToast } from "vue-toast-notification";
@@ -97,18 +91,17 @@ const saveComment = () => {
toast.success(trans(CHILL_TICKET_TICKET_EDIT_COMMENT_SUCCESS));
};
const maskComment = () => {
store.dispatch("maskComment", props.commentHistory.id);
const deleteComment = () => {
store.dispatch("deleteComment", props.commentHistory.id);
store.commit("addRemovedCommentIds", props.commentHistory.id);
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_MASK_COMMENT_SUCCESS));
};
const visibleComment = () => {
store.dispatch("visibleComment", props.commentHistory.id);
const restoreComment = () => {
store.dispatch("restoreComment", props.commentHistory.id);
editCommentModal.value = false;
toast.success(trans(CHILL_TICKET_TICKET_VISIBLE_COMMENT_SUCCESS));
};
const preprocess = (markdown: string): string => {
return markdown;
};

View File

@@ -0,0 +1,49 @@
<template>
<div class="input-group mb-3">
<input
v-model="ticketId"
type="number"
class="form-control"
:placeholder="trans(CHILL_TICKET_LIST_FILTER_TICKET_ID)"
@input="
ticketId = isNaN(Number(($event.target as HTMLInputElement).value))
? null
: Number(($event.target as HTMLInputElement).value)
"
/>
<span class="input-group-text" v-if="ticketId !== null">
<i
class="fa fa-times chill-red"
style="cursor: pointer"
@click="ticketId = null"
title="clear"
></i>
</span>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
// Translation
import { trans, CHILL_TICKET_LIST_FILTER_TICKET_ID } from "translator";
const props = defineProps<{
modelValue: number | null;
}>();
const ticketId = ref<number | null>(props.modelValue);
watch(
() => props.modelValue,
(newVal) => {
if (newVal === null) {
ticketId.value = null;
}
},
);
const emit = defineEmits(["update:modelValue"]);
watch(ticketId, (ticketId) => {
emit("update:modelValue", ticketId ? ticketId : null);
});
</script>

View File

@@ -8,23 +8,15 @@ import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/
export interface State {
comments: Comment[];
removedCommentIds: Comment["id"][];
}
export const moduleComment: Module<State, RootState> = {
state: () => ({
comments: [] as Comment[],
removedCommentIds: [] as Comment["id"][],
}),
getters: {
canBeDisplayed:
(state: State, getters: unknown, rootState: RootState) =>
(comment: Comment) => {
return (
(comment.deleted &&
comment.createdBy?.username ===
rootState.user.currentUser?.username) ||
!comment.deleted
);
},
canBeEdited:
(state: State, getters: unknown, rootState: RootState) =>
(comment: Comment) => {
@@ -32,8 +24,15 @@ export const moduleComment: Module<State, RootState> = {
comment.createdBy?.username === rootState.user.currentUser?.username
);
},
getRemovedCommentIds(state) {
return state.removedCommentIds;
},
},
mutations: {
addRemovedCommentIds(state, commentId: number) {
state.removedCommentIds.push(commentId);
},
},
mutations: {},
actions: {
async createComment({ commit, rootState }, content: Comment["content"]) {
try {
@@ -64,7 +63,7 @@ export const moduleComment: Module<State, RootState> = {
throw error.name;
}
},
async maskComment({ commit }, id: Comment["id"]) {
async deleteComment({ commit }, id: Comment["id"]) {
try {
const result: Comment = await makeFetch(
"POST",
@@ -77,7 +76,7 @@ export const moduleComment: Module<State, RootState> = {
}
},
async visibleComment({ commit }, id: Comment["id"]) {
async restoreComment({ commit }, id: Comment["id"]) {
try {
const result: Comment = await makeFetch(
"POST",

View File

@@ -73,7 +73,10 @@ export const moduleTicketList: Module<State, RootState> = {
([, value]) =>
value !== undefined &&
value !== null &&
(value === true || (value !== "" && value.length > 0)),
(value === true ||
(typeof value === "number" && !isNaN(value)) ||
(typeof value === "string" && value !== "") ||
(Array.isArray(value) && value.length > 0)),
),
);
params = new URLSearchParams(

View File

@@ -101,7 +101,6 @@ onMounted(async () => {
await store.dispatch("getCurrentUser");
await store.dispatch("fetchTicketList", filters);
await store.dispatch("fetchMotives");
await store.dispatch("fetchUserGroups");
} finally {
isLoading.value = false;
}

View File

@@ -0,0 +1,65 @@
<template>
<label :for="dateId" class="form-label col-12">
{{ label }}
</label>
<div class="d-flex gap-2">
<div class="input-group">
<input
type="date"
:id="dateId"
v-model="modelDate"
class="form-control"
:disabled="disabled"
/>
<input
type="time"
v-model="modelTime"
class="form-control"
:disabled="disabled"
style="max-width: 120px"
placeholder="hh:mm"
/>
<span class="input-group-text" v-if="modelDate">
<i
class="fa fa-times chill-red"
style="cursor: pointer"
@click="clear"
title="clear"
></i>
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
const props = defineProps<{
label: string;
dateId: string;
modelValueDate: string;
modelValueTime: string;
defaultValueTime: string;
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: "update:modelValueDate", value: string): void;
(e: "update:modelValueTime", value: string): void;
}>();
const modelDate = computed({
get: () => props.modelValueDate,
set: (val: string) => emit("update:modelValueDate", val),
});
const modelTime = computed({
get: () => props.modelValueTime,
set: (val: string) => emit("update:modelValueTime", val),
});
function clear() {
emit("update:modelValueDate", "");
emit("update:modelValueTime", props.defaultValueTime);
}
</script>

View File

@@ -43,12 +43,12 @@
}}</label>
<addressee-selector-component
v-model="selectedAddressees"
:suggested="userGroups"
:suggested="[]"
:label="trans(CHILL_TICKET_LIST_FILTER_BY_ADDRESSEES)"
id="addresseeSelector"
/>
</div>
<div class="col-md-6">
<div class="col-md-6 mb-3">
<!-- Filtre par motifs -->
<div class="row">
<label class="form-label" for="motiveSelector">{{
@@ -60,7 +60,7 @@
id="motiveSelector"
/>
<div class="mt-1" style="min-height: 2.2em">
<div class="mb-2" style="min-height: 2em">
<div class="d-flex flex-wrap gap-2">
<span
v-for="motive in selectedMotives"
@@ -79,43 +79,40 @@
</div>
</div>
<!-- Filtre par état actuel -->
<div class="row">
<div class="col-6">
<div class="mb-2">
<label class="form-label pe-2" for="currentState">
{{ trans(CHILL_TICKET_LIST_FILTER_CURRENT_STATE) }}
</label>
<toggle-component
v-model="isClosedToggled"
:on-label="trans(CHILL_TICKET_LIST_FILTER_CLOSED)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_OPEN)"
:classColor="{
on: 'bg-chill-red',
off: 'bg-chill-green',
}"
@update:model-value="handleStateToggle"
id="currentState"
/>
</div>
<div class="d-flex gap-3">
<div>
<label class="form-label pe-2" for="currentState">
{{ trans(CHILL_TICKET_LIST_FILTER_CURRENT_STATE) }}
</label>
<toggle-component
v-model="isClosedToggled"
:on-label="trans(CHILL_TICKET_LIST_FILTER_CLOSED)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_OPEN)"
:classColor="{
on: 'bg-chill-red',
off: 'bg-chill-green',
}"
@update:model-value="handleStateToggle"
id="currentState"
/>
</div>
<!-- Filtre par état d'urgence -->
<div class="col-6">
<div class="mb-2">
<label class="form-label pe-2" for="emergency">
{{ trans(CHILL_TICKET_LIST_FILTER_EMERGENCY) }}
</label>
<toggle-component
v-model="isEmergencyToggled"
:on-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:classColor="{
on: 'bg-warning',
off: 'bg-secondary',
}"
@update:model-value="handleEmergencyToggle"
id="emergency"
/>
</div>
<div>
<label class="form-label pe-2" for="emergency">
{{ trans(CHILL_TICKET_LIST_FILTER_EMERGENCY) }}
</label>
<toggle-component
v-model="isEmergencyToggled"
:on-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:off-label="trans(CHILL_TICKET_LIST_FILTER_EMERGENCY)"
:classColor="{
on: 'bg-warning',
off: 'bg-secondary',
}"
@update:model-value="handleEmergencyToggle"
id="emergency"
/>
</div>
</div>
</div>
@@ -124,6 +121,18 @@
<div class="row">
<!-- Filtre pour temps de réponse dépassé -->
<div class="col-md-6 mb-3">
<div class="form-check">
<input
v-model="filters.byAddresseeToMe"
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 class="form-check">
<input
class="form-check-input"
@@ -141,48 +150,40 @@
{{ trans(CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_WARNING) }}
</small>
</div>
<!-- Filtre par mes tickets -->
<!-- Filtre par numéro de ticket -->
<div class="col-md-6 mb-3">
<div class="d-flex gap-3">
<div class="form-check">
<input
v-model="filters.byAddresseeToMe"
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>
<label class="form-label pe-2" for="ticketSelector">
{{ trans(CHILL_TICKET_LIST_FILTER_BY_TICKET_ID) }}
</label>
<ticket-selector v-model="filters.byTicketId" id="ticketSelector" />
</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"
<date-and-time-selector-component
:label="trans(CHILL_TICKET_LIST_FILTER_CREATED_AFTER)"
date-id="byCreatedAfter"
default-value-time="00:00"
:model-value-date="filters.byCreatedAfter"
:model-value-time="byCreatedAfterTime"
:disabled="filters.byResponseTimeExceeded"
@update:modelValueDate="filters.byCreatedAfter = $event"
@update:modelValueTime="byCreatedAfterTime = $event"
/>
</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"
<date-and-time-selector-component
:label="trans(CHILL_TICKET_LIST_FILTER_CREATED_BEFORE)"
date-id="byCreatedBefore"
default-value-time="23:59"
:model-value-date="filters.byCreatedBefore"
:model-value-time="byCreatedBeforeTime"
:disabled="filters.byResponseTimeExceeded"
@update:modelValueDate="filters.byCreatedBefore = $event"
@update:modelValueTime="byCreatedBeforeTime = $event"
/>
</div>
</div>
@@ -218,10 +219,13 @@
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { useStore } from "vuex";
import type { Person } from "ChillPersonAssets/types";
import type { Motive, TicketFilterParams, TicketFilters } from "../../../types";
import { User, UserGroup, UserGroupOrUser } from "ChillMainAssets/types";
import {
type Motive,
type TicketFilterParams,
type TicketFilters,
} from "../../../types";
import { User, UserGroupOrUser } from "ChillMainAssets/types";
// Translation
import {
@@ -234,6 +238,7 @@ import {
CHILL_TICKET_LIST_FILTER_ADDRESSEES,
CHILL_TICKET_LIST_FILTER_BY_ADDRESSEES,
CHILL_TICKET_LIST_FILTER_BY_MOTIVES,
CHILL_TICKET_LIST_FILTER_BY_TICKET_ID,
CHILL_TICKET_LIST_FILTER_REMOVE,
CHILL_TICKET_LIST_FILTER_OPEN,
CHILL_TICKET_LIST_FILTER_CLOSED,
@@ -254,6 +259,8 @@ import PersonsSelector from "../../TicketApp/components/Person/PersonsSelectorCo
import MotiveSelector from "../../TicketApp/components/Motive/MotiveSelectorComponent.vue";
import AddresseeSelectorComponent from "../../TicketApp/components/Addressee/AddresseeSelectorComponent.vue";
import ToggleComponent from "./ToggleComponent.vue";
import TicketSelector from "../../TicketApp/components/Ticket/TicketSelector.vue";
import DateAndTimeSelectorComponent from "./DateAndTimeSelectorComponent.vue";
// Props
const props = defineProps<{
@@ -267,8 +274,6 @@ const emit = defineEmits<{
"filters-changed": [filters: TicketFilterParams];
}>();
const store = useStore();
// État réactif
const filters = ref<TicketFilters>({
byCurrentState: ["open"],
@@ -277,15 +282,18 @@ const filters = ref<TicketFilters>({
byCreatedBefore: "",
byResponseTimeExceeded: false,
byAddresseeToMe: false,
byTicketId: null,
});
const byCreatedAfterTime = ref("00:00");
const byCreatedBeforeTime = ref("23:59");
// Sélection des personnes
const selectedPersons = ref<Person[]>([]);
const availablePersons = ref<Person[]>(props.availablePersons || []);
// Sélection des utilisateur assigné
const selectedAddressees = ref<UserGroupOrUser[]>([]);
const userGroups = computed(() => store.getters.getUserGroups as UserGroup[]);
// Séléction des créateurs
const selectedCreator = ref<User[]>([]);
@@ -351,10 +359,10 @@ const handleEmergencyToggle = (value: boolean) => {
}
};
// Méthodes
const formatDateToISO = (dateString: string): string => {
if (!dateString) return dateString;
const formatDateToISO = (dateString: string, timeString: string): string => {
const [hours, minutes] = timeString.split(":").map(Number);
const date = new Date(dateString);
date.setHours(hours, minutes, 0, 0);
return date.toISOString();
};
@@ -406,11 +414,17 @@ const applyFilters = (): void => {
}
if (filters.value.byCreatedAfter) {
apiFilters.byCreatedAfter = formatDateToISO(filters.value.byCreatedAfter);
apiFilters.byCreatedAfter = formatDateToISO(
filters.value.byCreatedAfter,
byCreatedAfterTime.value,
);
}
if (filters.value.byCreatedBefore) {
apiFilters.byCreatedBefore = formatDateToISO(filters.value.byCreatedBefore);
apiFilters.byCreatedBefore = formatDateToISO(
filters.value.byCreatedBefore,
byCreatedBeforeTime.value,
);
}
if (filters.value.byResponseTimeExceeded) {
@@ -419,7 +433,12 @@ const applyFilters = (): void => {
if (filters.value.byAddresseeToMe) {
apiFilters.byAddresseeToMe = true;
}
if (filters.value.byAddresseeToMe) {
apiFilters.byAddresseeToMe = true;
}
if (filters.value.byTicketId) {
apiFilters.byTicketId = filters.value.byTicketId;
}
emit("filters-changed", apiFilters);
};
@@ -431,13 +450,14 @@ const resetFilters = (): void => {
byCreatedBefore: "",
byResponseTimeExceeded: false,
byAddresseeToMe: false,
byTicketId: null,
};
selectedPersons.value = [];
selectedCreator.value = [];
selectedAddressees.value = [];
selectedMotives.value = [];
selectedMotive.value = undefined;
isClosedToggled.value = true;
isClosedToggled.value = false;
isEmergencyToggled.value = false;
applyFilters();
};
@@ -446,7 +466,7 @@ const handleResponseTimeExceededChange = (): void => {
if (filters.value.byResponseTimeExceeded) {
filters.value.byCreatedBefore = "";
filters.value.byCreatedAfter = "";
isClosedToggled.value = true;
isClosedToggled.value = false;
}
};
</script>

View File

@@ -11,16 +11,6 @@
<div class="d-flex align-items-center fw-bold">
<i :class="`${actionIcons[history_line.event_type]} me-1`"></i>
<span>{{ explainSentence(history_line) }}</span>
<span
v-if="
history_line.event_type === 'add_comment' &&
history_line.data.deleted
"
class="badge bg-danger ms-2"
>
{{ trans(CHILL_TICKET_TICKET_HISTORY_MASK_COMMENT) }}
</span>
<state-component
:new_state="history_line.data.new_state"
v-if="history_line.event_type == 'state_change'"
@@ -59,10 +49,7 @@
/>
<comment-component
:commentHistory="history_line.data"
v-else-if="
history_line.event_type == 'add_comment' &&
canBeDisplayed(history_line.data)
"
v-else-if="history_line.event_type == 'add_comment'"
/>
<addressee-component
:addressees="history_line.data.addressees"
@@ -105,19 +92,24 @@ import {
CHILL_TICKET_TICKET_HISTORY_STATE_CHANGE,
CHILL_TICKET_TICKET_HISTORY_EMERGENCY_CHANGE,
CHILL_TICKET_TICKET_HISTORY_SET_CALLER,
CHILL_TICKET_TICKET_HISTORY_MASK_COMMENT,
} from "translator";
const props = defineProps<{ history?: TicketHistoryLine[] }>();
const history = props.history ?? [];
const store = useStore();
const actionIcons = ref(store.getters.getActionIcons);
const canBeDisplayed = ref(store.getters.canBeDisplayed);
const actionIcons = ref<Record<string, string>>(store.getters.getActionIcons);
const removedCommentIds = ref<number[]>(store.getters.getRemovedCommentIds);
const filteredHistoryLines = computed(() =>
history.filter(
(line: TicketHistoryLine) => !(line.event_type === "add_person"),
(line: TicketHistoryLine) =>
line.event_type !== "add_person" &&
!(
line.event_type == "add_comment" &&
line.data.deleted &&
!removedCommentIds.value.includes(line.data.id)
),
),
);

View File

@@ -24,6 +24,8 @@ chill_ticket:
addressees: "Par destinataire"
by_addressees: "Par destinataire"
by_motives: "Par motifs"
by_ticket_id: "Par numéro de ticket"
ticket_id: "Numéro de ticket"
current_state: "État actuel"
open: "Ouvert"
closed: "Clôturé"
@@ -55,7 +57,7 @@ chill_ticket:
create_ticket: "Ticket créé"
state_change: ""
emergency_change: ""
mask_comment: "Masqué"
mask_comment: "Supprimer"
previous_tickets: "Précédents tickets"
actions_toolbar:
cancel: "Annuler"
@@ -67,10 +69,10 @@ chill_ticket:
reopen_success: "Rouverture du ticket réussie"
reopen_error: "Erreur lors de la rouverture du ticket"
visible_comment:
success: "Commentaire visible"
success: "Commentaire restauré"
mask_comment:
success: "Commentaire masqué"
hint: "Ce commentaire est masqué; il n'est visible que par vous."
success: "Commentaire supprimé"
hint: "Ce commentaire a été supprimé."
edit_comment:
title: "Éditer le commentaire"
success: "Commentaire modifié"