mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-17 02:32:50 +00:00
Merge branch 'ticket-app-master' into ticket/64-identifiants-person
This commit is contained in:
@@ -11,38 +11,22 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\TicketBundle\Controller;
|
namespace Chill\TicketBundle\Controller;
|
||||||
|
|
||||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
|
||||||
use Chill\TicketBundle\Entity\Motive;
|
|
||||||
use Chill\TicketBundle\Repository\MotiveRepository;
|
|
||||||
use Chill\TicketBundle\Repository\TicketRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Error\LoaderError;
|
|
||||||
use Twig\Error\RuntimeError;
|
|
||||||
use Twig\Error\SyntaxError;
|
|
||||||
|
|
||||||
final readonly class TicketListController
|
final readonly class TicketListController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private Security $security,
|
private Security $security,
|
||||||
private TicketRepositoryInterface $ticketRepository,
|
|
||||||
private Environment $twig,
|
private Environment $twig,
|
||||||
private FilterOrderHelperFactory $filterOrderHelperFactory,
|
|
||||||
// private MotiveRepository $motiveRepository,
|
|
||||||
// private TranslatableStringHelperInterface $translatableStringHelper,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws RuntimeError
|
|
||||||
* @throws SyntaxError
|
|
||||||
* @throws LoaderError
|
|
||||||
*/
|
|
||||||
#[Route('/{_locale}/ticket/ticket/list', name: 'chill_ticket_ticket_list')]
|
#[Route('/{_locale}/ticket/ticket/list', name: 'chill_ticket_ticket_list')]
|
||||||
public function __invoke(Request $request): Response
|
public function __invoke(Request $request): Response
|
||||||
{
|
{
|
||||||
@@ -50,35 +34,22 @@ final readonly class TicketListController
|
|||||||
throw new AccessDeniedHttpException('only user can access this page');
|
throw new AccessDeniedHttpException('only user can access this page');
|
||||||
}
|
}
|
||||||
|
|
||||||
$filter = $this->buildFilter();
|
|
||||||
|
|
||||||
$tickets = $this->ticketRepository->findAllOrdered();
|
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
$this->twig->render('@ChillTicket/Ticket/list.html.twig', [
|
$this->twig->render('@ChillTicket/Ticket/list.html.twig')
|
||||||
'tickets' => $tickets,
|
|
||||||
'filter' => $filter,
|
|
||||||
])
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildFilter(): FilterOrderHelper
|
#[Route('/{_locale}/ticket/by-person/{id}/list', name: 'chill_person_ticket_list')]
|
||||||
|
public function listByPerson(Request $request, Person $person): Response
|
||||||
{
|
{
|
||||||
// $motives = $this->motiveRepository->findAll();
|
if (!$this->security->isGranted(PersonVoter::SEE, $person)) {
|
||||||
|
throw new AccessDeniedHttpException('you are not allowed to see this person');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->filterOrderHelperFactory
|
return new Response(
|
||||||
->create(self::class)
|
$this->twig->render('@ChillTicket/Person/list.html.twig', [
|
||||||
->addSingleCheckbox('to_me', 'chill_ticket.list.filter.to_me')
|
'person' => $person,
|
||||||
->addSingleCheckbox('in_alert', 'chill_ticket.list.filter.in_alert')
|
|
||||||
->addDateRange('created_between', 'chill_ticket.list.filter.created_between')
|
|
||||||
/*
|
|
||||||
->addEntityChoice('by_motive', 'chill_ticket.list.filter.by_motive', Motive::class, $motives, [
|
|
||||||
'choice_label' => fn (Motive $motive) => $this->translatableStringHelper->localize($motive->getLabel()),
|
|
||||||
'expanded' => true,
|
|
||||||
'multiple' => true,
|
|
||||||
'attr' => ['class' => 'select2'],
|
|
||||||
])
|
])
|
||||||
*/
|
);
|
||||||
->build();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -46,7 +46,7 @@ final class LoadMotives extends Fixture implements FixtureGroupInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$labels = explode(' > ', $data[0]);
|
$labels = explode(' > ', (string) $data[0]);
|
||||||
$parent = null;
|
$parent = null;
|
||||||
|
|
||||||
while (count($labels) > 0) {
|
while (count($labels) > 0) {
|
||||||
|
69
src/Bundle/ChillTicketBundle/src/Menu/PersonMenuBuilder.php
Normal file
69
src/Bundle/ChillTicketBundle/src/Menu/PersonMenuBuilder.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\TicketBundle\Menu;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
|
use Chill\TicketBundle\Repository\TicketRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
|
use Knp\Menu\MenuItem;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add menu entrie to person menu.
|
||||||
|
*
|
||||||
|
* Menu entries added :
|
||||||
|
*
|
||||||
|
* - person details ;
|
||||||
|
* - accompanying period (if `visible`)
|
||||||
|
*
|
||||||
|
* @implements LocalMenuBuilderInterface<array{person: Person}>
|
||||||
|
*/
|
||||||
|
class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AuthorizationCheckerInterface $authorizationChecker,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly TicketRepositoryInterface $ticketRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{person: Person} $parameters
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||||
|
{
|
||||||
|
/** @var Person $person */
|
||||||
|
$person = $parameters['person'];
|
||||||
|
|
||||||
|
if ($this->authorizationChecker->isGranted(PersonVoter::SEE, $person)) {
|
||||||
|
$menu->addChild($this->translator->trans('chill_ticket.list.title_menu'), [
|
||||||
|
'route' => 'chill_person_ticket_list',
|
||||||
|
'routeParameters' => [
|
||||||
|
'id' => $person->getId(),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->setExtras([
|
||||||
|
'order' => 150,
|
||||||
|
'counter' => 0 < ($nbTickets = $this->ticketRepository->countOpenedByPerson($person))
|
||||||
|
? $nbTickets : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMenuIds(): array
|
||||||
|
{
|
||||||
|
return ['person'];
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\TicketBundle\Repository;
|
namespace Chill\TicketBundle\Repository;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\TicketBundle\Entity\Ticket;
|
use Chill\TicketBundle\Entity\Ticket;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
@@ -61,4 +62,19 @@ final readonly class TicketRepository implements TicketRepositoryInterface
|
|||||||
{
|
{
|
||||||
return $this->repository->findOneBy(['externalRef' => $extId]);
|
return $this->repository->findOneBy(['externalRef' => $extId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count tickets associated with a person where endDate is null.
|
||||||
|
*/
|
||||||
|
public function countOpenedByPerson(Person $person): int
|
||||||
|
{
|
||||||
|
return (int) $this->objectManager->createQuery(
|
||||||
|
'SELECT COUNT(DISTINCT t.id) FROM '.$this->getClassName().' t
|
||||||
|
JOIN t.personHistories ph
|
||||||
|
WHERE ph.person = :person
|
||||||
|
AND ph.endDate IS NULL'
|
||||||
|
)
|
||||||
|
->setParameter('person', $person)
|
||||||
|
->getSingleScalarResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\TicketBundle\Repository;
|
namespace Chill\TicketBundle\Repository;
|
||||||
|
|
||||||
use Chill\TicketBundle\Entity\Ticket;
|
use Chill\TicketBundle\Entity\Ticket;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,4 +26,6 @@ interface TicketRepositoryInterface extends ObjectRepository
|
|||||||
* @return list<Ticket>
|
* @return list<Ticket>
|
||||||
*/
|
*/
|
||||||
public function findAllOrdered(): array;
|
public function findAllOrdered(): array;
|
||||||
|
|
||||||
|
public function countOpenedByPerson(Person $person): int;
|
||||||
}
|
}
|
||||||
|
@@ -177,8 +177,12 @@ export interface Ticket extends BaseTicket<"ticket_ticket:extended"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketFilters {
|
export interface TicketFilters {
|
||||||
|
byPerson: Person[];
|
||||||
|
byCreator: User[];
|
||||||
|
byAddressee: UserGroupOrUser[];
|
||||||
byCurrentState: TicketState[];
|
byCurrentState: TicketState[];
|
||||||
byCurrentStateEmergency: TicketEmergencyState[];
|
byCurrentStateEmergency: TicketEmergencyState[];
|
||||||
|
byMotives: Motive[];
|
||||||
byCreatedAfter: string;
|
byCreatedAfter: string;
|
||||||
byCreatedBefore: string;
|
byCreatedBefore: string;
|
||||||
byResponseTimeExceeded: boolean;
|
byResponseTimeExceeded: boolean;
|
||||||
@@ -198,7 +202,7 @@ export interface TicketFilterParams {
|
|||||||
byCreatedBefore?: string;
|
byCreatedBefore?: string;
|
||||||
byResponseTimeExceeded?: string;
|
byResponseTimeExceeded?: string;
|
||||||
byAddresseeToMe?: boolean;
|
byAddresseeToMe?: boolean;
|
||||||
byTicketId?: number;
|
byTicketId?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TicketInitForm {
|
export interface TicketInitForm {
|
||||||
|
@@ -1,16 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<pick-entity
|
<div
|
||||||
uniqid="ticket-addressee-selector"
|
:class="{
|
||||||
:types="['user', 'user_group']"
|
'opacity-50': disabled,
|
||||||
:picked="selectedEntities"
|
}"
|
||||||
:suggested="suggestedValues"
|
:style="disabled ? 'pointer-events: none;' : ''"
|
||||||
:multiple="true"
|
>
|
||||||
:removable-if-set="true"
|
<pick-entity
|
||||||
:display-picked="true"
|
uniqid="ticket-addressee-selector"
|
||||||
:label="label"
|
:types="['user', 'user_group']"
|
||||||
@add-new-entity="addNewEntity"
|
:picked="selectedEntities"
|
||||||
@remove-entity="removeEntity"
|
:suggested="suggestedValues"
|
||||||
/>
|
:multiple="true"
|
||||||
|
:removable-if-set="true"
|
||||||
|
:display-picked="true"
|
||||||
|
:label="label"
|
||||||
|
@add-new-entity="addNewEntity"
|
||||||
|
@remove-entity="removeEntity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -33,9 +40,11 @@ const props = withDefaults(
|
|||||||
modelValue: Entities[];
|
modelValue: Entities[];
|
||||||
suggested: Entities[];
|
suggested: Entities[];
|
||||||
label?: string;
|
label?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
label: trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL),
|
label: trans(CHILL_TICKET_TICKET_ADD_ADDRESSEE_USER_LABEL),
|
||||||
|
disabled: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
v-model="motive"
|
v-model="motive"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@remove="(value: Motive) => $emit('remove', value)"
|
@remove="(value: Motive) => $emit('remove', value)"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
#option="{
|
#option="{
|
||||||
@@ -95,6 +96,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
|
@@ -1,16 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<pick-entity
|
<div
|
||||||
uniqid="ticket-person-selector"
|
:class="{
|
||||||
:types="types"
|
'opacity-50': disabled,
|
||||||
:picked="pickedEntities"
|
}"
|
||||||
:suggested="suggestedValues"
|
:style="disabled ? 'pointer-events: none;' : ''"
|
||||||
:multiple="multiple"
|
>
|
||||||
:removable-if-set="true"
|
<pick-entity
|
||||||
:display-picked="true"
|
uniqid="ticket-person-selector"
|
||||||
:label="label"
|
:types="types"
|
||||||
@add-new-entity="addNewEntity"
|
:picked="pickedEntities"
|
||||||
@remove-entity="removeEntity"
|
:suggested="suggestedValues"
|
||||||
/>
|
:multiple="multiple"
|
||||||
|
:removable-if-set="true"
|
||||||
|
:display-picked="true"
|
||||||
|
:label="label"
|
||||||
|
@add-new-entity="addNewEntity"
|
||||||
|
@remove-entity="removeEntity"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -22,13 +29,19 @@ import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
|
|||||||
// Types
|
// Types
|
||||||
import { Entities, EntitiesOrMe, EntityType } from "ChillPersonAssets/types";
|
import { Entities, EntitiesOrMe, EntityType } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
modelValue: EntitiesOrMe[] | EntitiesOrMe | null;
|
defineProps<{
|
||||||
suggested: Entities[];
|
modelValue: EntitiesOrMe[] | EntitiesOrMe | null;
|
||||||
multiple: boolean;
|
suggested: Entities[];
|
||||||
types: EntityType[];
|
multiple: boolean;
|
||||||
label: string;
|
types: EntityType[];
|
||||||
}>();
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: EntitiesOrMe[] | EntitiesOrMe | null];
|
"update:modelValue": [value: EntitiesOrMe[] | EntitiesOrMe | null];
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
:placeholder="trans(CHILL_TICKET_LIST_FILTER_TICKET_ID)"
|
:placeholder="trans(CHILL_TICKET_LIST_FILTER_TICKET_ID)"
|
||||||
|
:disabled="disabled"
|
||||||
@input="
|
@input="
|
||||||
ticketId = isNaN(Number(($event.target as HTMLInputElement).value))
|
ticketId = isNaN(Number(($event.target as HTMLInputElement).value))
|
||||||
? null
|
? null
|
||||||
@@ -26,9 +27,15 @@
|
|||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
// Translation
|
// Translation
|
||||||
import { trans, CHILL_TICKET_LIST_FILTER_TICKET_ID } from "translator";
|
import { trans, CHILL_TICKET_LIST_FILTER_TICKET_ID } from "translator";
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
modelValue: number | null;
|
defineProps<{
|
||||||
}>();
|
modelValue: number | null;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const ticketId = ref<number | null>(props.modelValue);
|
const ticketId = ref<number | null>(props.modelValue);
|
||||||
|
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<h1 class="text-primary">
|
|
||||||
{{ title }}
|
|
||||||
</h1>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 mb-4">
|
<div class="col-12 mb-4">
|
||||||
<ticket-filter-list-component
|
<ticket-filter-list-component
|
||||||
:resultCount="resultCount"
|
:resultCount="resultCount"
|
||||||
:available-persons="availablePersons"
|
:available-persons="availablePersons"
|
||||||
:available-motives="availableMotives"
|
:available-motives="availableMotives"
|
||||||
|
:ticket-filter-params="ticketFilterParams"
|
||||||
@filters-changed="handleFiltersChanged"
|
@filters-changed="handleFiltersChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +33,6 @@
|
|||||||
<ticket-list-component
|
<ticket-list-component
|
||||||
v-else
|
v-else
|
||||||
:tickets="ticketList"
|
:tickets="ticketList"
|
||||||
:title="title"
|
|
||||||
:hasMoreTickets="pagination.next !== null"
|
:hasMoreTickets="pagination.next !== null"
|
||||||
@fetchNextPage="fetchNextPage"
|
@fetchNextPage="fetchNextPage"
|
||||||
/>
|
/>
|
||||||
@@ -61,8 +58,10 @@ import { Pagination } from "ChillMainAssets/lib/api/apiMethods";
|
|||||||
import { trans, CHILL_TICKET_LIST_LOADING_TICKET } from "translator";
|
import { trans, CHILL_TICKET_LIST_LOADING_TICKET } from "translator";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const ticketFilterParams = window.ticketFilterParams
|
||||||
|
? window.ticketFilterParams
|
||||||
|
: null;
|
||||||
|
|
||||||
const title = window.title;
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const ticketList = computed(
|
const ticketList = computed(
|
||||||
() => store.getters.getTicketList as TicketSimple[],
|
() => store.getters.getTicketList as TicketSimple[],
|
||||||
@@ -90,12 +89,27 @@ const fetchNextPage = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const filters: TicketFilterParams = {
|
const filters: TicketFilterParams = {
|
||||||
byCurrentState: ["open"],
|
byPerson: ticketFilterParams?.byPerson
|
||||||
byCurrentStateEmergency: [],
|
? ticketFilterParams.byPerson.map((person) => person.id)
|
||||||
byCreatedAfter: "",
|
: [],
|
||||||
byCreatedBefore: "",
|
byCreator: ticketFilterParams?.byCreator
|
||||||
byResponseTimeExceeded: "",
|
? ticketFilterParams.byCreator.map((creator) => creator.id)
|
||||||
byAddresseeToMe: false,
|
: [],
|
||||||
|
byAddressee: ticketFilterParams?.byAddressee
|
||||||
|
? ticketFilterParams.byAddressee.map((addressee) => addressee.id)
|
||||||
|
: [],
|
||||||
|
byCurrentState: ticketFilterParams?.byCurrentState ?? ["open"],
|
||||||
|
byCurrentStateEmergency: ticketFilterParams?.byCurrentStateEmergency ?? [],
|
||||||
|
byMotives: ticketFilterParams?.byMotives
|
||||||
|
? ticketFilterParams.byMotives.map((motive) => motive.id)
|
||||||
|
: [],
|
||||||
|
byCreatedAfter: ticketFilterParams?.byCreatedAfter ?? "",
|
||||||
|
byCreatedBefore: ticketFilterParams?.byCreatedBefore ?? "",
|
||||||
|
byResponseTimeExceeded: ticketFilterParams?.byResponseTimeExceeded
|
||||||
|
? "true"
|
||||||
|
: "",
|
||||||
|
byAddresseeToMe: ticketFilterParams?.byAddresseeToMe ?? false,
|
||||||
|
byTicketId: ticketFilterParams?.byTicketId ?? null,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await store.dispatch("fetchTicketList", filters);
|
await store.dispatch("fetchTicketList", filters);
|
||||||
|
@@ -14,12 +14,13 @@
|
|||||||
trans(CHILL_TICKET_LIST_FILTER_PERSONS_CONCERNED)
|
trans(CHILL_TICKET_LIST_FILTER_PERSONS_CONCERNED)
|
||||||
}}</label>
|
}}</label>
|
||||||
<persons-selector
|
<persons-selector
|
||||||
v-model="selectedPersons"
|
v-model="filters.byPerson"
|
||||||
:suggested="availablePersons"
|
:suggested="availablePersons"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:types="['person']"
|
:types="['person']"
|
||||||
id="personSelector"
|
id="personSelector"
|
||||||
:label="trans(CHILL_TICKET_LIST_FILTER_BY_PERSON)"
|
:label="trans(CHILL_TICKET_LIST_FILTER_BY_PERSON)"
|
||||||
|
:disabled="ticketFilterParams?.byPerson ? true : false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -28,12 +29,13 @@
|
|||||||
trans(CHILL_TICKET_LIST_FILTER_CREATORS)
|
trans(CHILL_TICKET_LIST_FILTER_CREATORS)
|
||||||
}}</label>
|
}}</label>
|
||||||
<persons-selector
|
<persons-selector
|
||||||
v-model="selectedCreator"
|
v-model="filters.byCreator"
|
||||||
:suggested="[]"
|
:suggested="[]"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:types="['user']"
|
:types="['user']"
|
||||||
id="userSelector"
|
id="userSelector"
|
||||||
:label="trans(CHILL_TICKET_LIST_FILTER_BY_CREATOR)"
|
:label="trans(CHILL_TICKET_LIST_FILTER_BY_CREATOR)"
|
||||||
|
:disabled="ticketFilterParams?.byCreator ? true : false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,10 +44,11 @@
|
|||||||
trans(CHILL_TICKET_LIST_FILTER_ADDRESSEES)
|
trans(CHILL_TICKET_LIST_FILTER_ADDRESSEES)
|
||||||
}}</label>
|
}}</label>
|
||||||
<addressee-selector-component
|
<addressee-selector-component
|
||||||
v-model="selectedAddressees"
|
v-model="filters.byAddressee"
|
||||||
:suggested="[]"
|
:suggested="[]"
|
||||||
:label="trans(CHILL_TICKET_LIST_FILTER_BY_ADDRESSEES)"
|
:label="trans(CHILL_TICKET_LIST_FILTER_BY_ADDRESSEES)"
|
||||||
id="addresseeSelector"
|
id="addresseeSelector"
|
||||||
|
:disabled="ticketFilterParams?.byAddressee ? true : false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
@@ -60,12 +63,13 @@
|
|||||||
:allow-parent-selection="true"
|
:allow-parent-selection="true"
|
||||||
@remove="(motive) => removeMotive(motive)"
|
@remove="(motive) => removeMotive(motive)"
|
||||||
id="motiveSelector"
|
id="motiveSelector"
|
||||||
|
:disabled="ticketFilterParams?.byMotives ? true : false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mb-2" style="min-height: 2em">
|
<div class="mb-2" style="min-height: 2em">
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<span
|
<span
|
||||||
v-for="motive in selectedMotives"
|
v-for="motive in filters.byMotives"
|
||||||
:key="motive.id"
|
:key="motive.id"
|
||||||
class="badge bg-secondary d-flex align-items-center gap-1"
|
class="badge bg-secondary d-flex align-items-center gap-1"
|
||||||
>
|
>
|
||||||
@@ -75,6 +79,7 @@
|
|||||||
class="btn-close btn-close-white"
|
class="btn-close btn-close-white"
|
||||||
:aria-label="trans(CHILL_TICKET_LIST_FILTER_REMOVE)"
|
:aria-label="trans(CHILL_TICKET_LIST_FILTER_REMOVE)"
|
||||||
@click="removeMotive(motive)"
|
@click="removeMotive(motive)"
|
||||||
|
:disabled="ticketFilterParams?.byMotives ? true : false"
|
||||||
></button>
|
></button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +101,7 @@
|
|||||||
}"
|
}"
|
||||||
@update:model-value="handleStateToggle"
|
@update:model-value="handleStateToggle"
|
||||||
id="currentState"
|
id="currentState"
|
||||||
|
:disabled="ticketFilterParams?.byCurrentState ? true : false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,6 +120,9 @@
|
|||||||
}"
|
}"
|
||||||
@update:model-value="handleEmergencyToggle"
|
@update:model-value="handleEmergencyToggle"
|
||||||
id="emergency"
|
id="emergency"
|
||||||
|
:disabled="
|
||||||
|
ticketFilterParams?.byCurrentStateEmergency ? true : false
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +138,7 @@
|
|||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="stateMe"
|
id="stateMe"
|
||||||
|
:disabled="ticketFilterParams?.byAddresseeToMe ? true : false"
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="stateMe">
|
<label class="form-check-label" for="stateMe">
|
||||||
{{ trans(CHILL_TICKET_LIST_FILTER_TO_ME) }}
|
{{ trans(CHILL_TICKET_LIST_FILTER_TO_ME) }}
|
||||||
@@ -142,6 +152,9 @@
|
|||||||
v-model="filters.byResponseTimeExceeded"
|
v-model="filters.byResponseTimeExceeded"
|
||||||
@change="handleResponseTimeExceededChange"
|
@change="handleResponseTimeExceededChange"
|
||||||
id="responseTimeExceeded"
|
id="responseTimeExceeded"
|
||||||
|
:disabled="
|
||||||
|
ticketFilterParams?.byResponseTimeExceeded ? true : false
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="responseTimeExceeded">
|
<label class="form-check-label" for="responseTimeExceeded">
|
||||||
{{ trans(CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_EXCEEDED) }}
|
{{ trans(CHILL_TICKET_LIST_FILTER_RESPONSE_TIME_EXCEEDED) }}
|
||||||
@@ -157,7 +170,11 @@
|
|||||||
<label class="form-label pe-2" for="ticketSelector">
|
<label class="form-label pe-2" for="ticketSelector">
|
||||||
{{ trans(CHILL_TICKET_LIST_FILTER_BY_TICKET_ID) }}
|
{{ trans(CHILL_TICKET_LIST_FILTER_BY_TICKET_ID) }}
|
||||||
</label>
|
</label>
|
||||||
<ticket-selector v-model="filters.byTicketId" id="ticketSelector" />
|
<ticket-selector
|
||||||
|
v-model="filters.byTicketId"
|
||||||
|
id="ticketSelector"
|
||||||
|
:disabled="ticketFilterParams?.byTicketId ? true : false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -170,7 +187,12 @@
|
|||||||
default-value-time="00:00"
|
default-value-time="00:00"
|
||||||
:model-value-date="filters.byCreatedAfter"
|
:model-value-date="filters.byCreatedAfter"
|
||||||
:model-value-time="byCreatedAfterTime"
|
:model-value-time="byCreatedAfterTime"
|
||||||
:disabled="filters.byResponseTimeExceeded"
|
:disabled="
|
||||||
|
filters.byResponseTimeExceeded ||
|
||||||
|
ticketFilterParams?.byCreatedAfter
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
"
|
||||||
@update:modelValueDate="filters.byCreatedAfter = $event"
|
@update:modelValueDate="filters.byCreatedAfter = $event"
|
||||||
@update:modelValueTime="byCreatedAfterTime = $event"
|
@update:modelValueTime="byCreatedAfterTime = $event"
|
||||||
/>
|
/>
|
||||||
@@ -183,7 +205,12 @@
|
|||||||
default-value-time="23:59"
|
default-value-time="23:59"
|
||||||
:model-value-date="filters.byCreatedBefore"
|
:model-value-date="filters.byCreatedBefore"
|
||||||
:model-value-time="byCreatedBeforeTime"
|
:model-value-time="byCreatedBeforeTime"
|
||||||
:disabled="filters.byResponseTimeExceeded"
|
:disabled="
|
||||||
|
filters.byResponseTimeExceeded ||
|
||||||
|
ticketFilterParams?.byCreatedBefore
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
"
|
||||||
@update:modelValueDate="filters.byCreatedBefore = $event"
|
@update:modelValueDate="filters.byCreatedBefore = $event"
|
||||||
@update:modelValueTime="byCreatedBeforeTime = $event"
|
@update:modelValueTime="byCreatedBeforeTime = $event"
|
||||||
/>
|
/>
|
||||||
@@ -227,7 +254,6 @@ import {
|
|||||||
type TicketFilterParams,
|
type TicketFilterParams,
|
||||||
type TicketFilters,
|
type TicketFilters,
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
import { User, UserGroupOrUser } from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
// Translation
|
// Translation
|
||||||
import {
|
import {
|
||||||
@@ -269,6 +295,7 @@ const props = defineProps<{
|
|||||||
availablePersons?: Person[];
|
availablePersons?: Person[];
|
||||||
availableMotives: Motive[];
|
availableMotives: Motive[];
|
||||||
resultCount: number;
|
resultCount: number;
|
||||||
|
ticketFilterParams: TicketFilters | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
@@ -276,74 +303,66 @@ const emit = defineEmits<{
|
|||||||
"filters-changed": [filters: TicketFilterParams];
|
"filters-changed": [filters: TicketFilterParams];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const filtersInitValues: TicketFilters = {
|
||||||
|
byPerson: props.ticketFilterParams?.byPerson ?? [],
|
||||||
|
byCreator: props.ticketFilterParams?.byCreator ?? [],
|
||||||
|
byAddressee: props.ticketFilterParams?.byAddressee ?? [],
|
||||||
|
byCurrentState: props.ticketFilterParams?.byCurrentState ?? ["open"],
|
||||||
|
byCurrentStateEmergency:
|
||||||
|
props.ticketFilterParams?.byCurrentStateEmergency ?? [],
|
||||||
|
byMotives: props.ticketFilterParams?.byMotives ?? [],
|
||||||
|
byCreatedAfter: props.ticketFilterParams?.byCreatedAfter ?? "",
|
||||||
|
byCreatedBefore: props.ticketFilterParams?.byCreatedBefore ?? "",
|
||||||
|
byResponseTimeExceeded:
|
||||||
|
props.ticketFilterParams?.byResponseTimeExceeded ?? false,
|
||||||
|
byAddresseeToMe: props.ticketFilterParams?.byAddresseeToMe ?? false,
|
||||||
|
byTicketId: props.ticketFilterParams?.byTicketId ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
// État réactif
|
// État réactif
|
||||||
const filters = ref<TicketFilters>({
|
const filters = ref<TicketFilters>({ ...filtersInitValues });
|
||||||
byCurrentState: ["open"],
|
|
||||||
byCurrentStateEmergency: [],
|
|
||||||
byCreatedAfter: "",
|
|
||||||
byCreatedBefore: "",
|
|
||||||
byResponseTimeExceeded: false,
|
|
||||||
byAddresseeToMe: false,
|
|
||||||
byTicketId: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const byCreatedAfterTime = ref("00:00");
|
const byCreatedAfterTime = ref("00:00");
|
||||||
const byCreatedBeforeTime = ref("23:59");
|
const byCreatedBeforeTime = ref("23:59");
|
||||||
|
const isClosedToggled = ref(false);
|
||||||
|
const isEmergencyToggled = ref(false);
|
||||||
|
|
||||||
// Sélection des personnes
|
|
||||||
const selectedPersons = ref<Person[]>([]);
|
|
||||||
const availablePersons = ref<Person[]>(props.availablePersons || []);
|
const availablePersons = ref<Person[]>(props.availablePersons || []);
|
||||||
|
|
||||||
// Sélection des utilisateur assigné
|
|
||||||
const selectedAddressees = ref<UserGroupOrUser[]>([]);
|
|
||||||
|
|
||||||
// Séléction des créateurs
|
|
||||||
const selectedCreator = ref<User[]>([]);
|
|
||||||
|
|
||||||
// Sélection des motifs
|
|
||||||
const selectedMotive = ref<Motive | null>();
|
const selectedMotive = ref<Motive | null>();
|
||||||
const selectedMotives = ref<Motive[]>([]);
|
|
||||||
|
|
||||||
// Watchers pour les sélecteurs
|
|
||||||
watch(selectedMotive, (newMotive) => {
|
watch(selectedMotive, (newMotive) => {
|
||||||
if (newMotive && !selectedMotives.value.find((m) => m.id === newMotive.id)) {
|
if (
|
||||||
selectedMotives.value.push(newMotive);
|
newMotive &&
|
||||||
|
!filters.value.byMotives.find((m) => m.id === newMotive.id)
|
||||||
|
) {
|
||||||
|
filters.value.byMotives = [...filters.value.byMotives, newMotive];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed pour les IDs des personnes sélectionnées
|
|
||||||
const selectedPersonIds = computed(() =>
|
const selectedPersonIds = computed(() =>
|
||||||
selectedPersons.value.map((person) => person.id),
|
filters.value.byPerson.map((person) => person.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Computed pour les IDs des utilisateur ou groupes sélectionnées
|
|
||||||
const selectedUserAddresseesIds = computed(() =>
|
const selectedUserAddresseesIds = computed(() =>
|
||||||
selectedAddressees.value
|
filters.value.byAddressee
|
||||||
.filter((addressee) => addressee.type === "user")
|
.filter((addressee) => addressee.type === "user")
|
||||||
.map((addressee) => addressee.id),
|
.map((addressee) => addressee.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedGroupAddresseesIds = computed(() =>
|
const selectedGroupAddresseesIds = computed(() =>
|
||||||
selectedAddressees.value
|
filters.value.byAddressee
|
||||||
.filter((addressee) => addressee.type === "user_group")
|
.filter((addressee) => addressee.type === "user_group")
|
||||||
.map((addressee) => addressee.id),
|
.map((addressee) => addressee.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Computed pour les IDs des créateurs
|
|
||||||
const selectedCreatorIds = computed(() =>
|
const selectedCreatorIds = computed(() =>
|
||||||
selectedCreator.value.map((creator) => creator.id),
|
filters.value.byCreator.map((creator) => creator.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Computed pour les IDs des motifs sélectionnés
|
|
||||||
const selectedMotiveIds = computed(() =>
|
const selectedMotiveIds = computed(() =>
|
||||||
selectedMotives.value.map((motive) => motive.id),
|
filters.value.byMotives.map((motive) => motive.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Nouveaux états pour les toggles
|
|
||||||
const isClosedToggled = ref(false);
|
|
||||||
const isEmergencyToggled = ref(false);
|
|
||||||
|
|
||||||
// Méthodes pour gérer les toggles
|
|
||||||
const handleStateToggle = (value: boolean) => {
|
const handleStateToggle = (value: boolean) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
filters.value.byCurrentState = ["closed"];
|
filters.value.byCurrentState = ["closed"];
|
||||||
@@ -379,12 +398,9 @@ const getMotiveDisplayName = (motive: Motive): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeMotive = (motiveToRemove: Motive): void => {
|
const removeMotive = (motiveToRemove: Motive): void => {
|
||||||
const index = selectedMotives.value.findIndex(
|
filters.value.byMotives = filters.value.byMotives.filter(
|
||||||
(m) => m.id === motiveToRemove.id,
|
(m) => m.id !== motiveToRemove.id,
|
||||||
);
|
);
|
||||||
if (index !== -1) {
|
|
||||||
selectedMotives.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
if (selectedMotive.value && motiveToRemove.id == selectedMotive.value.id) {
|
if (selectedMotive.value && motiveToRemove.id == selectedMotive.value.id) {
|
||||||
selectedMotive.value = null;
|
selectedMotive.value = null;
|
||||||
}
|
}
|
||||||
@@ -447,22 +463,12 @@ const applyFilters = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetFilters = (): void => {
|
const resetFilters = (): void => {
|
||||||
filters.value = {
|
filters.value = { ...filtersInitValues };
|
||||||
byCurrentState: ["open"],
|
|
||||||
byCurrentStateEmergency: [],
|
|
||||||
byCreatedAfter: "",
|
|
||||||
byCreatedBefore: "",
|
|
||||||
byResponseTimeExceeded: false,
|
|
||||||
byAddresseeToMe: false,
|
|
||||||
byTicketId: null,
|
|
||||||
};
|
|
||||||
selectedPersons.value = [];
|
|
||||||
selectedCreator.value = [];
|
|
||||||
selectedAddressees.value = [];
|
|
||||||
selectedMotives.value = [];
|
|
||||||
selectedMotive.value = null;
|
selectedMotive.value = null;
|
||||||
isClosedToggled.value = false;
|
isClosedToggled.value = false;
|
||||||
isEmergencyToggled.value = false;
|
isEmergencyToggled.value = false;
|
||||||
|
byCreatedAfterTime.value = "00:00";
|
||||||
|
byCreatedBeforeTime.value = "23:59";
|
||||||
applyFilters();
|
applyFilters();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -97,7 +97,6 @@ import { useStore } from "vuex";
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
tickets: TicketSimple[];
|
tickets: TicketSimple[];
|
||||||
hasMoreTickets: boolean;
|
hasMoreTickets: boolean;
|
||||||
title: string;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@@ -3,10 +3,12 @@ import { createApp } from "vue";
|
|||||||
import { store } from "../TicketApp/store";
|
import { store } from "../TicketApp/store";
|
||||||
import VueToast from "vue-toast-notification";
|
import VueToast from "vue-toast-notification";
|
||||||
import "vue-toast-notification/dist/theme-sugar.css";
|
import "vue-toast-notification/dist/theme-sugar.css";
|
||||||
|
import { TicketFilters } from "../../types";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
title: string;
|
title: string;
|
||||||
|
ticketFilterParams: TicketFilters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "@ChillPerson/Person/layout.html.twig" %}
|
||||||
|
{% set ticketTitle = 'chill_ticket.list.title_with_name'|trans({'%name%': person|chill_entity_render_string }) %}
|
||||||
|
{% set activeRouteKey = 'chill_person_ticket_list' %}
|
||||||
|
{% set ticketFilterParams = {
|
||||||
|
'byPerson': [person]
|
||||||
|
} %}
|
||||||
|
{% block title %}{{ ticketTitle }}{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ parent() }}
|
||||||
|
{{ encore_entry_link_tags('vue_ticket_list') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ parent() }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.ticketFilterParams = {{ ticketFilterParams|serialize|raw }};
|
||||||
|
</script>
|
||||||
|
{{ encore_entry_script_tags('vue_ticket_list') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ ticketTitle }}</h1>
|
||||||
|
|
||||||
|
<div id="ticketList"></div>
|
||||||
|
|
||||||
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
<li>
|
||||||
|
<a href="{{ chill_path_add_return_path('chill_ticket_createticket__invoke') }}" class="btn btn-create">{{ 'Create'|trans }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
@@ -1,4 +1,6 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
{% extends '@ChillMain/layout.html.twig' %}
|
||||||
|
{% set ticketTitle = 'chill_ticket.list.title'|trans %}
|
||||||
|
{% block title %}{{ ticketTitle }}{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
@@ -7,13 +9,11 @@
|
|||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
<script type="text/javascript">
|
|
||||||
window.title = "{{ 'chill_ticket.list.title'|trans|escape('js') }}";
|
|
||||||
</script>
|
|
||||||
{{ encore_entry_script_tags('vue_ticket_list') }}
|
{{ encore_entry_script_tags('vue_ticket_list') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h1>{{ ticketTitle }}</h1>
|
||||||
<div id="ticketList"></div>
|
<div id="ticketList"></div>
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
restore: Restaurer
|
restore: Restaurer
|
||||||
chill_ticket:
|
chill_ticket:
|
||||||
list:
|
list:
|
||||||
title: Tickets
|
title: "Tickets"
|
||||||
|
title_with_name: "{name, select, null {Tickets} undefined {Tickets} other {Tickets de {name}}}"
|
||||||
|
title_menu: "Tickets de l'usager"
|
||||||
title_previous_tickets: "{name, select, other {Précédent ticket de {name}} undefined {Précédent ticket}}"
|
title_previous_tickets: "{name, select, other {Précédent ticket de {name}} undefined {Précédent ticket}}"
|
||||||
no_tickets: "Aucun ticket"
|
no_tickets: "Aucun ticket"
|
||||||
loading_ticket: "Chargement des tickets..."
|
loading_ticket: "Chargement des tickets..."
|
||||||
|
Reference in New Issue
Block a user