Merge branch 'ticket-app-master' into ticket/64-identifiants-person

This commit is contained in:
2025-10-16 10:48:59 +02:00
17 changed files with 308 additions and 156 deletions

View File

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

View File

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

View 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'];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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