1277 refacto use symfony translation

This commit is contained in:
Boris Waaub
2025-06-16 10:59:42 +00:00
committed by Julien Fastré
parent 377ae9a9dc
commit a8dd1b3548
47 changed files with 2646 additions and 2400 deletions

View File

@@ -107,3 +107,15 @@ export interface Ticket {
createdAt: DateTime | null;
updatedBy: User | null;
}
export interface addNewPersons {
selected: Selected[];
modal: Modal;
}
export interface Modal {
showModal: boolean;
modalDialogClass: string;
}
export interface Selected {
result: User;
}

View File

@@ -6,13 +6,13 @@
</div>
<action-toolbar-component />
</template>
<script lang="ts">
import { computed, defineComponent, inject, onMounted, ref } from "vue";
<script setup lang="ts">
import { useToast } from "vue-toast-notification";
import { computed, onMounted } from "vue";
import { useStore } from "vuex";
// Types
import { Motive, Ticket } from "../../types";
import { Ticket } from "../../types";
// Components
import TicketSelectorComponent from "./components/TicketSelectorComponent.vue";
@@ -20,43 +20,23 @@ import TicketHistoryListComponent from "./components/TicketHistoryListComponent.
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
import BannerComponent from "./components/BannerComponent.vue";
export default defineComponent({
name: "App",
components: {
TicketSelectorComponent,
TicketHistoryListComponent,
ActionToolbarComponent,
BannerComponent,
},
setup() {
const store = useStore();
const toast = inject("toast") as any;
const store = useStore();
const toast = useToast();
store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket);
store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket);
const motives = computed(() => store.getters.getMotives as Motive[]);
const ticket = computed(() => store.getters.getTicket as Ticket);
const ticket = computed(() => store.getters.getTicket as Ticket);
const ticketHistory = computed(
() => store.getters.getDistinctAddressesHistory,
);
const ticketHistory = computed(() => store.getters.getDistinctAddressesHistory);
onMounted(async () => {
try {
await store.dispatch("fetchMotives");
await store.dispatch("fetchUserGroups");
await store.dispatch("fetchUsers");
} catch (error) {
toast.error(error);
}
});
return {
ticketHistory,
motives,
ticket,
};
},
onMounted(async () => {
try {
await store.dispatch("fetchMotives");
await store.dispatch("fetchUserGroups");
await store.dispatch("fetchUsers");
} catch (error) {
toast.error(error as string);
}
});
</script>

View File

@@ -4,7 +4,7 @@
<div class="tab-content p-2">
<div>
<label class="col-form-label">
{{ $t(`${activeTab}.title`) }}
{{ activeTabTitle }}
</label>
</div>
@@ -36,12 +36,20 @@
@click="activeTab = ''"
class="btn btn-cancel"
>
{{ $t("ticket.cancel") }}
{{
trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
)
}}
</button>
</li>
<li>
<button class="btn btn-save" type="submit">
{{ $t("ticket.save") }}
{{
trans(
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
)
}}
</button>
</li>
</ul>
@@ -78,7 +86,7 @@
"
>
<i :class="actionIcons['set_motive']"></i>
{{ $t("set_motive.title") }}
{{ trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE) }}
</button>
</li>
<li class="nav-item p-2">
@@ -96,7 +104,7 @@
"
>
<i :class="actionIcons['add_comment']"></i>
{{ $t("add_comment.title") }}
{{ trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE) }}
</button>
</li>
<li class="nav-item p-2">
@@ -114,7 +122,7 @@
"
>
<i :class="actionIcons['addressees_state']"></i>
{{ $t("add_addressee.title") }}
{{ trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE) }}
</button>
</li>
<li class="nav-item p-2">
@@ -132,7 +140,7 @@
"
>
<i :class="actionIcons['set_persons']"></i>
Patients concernés
{{ trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE) }}
</button>
</li>
@@ -143,7 +151,7 @@
@click="handleClick()"
>
<i class="fa fa-bolt"></i>
{{ $t("ticket.close") }}
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE) }}
</button>
</li>
</ul>
@@ -151,10 +159,34 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, ref } from "vue";
import { useI18n } from "vue-i18n";
<script setup lang="ts">
import { computed, ref } from "vue";
import { useStore } from "vuex";
import { useToast } from "vue-toast-notification";
// Component
import MotiveSelectorComponent from "./MotiveSelectorComponent.vue";
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
import AddCommentComponent from "./AddCommentComponent.vue";
import PersonsSelectorComponent from "./PersonsSelectorComponent.vue";
// Translations
import {
trans,
CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE,
CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR,
CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS,
CHILL_TICKET_TICKET_ADD_COMMENT_TITLE,
CHILL_TICKET_TICKET_ADD_COMMENT_ERROR,
CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS,
CHILL_TICKET_TICKET_SET_MOTIVE_TITLE,
CHILL_TICKET_TICKET_SET_MOTIVE_ERROR,
CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS,
CHILL_TICKET_TICKET_SET_PERSONS_TITLE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CLOSE,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
} from "translator";
// Types
import {
@@ -164,140 +196,118 @@ import {
} from "../../../../../../../ChillMainBundle/Resources/public/types";
import { Comment, Motive, Ticket } from "../../../types";
// Component
import MotiveSelectorComponent from "./MotiveSelectorComponent.vue";
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
import AddCommentComponent from "./AddCommentComponent.vue";
import PersonsSelectorComponent from "./PersonsSelectorComponent.vue";
const store = useStore();
const toast = useToast();
export default defineComponent({
name: "ActionToolbarComponent",
components: {
PersonsSelectorComponent,
AddCommentComponent,
MotiveSelectorComponent,
AddresseeSelectorComponent,
},
setup() {
const store = useStore();
const { t } = useI18n();
const toast = inject("toast") as any;
const activeTab = ref(
"" as
| ""
| "add_comment"
| "set_motive"
| "add_addressee"
| "set_persons",
);
const activeTab = ref(
"" as "" | "add_comment" | "set_motive" | "add_addressee" | "set_persons",
);
const ticket = computed(() => store.getters.getTicket as Ticket);
const motives = computed(() => store.getters.getMotives as Motive[]);
const userGroups = computed(
() => store.getters.getUserGroups as UserGroup[],
);
const users = computed(() => store.getters.getUsers as User[]);
const hasReturnPath = computed((): boolean => {
const params = new URL(document.location.toString()).searchParams;
return params.has("returnPath");
});
const returnPath = computed((): string => {
const params = new URL(document.location.toString()).searchParams;
const returnPath = params.get("returnPath");
if (null === returnPath) {
throw new Error(
"there isn't any returnPath, please check the existence before",
);
}
return returnPath;
});
const motive = ref(
ticket.value.currentMotive
? ticket.value.currentMotive
: ({} as Motive),
);
const content = ref("" as Comment["content"]);
const addressees = ref(
ticket.value.currentAddressees as UserGroupOrUser[],
);
async function submitAction() {
try {
switch (activeTab.value) {
case "add_comment":
if (!content.value) {
toast.error(t("add_comment.error"));
} else {
await store.dispatch("createComment", {
ticketId: ticket.value.id,
content: content.value,
});
content.value = "";
activeTab.value = "";
toast.success(t("add_comment.success"));
}
break;
case "set_motive":
if (!motive.value.id) {
toast.error(t("set_motive.error"));
} else {
await store.dispatch("createMotive", {
ticketId: ticket.value.id,
motive: motive.value,
});
activeTab.value = "";
toast.success(t("set_motive.success"));
}
break;
case "add_addressee":
if (!addressees.value.length) {
toast.error(t("add_addressee.error"));
} else {
await store.dispatch("setAdressees", {
ticketId: ticket.value.id,
addressees: addressees.value,
});
activeTab.value = "";
toast.success(t("add_addressee.success"));
}
break;
}
} catch (error) {
toast.error(error);
}
}
function handleClick() {
alert("Sera disponible plus tard");
}
const closeAllActions = function () {
activeTab.value = "";
};
return {
actionIcons: ref(store.getters.getActionIcons),
activeTab,
ticket,
motives,
motive,
userGroups,
addressees,
users,
content,
submitAction,
handleClick,
hasReturnPath,
returnPath,
closeAllActions,
};
},
const activeTabTitle = computed((): string => {
switch (activeTab.value) {
case "add_comment":
return trans(CHILL_TICKET_TICKET_ADD_COMMENT_TITLE);
case "set_motive":
return trans(CHILL_TICKET_TICKET_SET_MOTIVE_TITLE);
case "add_addressee":
return trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_TITLE);
case "set_persons":
return trans(CHILL_TICKET_TICKET_SET_PERSONS_TITLE);
default:
return "";
}
});
const ticket = computed(() => store.getters.getTicket as Ticket);
const motives = computed(() => store.getters.getMotives as Motive[]);
const userGroups = computed(() => store.getters.getUserGroups as UserGroup[]);
const users = computed(() => store.getters.getUsers as User[]);
const hasReturnPath = computed((): boolean => {
const params = new URL(document.location.toString()).searchParams;
return params.has("returnPath");
});
const returnPath = computed((): string => {
const params = new URL(document.location.toString()).searchParams;
const returnPath = params.get("returnPath");
if (null === returnPath) {
throw new Error(
"there isn't any returnPath, please check the existence before",
);
}
return returnPath;
});
const motive = ref(
ticket.value.currentMotive ? ticket.value.currentMotive : ({} as Motive),
);
const content = ref("" as Comment["content"]);
const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]);
async function submitAction() {
try {
switch (activeTab.value) {
case "add_comment":
if (!content.value) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_COMMENT_ERROR));
} else {
await store.dispatch("createComment", {
ticketId: ticket.value.id,
content: content.value,
});
content.value = "";
activeTab.value = "";
toast.success(
trans(CHILL_TICKET_TICKET_ADD_COMMENT_SUCCESS),
);
}
break;
case "set_motive":
if (!motive.value.id) {
toast.error(trans(CHILL_TICKET_TICKET_SET_MOTIVE_ERROR));
} else {
await store.dispatch("createMotive", {
ticketId: ticket.value.id,
motive: motive.value,
});
activeTab.value = "";
toast.success(
trans(CHILL_TICKET_TICKET_SET_MOTIVE_SUCCESS),
);
}
break;
case "add_addressee":
if (!addressees.value.length) {
toast.error(trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_ERROR));
} else {
await store.dispatch("setAdressees", {
ticketId: ticket.value.id,
addressees: addressees.value,
});
activeTab.value = "";
toast.success(
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_SUCCESS),
);
}
break;
}
} catch (error) {
toast.error(error as string);
}
}
function handleClick() {
alert("Sera disponible plus tard");
}
function closeAllActions() {
activeTab.value = "";
}
const actionIcons = ref(store.getters.getActionIcons);
</script>
<style lang="scss" scoped>

View File

@@ -6,35 +6,21 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
<script setup lang="ts">
import { ref, watch } from "vue";
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
export default defineComponent({
name: "AddCommentComponent",
props: {
modelValue: {
type: String,
required: false,
},
},
components: {
CommentEditor,
},
emits: ["update:modelValue"],
const props = defineProps<{
modelValue?: string;
}>();
setup(props, ctx) {
const content = ref(props.modelValue);
const emit =
defineEmits<(e: "update:modelValue", value: string | undefined) => void>();
watch(content, (content) => {
ctx.emit("update:modelValue", content);
});
const content = ref(props.modelValue);
return {
content,
};
},
watch(content, (value) => {
emit("update:modelValue", value);
});
</script>

View File

@@ -30,8 +30,11 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent, ref } from "vue";
<script setup lang="ts">
import { computed } from "vue";
// Components
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
// Types
import {
@@ -39,43 +42,32 @@ import {
UserGroup,
UserGroupOrUser,
} from "../../../../../../../ChillMainBundle/Resources/public/types";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
export default defineComponent({
name: "AddresseeComponent",
components: { UserRenderBoxBadge },
props: {
addressees: {
type: Array as PropType<UserGroupOrUser[]>,
required: true,
},
},
setup(props, ctx) {
const userGroups = computed(
() =>
props.addressees.filter(
(addressee) =>
addressee.type == "user_group" &&
addressee.excludeKey == "",
) as UserGroup[],
);
const userGroupLevels = computed(
() =>
props.addressees.filter(
(addressee) =>
addressee.type == "user_group" &&
addressee.excludeKey == "level",
) as UserGroup[],
);
const users = computed(
() =>
props.addressees.filter(
(addressee) => addressee.type == "user",
) as User[],
);
return { userGroups, users, userGroupLevels };
},
});
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[],
);
</script>
<style lang="scss" scoped></style>

View File

@@ -66,9 +66,14 @@
<add-persons
:options="addPersonsOptions"
key="add-person-ticket"
buttonTitle="add_addressee.user_label"
modalTitle="add_addressee.user_label"
ref="addPersons"
:buttonTitle="
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)
"
:modalTitle="
trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL)
"
:selected="selectedValues"
:suggested="suggestedValues"
@addNewPersons="addNewEntity"
/>
<div class="p-2">
@@ -84,145 +89,120 @@
</div>
</template>
<script lang="ts">
import { PropType, computed, defineComponent, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
// Types
import { User, UserGroup, UserGroupOrUser } from "ChillMainAssets/types";
<script lang="ts" setup>
import { ref, watch, defineProps, defineEmits } from "vue";
// Components
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
export default defineComponent({
name: "AddresseeSelectorComponent",
props: {
modelValue: {
type: Array as PropType<UserGroupOrUser[]>,
default: [],
required: false,
},
userGroups: {
type: Array as PropType<UserGroup[]>,
required: true,
},
users: {
type: Array as PropType<User[]>,
required: true,
},
// Types
import type { User, UserGroup, UserGroupOrUser } from "ChillMainAssets/types";
import { SearchOptions, Suggestion } from "ChillPersonAssets/types";
import type { addNewPersons } from "../../../types";
// Translations
import {
CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL,
trans,
} from "translator";
const props = defineProps<{
modelValue?: UserGroupOrUser[];
userGroups: UserGroup[];
users: User[];
}>();
const selectedValues = ref<Suggestion[]>([]);
const suggestedValues = ref<Suggestion[]>([]);
const emit =
defineEmits<(e: "update:modelValue", value: UserGroupOrUser[]) => void>();
const addressees = ref<UserGroupOrUser[]>([...(props.modelValue ?? [])]);
const userGroupsInit = [
...(props.modelValue ?? []).filter(
(addressee: UserGroupOrUser) => addressee.type == "user_group",
),
] as UserGroup[];
const userGroupLevel = ref<UserGroup | Record<string, never>>(
(userGroupsInit.filter(
(userGroup: UserGroup) => userGroup.excludeKey == "level",
)[0] as UserGroup) ?? {},
);
const userGroup = ref<UserGroup[]>(
userGroupsInit.filter(
(userGroup: UserGroup) => userGroup.excludeKey == "",
) as UserGroup[],
);
const users = ref<User[]>([
...(props.modelValue ?? []).filter(
(addressee: UserGroupOrUser) => addressee.type == "user",
),
] as User[]);
const addPersonsOptions = {
uniq: false,
type: ["user"],
priority: null,
button: {
size: "btn-sm",
class: "btn-submit",
},
components: {
AddPersons,
},
emits: ["update:modelValue"],
} as SearchOptions;
setup(props, ctx) {
const addressees = ref([...props.modelValue] as UserGroupOrUser[]);
const userGroups = [
...props.modelValue.filter(
(addressee) => addressee.type == "user_group",
),
] as UserGroup[];
function getUserGroupBtnColor(userGroup: UserGroup) {
return [
`color: ${userGroup.foregroundColor};
.btn-check:checked + .btn-${userGroup.id} {
color: ${userGroup.foregroundColor};
background-color: ${userGroup.backgroundColor};
}`,
];
}
const userGroupLevel = ref(
userGroups.filter(
(userGroup) => userGroup.excludeKey == "level",
)[0] as UserGroup | {},
);
const userGroup = ref(
userGroups.filter(
(userGroup) => userGroup.excludeKey == "",
) as UserGroup[],
);
const users = ref([
...props.modelValue.filter((addressee) => addressee.type == "user"),
] as User[]);
const addPersons = ref();
function addNewEntity(datas: addNewPersons) {
const { selected } = datas;
users.value = selected.map((selected) => selected.result);
addressees.value = addressees.value.filter(
(addressee) => addressee.type === "user_group",
);
addressees.value = [...addressees.value, ...users.value];
emit("update:modelValue", addressees.value);
selectedValues.value = [];
suggestedValues.value = [];
}
const { t } = useI18n();
function removeUser(user: User) {
users.value.splice(users.value.indexOf(user), 1);
addressees.value = addressees.value.filter(
(addressee) => addressee.id !== user.id,
);
emit("update:modelValue", addressees.value);
}
function getUserGroupBtnColor(userGroup: UserGroup) {
return [
`color: ${userGroup.foregroundColor};
.btn-check:checked + .btn-${userGroup.id} {
color: ${userGroup.foregroundColor};
background-color: ${userGroup.backgroundColor};
}`,
];
}
function addNewEntity(datas: any) {
const { selected, modal } = datas;
users.value = selected.map((selected: any) => selected.result);
addressees.value = addressees.value.filter(
(addressee) => addressee.type === "user_group",
);
addressees.value = [...addressees.value, ...users.value];
ctx.emit("update:modelValue", addressees.value);
addPersons.value.resetSearch();
modal.showModal = false;
}
watch(userGroupLevel, (userGroupLevelAdd, userGroupLevelRem) => {
const index = addressees.value.indexOf(userGroupLevelRem as UserGroup);
if (index !== -1) {
addressees.value.splice(index, 1);
}
addressees.value.push(userGroupLevelAdd as UserGroup);
emit("update:modelValue", addressees.value);
});
const addPersonsOptions = computed(() => {
return {
uniq: false,
type: ["user"],
priority: null,
button: {
size: "btn-sm",
class: "btn-submit",
},
};
});
function removeUser(user: User) {
users.value.splice(users.value.indexOf(user), 1);
addressees.value = addressees.value.filter(
(addressee) => addressee.id !== user.id,
);
ctx.emit("update:modelValue", addressees.value);
}
watch(userGroupLevel, (userGroupLevelAdd, userGroupLevelRem) => {
const index = addressees.value.indexOf(
userGroupLevelRem as UserGroup,
);
if (index !== -1) {
addressees.value.splice(index, 1);
}
addressees.value.push(userGroupLevelAdd as UserGroup);
ctx.emit("update:modelValue", addressees.value);
});
watch(userGroup, (userGroupAdd) => {
const userGroupLevel = addressees.value.filter(
(addressee) =>
addressee.type == "user_group" &&
addressee.excludeKey == "level",
) as UserGroup[];
const users = addressees.value.filter(
(addressee) => addressee.type == "user",
) as User[];
addressees.value = [...users, ...userGroupLevel, ...userGroupAdd];
ctx.emit("update:modelValue", addressees.value);
});
return {
addressees,
userGroupLevel,
userGroup,
users,
addPersons,
addPersonsOptions,
addNewEntity,
removeUser,
getUserGroupBtnColor,
customUserGroupLabel(selectedUserGroup: UserGroup) {
return selectedUserGroup.label
? selectedUserGroup.label.fr
: t("add_addresseeuser_group_label");
},
};
},
watch(userGroup, (userGroupAdd) => {
const userGroupLevelArr = addressees.value.filter(
(addressee) =>
addressee.type == "user_group" && addressee.excludeKey == "level",
) as UserGroup[];
const usersArr = addressees.value.filter(
(addressee) => addressee.type == "user",
) as User[];
addressees.value = [...usersArr, ...userGroupLevelArr, ...userGroupAdd];
emit("update:modelValue", addressees.value);
});
</script>

View File

@@ -8,7 +8,7 @@
{{ ticket.currentMotive.label.fr }}
</h1>
<p class="chill-no-data-statement" v-else>
{{ $t("banner.no_motive") }}
{{ trans(CHILL_TICKET_TICKET_BANNER_NO_MOTIVE) }}
</p>
</div>
@@ -18,13 +18,13 @@
class="badge text-bg-chill-green text-white"
style="font-size: 1rem"
>
{{ $t("banner.open") }}
{{ trans(CHILL_TICKET_TICKET_BANNER_OPEN) }}
</span>
</div>
<div class="d-flex justify-content-end">
<p class="created-at-timespan" v-if="ticket.createdAt">
{{
$t("banner.since", {
trans(CHILL_TICKET_TICKET_BANNER_SINCE, {
time: since,
})
}}
@@ -39,7 +39,7 @@
<div class="row justify-content-between">
<div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0">
<h3 class="text-primary">
{{ $t("banner.concerned_patient") }}
{{ trans(CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER) }}
</h3>
<on-the-fly
v-for="person in ticket.currentPersons"
@@ -49,10 +49,13 @@
:buttonText="person.textAge"
:displayBadge="'true' === 'true'"
action="show"
CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER
></on-the-fly>
</div>
<div class="col-md-6 col-sm-12">
<h3 class="text-primary">{{ $t("banner.speaker") }}</h3>
<h3 class="text-primary">
{{ trans(CHILL_TICKET_TICKET_BANNER_SPEAKER) }}
</h3>
<addressee-component
:addressees="ticket.currentAddressees"
/>
@@ -69,81 +72,91 @@
}
</style>
<script lang="ts">
import { PropType, computed, defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
<script setup lang="ts">
import { ref, computed } from "vue";
// Components
import PersonRenderBox from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue";
import AddresseeComponent from "./AddresseeComponent.vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
// Types
import { Ticket } from "../../../types";
import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
export default defineComponent({
name: "BannerComponent",
props: {
ticket: {
type: Object as PropType<Ticket>,
required: true,
},
},
components: {
OnTheFly,
PersonRenderBox,
AddresseeComponent,
},
setup(props) {
const { t } = useI18n();
const today = ref(new Date());
const createdAt = ref(props.ticket.createdAt);
// Translations
import {
trans,
CHILL_TICKET_TICKET_BANNER_NO_MOTIVE,
CHILL_TICKET_TICKET_BANNER_OPEN,
CHILL_TICKET_TICKET_BANNER_SINCE,
CHILL_TICKET_TICKET_BANNER_CONCERNED_USAGER,
CHILL_TICKET_TICKET_BANNER_SPEAKER,
CHILL_TICKET_TICKET_BANNER_DAYS,
CHILL_TICKET_TICKET_BANNER_HOURS,
CHILL_TICKET_TICKET_BANNER_MINUTES,
CHILL_TICKET_TICKET_BANNER_SECONDS,
CHILL_TICKET_TICKET_BANNER_AND,
} from "translator";
setInterval(function () {
today.value = new Date();
}, 5000);
const props = defineProps<{
ticket: Ticket;
}>();
const since = computed(() => {
if (null === createdAt.value) {
return "";
}
const date = ISOToDatetime(createdAt.value.datetime);
const today = ref(new Date());
const createdAt = ref(props.ticket.createdAt);
if (null === date) {
return "";
}
setInterval(() => {
today.value = new Date();
}, 5000);
const timeDiff = Math.abs(today.value.getTime() - date.getTime());
const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
const hoursDiff = Math.floor(
(timeDiff % (1000 * 3600 * 24)) / (1000 * 3600),
);
const minutesDiff = Math.floor(
(timeDiff % (1000 * 3600)) / (1000 * 60),
);
const secondsDiff = Math.floor((timeDiff % (1000 * 60)) / 1000);
const since = computed(() => {
if (createdAt.value == null) {
return "";
}
const date = ISOToDatetime(createdAt.value.datetime);
if (daysDiff < 1 && hoursDiff < 1 && minutesDiff < 1) {
return `${t("banner.seconds", { count: secondsDiff })}`;
} else if (daysDiff < 1 && hoursDiff < 1) {
return `${t("banner.minutes", { count: minutesDiff })}`;
} else if (daysDiff < 1) {
return `${t("banner.hours", { count: hoursDiff })}
${t("banner.minutes", { count: minutesDiff })}`;
} else {
return `${t("banner.days", { count: daysDiff })}, ${t(
"banner.hours",
{
count: hoursDiff,
},
)} ${t("banner.minutes", {
count: minutesDiff,
})}`;
}
if (date == null) {
return "";
}
const timeDiff = Math.abs(today.value.getTime() - date.getTime());
const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
const hoursDiff = Math.floor(
(timeDiff % (1000 * 3600 * 24)) / (1000 * 3600),
);
const minutesDiff = Math.floor((timeDiff % (1000 * 3600)) / (1000 * 60));
const secondsDiff = Math.floor((timeDiff % (1000 * 60)) / 1000);
// On construit la liste des parties à afficher
const parts: string[] = [];
if (daysDiff > 0) {
parts.push(trans(CHILL_TICKET_TICKET_BANNER_DAYS, { count: daysDiff }));
}
if (hoursDiff > 0 || daysDiff > 0) {
parts.push(
trans(CHILL_TICKET_TICKET_BANNER_HOURS, { count: hoursDiff }),
);
}
if (minutesDiff > 0 || hoursDiff > 0 || daysDiff > 0) {
parts.push(
trans(CHILL_TICKET_TICKET_BANNER_MINUTES, { count: minutesDiff }),
);
}
if (parts.length === 0) {
return trans(CHILL_TICKET_TICKET_BANNER_SECONDS, {
count: secondsDiff,
});
return { since };
},
}
if (parts.length > 1) {
const last = parts.pop();
return (
parts.join(", ") +
" " +
trans(CHILL_TICKET_TICKET_BANNER_AND) +
" " +
last
);
}
return parts[0];
});
</script>

View File

@@ -10,10 +10,10 @@
open-direction="top"
:multiple="false"
:searchable="true"
:placeholder="$t('set_motive.label')"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
:placeholder="trans(CHILL_TICKET_TICKET_SET_MOTIVE_LABEL)"
:select-label="trans(MULTISELECT_SELECT_LABEL)"
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
:options="motives"
v-model="motive"
class="mb-4"
@@ -22,47 +22,46 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
<script setup lang="ts">
import { ref, watch } from "vue";
import VueMultiselect from "vue-multiselect";
// Types
import { Motive } from "../../../types";
export default defineComponent({
name: "MotiveSelectorComponent",
props: {
modelValue: {
type: Object as PropType<Motive>,
required: false,
},
motives: {
type: Object as PropType<Motive[]>,
required: true,
},
},
components: {
VueMultiselect,
},
emits: ["update:modelValue"],
// Translations
import {
trans,
CHILL_TICKET_TICKET_SET_MOTIVE_LABEL,
MULTISELECT_SELECT_LABEL,
MULTISELECT_DESELECT_LABEL,
MULTISELECT_SELECTED_LABEL,
} from "translator";
setup(props, ctx) {
const motive = ref(props.modelValue);
const { t } = useI18n();
const props = defineProps<{
modelValue?: Motive;
motives: Motive[];
}>();
watch(motive, (motive) => {
ctx.emit("update:modelValue", motive);
});
const emit =
defineEmits<(e: "update:modelValue", value: Motive | undefined) => void>();
return {
motive,
customLabel(motive: Motive) {
return motive.label ? motive.label.fr : t("set_motive.label");
},
};
},
const motive = ref(props.modelValue);
watch(motive, (val) => {
emit("update:modelValue", val);
});
watch(
() => props.modelValue,
(val) => {
motive.value = val;
},
);
function customLabel(motive: Motive) {
return motive?.label?.fr ?? trans(CHILL_TICKET_TICKET_SET_MOTIVE_LABEL);
}
</script>
<style lang="scss" scoped>

View File

@@ -1,12 +1,68 @@
<template>
<div>
<div style="display: flex; flex-direction: column; align-items: center">
<add-persons
:options="addPersonsOptions"
key="add-person-selector"
:buttonTitle="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
:modalTitle="trans(CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL)"
:selected="selectedValues"
:suggested="suggestedValues"
@addNewPersons="addNewEntity"
/>
<div class="p-2">
<ul class="list-suggest inline remove-items">
<li v-for="person in currentPersons" :key="person.id">
<span
:title="`${person.firstName} ${person.lastName}`"
@click="removePerson(person)"
>
{{ `${person.firstName} ${person.lastName}` }}
</span>
</li>
</ul>
</div>
</div>
</div>
<ul class="record_actions">
<li class="cancel">
<button
class="btn btn-cancel"
type="button"
@click="emit('closeRequested')"
>
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL) }}
</button>
</li>
<li>
<button class="btn btn-save" type="submit" @click.prevent="save">
{{ trans(CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE) }}
</button>
</li>
</ul>
</template>
<script setup lang="ts">
import { computed, inject, reactive, ref } from "vue";
import { useStore } from "vuex";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import { computed, inject, reactive } from "vue";
import { Ticket } from "../../../types";
import { Person } from "../../../../../../../ChillPersonBundle/Resources/public/types";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import { ToastPluginApi } from "vue-toast-notification";
// Components
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
// Types
import { SearchOptions, Suggestion, Person } from "ChillPersonAssets/types";
import { Ticket } from "../../../types";
// Translations
import {
trans,
CHILL_TICKET_TICKET_SET_PERSONS_USER_LABEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_CANCEL,
CHILL_TICKET_TICKET_ACTIONS_TOOLBAR_SAVE,
CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS,
} from "translator";
const emit = defineEmits<(e: "closeRequested") => void>();
const store = useStore();
@@ -19,9 +75,13 @@ const addPersonsOptions = {
type: ["person"],
priority: null,
button: {
size: "btn-sm",
class: "btn-submit",
},
};
} as SearchOptions;
const selectedValues = ref<Suggestion[]>([]);
const suggestedValues = ref<Suggestion[]>([]);
const added: Person[] = reactive([]);
const removed: Person[] = reactive([]);
@@ -50,14 +110,12 @@ const removePerson = (p: Person) => {
removed.push(p);
};
const addNewEntity = (n: {
modal: { showModal: boolean };
selected: { result: Person }[];
}) => {
n.modal.showModal = false;
const addNewEntity = (n: { selected: { result: Person }[] }) => {
for (let p of n.selected) {
added.push(p.result);
}
selectedValues.value = [];
suggestedValues.value = [];
};
const save = async function (): Promise<void> {
@@ -65,64 +123,14 @@ const save = async function (): Promise<void> {
await store.dispatch("setPersons", {
persons: computeCurrentPersons(persons.value, added, removed),
});
toast.success("Patients concernés sauvegardés");
} catch (e: any) {
console.error("error while saving", e);
toast.error((e as Error).message);
toast.success(trans(CHILL_TICKET_TICKET_SET_PERSONS_SUCCESS));
} catch (error) {
toast.error((error as Error).message);
return Promise.resolve();
}
emit("closeRequested");
};
</script>
<template>
<div>
<ul v-if="currentPersons.length > 0" class="person-list">
<li v-for="person in currentPersons" :key="person.id">
<on-the-fly
:type="person.type"
:id="person.id"
:buttonText="person.textAge"
:displayBadge="'true' === 'true'"
action="show"
></on-the-fly>
<button
type="button"
class="btn btn-delete remove-person"
@click="removePerson(person)"
></button>
</li>
</ul>
<p v-else class="chill-no-data-statement">Aucun patient</p>
</div>
<ul class="record_actions">
<li class="cancel">
<button
class="btn btn-cancel"
type="button"
@click="emit('closeRequested')"
>
{{ $t("ticket.cancel") }}
</button>
</li>
<li>
<add-persons
:options="addPersonsOptions"
key="add-person-ticket"
buttonTitle="set_persons.user_label"
modalTitle="set_persons.user_label"
ref="addPersons"
@addNewPersons="addNewEntity"
/>
</li>
<li>
<button class="btn btn-save" type="submit" @click.prevent="save">
{{ $t("ticket.save") }}
</button>
</li>
</ul>
</template>
<style scoped lang="scss">
ul.person-list {
list-style-type: none;

View File

@@ -3,31 +3,16 @@
<addressee-component :addressees="addressees" />
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from "vue";
<script setup lang="ts">
// Types
import { UserGroupOrUser } from "../../../../../../../ChillMainBundle/Resources/public/types";
// Components
import AddresseeComponent from "./AddresseeComponent.vue";
export default defineComponent({
name: "TicketHistoryAddresseeComponenvt",
props: {
addressees: {
type: Array as PropType<UserGroupOrUser[]>,
required: true,
},
},
components: {
AddresseeComponent,
},
setup() {
return {};
},
});
defineProps<{
addressees: UserGroupOrUser[];
}>();
</script>
<style lang="scss" scoped></style>

View File

@@ -6,55 +6,41 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from "vue";
<script setup lang="ts">
import { marked } from "marked";
import DOMPurify from "dompurify";
// Types
import { Comment } from "../../../types";
export default defineComponent({
name: "TicketHistoryCommentComponent",
props: {
commentHistory: {
type: Object as PropType<Comment>,
required: true,
},
},
setup() {
const preprocess = (markdown: string): string => {
return markdown;
};
defineProps<{ commentHistory: Comment }>();
const postprocess = (html: string): string => {
DOMPurify.addHook("afterSanitizeAttributes", (node: any) => {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
if (
!node.hasAttribute("target") &&
(node.hasAttribute("xlink:href") ||
node.hasAttribute("href"))
) {
node.setAttribute("xlink:show", "new");
}
});
const preprocess = (markdown: string): string => {
return markdown;
};
return DOMPurify.sanitize(html);
};
const postprocess = (html: string): string => {
DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
if (
!node.hasAttribute("target") &&
(node.hasAttribute("xlink:href") || node.hasAttribute("href"))
) {
node.setAttribute("xlink:show", "new");
}
});
const convertMarkdownToHtml = (markdown: string): string => {
marked.use({ hooks: { postprocess, preprocess } });
const rawHtml = marked(markdown) as string;
return rawHtml;
};
return {
convertMarkdownToHtml,
};
},
});
return DOMPurify.sanitize(html);
};
const convertMarkdownToHtml = (markdown: string): string => {
marked.use({ hooks: { postprocess, preprocess } });
const rawHtml = marked(markdown) as string;
return rawHtml;
};
</script>
<style lang="scss" scoped></style>

View File

@@ -1,4 +1,9 @@
<template>
<p>Ticket créé par {{ props.by.text }}</p>
</template>
<script setup lang="ts">
// Types
import { User } from "../../../../../../../ChillMainBundle/Resources/public/types";
interface TicketHistoryCreateComponentConfig {
@@ -8,8 +13,4 @@ interface TicketHistoryCreateComponentConfig {
const props = defineProps<TicketHistoryCreateComponentConfig>();
</script>
<template>
<p>Ticket créé par {{ props.by.text }}</p>
</template>
<style scoped lang="scss"></style>

View File

@@ -50,9 +50,8 @@
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent, ref, computed } from "vue";
<script setup lang="ts">
import { ref } from "vue";
import { useStore } from "vuex";
// Types
@@ -66,72 +65,41 @@ import TicketHistoryCommentComponent from "./TicketHistoryCommentComponent.vue";
import TicketHistoryAddresseeComponent from "./TicketHistoryAddresseeComponent.vue";
import TicketHistoryCreateComponent from "./TicketHistoryCreateComponent.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
// Utils
import { ISOToDatetime } from "../../../../../../../ChillMainBundle/Resources/public/chill/js/date";
export default defineComponent({
name: "TicketHistoryListComponent",
components: {
UserRenderBoxBadge,
TicketHistoryPersonComponent,
TicketHistoryMotiveComponent,
TicketHistoryCommentComponent,
TicketHistoryAddresseeComponent,
TicketHistoryCreateComponent,
},
props: {
history: {
type: Array as PropType<TicketHistoryLine[]>,
required: true,
},
},
defineProps<{ history: TicketHistoryLine[] }>();
setup() {
const store = useStore();
const store = useStore();
const explainSentence = (history: TicketHistoryLine): string => {
switch (history.event_type) {
case "add_comment":
return "Nouveau commentaire";
case "addressees_state":
return "Attributions";
case "persons_state":
return "Patients concernés";
case "set_motive":
return "Nouveau motifs";
case "create_ticket":
return "Ticket créé";
}
};
const actionIcons = ref(store.getters.getActionIcons);
function formatDate(d: DateTime): string {
const date = ISOToDatetime(d.datetime);
if (date === null) {
return "";
}
const month = date.toLocaleString("default", { month: "long" });
return `${date.getDate()} ${month} ${date.getFullYear()}, ${date.toLocaleTimeString()}`;
}
return {
actionIcons: ref(store.getters.getActionIcons),
formatDate,
explainSentence,
};
},
});
</script>
<style lang="scss" scoped>
div.history-header {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
& > div.description {
margin-right: auto;
function explainSentence(history: TicketHistoryLine): string {
switch (history.event_type) {
case "add_comment":
return "Nouveau commentaire";
case "addressees_state":
return "Attributions";
case "persons_state":
return "Usagés concernés";
case "set_motive":
return "Nouveau motifs";
case "create_ticket":
return "Ticket créé";
default:
return "";
}
}
</style>
function formatDate(d: DateTime): string {
const date = ISOToDatetime(d.datetime);
if (date === null) {
return "";
}
const month = date.toLocaleString("default", { month: "long" });
return `${date.getDate()} ${month} ${date.getFullYear()}, ${date.toLocaleTimeString()}`;
}
</script>

View File

@@ -4,23 +4,11 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from "vue";
<script lang="ts" setup>
// Types
import { MotiveHistory } from "../../../types";
export default defineComponent({
name: "TicketHistoryMotiveComponent",
props: {
motiveHistory: {
type: Object as PropType<MotiveHistory>,
required: true,
},
},
setup() {},
});
defineProps<{ motiveHistory: MotiveHistory }>();
</script>
<style lang="scss" scoped></style>

View File

@@ -6,7 +6,7 @@
:type="person.type"
:id="person.id"
:buttonText="person.textAge"
:displayBadge="'true' === 'true'"
:displayBadge="true"
action="show"
></on-the-fly>
</li>
@@ -14,27 +14,13 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from "vue";
// Type
import { PersonsState } from "../../../types";
<script setup lang="ts">
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
export default defineComponent({
name: "TicketHistoryPersonComponent",
props: {
personHistory: {
type: Object as PropType<PersonsState>,
required: true,
},
},
components: {
OnTheFly,
},
// Types
import { PersonsState } from "../../../types";
setup() {},
});
defineProps<{ personHistory: PersonsState }>();
</script>
<style lang="scss" scoped>

View File

@@ -7,7 +7,7 @@
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ $t("ticket.previous_tickets") }}
{{ trans(CHILL_TICKET_TICKET_PREVIOUS_TICKETS) }}
<span
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-chill-green"
>
@@ -19,27 +19,18 @@
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from "vue";
<script setup lang="ts">
// Translations
import { trans, CHILL_TICKET_TICKET_PREVIOUS_TICKETS } from "translator";
// Types
import { Ticket } from "../../../types";
export default defineComponent({
name: "TicketSelectorComponent",
props: {
tickets: {
type: Object as PropType<Ticket[]>,
required: true,
},
},
setup() {
function handleClick() {
alert("Sera disponible plus tard");
}
return { handleClick };
},
});
defineProps<{ tickets: Ticket[] }>();
function handleClick() {
alert("Sera disponible plus tard");
}
</script>
<style lang="scss" scoped></style>

View File

@@ -1,56 +0,0 @@
import { multiSelectMessages } from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import { personMessages } from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_js/i18n";
const messages = {
fr: {
ticket: {
previous_tickets: "Précédents tickets",
cancel: "Annuler",
save: "Enregistrer",
close: "Fermer",
},
history: {
person: "Ouverture par appel téléphonique de ",
user: "Prise en charge par ",
},
add_comment: {
title: "Commentaire",
label: "Ajouter un commentaire",
success: "Commentaire enregistré",
content: "Ajouter un commentaire",
error: "Aucun commentaire ajouté",
},
set_motive: {
title: "Motif",
label: "Choisir un motif",
success: "Motif enregistré",
error: "Aucun motif sélectionné",
},
add_addressee: {
title: "Attribuer",
user_group_label: "Attributer à un groupe",
user_label: "Attribuer à un ou plusieurs utilisateurs",
success: "Attribution effectuée",
error: "Aucun destinataire sélectionné",
},
set_persons: {
title: "Patients concernés",
user_label: "Ajouter un patient",
},
banner: {
concerned_patient: "Patients concernés",
speaker: "Attribué à",
open: "Ouvert",
since: "Depuis {time}",
and: "et",
days: "|1 jour|{count} jours",
hours: "|1 heure et|{count} heures",
minutes: "|1 minute|{count} minutes",
seconds: "|1 seconde|{count} secondes",
no_motive: "Pas de motif",
},
},
};
Object.assign(messages.fr, multiSelectMessages.fr);
Object.assign(messages.fr, personMessages.fr);
export default messages;

View File

@@ -1,13 +1,10 @@
import App from "./App.vue";
import { createApp } from "vue";
import { _createI18n } from "../../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
import VueToast from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
import { store } from "./store";
import messages from "./i18n/messages";
declare global {
interface Window {
@@ -15,16 +12,11 @@ declare global {
}
}
const i18n = _createI18n(messages, false);
const _app = createApp({
template: "<app></app>",
});
_app.use(store)
.use(i18n)
// Cant use this.$toast in components in composition API so we need to provide it
// Fix: with vue-toast-notification@^3
.use(VueToast)
.provide("toast", _app.config.globalProperties.$toast)
.component("app", App)

View File

@@ -0,0 +1,66 @@
chill_ticket:
list:
title: Tickets
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
ticket:
previous_tickets: "Précédents tickets"
actions_toolbar:
cancel: "Annuler"
save: "Enregistrer"
close: "Fermer"
add_comment:
title: "Commentaire"
label: "Ajouter un commentaire"
success: "Commentaire enregistré"
content: "Ajouter un commentaire"
error: "Aucun commentaire ajouté"
set_motive:
title: "Motif"
label: "Choisir un motif"
success: "Motif enregistré"
error: "Aucun motif sélectionné"
add_addressee:
title: "Attribuer"
user_group_label: "Attributer à un groupe"
user_label: "Attribuer à un ou plusieurs utilisateurs"
success: "Attribution effectuée"
error: "Aucun destinataire sélectionné"
set_persons:
title: "Usagers concernés"
user_label: "Ajouter un usager"
success: "Usager ajouté"
error: "Aucun usager sélectionné"
banner:
concerned_usager: "Usagers concernés"
speaker: "Attribué à"
open: "Ouvert"
since: "Depuis {time}"
and: "et"
days: >-
{count, plural,
=0 {aucun jour}
=1 {1 jour}
other {# jours}
}
hours: >-
{count, plural,
=0 {aucune heure}
=1 {1 heure}
other {# heures}
}
minutes: >-
{count, plural,
=0 {aucune minute}
=1 {1 minute}
other {# minutes}
}
seconds: >-
{count, plural,
=0 {aucune seconde}
=1 {1 seconde}
other {# secondes}
}
no_motive: "Pas de motif"

View File

@@ -1,9 +0,0 @@
chill_ticket:
list:
title: Tickets
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