mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-26 16:45:01 +00:00
Compare commits
37 Commits
ticket-app
...
ticket/64-
Author | SHA1 | Date | |
---|---|---|---|
13b1c45271
|
|||
ad2b6d63ac
|
|||
bfbde078b7
|
|||
d42a1296c4
|
|||
4b7e3c1601
|
|||
6ea9af588b
|
|||
0fd76d3fa8
|
|||
34af53130b
|
|||
a1fd395868
|
|||
b8a7cbb321
|
|||
6124eb9e34
|
|||
a5b06de92a
|
|||
52404956d2
|
|||
4207efd6bf
|
|||
840fde4ad4
|
|||
3611ea2518
|
|||
bbd4292cb9
|
|||
54f8c92240
|
|||
5330befc8f
|
|||
c19206be0c
|
|||
5ff374d2fa
|
|||
4a73aaae94
|
|||
ff2c567d05
|
|||
a734e84f28
|
|||
4367ed086e
|
|||
3227bfcd3a
|
|||
8d29fb260a
|
|||
bda0743c63
|
|||
d9b730627f
|
|||
27548ad654
|
|||
bec7297039
|
|||
852523e644
|
|||
c05d0aad47
|
|||
1c0ed9abc8
|
|||
9aed5cc216
|
|||
e4fe5bff68
|
|||
4c73c4d9d0
|
@@ -236,12 +236,14 @@ This must be a decision made by a human, not by an AI. Every AI task must abort
|
|||||||
|
|
||||||
The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests).
|
The tests are run from the project's root (not from the bundle's root: so, do not change the directory to any bundle directory before running tests).
|
||||||
|
|
||||||
|
Tests must be run using the `symfony` command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run a specific test file
|
# Run a specific test file
|
||||||
vendor/bin/phpunit path/to/TestFile.php
|
symfony composer exec phpunit -- path/to/TestFile.php
|
||||||
|
|
||||||
# Run a specific test method
|
# Run a specific test method
|
||||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
|
symfony composer exec phpunit -- --filter methodName path/to/TestFile.php
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Test Structure
|
#### Test Structure
|
||||||
|
@@ -19,7 +19,6 @@ use Chill\MainBundle\Entity\User;
|
|||||||
use Chill\MainBundle\Entity\Address;
|
use Chill\MainBundle\Entity\Address;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immersion.
|
* Immersion.
|
||||||
@@ -86,14 +85,14 @@ class Immersion implements \Stringable
|
|||||||
* @Assert\NotBlank()
|
* @Assert\NotBlank()
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
|
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $tuteurPhoneNumber = null;
|
private ?PhoneNumber $tuteurPhoneNumber = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||||
private ?string $structureAccName = null;
|
private ?string $structureAccName = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
|
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $structureAccPhonenumber = null;
|
private ?PhoneNumber $structureAccPhonenumber = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'structureAccEmail', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
#[ORM\Column(name: 'structureAccEmail', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||||
|
@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Entity;
|
|||||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||||
use Chill\MainBundle\Repository\LocationRepository;
|
use Chill\MainBundle\Repository\LocationRepository;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||||
|
|
||||||
@@ -67,12 +67,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
|
||||||
private ?PhoneNumber $phonenumber1 = null;
|
private ?PhoneNumber $phonenumber1 = null;
|
||||||
|
|
||||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
|
||||||
private ?PhoneNumber $phonenumber2 = null;
|
private ?PhoneNumber $phonenumber2 = null;
|
||||||
|
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
|
@@ -23,7 +23,6 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
|||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.
|
* User.
|
||||||
@@ -116,7 +115,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
* The user's mobile phone number.
|
* The user's mobile phone number.
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $phonenumber = null;
|
private ?PhoneNumber $phonenumber = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -31,6 +31,8 @@ interface PhoneNumberHelperInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the validation is configured and available.
|
* Return true if the validation is configured and available.
|
||||||
|
*
|
||||||
|
* @deprecated this is an internal behaviour of the helper and should not be taken into account outside of the implementation
|
||||||
*/
|
*/
|
||||||
public function isPhonenumberValidationConfigured(): bool;
|
public function isPhonenumberValidationConfigured(): bool;
|
||||||
|
|
||||||
|
@@ -122,7 +122,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
*/
|
*/
|
||||||
public function isValidPhonenumberAny($phonenumber): bool
|
public function isValidPhonenumberAny($phonenumber): bool
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
$validation = $this->performTwilioLookup($phonenumber);
|
$validation = $this->performTwilioLookup($phonenumber);
|
||||||
@@ -142,7 +142,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
*/
|
*/
|
||||||
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
*/
|
*/
|
||||||
public function isValidPhonenumberMobile($phonenumber): bool
|
public function isValidPhonenumberMobile($phonenumber): bool
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
|||||||
|
|
||||||
private function performTwilioLookup($phonenumber)
|
private function performTwilioLookup($phonenumber)
|
||||||
{
|
{
|
||||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
if (false === $this->isConfigured) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -158,3 +158,18 @@ export const intervalISOToDays = (str: string | null): number | null => {
|
|||||||
|
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getTimezoneOffsetString(date: Date, timeZone: string): string {
|
||||||
|
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
|
||||||
|
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
|
||||||
|
const offsetMinutes = (utcDate.getTime() - tzDate.getTime()) / (60 * 1000);
|
||||||
|
|
||||||
|
// Inverser le signe pour avoir la convention ±HH:MM
|
||||||
|
const sign = offsetMinutes <= 0 ? "+" : "-";
|
||||||
|
const absMinutes = Math.abs(offsetMinutes);
|
||||||
|
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
|
||||||
|
const minutes = String(absMinutes % 60).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${sign}${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
import { Scope } from "../../types";
|
import {
|
||||||
|
DynamicKeys,
|
||||||
|
Scope,
|
||||||
|
ValidationExceptionInterface,
|
||||||
|
ValidationProblemFromMap,
|
||||||
|
ViolationFromMap
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
export type body = Record<string, boolean | string | number | null>;
|
export type body = Record<string, boolean | string | number | null>;
|
||||||
export type fetchOption = Record<string, boolean | string | number | null>;
|
export type fetchOption = Record<string, boolean | string | number | null>;
|
||||||
|
export type Primitive = string | number | boolean | null;
|
||||||
export type Params = Record<string, number | string>;
|
export type Params = Record<string, number | string>;
|
||||||
|
|
||||||
export interface Pagination {
|
export interface Pagination {
|
||||||
@@ -25,20 +31,115 @@ export interface TransportExceptionInterface {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationExceptionInterface
|
export class ValidationException<
|
||||||
extends TransportExceptionInterface {
|
M extends Record<string, Record<string, string|number>> = Record<
|
||||||
name: "ValidationException";
|
string,
|
||||||
error: object;
|
Record<string, string|number>
|
||||||
violations: string[];
|
>,
|
||||||
titles: string[];
|
>
|
||||||
propertyPaths: string[];
|
extends Error
|
||||||
|
implements ValidationExceptionInterface<M>
|
||||||
|
{
|
||||||
|
public readonly name = "ValidationException" as const;
|
||||||
|
public readonly problems: ValidationProblemFromMap<M>;
|
||||||
|
public readonly violations: string[];
|
||||||
|
public readonly violationsList: ViolationFromMap<M>[];
|
||||||
|
public readonly titles: string[];
|
||||||
|
public readonly propertyPaths: DynamicKeys<M> & string[];
|
||||||
|
public readonly byProperty: Record<Extract<keyof M, string>, string[]>;
|
||||||
|
|
||||||
|
constructor(problem: ValidationProblemFromMap<M>) {
|
||||||
|
const message = [problem.title, problem.detail].filter(Boolean).join(" — ");
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
|
||||||
|
this.problems = problem;
|
||||||
|
|
||||||
|
this.violationsList = problem.violations;
|
||||||
|
this.violations = problem.violations.map(
|
||||||
|
(v) => `${v.title}: ${v.propertyPath}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.titles = problem.violations.map((v) => v.title);
|
||||||
|
|
||||||
|
this.propertyPaths = problem.violations.map(
|
||||||
|
(v) => v.propertyPath,
|
||||||
|
) as DynamicKeys<M> & string[];
|
||||||
|
|
||||||
|
this.byProperty = problem.violations.reduce(
|
||||||
|
(acc, v) => {
|
||||||
|
const key = v.propertyPath.replace('/\[\d+\]$/', "") as Extract<keyof M, string>;
|
||||||
|
(acc[key] ||= []).push(v.title);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<Extract<keyof M, string>, string[]>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, ValidationException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
violationsByNormalizedProperty(property: Extract<keyof M, string>): ViolationFromMap<M>[] {
|
||||||
|
return this.violationsList.filter((v) => v.propertyPath.replace(/\[\d+\]$/, "") === property);
|
||||||
|
}
|
||||||
|
|
||||||
|
violationsByNormalizedPropertyAndParams<
|
||||||
|
P extends Extract<keyof M, string>,
|
||||||
|
K extends Extract<keyof M[P], string>
|
||||||
|
>(
|
||||||
|
property: P,
|
||||||
|
param: K,
|
||||||
|
param_value: M[P][K]
|
||||||
|
): ViolationFromMap<M>[]
|
||||||
|
{
|
||||||
|
const list = this.violationsByNormalizedProperty(property);
|
||||||
|
|
||||||
|
return list.filter(
|
||||||
|
(v): boolean =>
|
||||||
|
!!v.parameters &&
|
||||||
|
// `with_parameter in v.parameters` check indexing
|
||||||
|
param in v.parameters &&
|
||||||
|
// the cast is safe, because we have overloading that bind the types
|
||||||
|
(v.parameters as M[P])[param] === param_value
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationErrorResponse extends TransportExceptionInterface {
|
/**
|
||||||
violations: {
|
* Check that the exception is a ValidationExceptionInterface
|
||||||
|
* @param x
|
||||||
|
*/
|
||||||
|
export function isValidationException<M extends Record<string, Record<string, string|number>>>(
|
||||||
|
x: unknown,
|
||||||
|
): x is ValidationExceptionInterface<M> {
|
||||||
|
return (
|
||||||
|
x instanceof ValidationException ||
|
||||||
|
(typeof x === "object" &&
|
||||||
|
x !== null &&
|
||||||
|
(x as any).name === "ValidationException")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidationProblem(x: unknown): x is {
|
||||||
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
propertyPath: string;
|
violations: { propertyPath: string; title: string }[];
|
||||||
}[];
|
} {
|
||||||
|
if (!x || typeof x !== "object") return false;
|
||||||
|
const o = x as any;
|
||||||
|
return (
|
||||||
|
typeof o.type === "string" &&
|
||||||
|
typeof o.title === "string" &&
|
||||||
|
Array.isArray(o.violations) &&
|
||||||
|
o.violations.every(
|
||||||
|
(v: any) =>
|
||||||
|
v &&
|
||||||
|
typeof v === "object" &&
|
||||||
|
typeof v.propertyPath === "string" &&
|
||||||
|
typeof v.title === "string",
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
||||||
@@ -65,12 +166,151 @@ export interface ConflictHttpExceptionInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic api method that can be adapted to any fetch request
|
* Generic api method that can be adapted to any fetch request.
|
||||||
*
|
*
|
||||||
* This method is suitable make a single fetch. When performing a GET to fetch a list of elements, always consider pagination
|
* What this does
|
||||||
* and use of the @link{fetchResults} method.
|
* - Performs a single HTTP request using fetch and returns the parsed JSON as Output.
|
||||||
|
* - Interprets common API errors and throws typed exceptions you can catch in your UI.
|
||||||
|
* - When the server returns a Symfony validation problem (HTTP 422), the error is
|
||||||
|
* rethrown as a typed ValidationException that is aware of your Violation Map (see below).
|
||||||
|
*
|
||||||
|
* Important: For GET endpoints that return lists, prefer using fetchResults, which
|
||||||
|
* handles pagination and aggregation for you.
|
||||||
|
*
|
||||||
|
* Violation Map (M): make your 422 errors strongly typed
|
||||||
|
* ------------------------------------------------------
|
||||||
|
* Symfony’s validation problem+json payload looks like this (simplified):
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "type": "https://symfony.com/errors/validation",
|
||||||
|
* "title": "Validation Failed",
|
||||||
|
* "violations": [
|
||||||
|
* {
|
||||||
|
* "propertyPath": "mobilenumber",
|
||||||
|
* "title": "This value is not a valid phone number.",
|
||||||
|
* "parameters": {
|
||||||
|
* "{{ value }}": "+33 1 02 03 04 05",
|
||||||
|
* "{{ types }}": "mobile number"
|
||||||
|
* },
|
||||||
|
* "type": "urn:uuid:..."
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* The makeFetch generic type parameter M lets you describe, field by field, which
|
||||||
|
* parameters may appear for each propertyPath. Doing so gives you full type-safety when
|
||||||
|
* consuming ValidationException in your UI code.
|
||||||
|
*
|
||||||
|
* How to build M (Violation Map)
|
||||||
|
* - M is a map where each key is a server-side propertyPath (string), and the value is a
|
||||||
|
* record describing the allowed keys in the parameters object for that property.
|
||||||
|
* - Keys in parameters are the exact strings you receive from Symfony, including the
|
||||||
|
* curly-braced placeholders such as "{{ value }}", "{{ types }}", etc.
|
||||||
|
*
|
||||||
|
* Example from Person creation (WritePersonViolationMap)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* In ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts you’ll find:
|
||||||
|
*
|
||||||
|
* export type WritePersonViolationMap = {
|
||||||
|
* gender: {
|
||||||
|
* "{{ value }}": string | null;
|
||||||
|
* };
|
||||||
|
* mobilenumber: {
|
||||||
|
* "{{ types }}": string; // ex: "mobile number"
|
||||||
|
* "{{ value }}": string; // ex: "+33 1 02 03 04 05"
|
||||||
|
* };
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* This means:
|
||||||
|
* - If the server reports a violation for propertyPath "gender", the parameters object
|
||||||
|
* is expected to contain a key "{{ value }}" with a string or null value.
|
||||||
|
* - If the server reports a violation for propertyPath "mobilenumber", the parameters
|
||||||
|
* may include "{{ value }}" and "{{ types }}" as strings.
|
||||||
|
*
|
||||||
|
* How makeFetch uses M
|
||||||
|
* - When the response has status 422 and the payload matches a Symfony validation
|
||||||
|
* problem, makeFetch casts it to ValidationProblemFromMap<M> and throws a
|
||||||
|
* ValidationException<M>.
|
||||||
|
* - The ValidationException exposes helpful, pre-computed fields:
|
||||||
|
* - exception.problem: the full typed payload
|
||||||
|
* - exception.violations: ["Title: propertyPath", ...]
|
||||||
|
* - exception.titles: ["Title 1", "Title 2", ...]
|
||||||
|
* - exception.propertyPaths: ["gender", "mobilenumber", ...] (typed from M)
|
||||||
|
* - exception.byProperty: { gender: [titles...], mobilenumber: [titles...] }
|
||||||
|
*
|
||||||
|
* Typical usage patterns
|
||||||
|
* ----------------------
|
||||||
|
* 1) GET without Validation Map (no 422 expected):
|
||||||
|
*
|
||||||
|
* const centers = await makeFetch<null, { showCenters: boolean; centers: Center[] }>(
|
||||||
|
* "GET",
|
||||||
|
* "/api/1.0/person/creation/authorized-centers",
|
||||||
|
* null
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* 2) POST with body and Violation Map:
|
||||||
|
*
|
||||||
|
* type WritePersonViolationMap = {
|
||||||
|
* gender: { "{{ value }}": string | null };
|
||||||
|
* mobilenumber: { "{{ types }}": string; "{{ value }}": string };
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* try {
|
||||||
|
* const created = await makeFetch<PersonWrite, Person, WritePersonViolationMap>(
|
||||||
|
* "POST",
|
||||||
|
* "/api/1.0/person/person.json",
|
||||||
|
* personPayload
|
||||||
|
* );
|
||||||
|
* // Success path
|
||||||
|
* } catch (e) {
|
||||||
|
* if (isValidationException(e)) {
|
||||||
|
* // Fully typed:
|
||||||
|
* e.propertyPaths.includes("mobilenumber");
|
||||||
|
* const firstTitleForMobile = e.byProperty.mobilenumber?.[0];
|
||||||
|
* // You can also inspect parameter values:
|
||||||
|
* const v = e.problem.violations.find(v => v.propertyPath === "mobilenumber");
|
||||||
|
* const rawValue = v?.parameters?.["{{ value }}"]; // typed as string
|
||||||
|
* } else {
|
||||||
|
* // Other error handling (AccessException, ConflictHttpException, etc.)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Tips to design your Violation Map
|
||||||
|
* - Use exact propertyPath strings as exposed by the API (they usually match your
|
||||||
|
* DTO field names or entity property paths used by the validator).
|
||||||
|
* - Inside each property, list only the placeholders that you actually read in the UI
|
||||||
|
* (you can always add more later). This keeps your types strict but pragmatic.
|
||||||
|
* - If a field may not include parameters at all, you can set it to an empty object {}.
|
||||||
|
* - If you don’t care about parameter typing, you can omit M entirely and rely on the
|
||||||
|
* default loose typing (Record<string, Primitive>), but you’ll lose safety.
|
||||||
|
*
|
||||||
|
* Error taxonomy thrown by makeFetch
|
||||||
|
* - ValidationException<M> when status = 422 and payload is a validation problem.
|
||||||
|
* - AccessException when status = 403.
|
||||||
|
* - ConflictHttpException when status = 409.
|
||||||
|
* - A generic error object for other non-ok statuses.
|
||||||
|
*
|
||||||
|
* @typeParam Input - Shape of the request body you send (if any)
|
||||||
|
* @typeParam Output - Shape of the successful JSON response you expect
|
||||||
|
* @typeParam M - Violation Map describing the per-field parameters you expect
|
||||||
|
* in Symfony validation violations. See examples above.
|
||||||
|
*
|
||||||
|
* @param method The HTTP method to use (POST, GET, PUT, PATCH, DELETE)
|
||||||
|
* @param url The absolute or relative URL to call
|
||||||
|
* @param body The request payload. If null/undefined, no body is sent
|
||||||
|
* @param options Extra fetch options/headers merged into the request
|
||||||
|
*
|
||||||
|
* @returns The parsed JSON response typed as Output. For 204 No Content, resolves
|
||||||
|
* with undefined (void).
|
||||||
*/
|
*/
|
||||||
export const makeFetch = <Input, Output>(
|
export const makeFetch = async <
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
M extends Record<string, Record<string, string|number>> = Record<
|
||||||
|
string,
|
||||||
|
Record<string, string|number>
|
||||||
|
>,
|
||||||
|
>(
|
||||||
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
|
method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE",
|
||||||
url: string,
|
url: string,
|
||||||
body?: body | Input | null,
|
body?: body | Input | null,
|
||||||
@@ -90,7 +330,8 @@ export const makeFetch = <Input, Output>(
|
|||||||
if (typeof options !== "undefined") {
|
if (typeof options !== "undefined") {
|
||||||
opts = Object.assign(opts, options);
|
opts = Object.assign(opts, options);
|
||||||
}
|
}
|
||||||
return fetch(url, opts).then((response) => {
|
|
||||||
|
return fetch(url, opts).then(async (response) => {
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -100,9 +341,20 @@ export const makeFetch = <Input, Output>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 422) {
|
if (response.status === 422) {
|
||||||
return response.json().then((response) => {
|
// Unprocessable Entity -> payload de validation Symfony
|
||||||
throw ValidationException(response);
|
const json = await response.json().catch(() => undefined);
|
||||||
});
|
|
||||||
|
if (isValidationProblem(json)) {
|
||||||
|
// On ré-interprète le payload selon M (ParamMap) pour typer les violations
|
||||||
|
const problem = json as unknown as ValidationProblemFromMap<M>;
|
||||||
|
throw new ValidationException<M>(problem);
|
||||||
|
}
|
||||||
|
|
||||||
|
const err = new Error(
|
||||||
|
"Validation failed but payload is not a ValidationProblem",
|
||||||
|
);
|
||||||
|
(err as any).raw = json;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
@@ -167,12 +419,6 @@ function _fetchAction<T>(
|
|||||||
throw NotFoundException(response);
|
throw NotFoundException(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 422) {
|
|
||||||
return response.json().then((response) => {
|
|
||||||
throw ValidationException(response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
throw AccessException(response);
|
throw AccessException(response);
|
||||||
}
|
}
|
||||||
@@ -231,24 +477,6 @@ export const fetchScopes = (): Promise<Scope[]> => {
|
|||||||
return fetchResults("/api/1.0/main/scope.json");
|
return fetchResults("/api/1.0/main/scope.json");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Error objects to be thrown
|
|
||||||
*/
|
|
||||||
const ValidationException = (
|
|
||||||
response: ValidationErrorResponse,
|
|
||||||
): ValidationExceptionInterface => {
|
|
||||||
const error = {} as ValidationExceptionInterface;
|
|
||||||
error.name = "ValidationException";
|
|
||||||
error.violations = response.violations.map(
|
|
||||||
(violation) => `${violation.title}: ${violation.propertyPath}`,
|
|
||||||
);
|
|
||||||
error.titles = response.violations.map((violation) => violation.title);
|
|
||||||
error.propertyPaths = response.violations.map(
|
|
||||||
(violation) => violation.propertyPath,
|
|
||||||
);
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const AccessException = (response: Response): AccessExceptionInterface => {
|
const AccessException = (response: Response): AccessExceptionInterface => {
|
||||||
const error = {} as AccessExceptionInterface;
|
const error = {} as AccessExceptionInterface;
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import {Gender, GenderTranslation} from "ChillMainAssets/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a given gender object into its corresponding gender translation string.
|
||||||
|
*
|
||||||
|
* @param {Gender|null} gender - The gender object to be translated, null values are also supported
|
||||||
|
* @return {GenderTranslation} Returns the gender translation string corresponding to the provided gender,
|
||||||
|
* or "unknown" if the gender is null.
|
||||||
|
*/
|
||||||
|
export function toGenderTranslation(gender: Gender|null): GenderTranslation
|
||||||
|
{
|
||||||
|
if (null === gender) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
return gender.genderTranslation;
|
||||||
|
}
|
@@ -1,14 +1,64 @@
|
|||||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
|
import { CreatableEntityType } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
export interface DateTime {
|
export interface DateTime {
|
||||||
datetime: string;
|
datetime: string;
|
||||||
datetime8601: string;
|
datetime8601: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A date representation to use when we create or update a date
|
||||||
|
*/
|
||||||
|
export interface DateTimeWrite {
|
||||||
|
/**
|
||||||
|
* Must be a string in format Y-m-d\TH:i:sO
|
||||||
|
*/
|
||||||
|
datetime: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Civility {
|
export interface Civility {
|
||||||
|
type: "chill_main_civility";
|
||||||
|
id: number;
|
||||||
|
abbreviation: TranslatableString;
|
||||||
|
active: boolean;
|
||||||
|
name: TranslatableString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight reference to Civility, to use in POST or PUT requests.
|
||||||
|
*/
|
||||||
|
export interface SetCivility {
|
||||||
|
type: "chill_main_civility";
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gender translation.
|
||||||
|
*
|
||||||
|
* Match the GenderEnum in PHP code.
|
||||||
|
*/
|
||||||
|
export type GenderTranslation = "male" | "female" | "neutral" | "unknown";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A gender
|
||||||
|
*
|
||||||
|
* See also
|
||||||
|
*/
|
||||||
|
export interface Gender {
|
||||||
|
type: "chill_main_gender";
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
genderTranslation: GenderTranslation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight reference to a Gender, used in POST / PUT requests.
|
||||||
|
*/
|
||||||
|
export interface SetGender {
|
||||||
|
type: "chill_main_gender";
|
||||||
id: number;
|
id: number;
|
||||||
// TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Household {
|
export interface Household {
|
||||||
@@ -28,6 +78,18 @@ export interface Center {
|
|||||||
id: number;
|
id: number;
|
||||||
type: "center";
|
type: "center";
|
||||||
name: string;
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetCenter is a lightweight reference used in POST/PUT requests to associate an existing center with a resource.
|
||||||
|
* It links by id only and does not create or modify centers.
|
||||||
|
* Expected shape: { type: "center", id: number }.
|
||||||
|
* Requests will fail if the id is invalid, the center doesn't exist, or permissions are insufficient.
|
||||||
|
*/
|
||||||
|
export interface SetCenter {
|
||||||
|
id: number;
|
||||||
|
type: "center";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Scope {
|
export interface Scope {
|
||||||
@@ -226,13 +288,63 @@ export interface TransportExceptionInterface {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationExceptionInterface
|
type IndexedKey<Base extends string> = `${Base}[${number}]`;
|
||||||
extends TransportExceptionInterface {
|
type BaseKeys<M> = Extract<keyof M, string>;
|
||||||
|
|
||||||
|
export type DynamicKeys<M extends Record<string, Record<string, unknown>>> =
|
||||||
|
| BaseKeys<M>
|
||||||
|
| { [K in BaseKeys<M> as IndexedKey<K>]: K }[IndexedKey<BaseKeys<M>>];
|
||||||
|
|
||||||
|
type NormalizeKey<K extends string> = K extends `${infer B}[${number}]` ? B : K;
|
||||||
|
|
||||||
|
export type ViolationFromMap<M extends Record<string, Record<string, unknown>>> = {
|
||||||
|
[K in DynamicKeys<M> & string]: { // <- note le "& string" ici
|
||||||
|
propertyPath: K;
|
||||||
|
title: string;
|
||||||
|
parameters?: M[NormalizeKey<K>];
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
}[DynamicKeys<M> & string];
|
||||||
|
|
||||||
|
export type ValidationProblemFromMap<
|
||||||
|
M extends Record<string, Record<string, string|number>>,
|
||||||
|
> = {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
detail?: string;
|
||||||
|
violations: ViolationFromMap<M>[];
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface ValidationExceptionInterface<
|
||||||
|
M extends Record<string, Record<string, string|number>> = Record<
|
||||||
|
string,
|
||||||
|
Record<string, string|number>
|
||||||
|
>,
|
||||||
|
> extends Error {
|
||||||
name: "ValidationException";
|
name: "ValidationException";
|
||||||
error: object;
|
/** Full server payload copy */
|
||||||
|
problems: ValidationProblemFromMap<M>;
|
||||||
|
/** A list of all violations, with property key */
|
||||||
|
violationsList: ViolationFromMap<M>[];
|
||||||
|
/** Compact list "Title: path" */
|
||||||
violations: string[];
|
violations: string[];
|
||||||
|
/** Only titles */
|
||||||
titles: string[];
|
titles: string[];
|
||||||
propertyPaths: string[];
|
/** Only property paths */
|
||||||
|
propertyPaths: DynamicKeys<M> & string[];
|
||||||
|
/** Indexing by property (useful for display by field) */
|
||||||
|
byProperty: Record<Extract<keyof M, string>, string[]>;
|
||||||
|
|
||||||
|
violationsByNormalizedProperty(property: Extract<keyof M, string>): ViolationFromMap<M>[];
|
||||||
|
|
||||||
|
violationsByNormalizedPropertyAndParams<
|
||||||
|
P extends Extract<keyof M, string>,
|
||||||
|
K extends Extract<keyof M[P], string>
|
||||||
|
>(
|
||||||
|
property: P,
|
||||||
|
param: K,
|
||||||
|
param_value: M[P][K]
|
||||||
|
): ViolationFromMap<M>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
export interface AccessExceptionInterface extends TransportExceptionInterface {
|
||||||
@@ -300,3 +412,12 @@ export interface TabDefinition {
|
|||||||
icon: string | null;
|
icon: string | null;
|
||||||
counter: () => number;
|
counter: () => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for the CreateModal and Create component
|
||||||
|
*/
|
||||||
|
export interface CreateComponentConfig {
|
||||||
|
action?: string;
|
||||||
|
allowedTypes: CreatableEntityType[];
|
||||||
|
query?: string;
|
||||||
|
}
|
||||||
|
@@ -24,9 +24,7 @@
|
|||||||
{{ trans(getTextTitle) }}
|
{{ trans(getTextTitle) }}
|
||||||
<span v-if="flag.loading" class="loading">
|
<span v-if="flag.loading" class="loading">
|
||||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||||
<span class="sr-only">{{
|
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
|
||||||
trans(ADDRESS_LOADING)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
@@ -90,9 +88,7 @@
|
|||||||
{{ trans(getTextTitle) }}
|
{{ trans(getTextTitle) }}
|
||||||
<span v-if="flag.loading" class="loading">
|
<span v-if="flag.loading" class="loading">
|
||||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||||
<span class="sr-only">{{
|
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
|
||||||
trans(ADDRESS_LOADING)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
@@ -175,9 +171,7 @@
|
|||||||
{{ trans(getTextTitle) }}
|
{{ trans(getTextTitle) }}
|
||||||
<span v-if="flag.loading" class="loading">
|
<span v-if="flag.loading" class="loading">
|
||||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||||
<span class="sr-only">{{
|
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
|
||||||
trans(ADDRESS_LOADING)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
@@ -274,7 +268,8 @@ export default {
|
|||||||
PREVIOUS,
|
PREVIOUS,
|
||||||
NEXT,
|
NEXT,
|
||||||
};
|
};
|
||||||
},props: ["context", "options", "addressChangedCallback"],
|
},
|
||||||
|
props: ["context", "options", "addressChangedCallback"],
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
ShowPane,
|
ShowPane,
|
||||||
|
@@ -55,9 +55,7 @@
|
|||||||
:placeholder="trans(ADDRESS_BUILDING_NAME)"
|
:placeholder="trans(ADDRESS_BUILDING_NAME)"
|
||||||
v-model="buildingName"
|
v-model="buildingName"
|
||||||
/>
|
/>
|
||||||
<label for="buildingName">{{
|
<label for="buildingName">{{ trans(ADDRESS_BUILDING_NAME) }}</label>
|
||||||
trans(ADDRESS_BUILDING_NAME)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating my-1">
|
<div class="form-floating my-1">
|
||||||
<input
|
<input
|
||||||
@@ -79,9 +77,7 @@
|
|||||||
:placeholder="trans(ADDRESS_DISTRIBUTION)"
|
:placeholder="trans(ADDRESS_DISTRIBUTION)"
|
||||||
v-model="distribution"
|
v-model="distribution"
|
||||||
/>
|
/>
|
||||||
<label for="distribution">{{
|
<label for="distribution">{{ trans(ADDRESS_DISTRIBUTION) }}</label>
|
||||||
trans(ADDRESS_DISTRIBUTION)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +113,8 @@ export default {
|
|||||||
ADDRESS_FILL_AN_ADDRESS,
|
ADDRESS_FILL_AN_ADDRESS,
|
||||||
trans,
|
trans,
|
||||||
};
|
};
|
||||||
},props: ["entity", "isNoAddress"],
|
},
|
||||||
|
props: ["entity", "isNoAddress"],
|
||||||
computed: {
|
computed: {
|
||||||
floor: {
|
floor: {
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@@ -57,9 +57,7 @@
|
|||||||
:placeholder="trans(ADDRESS_STREET_NUMBER)"
|
:placeholder="trans(ADDRESS_STREET_NUMBER)"
|
||||||
v-model="streetNumber"
|
v-model="streetNumber"
|
||||||
/>
|
/>
|
||||||
<label for="streetNumber">{{
|
<label for="streetNumber">{{ trans(ADDRESS_STREET_NUMBER) }}</label>
|
||||||
trans(ADDRESS_STREET_NUMBER)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +94,8 @@ export default {
|
|||||||
ADDRESS_CREATE_ADDRESS,
|
ADDRESS_CREATE_ADDRESS,
|
||||||
trans,
|
trans,
|
||||||
};
|
};
|
||||||
},props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
|
},
|
||||||
|
props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: this.context.edit ? this.entity.address.addressReference : null,
|
value: this.context.edit ? this.entity.address.addressReference : null,
|
||||||
|
@@ -85,7 +85,8 @@ export default {
|
|||||||
ADDRESS_CREATE_POSTAL_CODE,
|
ADDRESS_CREATE_POSTAL_CODE,
|
||||||
trans,
|
trans,
|
||||||
};
|
};
|
||||||
},props: [
|
},
|
||||||
|
props: [
|
||||||
"entity",
|
"entity",
|
||||||
"context",
|
"context",
|
||||||
"focusOnAddress",
|
"focusOnAddress",
|
||||||
|
@@ -44,7 +44,8 @@ export default {
|
|||||||
ADDRESS_SELECT_COUNTRY,
|
ADDRESS_SELECT_COUNTRY,
|
||||||
trans,
|
trans,
|
||||||
};
|
};
|
||||||
},props: ["context", "entity", "flag", "checkErrors"],
|
},
|
||||||
|
props: ["context", "entity", "flag", "checkErrors"],
|
||||||
emits: ["getCities"],
|
emits: ["getCities"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@@ -129,7 +129,8 @@ export default {
|
|||||||
ADDRESS_IS_CONFIDENTIAL,
|
ADDRESS_IS_CONFIDENTIAL,
|
||||||
ADDRESS_IS_NO_ADDRESS,
|
ADDRESS_IS_NO_ADDRESS,
|
||||||
};
|
};
|
||||||
},props: [
|
},
|
||||||
|
props: [
|
||||||
"context",
|
"context",
|
||||||
"options",
|
"options",
|
||||||
"defaultz",
|
"defaultz",
|
||||||
|
@@ -11,9 +11,7 @@
|
|||||||
|
|
||||||
<div v-if="flag.success" class="alert alert-success">
|
<div v-if="flag.success" class="alert alert-success">
|
||||||
{{ trans(getSuccessText) }}
|
{{ trans(getSuccessText) }}
|
||||||
<span v-if="forceRedirect">{{
|
<span v-if="forceRedirect">{{ trans(ADDRESS_WAIT_REDIRECTION) }}</span>
|
||||||
trans(ADDRESS_WAIT_REDIRECTION)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -113,7 +111,8 @@ import {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ShowPane",
|
name: "ShowPane",
|
||||||
methods: {},components: {
|
methods: {},
|
||||||
|
components: {
|
||||||
AddressRenderBox,
|
AddressRenderBox,
|
||||||
ActionButtons,
|
ActionButtons,
|
||||||
},
|
},
|
||||||
@@ -128,7 +127,8 @@ export default {
|
|||||||
ADDRESS_ADDRESS_NEW_SUCCESS,
|
ADDRESS_ADDRESS_NEW_SUCCESS,
|
||||||
ADDRESS_ADDRESS_EDIT_SUCCESS,
|
ADDRESS_ADDRESS_EDIT_SUCCESS,
|
||||||
};
|
};
|
||||||
},props: [
|
},
|
||||||
|
props: [
|
||||||
"context",
|
"context",
|
||||||
"defaultz",
|
"defaultz",
|
||||||
"options",
|
"options",
|
||||||
@@ -179,7 +179,9 @@ export default {
|
|||||||
: this.defaultz.button.text.create;
|
: this.defaultz.button.text.create;
|
||||||
},
|
},
|
||||||
getSuccessText() {
|
getSuccessText() {
|
||||||
return this.context.edit ? ADDRESS_ADDRESS_EDIT_SUCCESS : ADDRESS_ADDRESS_NEW_SUCCESS;
|
return this.context.edit
|
||||||
|
? ADDRESS_ADDRESS_EDIT_SUCCESS
|
||||||
|
: ADDRESS_ADDRESS_NEW_SUCCESS;
|
||||||
},
|
},
|
||||||
onlyButton() {
|
onlyButton() {
|
||||||
return typeof this.options.onlyButton !== "undefined"
|
return typeof this.options.onlyButton !== "undefined"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li v-if="allowedTypes.includes('person')" class="nav-item">
|
<li v-if="containsPerson" class="nav-item">
|
||||||
<a class="nav-link" :class="{ active: isActive('person') }">
|
<a class="nav-link" :class="{ active: isActive('person') }">
|
||||||
<label for="person">
|
<label for="person">
|
||||||
<input
|
<input
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="allowedTypes.includes('thirdparty')" class="nav-item">
|
<li v-if="containsThirdParty" class="nav-item">
|
||||||
<a class="nav-link" :class="{ active: isActive('thirdparty') }">
|
<a class="nav-link" :class="{ active: isActive('thirdparty') }">
|
||||||
<label for="thirdparty">
|
<label for="thirdparty">
|
||||||
<input
|
<input
|
||||||
@@ -31,11 +31,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="my-4">
|
<div class="my-4">
|
||||||
<on-the-fly-person
|
<PersonEdit
|
||||||
v-if="type === 'person'"
|
v-if="type === 'person'"
|
||||||
:action="action"
|
action="create"
|
||||||
:query="query"
|
:query="query"
|
||||||
ref="castPerson"
|
ref="castPerson"
|
||||||
|
@onPersonCreated="(payload) => emit('onPersonCreated', payload)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<on-the-fly-thirdparty
|
<on-the-fly-thirdparty
|
||||||
@@ -46,34 +47,47 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
|
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
|
||||||
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
|
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
|
||||||
import {
|
import {
|
||||||
trans,
|
|
||||||
ONTHEFLY_CREATE_PERSON,
|
ONTHEFLY_CREATE_PERSON,
|
||||||
ONTHEFLY_CREATE_THIRDPARTY,
|
ONTHEFLY_CREATE_THIRDPARTY,
|
||||||
|
trans,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
import { CreatableEntityType, Person } from "ChillPersonAssets/types";
|
||||||
|
import { CreateComponentConfig } from "ChillMainAssets/types";
|
||||||
|
import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(defineProps<CreateComponentConfig>(), {
|
||||||
action: String,
|
allowedTypes: ["person"],
|
||||||
allowedTypes: Array,
|
action: "create",
|
||||||
query: String,
|
query: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const type = ref(null);
|
const emit =
|
||||||
|
defineEmits<(e: "onPersonCreated", payload: { person: Person }) => void>();
|
||||||
|
|
||||||
const radioType = computed({
|
const type = ref<CreatableEntityType | null>(null);
|
||||||
|
|
||||||
|
const radioType = computed<CreatableEntityType | null>({
|
||||||
get: () => type.value,
|
get: () => type.value,
|
||||||
set: (val) => {
|
set: (val: CreatableEntityType | null) => {
|
||||||
type.value = val;
|
type.value = val;
|
||||||
console.log("## type:", val, ", action:", props.action);
|
console.log("## type:", val, ", action:", props.action);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const castPerson = ref(null);
|
type PersonEditComponent = InstanceType<typeof PersonEdit>;
|
||||||
const castThirdparty = ref(null);
|
|
||||||
|
type AnyComponentInstance =
|
||||||
|
| InstanceType<typeof OnTheFlyPerson>
|
||||||
|
| InstanceType<typeof OnTheFlyThirdparty>
|
||||||
|
| null;
|
||||||
|
|
||||||
|
const castPerson = ref<PersonEditComponent>(null);
|
||||||
|
const castThirdparty = ref<AnyComponentInstance>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
type.value =
|
type.value =
|
||||||
@@ -82,30 +96,22 @@ onMounted(() => {
|
|||||||
: "person";
|
: "person";
|
||||||
});
|
});
|
||||||
|
|
||||||
function isActive(tab) {
|
function isActive(tab: CreatableEntityType) {
|
||||||
return type.value === tab;
|
return type.value === tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
function castDataByType() {
|
const containsThirdParty = computed<boolean>(() =>
|
||||||
switch (radioType.value) {
|
props.allowedTypes.includes("thirdparty"),
|
||||||
case "person":
|
);
|
||||||
return castPerson.value.$data.person;
|
const containsPerson = computed<boolean>(() => {
|
||||||
case "thirdparty":
|
return props.allowedTypes.includes("person");
|
||||||
let data = castThirdparty.value.$data.thirdparty;
|
});
|
||||||
if (data.address !== undefined && data.address !== null) {
|
|
||||||
data.address = { id: data.address.address_id };
|
function save(): void {
|
||||||
} else {
|
castPerson.value.postPerson();
|
||||||
data.address = null;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
default:
|
|
||||||
throw Error("Invalid type of entity");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({ save });
|
||||||
castDataByType,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
@@ -0,0 +1,59 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||||
|
import Create from "ChillMainAssets/vuejs/OnTheFly/components/Create.vue";
|
||||||
|
import { CreateComponentConfig } from "ChillMainAssets/types";
|
||||||
|
import { trans, SAVE } from "translator";
|
||||||
|
import { useTemplateRef } from "vue";
|
||||||
|
import { Person } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "onPersonCreated", payload: { person: Person }): void;
|
||||||
|
(e: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<CreateComponentConfig>();
|
||||||
|
const modalDialogClass = { "modal-xl": true, "modal-scrollable": true };
|
||||||
|
|
||||||
|
type CreateComponentType = InstanceType<typeof Create>;
|
||||||
|
|
||||||
|
const create = useTemplateRef<CreateComponentType>("create");
|
||||||
|
|
||||||
|
function save(): void {
|
||||||
|
console.log("save from CreateModal");
|
||||||
|
create.value?.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ save });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<modal
|
||||||
|
@close="() => emit('close')"
|
||||||
|
:modal-dialog-class="modalDialogClass"
|
||||||
|
:hide-footer="false"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="modal-title">{{ modalTitle }}</h3>
|
||||||
|
</template>
|
||||||
|
<template #body-head>
|
||||||
|
<div class="modal-body">
|
||||||
|
<Create
|
||||||
|
ref="create"
|
||||||
|
:allowedTypes="props.allowedTypes"
|
||||||
|
:action="props.action"
|
||||||
|
:query="props.query"
|
||||||
|
@onPersonCreated="(payload) => emit('onPersonCreated', payload)"
|
||||||
|
></Create>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<button class="btn btn-save" type="button" @click.prevent="save">
|
||||||
|
{{ trans(SAVE) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
@@ -40,6 +40,7 @@
|
|||||||
:key="uniqid"
|
:key="uniqid"
|
||||||
:buttonTitle="translatedListOfTypes"
|
:buttonTitle="translatedListOfTypes"
|
||||||
:modalTitle="translatedListOfTypes"
|
:modalTitle="translatedListOfTypes"
|
||||||
|
:allowCreate="true"
|
||||||
@addNewPersons="addNewEntity"
|
@addNewPersons="addNewEntity"
|
||||||
>
|
>
|
||||||
</add-persons>
|
</add-persons>
|
||||||
@@ -76,6 +77,7 @@ import {
|
|||||||
EntitiesOrMe,
|
EntitiesOrMe,
|
||||||
EntityType,
|
EntityType,
|
||||||
SearchOptions,
|
SearchOptions,
|
||||||
|
Suggestion,
|
||||||
} from "ChillPersonAssets/types";
|
} from "ChillPersonAssets/types";
|
||||||
import {
|
import {
|
||||||
PICK_ENTITY_MODAL_TITLE,
|
PICK_ENTITY_MODAL_TITLE,
|
||||||
@@ -182,7 +184,7 @@ function addNewSuggested(entity: EntitiesOrMe) {
|
|||||||
emits("addNewEntity", { entity });
|
emits("addNewEntity", { entity });
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNewEntity({ selected }: addNewEntities) {
|
function addNewEntity({ selected }: { selected: Suggestion[] }) {
|
||||||
Object.values(selected).forEach((item) => {
|
Object.values(selected).forEach((item) => {
|
||||||
emits("addNewEntity", { entity: item.result });
|
emits("addNewEntity", { entity: item.result });
|
||||||
});
|
});
|
||||||
|
@@ -1,28 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<i :class="['fa', genderClass, 'px-1']" />
|
<i :class="['bi', genderClass]"></i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
const props = defineProps({
|
import type { Gender } from "ChillMainAssets/types";
|
||||||
gender: {
|
import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const genderClass = computed(() => {
|
interface GenderIconRenderBoxProps {
|
||||||
switch (props.gender.genderTranslation) {
|
gender: Gender;
|
||||||
case "woman":
|
}
|
||||||
return "fa-venus";
|
|
||||||
case "man":
|
const props = defineProps<GenderIconRenderBoxProps>();
|
||||||
return "fa-mars";
|
|
||||||
case "both":
|
const genderClass = computed<string>(() => {
|
||||||
return "fa-neuter";
|
switch (toGenderTranslation(props.gender)) {
|
||||||
|
case "female":
|
||||||
|
return "bi-gender-female";
|
||||||
|
case "male":
|
||||||
|
return "bi-gender-male";
|
||||||
|
case "neutral":
|
||||||
case "unknown":
|
case "unknown":
|
||||||
return "fa-genderless";
|
|
||||||
default:
|
default:
|
||||||
return "fa-genderless";
|
return "bi-gender-neuter";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@@ -52,23 +52,15 @@ import { trans, MODAL_ACTION_CLOSE } from "translator";
|
|||||||
import { defineProps } from "vue";
|
import { defineProps } from "vue";
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
modalDialogClass: string;
|
modalDialogClass?: string | Record<string, boolean>;
|
||||||
hideFooter: boolean;
|
hideFooter?: boolean;
|
||||||
|
show?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps({
|
const props = withDefaults(defineProps<ModalProps>(), {
|
||||||
modalDialogClass: {
|
modalDialogClass: "",
|
||||||
type: String,
|
hideFooter: false,
|
||||||
default: "",
|
show: true,
|
||||||
},
|
|
||||||
hideFooter: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
|
@@ -22,7 +22,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
|
|||||||
{
|
{
|
||||||
public function __construct(private readonly RequestStack $requestStack, private readonly ParameterBagInterface $parameterBag) {}
|
public function __construct(private readonly RequestStack $requestStack, private readonly ParameterBagInterface $parameterBag) {}
|
||||||
|
|
||||||
public function denormalize($data, $type, $format = null, array $context = [])
|
public function denormalize($data, $type, $format = null, array $context = []): ?\DateTimeInterface
|
||||||
{
|
{
|
||||||
if (null === $data) {
|
if (null === $data) {
|
||||||
return null;
|
return null;
|
||||||
@@ -51,7 +51,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function normalize($date, $format = null, array $context = [])
|
public function normalize($date, $format = null, array $context = []): array
|
||||||
{
|
{
|
||||||
/* @var DateTimeInterface $date */
|
/* @var DateTimeInterface $date */
|
||||||
switch ($format) {
|
switch ($format) {
|
||||||
|
@@ -46,7 +46,10 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
|
|||||||
try {
|
try {
|
||||||
return $this->phoneNumberUtil->parse($data, $this->defaultCarrierCode);
|
return $this->phoneNumberUtil->parse($data, $this->defaultCarrierCode);
|
||||||
} catch (NumberParseException $e) {
|
} catch (NumberParseException $e) {
|
||||||
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
|
$phonenumber = new PhoneNumber();
|
||||||
|
$phonenumber->setRawInput($data);
|
||||||
|
|
||||||
|
return $phonenumber;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,6 +13,9 @@ namespace Chill\MainBundle\Validation\Constraint;
|
|||||||
|
|
||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use odolbeau/phonenumber validator instead
|
||||||
|
*/
|
||||||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
|
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
|
||||||
class PhonenumberConstraint extends Constraint
|
class PhonenumberConstraint extends Constraint
|
||||||
{
|
{
|
||||||
|
@@ -16,6 +16,9 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
use Symfony\Component\Validator\ConstraintValidator;
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use https://github.com/odolbeau/phone-number-bundle/blob/master/src/Validator/Constraints/PhoneNumberValidator.php instead
|
||||||
|
*/
|
||||||
final class ValidPhonenumber extends ConstraintValidator
|
final class ValidPhonenumber extends ConstraintValidator
|
||||||
{
|
{
|
||||||
public function __construct(private readonly LoggerInterface $logger, private readonly PhoneNumberHelperInterface $phonenumberHelper) {}
|
public function __construct(private readonly LoggerInterface $logger, private readonly PhoneNumberHelperInterface $phonenumberHelper) {}
|
||||||
|
@@ -136,34 +136,6 @@ filter_order:
|
|||||||
Search: Chercher dans la liste
|
Search: Chercher dans la liste
|
||||||
By date: Filtrer par date
|
By date: Filtrer par date
|
||||||
search_box: Filtrer par contenu
|
search_box: Filtrer par contenu
|
||||||
renderbox:
|
|
||||||
person: "Usager"
|
|
||||||
birthday:
|
|
||||||
man: "Né le"
|
|
||||||
woman: "Née le"
|
|
||||||
neutral: "Né·e le"
|
|
||||||
unknown: "Né·e le"
|
|
||||||
deathdate: "Date de décès"
|
|
||||||
household_without_address: "Le ménage de l'usager est sans adresse"
|
|
||||||
no_data: "Aucune information renseignée"
|
|
||||||
type:
|
|
||||||
thirdparty: "Tiers"
|
|
||||||
person: "Usager"
|
|
||||||
holder: "Titulaire"
|
|
||||||
years_old: >-
|
|
||||||
{n, plural,
|
|
||||||
=0 {0 an}
|
|
||||||
one {1 an}
|
|
||||||
other {# ans}
|
|
||||||
}
|
|
||||||
residential_address: "Adresse de résidence"
|
|
||||||
located_at: "réside chez"
|
|
||||||
household_number: "Ménage n°{number}"
|
|
||||||
current_members: "Membres actuels"
|
|
||||||
no_current_address: "Sans adresse actuellement"
|
|
||||||
new_household: "Nouveau ménage"
|
|
||||||
no_members_yet: "Aucun membre actuellement"
|
|
||||||
|
|
||||||
pick_entity:
|
pick_entity:
|
||||||
add: "Ajouter"
|
add: "Ajouter"
|
||||||
modal_title: >-
|
modal_title: >-
|
||||||
|
@@ -0,0 +1,47 @@
|
|||||||
|
<?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\PersonBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||||
|
use Chill\MainBundle\Serializer\Model\Collection;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
final readonly class PersonIdentifierListApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
private SerializerInterface $serializer,
|
||||||
|
private PersonIdentifierManagerInterface $personIdentifierManager,
|
||||||
|
private PaginatorFactoryInterface $paginatorFactory,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/1.0/person/identifiers/workers', name: 'person_person_identifiers_worker_list')]
|
||||||
|
public function list(): Response
|
||||||
|
{
|
||||||
|
if (!$this->security->isGranted('ROLE_USER')) {
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$workers = $this->personIdentifierManager->getWorkers();
|
||||||
|
$paginator = $this->paginatorFactory->create(count($workers));
|
||||||
|
$paginator->setItemsPerPage(count($workers));
|
||||||
|
$collection = new Collection($workers, $paginator);
|
||||||
|
|
||||||
|
return new JsonResponse($this->serializer->serialize($collection, 'json'), json: true);
|
||||||
|
}
|
||||||
|
}
|
@@ -96,7 +96,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
|
|||||||
// We can get rid of this file when the service 'chill.person.repository.person' is no more used.
|
// We can get rid of this file when the service 'chill.person.repository.person' is no more used.
|
||||||
// We should use the PersonRepository service instead of a custom service name.
|
// We should use the PersonRepository service instead of a custom service name.
|
||||||
$loader->load('services/repository.yaml');
|
$loader->load('services/repository.yaml');
|
||||||
$loader->load('services/serializer.yaml');
|
|
||||||
$loader->load('services/security.yaml');
|
$loader->load('services/security.yaml');
|
||||||
$loader->load('services/doctrineEventListener.yaml');
|
$loader->load('services/doctrineEventListener.yaml');
|
||||||
$loader->load('services/accompanyingPeriodConsistency.yaml');
|
$loader->load('services/accompanyingPeriodConsistency.yaml');
|
||||||
|
@@ -0,0 +1,42 @@
|
|||||||
|
<?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\PersonBundle\Entity\Identifier;
|
||||||
|
|
||||||
|
enum IdentifierPresenceEnum: string
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The person identifier is not editable by any user.
|
||||||
|
*
|
||||||
|
* The identifier is intended to be added by an import script, for instance.
|
||||||
|
*/
|
||||||
|
case NOT_EDITABLE = 'NOT_EDITABLE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The person identifier is present on the edit form only.
|
||||||
|
*/
|
||||||
|
case ON_EDIT = 'ON_EDIT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The person identifier is present on both person's creation form, and edit form.
|
||||||
|
*/
|
||||||
|
case ON_CREATION = 'ON_CREATION';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The person identifier is required to create the person. It should not be empty.
|
||||||
|
*/
|
||||||
|
case REQUIRED = 'REQUIRED';
|
||||||
|
|
||||||
|
public function isEditableByUser(): bool
|
||||||
|
{
|
||||||
|
return IdentifierPresenceEnum::NOT_EDITABLE !== $this;
|
||||||
|
}
|
||||||
|
}
|
@@ -12,10 +12,13 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Entity\Identifier;
|
namespace Chill\PersonBundle\Entity\Identifier;
|
||||||
|
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraint;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity]
|
||||||
#[ORM\Table(name: 'chill_person_identifier')]
|
#[ORM\Table(name: 'chill_person_identifier')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'chill_person_identifier_unique', columns: ['definition_id', 'canonical'])]
|
||||||
|
#[UniqueIdentifierConstraint]
|
||||||
class PersonIdentifier
|
class PersonIdentifier
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -30,12 +33,12 @@ class PersonIdentifier
|
|||||||
#[ORM\Column(name: 'value', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
#[ORM\Column(name: 'value', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||||
private array $value = [];
|
private array $value = [];
|
||||||
|
|
||||||
#[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '[]'])]
|
#[ORM\Column(name: 'canonical', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||||
private string $canonical = '';
|
private string $canonical = '';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)]
|
#[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)]
|
||||||
#[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
|
||||||
private PersonIdentifierDefinition $definition,
|
private PersonIdentifierDefinition $definition,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Entity\Identifier;
|
namespace Chill\PersonBundle\Entity\Identifier;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
#[ORM\Entity]
|
#[ORM\Entity]
|
||||||
@@ -18,23 +19,23 @@ use Doctrine\ORM\Mapping as ORM;
|
|||||||
class PersonIdentifierDefinition
|
class PersonIdentifierDefinition
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
#[ORM\Column(name: 'id', type: Types::INTEGER)]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'active', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
#[ORM\Column(name: 'active', type: Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
||||||
private bool $active = true;
|
private bool $active = true;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[ORM\Column(name: 'label', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
#[ORM\Column(name: 'label', type: Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||||
private array $label,
|
private array $label,
|
||||||
#[ORM\Column(name: 'engine', type: \Doctrine\DBAL\Types\Types::STRING, length: 100)]
|
#[ORM\Column(name: 'engine', type: Types::STRING, length: 100)]
|
||||||
private string $engine,
|
private string $engine,
|
||||||
#[ORM\Column(name: 'is_searchable', type: \Doctrine\DBAL\Types\Types::BOOLEAN, options: ['default' => false])]
|
#[ORM\Column(name: 'is_searchable', type: Types::BOOLEAN, options: ['default' => false])]
|
||||||
private bool $isSearchable = false,
|
private bool $isSearchable = false,
|
||||||
#[ORM\Column(name: 'is_editable_by_users', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => false])]
|
#[ORM\Column(name: 'presence', type: Types::STRING, nullable: false, enumType: IdentifierPresenceEnum::class, options: ['default' => IdentifierPresenceEnum::ON_EDIT])]
|
||||||
private bool $isEditableByUsers = false,
|
private IdentifierPresenceEnum $presence = IdentifierPresenceEnum::ON_EDIT,
|
||||||
#[ORM\Column(name: 'data', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
#[ORM\Column(name: 'data', type: Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||||
private array $data = [],
|
private array $data = [],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -58,11 +59,6 @@ class PersonIdentifierDefinition
|
|||||||
return $this->engine;
|
return $this->engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setEngine(string $engine): void
|
|
||||||
{
|
|
||||||
$this->engine = $engine;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isSearchable(): bool
|
public function isSearchable(): bool
|
||||||
{
|
{
|
||||||
return $this->isSearchable;
|
return $this->isSearchable;
|
||||||
@@ -75,12 +71,7 @@ class PersonIdentifierDefinition
|
|||||||
|
|
||||||
public function isEditableByUsers(): bool
|
public function isEditableByUsers(): bool
|
||||||
{
|
{
|
||||||
return $this->isEditableByUsers;
|
return $this->presence->isEditableByUser();
|
||||||
}
|
|
||||||
|
|
||||||
public function setIsEditableByUsers(bool $isEditableByUsers): void
|
|
||||||
{
|
|
||||||
$this->isEditableByUsers = $isEditableByUsers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isActive(): bool
|
public function isActive(): bool
|
||||||
@@ -104,4 +95,16 @@ class PersonIdentifierDefinition
|
|||||||
{
|
{
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPresence(): IdentifierPresenceEnum
|
||||||
|
{
|
||||||
|
return $this->presence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPresence(IdentifierPresenceEnum $presence): self
|
||||||
|
{
|
||||||
|
$this->presence = $presence;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,6 @@ use Chill\MainBundle\Entity\Language;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
use Chill\PersonBundle\Entity\Household\Household;
|
use Chill\PersonBundle\Entity\Household\Household;
|
||||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||||
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
|
||||||
@@ -36,6 +35,7 @@ use Chill\PersonBundle\Entity\Person\PersonCenterCurrent;
|
|||||||
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
|
||||||
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
|
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
|
||||||
use Chill\PersonBundle\Entity\Person\PersonResource;
|
use Chill\PersonBundle\Entity\Person\PersonResource;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Validator\RequiredIdentifierConstraint;
|
||||||
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
|
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
|
||||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||||
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
|
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
|
||||||
@@ -47,6 +47,7 @@ use Doctrine\Common\Collections\ReadableCollection;
|
|||||||
use Doctrine\Common\Collections\Selectable;
|
use Doctrine\Common\Collections\Selectable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
@@ -273,6 +274,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
|
#[RequiredIdentifierConstraint]
|
||||||
|
#[Assert\Valid]
|
||||||
private Collection $identifiers;
|
private Collection $identifiers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,7 +322,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
* The person's mobile phone number.
|
* The person's mobile phone number.
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'mobile')]
|
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::MOBILE])]
|
||||||
private ?PhoneNumber $mobilenumber = null;
|
private ?PhoneNumber $mobilenumber = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -359,7 +362,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
* The person's phonenumber.
|
* The person's phonenumber.
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'landline')]
|
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::FIXED_LINE, MisdPhoneNumberConstraint::VOIP, MisdPhoneNumberConstraint::PERSONAL_NUMBER])]
|
||||||
private ?PhoneNumber $phonenumber = null;
|
private ?PhoneNumber $phonenumber = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -89,6 +89,11 @@ final class CreationPersonType extends AbstractType
|
|||||||
'label' => false,
|
'label' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$builder->add('identifiers', PersonIdentifiersType::class, [
|
||||||
|
'by_reference' => false,
|
||||||
|
'step' => 'on_create',
|
||||||
|
]);
|
||||||
|
|
||||||
if ($this->askCenters) {
|
if ($this->askCenters) {
|
||||||
$builder
|
$builder
|
||||||
->add('center', PickCenterType::class, [
|
->add('center', PickCenterType::class, [
|
||||||
|
@@ -12,10 +12,12 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Form;
|
namespace Chill\PersonBundle\Form;
|
||||||
|
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\IdentifierPresenceEnum;
|
||||||
use Chill\PersonBundle\Form\DataMapper\PersonIdentifiersDataMapper;
|
use Chill\PersonBundle\Form\DataMapper\PersonIdentifiersDataMapper;
|
||||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
final class PersonIdentifiersType extends AbstractType
|
final class PersonIdentifiersType extends AbstractType
|
||||||
{
|
{
|
||||||
@@ -32,6 +34,12 @@ final class PersonIdentifiersType extends AbstractType
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skip some on creation
|
||||||
|
if ('on_create' === $options['step']
|
||||||
|
&& IdentifierPresenceEnum::ON_EDIT === $worker->getDefinition()->getPresence()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$subBuilder = $builder->create(
|
$subBuilder = $builder->create(
|
||||||
'identifier_'.$worker->getDefinition()->getId(),
|
'identifier_'.$worker->getDefinition()->getId(),
|
||||||
options: [
|
options: [
|
||||||
@@ -45,4 +53,10 @@ final class PersonIdentifiersType extends AbstractType
|
|||||||
|
|
||||||
$builder->setDataMapper($this->identifiersDataMapper);
|
$builder->setDataMapper($this->identifiersDataMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefault('step', 'on_edit')
|
||||||
|
->setAllowedValues('step', ['on_edit', 'on_create']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,14 +19,16 @@ use Symfony\Component\Form\FormBuilderInterface;
|
|||||||
|
|
||||||
final readonly class StringIdentifier implements PersonIdentifierEngineInterface
|
final readonly class StringIdentifier implements PersonIdentifierEngineInterface
|
||||||
{
|
{
|
||||||
|
public const NAME = 'chill-person-bundle.string-identifier';
|
||||||
|
|
||||||
public static function getName(): string
|
public static function getName(): string
|
||||||
{
|
{
|
||||||
return 'chill-person-bundle.string-identifier';
|
return self::NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
{
|
{
|
||||||
return $value['content'] ?? '';
|
return trim($value['content'] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void
|
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void
|
||||||
@@ -36,6 +38,11 @@ final readonly class StringIdentifier implements PersonIdentifierEngineInterface
|
|||||||
|
|
||||||
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
{
|
{
|
||||||
return $identifier?->getValue()['content'] ?? '';
|
return trim($identifier?->getValue()['content'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmpty(PersonIdentifier $identifier): bool
|
||||||
|
{
|
||||||
|
return '' === trim($identifier->getValue()['content'] ?? '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,39 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Normalizer;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
final readonly class PersonIdentifierWorkerNormalizer implements NormalizerInterface
|
||||||
|
{
|
||||||
|
public function normalize($object, ?string $format = null, array $context = []): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof PersonIdentifierWorker) {
|
||||||
|
throw new \Symfony\Component\Serializer\Exception\UnexpectedValueException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'person_identifier_worker',
|
||||||
|
'definition_id' => $object->getDefinition()->getId(),
|
||||||
|
'engine' => $object->getDefinition()->getEngine(),
|
||||||
|
'label' => $object->getDefinition()->getLabel(),
|
||||||
|
'isActive' => $object->getDefinition()->isActive(),
|
||||||
|
'presence' => $object->getDefinition()->getPresence()->value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsNormalization($data, ?string $format = null): bool
|
||||||
|
{
|
||||||
|
return $data instanceof PersonIdentifierWorker;
|
||||||
|
}
|
||||||
|
}
|
@@ -24,4 +24,12 @@ interface PersonIdentifierEngineInterface
|
|||||||
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void;
|
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void;
|
||||||
|
|
||||||
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string;
|
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the identifier must be considered as empty.
|
||||||
|
*
|
||||||
|
* This is in use when the identifier is validated and must be required. If the identifier is empty and is required
|
||||||
|
* by the definition, the validation will fails.
|
||||||
|
*/
|
||||||
|
public function isEmpty(PersonIdentifier $identifier): bool;
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\PersonIdentifier;
|
|||||||
|
|
||||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
use Chill\PersonBundle\PersonIdentifier\Exception\EngineNotFoundException;
|
use Chill\PersonBundle\PersonIdentifier\Exception\EngineNotFoundException;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Exception\PersonIdentifierDefinitionNotFoundException;
|
||||||
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
|
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
|
||||||
|
|
||||||
final readonly class PersonIdentifierManager implements PersonIdentifierManagerInterface
|
final readonly class PersonIdentifierManager implements PersonIdentifierManagerInterface
|
||||||
@@ -44,8 +45,16 @@ final readonly class PersonIdentifierManager implements PersonIdentifierManagerI
|
|||||||
return $workers;
|
return $workers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
|
public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
|
||||||
{
|
{
|
||||||
|
if (is_int($personIdentifierDefinition)) {
|
||||||
|
$id = $personIdentifierDefinition;
|
||||||
|
$personIdentifierDefinition = $this->personIdentifierDefinitionRepository->find($id);
|
||||||
|
if (null === $personIdentifierDefinition) {
|
||||||
|
throw new PersonIdentifierDefinitionNotFoundException($id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new PersonIdentifierWorker($this->getEngine($personIdentifierDefinition->getEngine()), $personIdentifierDefinition);
|
return new PersonIdentifierWorker($this->getEngine($personIdentifierDefinition->getEngine()), $personIdentifierDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,5 +22,8 @@ interface PersonIdentifierManagerInterface
|
|||||||
*/
|
*/
|
||||||
public function getWorkers(): array;
|
public function getWorkers(): array;
|
||||||
|
|
||||||
public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
|
/**
|
||||||
|
* @param int|PersonIdentifierDefinition $personIdentifierDefinition an instance of PersonIdentifierDefinition, or his id
|
||||||
|
*/
|
||||||
|
public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
|
||||||
}
|
}
|
||||||
|
@@ -46,4 +46,12 @@ final readonly class PersonIdentifierWorker
|
|||||||
{
|
{
|
||||||
return $this->identifierEngine->renderAsString($identifier, $this->definition);
|
return $this->identifierEngine->renderAsString($identifier, $this->definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the identifier must be considered as empty.
|
||||||
|
*/
|
||||||
|
public function isEmpty(PersonIdentifier $identifier): bool
|
||||||
|
{
|
||||||
|
return $this->identifierEngine->isEmpty($identifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the required constraints are present.
|
||||||
|
*/
|
||||||
|
#[\Attribute]
|
||||||
|
class RequiredIdentifierConstraint extends Constraint
|
||||||
|
{
|
||||||
|
public string $message = 'person_identifier.This identifier must be set';
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::PROPERTY_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,53 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Validator;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\IdentifierPresenceEnum;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||||
|
|
||||||
|
final class RequiredIdentifierConstraintValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PersonIdentifierManagerInterface $identifierManager) {}
|
||||||
|
|
||||||
|
public function validate($value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof RequiredIdentifierConstraint) {
|
||||||
|
throw new UnexpectedTypeException($constraint, RequiredIdentifierConstraint::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$value instanceof Collection) {
|
||||||
|
throw new UnexpectedValueException($value, Collection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->identifierManager->getWorkers() as $worker) {
|
||||||
|
if (IdentifierPresenceEnum::REQUIRED !== $worker->getDefinition()->getPresence()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $value->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getDefinition() === $worker->getDefinition());
|
||||||
|
|
||||||
|
if (null === $identifier || $worker->isEmpty($identifier)) {
|
||||||
|
$this->context->buildViolation($constraint->message)
|
||||||
|
->setParameter('{{ value }}', $worker->renderAsString($identifier))
|
||||||
|
->setParameter('definition_id', (string) $worker->getDefinition()->getId())
|
||||||
|
->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Validator;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
#[\Attribute]
|
||||||
|
class UniqueIdentifierConstraint extends Constraint
|
||||||
|
{
|
||||||
|
public string $message = 'person_identifier.Identifier must be unique. The same identifier already exists for {{ persons }}';
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,53 @@
|
|||||||
|
<?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\PersonBundle\PersonIdentifier\Validator;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository;
|
||||||
|
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||||
|
|
||||||
|
class UniqueIdentifierConstraintValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PersonIdentifierRepository $personIdentifierRepository,
|
||||||
|
private readonly PersonRenderInterface $personRender,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function validate($value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof UniqueIdentifierConstraint) {
|
||||||
|
throw new UnexpectedTypeException($constraint, UniqueIdentifierConstraint::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$value instanceof PersonIdentifier) {
|
||||||
|
throw new UnexpectedValueException($value, PersonIdentifier::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifiers = $this->personIdentifierRepository->findByDefinitionAndCanonical($value->getDefinition(), $value->getValue());
|
||||||
|
|
||||||
|
if (count($identifiers) > 0) {
|
||||||
|
if (count($identifiers) > 1 || $identifiers[0]->getPerson() !== $value->getPerson()) {
|
||||||
|
$persons = array_map(fn (PersonIdentifier $idf): string => $this->personRender->renderString($idf->getPerson(), []), $identifiers);
|
||||||
|
|
||||||
|
$this->context->buildViolation($constraint->message)
|
||||||
|
->setParameter('{{ persons }}', implode(', ', $persons))
|
||||||
|
->setParameter('definition_id', (string) $value->getDefinition()->getId())
|
||||||
|
->addViolation();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
<?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\PersonBundle\Repository\Identifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class PersonIdentifierRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry, private readonly PersonIdentifierManagerInterface $personIdentifierManager)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PersonIdentifier::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByDefinitionAndCanonical(PersonIdentifierDefinition $definition, array|string $valueOrCanonical): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->where('p.definition = :definition')
|
||||||
|
->andWhere('p.canonical = :canonical')
|
||||||
|
->setParameter('definition', $definition)
|
||||||
|
->setParameter(
|
||||||
|
'canonical',
|
||||||
|
is_string($valueOrCanonical) ?
|
||||||
|
$valueOrCanonical :
|
||||||
|
$this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($valueOrCanonical),
|
||||||
|
)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,8 @@ use Chill\MainBundle\Search\ParsingException;
|
|||||||
use Chill\MainBundle\Search\SearchApiQuery;
|
use Chill\MainBundle\Search\SearchApiQuery;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\NonUniqueResultException;
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
@@ -27,7 +29,13 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
|
|
||||||
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
|
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
|
||||||
{
|
{
|
||||||
public function __construct(private Security $security, private EntityManagerInterface $em, private CountryRepository $countryRepository, private AuthorizationHelperInterface $authorizationHelper) {}
|
public function __construct(
|
||||||
|
private Security $security,
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private CountryRepository $countryRepository,
|
||||||
|
private AuthorizationHelperInterface $authorizationHelper,
|
||||||
|
private PersonIdentifierManagerInterface $personIdentifierManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function buildAuthorizedQuery(
|
public function buildAuthorizedQuery(
|
||||||
?string $default = null,
|
?string $default = null,
|
||||||
@@ -107,6 +115,15 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
|
|||||||
$query
|
$query
|
||||||
->setFromClause('chill_person_person AS person');
|
->setFromClause('chill_person_person AS person');
|
||||||
|
|
||||||
|
$idDefinitionWorkers = array_map(
|
||||||
|
fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->getId(),
|
||||||
|
array_filter(
|
||||||
|
$this->personIdentifierManager->getWorkers(),
|
||||||
|
fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->isSearchable()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$idDefinitionWorkerQuestionMarks = implode(', ', array_fill(0, count($idDefinitionWorkers), '?'));
|
||||||
|
|
||||||
$pertinence = [];
|
$pertinence = [];
|
||||||
$pertinenceArgs = [];
|
$pertinenceArgs = [];
|
||||||
$andWhereSearchClause = [];
|
$andWhereSearchClause = [];
|
||||||
@@ -124,10 +141,25 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
|
|||||||
'(starts_with(LOWER(UNACCENT(lastname)), UNACCENT(LOWER(?))))::int';
|
'(starts_with(LOWER(UNACCENT(lastname)), UNACCENT(LOWER(?))))::int';
|
||||||
\array_push($pertinenceArgs, $str, $str, $str, $str);
|
\array_push($pertinenceArgs, $str, $str, $str, $str);
|
||||||
|
|
||||||
$andWhereSearchClause[] =
|
$q = [
|
||||||
'(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR '.
|
'LOWER(UNACCENT(?)) <<% person.fullnamecanonical',
|
||||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
|
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ",
|
||||||
\array_push($andWhereSearchClauseArgs, $str, $str);
|
];
|
||||||
|
$qArguments = [$str, $str];
|
||||||
|
|
||||||
|
if (count($idDefinitionWorkers) > 0) {
|
||||||
|
$q[] = $mq = "EXISTS (
|
||||||
|
SELECT 1 FROM chill_person_identifier AS identifier
|
||||||
|
WHERE identifier.canonical LIKE LOWER(UNACCENT(?)) || '%' AND identifier.definition_id IN ({$idDefinitionWorkerQuestionMarks})
|
||||||
|
AND person.id = identifier.person_id
|
||||||
|
)";
|
||||||
|
$pertinence[] = "({$mq})::int * 1_000_000";
|
||||||
|
$qArguments = [...$qArguments, $str, ...$idDefinitionWorkers];
|
||||||
|
$pertinenceArgs = [...$pertinenceArgs, $str, ...$idDefinitionWorkers];
|
||||||
|
}
|
||||||
|
|
||||||
|
$andWhereSearchClause[] = '('.implode(' OR ', $q).')';
|
||||||
|
$andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, ...$qArguments];
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->andWhereClause(
|
$query->andWhereClause(
|
||||||
|
@@ -10,16 +10,25 @@ import {
|
|||||||
Scope,
|
Scope,
|
||||||
Job,
|
Job,
|
||||||
PrivateCommentEmbeddable,
|
PrivateCommentEmbeddable,
|
||||||
|
TranslatableString,
|
||||||
|
DateTimeWrite,
|
||||||
|
SetGender,
|
||||||
|
SetCenter,
|
||||||
|
SetCivility,
|
||||||
} from "ChillMainAssets/types";
|
} from "ChillMainAssets/types";
|
||||||
import { StoredObject } from "ChillDocStoreAssets/types";
|
import { StoredObject } from "ChillDocStoreAssets/types";
|
||||||
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
|
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
|
||||||
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
|
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
|
||||||
import Person from "./vuejs/_components/OnTheFly/Person.vue";
|
|
||||||
|
|
||||||
export interface AltName {
|
export interface AltName {
|
||||||
label: string;
|
labels: TranslatableString;
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AltNameWrite {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
export interface Person {
|
export interface Person {
|
||||||
id: number;
|
id: number;
|
||||||
type: "person";
|
type: "person";
|
||||||
@@ -41,6 +50,36 @@ export interface Person {
|
|||||||
civility: Civility | null;
|
civility: Civility | null;
|
||||||
current_household_id: number;
|
current_household_id: number;
|
||||||
current_residential_addresses: Address[];
|
current_residential_addresses: Address[];
|
||||||
|
/**
|
||||||
|
* The person id as configured by the user
|
||||||
|
*/
|
||||||
|
personId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonIdentifierWrite {
|
||||||
|
type: "person_identifier";
|
||||||
|
definition_id: number;
|
||||||
|
value: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Person representation to create or update a Person
|
||||||
|
*/
|
||||||
|
export interface PersonWrite {
|
||||||
|
type: "person";
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
altNames: AltNameWrite[];
|
||||||
|
// address: number | null;
|
||||||
|
birthdate: DateTimeWrite | null;
|
||||||
|
deathdate: DateTimeWrite | null;
|
||||||
|
phonenumber: string;
|
||||||
|
mobilenumber: string;
|
||||||
|
email: string;
|
||||||
|
gender: SetGender | null;
|
||||||
|
center: SetCenter | null;
|
||||||
|
civility: SetCivility | null;
|
||||||
|
identifiers: PersonIdentifierWrite[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccompanyingPeriod {
|
export interface AccompanyingPeriod {
|
||||||
@@ -328,11 +367,18 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
|
|||||||
workflows: object[];
|
workflows: object[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity types that a user can create
|
||||||
|
*/
|
||||||
|
export type CreatableEntityType = "person" | "thirdparty";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entities that can be search and selected by a user
|
||||||
|
*/
|
||||||
export type EntityType =
|
export type EntityType =
|
||||||
|
| CreatableEntityType
|
||||||
| "user_group"
|
| "user_group"
|
||||||
| "user"
|
| "user"
|
||||||
| "person"
|
|
||||||
| "thirdparty"
|
|
||||||
| "household";
|
| "household";
|
||||||
|
|
||||||
export type Entities = (UserGroup | User | Person | Thirdparty | Household) & {
|
export type Entities = (UserGroup | User | Person | Thirdparty | Household) & {
|
||||||
@@ -370,7 +416,8 @@ export interface Search {
|
|||||||
|
|
||||||
export interface SearchOptions {
|
export interface SearchOptions {
|
||||||
uniq: boolean;
|
uniq: boolean;
|
||||||
type: string[];
|
/** @deprecated */
|
||||||
|
type: EntityType[];
|
||||||
priority: number | null;
|
priority: number | null;
|
||||||
button: {
|
button: {
|
||||||
size: string;
|
size: string;
|
||||||
@@ -380,6 +427,17 @@ export interface SearchOptions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PersonIdentifierPresence = 'NOT_EDITABLE' | 'ON_EDIT' | 'ON_CREATION' | 'REQUIRED';
|
||||||
|
|
||||||
|
export interface PersonIdentifierWorker {
|
||||||
|
type: "person_identifier_worker";
|
||||||
|
definition_id: number;
|
||||||
|
engine: string;
|
||||||
|
label: TranslatableString;
|
||||||
|
isActive: boolean;
|
||||||
|
presence: PersonIdentifierPresence;
|
||||||
|
}
|
||||||
|
|
||||||
export class MakeFetchException extends Error {
|
export class MakeFetchException extends Error {
|
||||||
sta: number;
|
sta: number;
|
||||||
txt: string;
|
txt: string;
|
||||||
|
@@ -1,88 +0,0 @@
|
|||||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* GET a person by id
|
|
||||||
*/
|
|
||||||
const getPerson = (id) => {
|
|
||||||
const url = `/api/1.0/person/person/${id}.json`;
|
|
||||||
return fetch(url).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Error with request resource response");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPersonAltNames = () =>
|
|
||||||
fetch("/api/1.0/person/config/alt_names.json").then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Error with request resource response");
|
|
||||||
});
|
|
||||||
|
|
||||||
const getCivilities = () =>
|
|
||||||
fetch("/api/1.0/main/civility.json").then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Error with request resource response");
|
|
||||||
});
|
|
||||||
|
|
||||||
const getGenders = () => makeFetch("GET", "/api/1.0/main/gender.json");
|
|
||||||
// .then(response => {
|
|
||||||
// console.log(response)
|
|
||||||
// if (response.ok) { return response.json(); }
|
|
||||||
// throw Error('Error with request resource response');
|
|
||||||
// });
|
|
||||||
|
|
||||||
const getCentersForPersonCreation = () =>
|
|
||||||
makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* POST a new person
|
|
||||||
*/
|
|
||||||
const postPerson = (body) => {
|
|
||||||
const url = `/api/1.0/person/person.json`;
|
|
||||||
return fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json;charset=utf-8",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Error with request resource response");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* PATCH an existing person
|
|
||||||
*/
|
|
||||||
const patchPerson = (id, body) => {
|
|
||||||
const url = `/api/1.0/person/person/${id}.json`;
|
|
||||||
return fetch(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json;charset=utf-8",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
}).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Error with request resource response");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
getCentersForPersonCreation,
|
|
||||||
getPerson,
|
|
||||||
getPersonAltNames,
|
|
||||||
getCivilities,
|
|
||||||
getGenders,
|
|
||||||
postPerson,
|
|
||||||
patchPerson,
|
|
||||||
};
|
|
@@ -0,0 +1,87 @@
|
|||||||
|
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||||
|
import { Center, Civility, Gender } from "ChillMainAssets/types";
|
||||||
|
import {
|
||||||
|
AltName,
|
||||||
|
Person,
|
||||||
|
PersonIdentifierWorker,
|
||||||
|
PersonWrite,
|
||||||
|
} from "ChillPersonAssets/types";
|
||||||
|
import person from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GET a person by id
|
||||||
|
*/
|
||||||
|
export const getPerson = async (id: number): Promise<Person> => {
|
||||||
|
const url = `/api/1.0/person/person/${id}.json`;
|
||||||
|
return fetch(url).then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error("Error with request resource response");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPersonAltNames = async (): Promise<AltName[]> =>
|
||||||
|
fetch("/api/1.0/person/config/alt_names.json").then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error("Error with request resource response");
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getCivilities = async (): Promise<Civility[]> =>
|
||||||
|
fetchResults("/api/1.0/main/civility.json");
|
||||||
|
|
||||||
|
export const getGenders = async (): Promise<Gender[]> =>
|
||||||
|
fetchResults("/api/1.0/main/gender.json");
|
||||||
|
|
||||||
|
export const getCentersForPersonCreation = async (): Promise<{
|
||||||
|
showCenters: boolean;
|
||||||
|
centers: Center[];
|
||||||
|
}> => makeFetch("GET", "/api/1.0/person/creation/authorized-centers", null);
|
||||||
|
|
||||||
|
export const getPersonIdentifiers = async (): Promise<
|
||||||
|
PersonIdentifierWorker[]
|
||||||
|
> => fetchResults("/api/1.0/person/identifiers/workers");
|
||||||
|
|
||||||
|
export interface WritePersonViolationMap
|
||||||
|
extends Record<string, Record<string, string>> {
|
||||||
|
firstName: {
|
||||||
|
"{{ value }}": string
|
||||||
|
};
|
||||||
|
lastName: {
|
||||||
|
"{{ value }}": string;
|
||||||
|
};
|
||||||
|
gender: {
|
||||||
|
"{{ value }}": string;
|
||||||
|
};
|
||||||
|
mobilenumber: {
|
||||||
|
"{{ types }}": string; // ex: "mobile number"
|
||||||
|
"{{ value }}": string; // ex: "+33 1 02 03 04 05"
|
||||||
|
};
|
||||||
|
phonenumber: {
|
||||||
|
"{{ types }}": string; // ex: "mobile number"
|
||||||
|
"{{ value }}": string; // ex: "+33 1 02 03 04 05"
|
||||||
|
};
|
||||||
|
email: {
|
||||||
|
"{{ value }}": string;
|
||||||
|
};
|
||||||
|
center: {
|
||||||
|
"{{ value }}": string;
|
||||||
|
};
|
||||||
|
civility: {
|
||||||
|
"{{ value }}": string;
|
||||||
|
};
|
||||||
|
birthdate: {};
|
||||||
|
identifiers: {
|
||||||
|
"{{ value }}": string;
|
||||||
|
"definition_id": string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const createPerson = async (person: PersonWrite): Promise<Person> => {
|
||||||
|
return makeFetch<PersonWrite, Person, WritePersonViolationMap>(
|
||||||
|
"POST",
|
||||||
|
"/api/1.0/person/person.json",
|
||||||
|
person,
|
||||||
|
);
|
||||||
|
};
|
@@ -3,487 +3,126 @@
|
|||||||
class="btn"
|
class="btn"
|
||||||
:class="getClassButton"
|
:class="getClassButton"
|
||||||
:title="buttonTitle"
|
:title="buttonTitle"
|
||||||
@click="openModal"
|
@click="openModalChoose"
|
||||||
>
|
>
|
||||||
<span v-if="displayTextButton">{{ buttonTitle }}</span>
|
<span v-if="displayTextButton">{{ buttonTitle }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<teleport to="body">
|
<person-choose-modal
|
||||||
<modal
|
v-if="showModalChoose"
|
||||||
v-if="showModal"
|
:modal-title="modalTitle"
|
||||||
@close="closeModal"
|
:options="options"
|
||||||
:modal-dialog-class="modalDialogClass"
|
:suggested="suggested"
|
||||||
:show="showModal"
|
:selected="selected"
|
||||||
:hide-footer="false"
|
:modal-dialog-class="'modal-dialog-scrollable modal-xl'"
|
||||||
>
|
:allow-create="props.allowCreate"
|
||||||
<template #header>
|
@close="closeModalChoose"
|
||||||
<h3 class="modal-title">
|
@addNewPersons="(payload) => emit('addNewPersons', payload)"
|
||||||
{{ modalTitle }}
|
@onAskForCreate="onAskForCreate"
|
||||||
</h3>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body-head>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="search">
|
|
||||||
<label class="col-form-label" style="float: right">
|
|
||||||
{{
|
|
||||||
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
|
|
||||||
count: suggestedCounter,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="search-persons"
|
|
||||||
name="query"
|
|
||||||
v-model="query"
|
|
||||||
:placeholder="trans(ADD_PERSONS_SEARCH_SOME_PERSONS)"
|
|
||||||
ref="searchRef"
|
|
||||||
/>
|
|
||||||
<i class="fa fa-search fa-lg" />
|
|
||||||
<i
|
|
||||||
class="fa fa-times"
|
|
||||||
v-if="queryLength >= 3"
|
|
||||||
@click="resetSuggestion"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" v-if="checkUniq === 'checkbox'">
|
|
||||||
<div class="count">
|
|
||||||
<span>
|
|
||||||
<a v-if="suggestedCounter > 2" @click="selectAll">
|
|
||||||
{{ trans(ACTION_CHECK_ALL) }}
|
|
||||||
</a>
|
|
||||||
<a v-if="selectedCounter > 0" @click="resetSelection">
|
|
||||||
<i v-if="suggestedCounter > 2"> • </i>
|
|
||||||
{{ trans(ACTION_RESET) }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-if="selectedCounter > 0">
|
|
||||||
{{
|
|
||||||
trans(ADD_PERSONS_SELECTED_COUNTER, {
|
|
||||||
count: selectedCounter,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
|
||||||
<div class="results">
|
|
||||||
<person-suggestion
|
|
||||||
v-for="item in selectedAndSuggested.slice().reverse()"
|
|
||||||
:key="itemKey(item)"
|
|
||||||
:item="item"
|
|
||||||
:search="search"
|
|
||||||
:type="checkUniq"
|
|
||||||
@save-form-on-the-fly="saveFormOnTheFly"
|
|
||||||
@new-prior-suggestion="newPriorSuggestion"
|
|
||||||
@update-selected="updateSelected"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="create-button">
|
<CreateModal
|
||||||
<on-the-fly
|
v-if="creatableEntityTypes.length > 0 && showModalCreate"
|
||||||
v-if="
|
:allowed-types="creatableEntityTypes"
|
||||||
queryLength >= 3 &&
|
|
||||||
(options.type.includes('person') ||
|
|
||||||
options.type.includes('thirdparty'))
|
|
||||||
"
|
|
||||||
:button-text="trans(ONTHEFLY_CREATE_BUTTON, { q: query })"
|
|
||||||
:allowed-types="options.type"
|
|
||||||
:query="query"
|
:query="query"
|
||||||
action="create"
|
@close="closeModalCreate"
|
||||||
@save-form-on-the-fly="saveFormOnTheFly"
|
@onPersonCreated="onPersonCreated"
|
||||||
ref="onTheFly"
|
></CreateModal>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<button
|
|
||||||
class="btn btn-create"
|
|
||||||
@click.prevent="
|
|
||||||
() => {
|
|
||||||
$emit('addNewPersons', {
|
|
||||||
selected: selectedComputed,
|
|
||||||
});
|
|
||||||
query = '';
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ trans(ACTION_ADD) }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
</teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
import { ref, computed } from "vue";
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
import PersonChooseModal from "./AddPersons/PersonChooseModal.vue";
|
||||||
import { ref, reactive, computed, nextTick, watch, shallowRef } from "vue";
|
import type {
|
||||||
import PersonSuggestion from "./AddPersons/PersonSuggestion.vue";
|
|
||||||
import { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons";
|
|
||||||
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
|
|
||||||
|
|
||||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
|
||||||
|
|
||||||
import {
|
|
||||||
trans,
|
|
||||||
ADD_PERSONS_SUGGESTED_COUNTER,
|
|
||||||
ADD_PERSONS_SEARCH_SOME_PERSONS,
|
|
||||||
ADD_PERSONS_SELECTED_COUNTER,
|
|
||||||
ONTHEFLY_CREATE_BUTTON,
|
|
||||||
ACTION_CHECK_ALL,
|
|
||||||
ACTION_RESET,
|
|
||||||
ACTION_ADD,
|
|
||||||
} from "translator";
|
|
||||||
import {
|
|
||||||
Suggestion,
|
Suggestion,
|
||||||
Search,
|
|
||||||
AddPersonResult as OriginalResult,
|
|
||||||
SearchOptions,
|
SearchOptions,
|
||||||
|
CreatableEntityType,
|
||||||
|
EntityType,
|
||||||
|
Person,
|
||||||
} from "ChillPersonAssets/types";
|
} from "ChillPersonAssets/types";
|
||||||
|
import { marked } from "marked";
|
||||||
|
import options = marked.options;
|
||||||
|
import CreateModal from "ChillMainAssets/vuejs/OnTheFly/components/CreateModal.vue";
|
||||||
|
|
||||||
// Extend Result type to include optional addressId
|
interface AddPersonsConfig {
|
||||||
type Result = OriginalResult & { addressId?: number };
|
suggested?: Suggestion[];
|
||||||
|
selected?: Suggestion[];
|
||||||
|
buttonTitle: string;
|
||||||
|
modalTitle: string;
|
||||||
|
options: SearchOptions;
|
||||||
|
allowCreate?: boolean;
|
||||||
|
types?: EntityType[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(defineProps<AddPersonsConfig>(), {
|
||||||
suggested: { type: Array as () => Suggestion[], default: () => [] },
|
suggested: () => [],
|
||||||
selected: { type: Array as () => Suggestion[], default: () => [] },
|
selected: () => [],
|
||||||
buttonTitle: { type: String, required: true },
|
allowCreate: () => true,
|
||||||
modalTitle: { type: String, required: true },
|
types: () => ["person"],
|
||||||
options: { type: Object as () => SearchOptions, required: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["addNewPersons"]);
|
const emit =
|
||||||
|
defineEmits<
|
||||||
|
(e: "addNewPersons", payload: { selected: Suggestion[] }) => void
|
||||||
|
>();
|
||||||
|
|
||||||
const showModal = ref(false);
|
const showModalChoose = ref(false);
|
||||||
const modalDialogClass = ref("modal-dialog-scrollable modal-xl");
|
const showModalCreate = ref(false);
|
||||||
|
const query = ref("");
|
||||||
const modal = shallowRef({
|
|
||||||
showModal: false,
|
|
||||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
|
||||||
});
|
|
||||||
|
|
||||||
const search = reactive({
|
|
||||||
query: "" as string,
|
|
||||||
previousQuery: "" as string,
|
|
||||||
currentSearchQueryController: null as AbortController | null,
|
|
||||||
suggested: props.suggested as Suggestion[],
|
|
||||||
selected: props.selected as Suggestion[],
|
|
||||||
priorSuggestion: {} as Partial<Suggestion>,
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchRef = ref<HTMLInputElement | null>(null);
|
|
||||||
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
|
|
||||||
|
|
||||||
const query = computed({
|
|
||||||
get: () => search.query,
|
|
||||||
set: (val) => setQuery(val),
|
|
||||||
});
|
|
||||||
const queryLength = computed(() => search.query.length);
|
|
||||||
const suggestedCounter = computed(() => search.suggested.length);
|
|
||||||
const selectedComputed = computed(() => search.selected);
|
|
||||||
const selectedCounter = computed(() => search.selected.length);
|
|
||||||
|
|
||||||
const getClassButton = computed(() => {
|
const getClassButton = computed(() => {
|
||||||
let size = props.options?.button?.size ?? "";
|
const size = props.options?.button?.size ?? "";
|
||||||
let type = props.options?.button?.type ?? "btn-create";
|
const type = props.options?.button?.type ?? "btn-create";
|
||||||
return size ? size + " " + type : type;
|
return size ? `${size} ${type}` : type;
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayTextButton = computed(() =>
|
const displayTextButton = computed(() =>
|
||||||
props.options?.button?.display !== undefined
|
props.options?.button?.display !== undefined
|
||||||
? props.options.button.display
|
? props.options.button.display
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const checkUniq = computed(() =>
|
const creatableEntityTypes = computed<CreatableEntityType[]>(() => {
|
||||||
props.options.uniq === true ? "radio" : "checkbox",
|
if (typeof props.options.type !== "undefined") {
|
||||||
);
|
return props.options.type.filter(
|
||||||
|
(e: EntityType) => e === "thirdparty" || e === "person",
|
||||||
const priorSuggestion = computed(() => search.priorSuggestion);
|
);
|
||||||
const hasPriorSuggestion = computed(() => !!search.priorSuggestion.key);
|
|
||||||
|
|
||||||
const itemKey = (item: Suggestion) => item.result.type + item.result.id;
|
|
||||||
|
|
||||||
function addPriorSuggestion() {
|
|
||||||
if (hasPriorSuggestion.value) {
|
|
||||||
// Type assertion is safe here due to the checks above
|
|
||||||
search.suggested.unshift(priorSuggestion.value as Suggestion);
|
|
||||||
search.selected.unshift(priorSuggestion.value as Suggestion);
|
|
||||||
newPriorSuggestion(null);
|
|
||||||
}
|
}
|
||||||
}
|
return props.types.filter(
|
||||||
|
(e: EntityType) => e === "thirdparty" || e === "person",
|
||||||
const selectedAndSuggested = computed(() => {
|
);
|
||||||
addPriorSuggestion();
|
|
||||||
const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
|
|
||||||
...new Map(a.map((x) => [key(x), x])).values(),
|
|
||||||
];
|
|
||||||
let union = [
|
|
||||||
...new Set([
|
|
||||||
...search.suggested.slice().reverse(),
|
|
||||||
...search.selected.slice().reverse(),
|
|
||||||
]),
|
|
||||||
];
|
|
||||||
return uniqBy(union, (k: Suggestion) => k.key);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function openModal() {
|
function onAskForCreate(payload: { query: string }) {
|
||||||
showModal.value = true;
|
query.value = payload.query;
|
||||||
nextTick(() => {
|
showModalChoose.value = false;
|
||||||
if (searchRef.value) searchRef.value.focus();
|
showModalCreate.value = true;
|
||||||
});
|
|
||||||
}
|
|
||||||
function closeModal() {
|
|
||||||
showModal.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setQuery(q: string) {
|
function openModalChoose() {
|
||||||
search.query = q;
|
showModalChoose.value = true;
|
||||||
|
|
||||||
// Clear previous search if any
|
|
||||||
if (search.currentSearchQueryController) {
|
|
||||||
search.currentSearchQueryController.abort();
|
|
||||||
search.currentSearchQueryController = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (q === "") {
|
|
||||||
loadSuggestions([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce delay based on query length
|
|
||||||
const delay = q.length > 3 ? 300 : 700;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Only search if query hasn't changed in the meantime
|
|
||||||
if (q !== search.query) return;
|
|
||||||
|
|
||||||
search.currentSearchQueryController = new AbortController();
|
|
||||||
|
|
||||||
searchEntities(
|
|
||||||
{ query: q, options: props.options },
|
|
||||||
search.currentSearchQueryController.signal,
|
|
||||||
)
|
|
||||||
.then((suggested: Search) => {
|
|
||||||
loadSuggestions(suggested.results);
|
|
||||||
})
|
|
||||||
.catch((error: DOMException) => {
|
|
||||||
if (error instanceof DOMException && error.name === "AbortError") {
|
|
||||||
// Request was aborted, ignore
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}, delay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSuggestions(suggestedArr: Suggestion[]) {
|
function closeModalChoose() {
|
||||||
search.suggested = suggestedArr;
|
showModalChoose.value = false;
|
||||||
search.suggested.forEach((item) => {
|
|
||||||
item.key = itemKey(item);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelected(value: Suggestion[]) {
|
function closeModalCreate() {
|
||||||
search.selected = value;
|
showModalCreate.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetSuggestion() {
|
function onPersonCreated(payload: { person: Person }) {
|
||||||
search.query = "";
|
console.log("onPersonCreated", payload);
|
||||||
search.suggested = [];
|
showModalCreate.value = false;
|
||||||
}
|
const suggestion = {
|
||||||
|
result: payload.person,
|
||||||
function resetSelection() {
|
relevance: 999999,
|
||||||
search.selected = [];
|
key: "person",
|
||||||
}
|
|
||||||
|
|
||||||
function resetSearch() {
|
|
||||||
resetSelection();
|
|
||||||
resetSuggestion();
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAll() {
|
|
||||||
search.suggested.forEach((item) => {
|
|
||||||
search.selected.push(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function newPriorSuggestion(entity: Result | null) {
|
|
||||||
if (entity !== null) {
|
|
||||||
let suggestion = {
|
|
||||||
key: entity.type + entity.id,
|
|
||||||
relevance: 0.5,
|
|
||||||
result: entity,
|
|
||||||
};
|
};
|
||||||
search.priorSuggestion = suggestion;
|
emit("addNewPersons", { selected: [suggestion] });
|
||||||
} else {
|
|
||||||
search.priorSuggestion = {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveFormOnTheFly({
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
type: string;
|
|
||||||
data: Result;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
if (type === "person") {
|
|
||||||
const responsePerson: Result = await makeFetch(
|
|
||||||
"POST",
|
|
||||||
"/api/1.0/person/person.json",
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
newPriorSuggestion(responsePerson);
|
|
||||||
if (onTheFly.value) onTheFly.value.closeModal();
|
|
||||||
|
|
||||||
if (data.addressId != null) {
|
|
||||||
const household = { type: "household" };
|
|
||||||
const address = { id: data.addressId };
|
|
||||||
try {
|
|
||||||
const responseHousehold: Result = await makeFetch(
|
|
||||||
"POST",
|
|
||||||
"/api/1.0/person/household.json",
|
|
||||||
household,
|
|
||||||
);
|
|
||||||
const member = {
|
|
||||||
concerned: [
|
|
||||||
{
|
|
||||||
person: {
|
|
||||||
type: "person",
|
|
||||||
id: responsePerson.id,
|
|
||||||
},
|
|
||||||
start_date: {
|
|
||||||
datetime: `${new Date().toISOString().split("T")[0]}T00:00:00+02:00`,
|
|
||||||
},
|
|
||||||
holder: false,
|
|
||||||
comment: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
destination: {
|
|
||||||
type: "household",
|
|
||||||
id: responseHousehold.id,
|
|
||||||
},
|
|
||||||
composition: null,
|
|
||||||
};
|
|
||||||
await makeFetch(
|
|
||||||
"POST",
|
|
||||||
"/api/1.0/person/household/members/move.json",
|
|
||||||
member,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const _response = await makeFetch(
|
|
||||||
"POST",
|
|
||||||
`/api/1.0/person/household/${responseHousehold.id}/address.json`,
|
|
||||||
address,
|
|
||||||
);
|
|
||||||
console.log(_response);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (type === "thirdparty") {
|
|
||||||
const response: Result = await makeFetch(
|
|
||||||
"POST",
|
|
||||||
"/api/1.0/thirdparty/thirdparty.json",
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
newPriorSuggestion(response);
|
|
||||||
if (onTheFly.value) onTheFly.value.closeModal();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.selected,
|
|
||||||
(newSelected) => {
|
|
||||||
search.selected = newSelected;
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.suggested,
|
|
||||||
(newSuggested) => {
|
|
||||||
search.suggested = newSuggested;
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => modal,
|
|
||||||
(val) => {
|
|
||||||
showModal.value = val.value.showModal;
|
|
||||||
modalDialogClass.value = val.value.modalDialogClass;
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
resetSearch,
|
|
||||||
showModal,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
li.add-persons {
|
/* Button styles can remain here if needed */
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div.body-head {
|
|
||||||
overflow-y: unset;
|
|
||||||
div.modal-body:first-child {
|
|
||||||
margin: auto 4em;
|
|
||||||
div.search {
|
|
||||||
position: relative;
|
|
||||||
input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 1.2em 1.5em 1.2em 2.5em;
|
|
||||||
//margin: 1em 0;
|
|
||||||
}
|
|
||||||
i {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0.5;
|
|
||||||
padding: 0.65em 0;
|
|
||||||
top: 50%;
|
|
||||||
}
|
|
||||||
i.fa-search {
|
|
||||||
left: 0.5em;
|
|
||||||
}
|
|
||||||
i.fa-times {
|
|
||||||
right: 1em;
|
|
||||||
padding: 0.75em 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div.modal-body:last-child {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
div.count {
|
|
||||||
margin: -0.5em 0 0.7em;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
a {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.create-button > a {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-left: 2.6em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<modal
|
||||||
|
@close="() => emit('close')"
|
||||||
|
:modal-dialog-class="modalDialogClass"
|
||||||
|
:hide-footer="false"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="modal-title">{{ modalTitle }}</h3>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-head>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="search">
|
||||||
|
<label class="col-form-label" style="float: right">
|
||||||
|
{{
|
||||||
|
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
|
||||||
|
count: suggestedCounter,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="search-persons"
|
||||||
|
name="query"
|
||||||
|
v-model="query"
|
||||||
|
:placeholder="trans(ADD_PERSONS_SEARCH_SOME_PERSONS)"
|
||||||
|
ref="searchRef"
|
||||||
|
/>
|
||||||
|
<i class="fa fa-search fa-lg" />
|
||||||
|
<i
|
||||||
|
class="fa fa-times"
|
||||||
|
v-if="queryLength >= 3"
|
||||||
|
@click="resetSuggestion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" v-if="checkUniq === 'checkbox'">
|
||||||
|
<div class="count">
|
||||||
|
<span>
|
||||||
|
<a v-if="suggestedCounter > 2" @click="selectAll">
|
||||||
|
{{ trans(ACTION_CHECK_ALL) }}
|
||||||
|
</a>
|
||||||
|
<a v-if="selectedCounter > 0" @click="resetSelection">
|
||||||
|
<i v-if="suggestedCounter > 2"> • </i>
|
||||||
|
{{ trans(ACTION_RESET) }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span v-if="selectedCounter > 0">
|
||||||
|
{{
|
||||||
|
trans(ADD_PERSONS_SELECTED_COUNTER, { count: selectedCounter })
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="results">
|
||||||
|
<person-suggestion
|
||||||
|
v-for="item in selectedAndSuggested.slice().reverse()"
|
||||||
|
:key="itemKey(item)"
|
||||||
|
:item="item"
|
||||||
|
:search="search"
|
||||||
|
:type="checkUniq"
|
||||||
|
@new-prior-suggestion="newPriorSuggestion"
|
||||||
|
@update-selected="updateSelected"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.allowCreate && query.length > 0"
|
||||||
|
class="create-button"
|
||||||
|
>
|
||||||
|
<button type="button" @click="emit('onAskForCreate', { query })">
|
||||||
|
{{ trans(ONTHEFLY_CREATE_BUTTON, { q: query }) }}
|
||||||
|
</button>
|
||||||
|
<!--
|
||||||
|
TODO remove this
|
||||||
|
<on-the-fly
|
||||||
|
v-if="
|
||||||
|
queryLength >= 3 &&
|
||||||
|
(options.type.includes('person') || options.type.includes('thirdparty'))
|
||||||
|
"
|
||||||
|
:button-text="trans(ONTHEFLY_CREATE_BUTTON, { q: query })"
|
||||||
|
:allowed-types="options.type"
|
||||||
|
:query="query"
|
||||||
|
action="create"
|
||||||
|
@save-form-on-the-fly="saveFormOnTheFly"
|
||||||
|
ref="onTheFly"
|
||||||
|
/>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-create"
|
||||||
|
@click.prevent="pickEntities"
|
||||||
|
>
|
||||||
|
{{ trans(ACTION_ADD) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, nextTick, watch, onMounted } from "vue";
|
||||||
|
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||||
|
import PersonSuggestion from "./PersonSuggestion.vue";
|
||||||
|
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
|
||||||
|
import { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons";
|
||||||
|
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||||
|
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
ADD_PERSONS_SUGGESTED_COUNTER,
|
||||||
|
ADD_PERSONS_SEARCH_SOME_PERSONS,
|
||||||
|
ADD_PERSONS_SELECTED_COUNTER,
|
||||||
|
ONTHEFLY_CREATE_BUTTON,
|
||||||
|
ACTION_CHECK_ALL,
|
||||||
|
ACTION_RESET,
|
||||||
|
ACTION_ADD,
|
||||||
|
} from "translator";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Suggestion,
|
||||||
|
Search,
|
||||||
|
AddPersonResult as OriginalResult,
|
||||||
|
SearchOptions,
|
||||||
|
EntitiesOrMe,
|
||||||
|
} from "ChillPersonAssets/types";
|
||||||
|
|
||||||
|
type Result = OriginalResult & { addressId?: number };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modalTitle: string;
|
||||||
|
options: SearchOptions;
|
||||||
|
suggested?: Suggestion[];
|
||||||
|
selected?: Suggestion[];
|
||||||
|
modalDialogClass?: string;
|
||||||
|
allowCreate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
suggested: () => [],
|
||||||
|
selected: () => [],
|
||||||
|
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||||
|
allowCreate: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "close"): void;
|
||||||
|
/** @deprecated use 'onPickEntities' */
|
||||||
|
(e: "addNewPersons", payload: { selected: Suggestion[] }): void;
|
||||||
|
(e: "onPickEntities", payload: { selected: EntitiesOrMe[] }): void;
|
||||||
|
(e: "onAskForCreate", payload: { query: string }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const searchRef = ref<HTMLInputElement | null>(null);
|
||||||
|
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// give the focus on the search bar
|
||||||
|
searchRef.value?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = reactive({
|
||||||
|
query: "" as string,
|
||||||
|
previousQuery: "" as string,
|
||||||
|
currentSearchQueryController: null as AbortController | null,
|
||||||
|
suggested: (props.suggested ?? []) as Suggestion[],
|
||||||
|
selected: (props.selected ?? []) as Suggestion[],
|
||||||
|
priorSuggestion: {} as Partial<Suggestion>,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selected,
|
||||||
|
(newSelected) => {
|
||||||
|
search.selected = newSelected ? [...newSelected] : [];
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.suggested,
|
||||||
|
(newSuggested) => {
|
||||||
|
search.suggested = newSuggested ? [...newSuggested] : [];
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = computed({
|
||||||
|
get: () => search.query,
|
||||||
|
set: (val: string) => setQuery(val),
|
||||||
|
});
|
||||||
|
const queryLength = computed(() => search.query.length);
|
||||||
|
const suggestedCounter = computed(() => search.suggested.length);
|
||||||
|
const selectedComputed = computed<Suggestion[]>(() => search.selected);
|
||||||
|
const selectedCounter = computed(() => search.selected.length);
|
||||||
|
|
||||||
|
const checkUniq = computed(() =>
|
||||||
|
props.options.uniq === true ? "radio" : "checkbox",
|
||||||
|
);
|
||||||
|
|
||||||
|
const priorSuggestion = computed(() => search.priorSuggestion);
|
||||||
|
const hasPriorSuggestion = computed(() => !!search.priorSuggestion.key);
|
||||||
|
|
||||||
|
const itemKey = (item: Suggestion) => item.result.type + item.result.id;
|
||||||
|
|
||||||
|
function addPriorSuggestion() {
|
||||||
|
if (hasPriorSuggestion.value) {
|
||||||
|
search.suggested.unshift(priorSuggestion.value as Suggestion);
|
||||||
|
search.selected.unshift(priorSuggestion.value as Suggestion);
|
||||||
|
newPriorSuggestion(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedAndSuggested = computed(() => {
|
||||||
|
addPriorSuggestion();
|
||||||
|
const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
|
||||||
|
...new Map(a.map((x) => [key(x), x])).values(),
|
||||||
|
];
|
||||||
|
const union = [
|
||||||
|
...new Set([
|
||||||
|
...search.suggested.slice().reverse(),
|
||||||
|
...search.selected.slice().reverse(),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
return uniqBy(union, (k: Suggestion) => k.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setQuery(q: string) {
|
||||||
|
search.query = q;
|
||||||
|
|
||||||
|
if (search.currentSearchQueryController) {
|
||||||
|
search.currentSearchQueryController.abort();
|
||||||
|
search.currentSearchQueryController = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q === "") {
|
||||||
|
loadSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = q.length > 3 ? 300 : 700;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (q !== search.query) return;
|
||||||
|
|
||||||
|
search.currentSearchQueryController = new AbortController();
|
||||||
|
|
||||||
|
searchEntities(
|
||||||
|
{ query: q, options: props.options },
|
||||||
|
search.currentSearchQueryController.signal,
|
||||||
|
)
|
||||||
|
.then((suggested: Search) => {
|
||||||
|
loadSuggestions(suggested.results);
|
||||||
|
})
|
||||||
|
.catch((error: DOMException) => {
|
||||||
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSuggestions(suggestedArr: Suggestion[]) {
|
||||||
|
search.suggested = suggestedArr;
|
||||||
|
search.suggested.forEach((item) => {
|
||||||
|
item.key = itemKey(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelected(value: Suggestion[]) {
|
||||||
|
search.selected = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSuggestion() {
|
||||||
|
search.query = "";
|
||||||
|
search.suggested = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSelection() {
|
||||||
|
search.selected = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSearch() {
|
||||||
|
resetSelection();
|
||||||
|
resetSuggestion();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAll() {
|
||||||
|
search.suggested.forEach((item) => {
|
||||||
|
search.selected.push(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function newPriorSuggestion(entity: Result | null) {
|
||||||
|
if (entity !== null) {
|
||||||
|
const suggestion: Suggestion = {
|
||||||
|
key: entity.type + entity.id,
|
||||||
|
relevance: 0.5,
|
||||||
|
result: entity,
|
||||||
|
} as Suggestion;
|
||||||
|
search.priorSuggestion = suggestion;
|
||||||
|
} else {
|
||||||
|
search.priorSuggestion = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered when the user clicks on the "add" button.
|
||||||
|
*/
|
||||||
|
function pickEntities(): void {
|
||||||
|
emit("addNewPersons", { selected: search.selected });
|
||||||
|
emit("onPickEntities", {
|
||||||
|
selected: search.selected.map((s: Suggestion) => s.result),
|
||||||
|
});
|
||||||
|
search.query = "";
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO remove this
|
||||||
|
async function saveFormOnTheFly({ type, data }: { type: string; data: Result }) {
|
||||||
|
try {
|
||||||
|
if (type === 'person') {
|
||||||
|
const responsePerson: Result = await makeFetch('POST', '/api/1.0/person/person.json', data);
|
||||||
|
newPriorSuggestion(responsePerson);
|
||||||
|
onTheFly.value?.closeModal();
|
||||||
|
|
||||||
|
if (data.addressId != null) {
|
||||||
|
const household = { type: 'household' };
|
||||||
|
const address = { id: data.addressId };
|
||||||
|
try {
|
||||||
|
const responseHousehold: Result = await makeFetch('POST', '/api/1.0/person/household.json', household);
|
||||||
|
const member = {
|
||||||
|
concerned: [
|
||||||
|
{
|
||||||
|
person: { type: 'person', id: responsePerson.id },
|
||||||
|
start_date: { datetime: `${new Date().toISOString().split('T')[0]}T00:00:00+02:00` },
|
||||||
|
holder: false,
|
||||||
|
comment: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
destination: { type: 'household', id: responseHousehold.id },
|
||||||
|
composition: null,
|
||||||
|
};
|
||||||
|
await makeFetch('POST', '/api/1.0/person/household/members/move.json', member);
|
||||||
|
try {
|
||||||
|
await makeFetch('POST', `/api/1.0/person/household/${responseHousehold.id}/address.json`, address);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type === 'thirdparty') {
|
||||||
|
const response: Result = await makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data);
|
||||||
|
newPriorSuggestion(response);
|
||||||
|
onTheFly.value?.closeModal();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
defineExpose({ resetSearch });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
li.add-persons {
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body-head {
|
||||||
|
overflow-y: unset;
|
||||||
|
div.modal-body:first-child {
|
||||||
|
margin: auto 4em;
|
||||||
|
div.search {
|
||||||
|
position: relative;
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.2em 1.5em 1.2em 2.5em;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: 0.65em 0;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
i.fa-search {
|
||||||
|
left: 0.5em;
|
||||||
|
}
|
||||||
|
i.fa-times {
|
||||||
|
right: 1em;
|
||||||
|
padding: 0.75em 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.modal-body:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
div.count {
|
||||||
|
margin: -0.5em 0 0.7em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-button > a {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-left: 2.6em;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -5,46 +5,16 @@
|
|||||||
<div class="item-col">
|
<div class="item-col">
|
||||||
<div class="entity-label">
|
<div class="entity-label">
|
||||||
<div :class="'denomination h' + options.hLevel">
|
<div :class="'denomination h' + options.hLevel">
|
||||||
|
<template v-if="options.addLink === true">
|
||||||
<a v-if="options.addLink === true" :href="getUrl">
|
<a v-if="options.addLink === true" :href="getUrl">
|
||||||
<!-- use person-text here to avoid code duplication ? TODO -->
|
<span>{{ person.text }}</span>
|
||||||
<span class="firstname">{{ person.firstName }}</span>
|
|
||||||
<span class="lastname">{{ person.lastName }}</span>
|
|
||||||
<span v-if="person.suffixText" class="suffixtext"
|
|
||||||
> {{ person.suffixText }}</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="person.altNames && options.addAltNames == true"
|
|
||||||
class="altnames"
|
|
||||||
>
|
|
||||||
<span :class="'altname altname-' + altNameKey">{{
|
|
||||||
altNameLabel
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- use person-text here to avoid code duplication ? TODO -->
|
|
||||||
<span class="firstname">{{ person.firstName + " " }}</span>
|
|
||||||
<span class="lastname">{{ person.lastName }}</span>
|
|
||||||
<span v-if="person.suffixText" class="suffixtext"
|
|
||||||
> {{ person.suffixText }}</span
|
|
||||||
>
|
|
||||||
<span v-if="person.deathdate" class="deathdate"> (‡)</span>
|
<span v-if="person.deathdate" class="deathdate"> (‡)</span>
|
||||||
<span
|
</a>
|
||||||
v-if="person.altNames && options.addAltNames == true"
|
</template>
|
||||||
class="altnames"
|
<template v-else>
|
||||||
>
|
<span>{{ person.text }}</span>
|
||||||
<span :class="'altname altname-' + altNameKey">{{
|
<span v-if="person.deathdate" class="deathdate"> (‡)</span>
|
||||||
altNameLabel
|
</template>
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="options.addId == true"
|
|
||||||
class="id-number"
|
|
||||||
:title="'n° ' + person.id"
|
|
||||||
>{{ person.id }}</span
|
|
||||||
>
|
|
||||||
|
|
||||||
<badge-entity
|
<badge-entity
|
||||||
v-if="options.addEntity === true"
|
v-if="options.addEntity === true"
|
||||||
:entity="person"
|
:entity="person"
|
||||||
@@ -52,61 +22,36 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
v-if="options.addId == true"
|
||||||
|
:title="person.personId"
|
||||||
|
><i class="bi bi-info-circle"></i> {{ person.personId }}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p v-if="options.addInfo === true" class="moreinfo">
|
<p v-if="options.addInfo === true" class="moreinfo">
|
||||||
<gender-icon-render-box
|
<gender-icon-render-box
|
||||||
v-if="person.gender"
|
v-if="person.gender"
|
||||||
:gender="person.gender"
|
:gender="person.gender"
|
||||||
/>
|
/> <span
|
||||||
<time
|
v-if="person.birthdate"
|
||||||
v-if="person.birthdate && !person.deathdate"
|
|
||||||
:datetime="person.birthdate"
|
|
||||||
:title="birthdate"
|
|
||||||
>
|
>
|
||||||
{{
|
{{ trans(RENDERBOX_BIRTHDAY_STATEMENT, {gender: toGenderTranslation(person.gender), birthdate: ISOToDate(person.birthdate?.datetime)}) }}
|
||||||
trans(birthdateTranslation) +
|
</span>
|
||||||
" " +
|
|
||||||
new Intl.DateTimeFormat("fr-FR", {
|
|
||||||
dateStyle: "long",
|
|
||||||
}).format(birthdate)
|
|
||||||
}}
|
|
||||||
</time>
|
|
||||||
|
|
||||||
<time
|
|
||||||
v-else-if="person.birthdate && person.deathdate"
|
|
||||||
:datetime="person.deathdate"
|
|
||||||
:title="person.deathdate"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
new Intl.DateTimeFormat("fr-FR", {
|
|
||||||
dateStyle: "long",
|
|
||||||
}).format(birthdate)
|
|
||||||
}}
|
|
||||||
-
|
|
||||||
{{
|
|
||||||
new Intl.DateTimeFormat("fr-FR", {
|
|
||||||
dateStyle: "long",
|
|
||||||
}).format(deathdate)
|
|
||||||
}}
|
|
||||||
</time>
|
|
||||||
|
|
||||||
<time
|
|
||||||
v-else-if="person.deathdate"
|
|
||||||
:datetime="person.deathdate"
|
|
||||||
:title="person.deathdate"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
trans(RENDERBOX_DEATHDATE) +
|
|
||||||
" " +
|
|
||||||
new Intl.DateTimeFormat("fr-FR", {
|
|
||||||
dateStyle: "long",
|
|
||||||
}).format(deathdate)
|
|
||||||
}}
|
|
||||||
</time>
|
|
||||||
|
|
||||||
<span v-if="options.addAge && person.birthdate" class="age">
|
<span v-if="options.addAge && person.birthdate" class="age">
|
||||||
({{ trans(RENDERBOX_YEARS_OLD, { n: person.age }) }})
|
({{ trans(RENDERBOX_YEARS_OLD, {n: person.age}) }})
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
v-if="person.deathdate"
|
||||||
|
>
|
||||||
|
{{ trans(RENDERBOX_DEATHDATE_STATEMENT, {gender: toGenderTranslation(person.gender), deathdate: ISOToDate(person.deathdate?.datetime)}) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,11 +59,11 @@
|
|||||||
<div class="float-button bottom">
|
<div class="float-button bottom">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<slot name="record-actions" />
|
<slot name="record-actions"/>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-content fa-ul">
|
<ul class="list-content fa-ul">
|
||||||
<li v-if="person.current_household_id">
|
<li v-if="person.current_household_id">
|
||||||
<i class="fa fa-li fa-map-marker" />
|
<i class="fa fa-li fa-map-marker"/>
|
||||||
<address-render-box
|
<address-render-box
|
||||||
v-if="person.current_household_address"
|
v-if="person.current_household_address"
|
||||||
:address="person.current_household_address"
|
:address="person.current_household_address"
|
||||||
@@ -130,11 +75,6 @@
|
|||||||
<a
|
<a
|
||||||
v-if="options.addHouseholdLink === true"
|
v-if="options.addHouseholdLink === true"
|
||||||
:href="getCurrentHouseholdUrl"
|
:href="getCurrentHouseholdUrl"
|
||||||
:title="
|
|
||||||
trans(PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER, {
|
|
||||||
id: person.current_household_id,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<span class="badge rounded-pill bg-chill-beige">
|
<span class="badge rounded-pill bg-chill-beige">
|
||||||
<i
|
<i
|
||||||
@@ -144,7 +84,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="options.addNoData">
|
<li v-else-if="options.addNoData">
|
||||||
<i class="fa fa-li fa-map-marker" />
|
<i class="fa fa-li fa-map-marker"/>
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">
|
||||||
{{ trans(RENDERBOX_NO_DATA) }}
|
{{ trans(RENDERBOX_NO_DATA) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -160,7 +100,7 @@
|
|||||||
v-for="(addr, i) in person.current_residential_addresses"
|
v-for="(addr, i) in person.current_residential_addresses"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
<i class="fa fa-li fa-map-marker" />
|
<i class="fa fa-li fa-map-marker"/>
|
||||||
<div v-if="addr.address">
|
<div v-if="addr.address">
|
||||||
<span class="item-key">
|
<span class="item-key">
|
||||||
{{ trans(RENDERBOX_RESIDENTIAL_ADDRESS) }}:
|
{{ trans(RENDERBOX_RESIDENTIAL_ADDRESS) }}:
|
||||||
@@ -180,6 +120,7 @@
|
|||||||
:person="addr.hostPerson"
|
:person="addr.hostPerson"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<address-render-box
|
<address-render-box
|
||||||
v-if="addr.hostPerson.address"
|
v-if="addr.hostPerson.address"
|
||||||
:address="addr.hostPerson.address"
|
:address="addr.hostPerson.address"
|
||||||
@@ -204,36 +145,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<li v-if="person.email">
|
<li v-if="person.email">
|
||||||
<i class="fa fa-li fa-envelope-o" />
|
<i class="fa fa-li fa-envelope-o"/>
|
||||||
<a :href="'mailto: ' + person.email">{{ person.email }}</a>
|
<a :href="'mailto: ' + person.email">{{ person.email }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="options.addNoData">
|
<li v-else-if="options.addNoData">
|
||||||
<i class="fa fa-li fa-envelope-o" />
|
<i class="fa fa-li fa-envelope-o"/>
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">
|
||||||
{{ trans(RENDERBOX_NO_DATA) }}
|
{{ trans(RENDERBOX_NO_DATA) }}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="person.mobilenumber">
|
<li v-if="person.mobilenumber">
|
||||||
<i class="fa fa-li fa-mobile" />
|
<i class="fa fa-li fa-mobile"/>
|
||||||
<a :href="'tel: ' + person.mobilenumber">
|
<a :href="'tel: ' + person.mobilenumber">
|
||||||
{{ person.mobilenumber }}
|
{{ person.mobilenumber }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="options.addNoData">
|
<li v-else-if="options.addNoData">
|
||||||
<i class="fa fa-li fa-mobile" />
|
<i class="fa fa-li fa-mobile"/>
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">
|
||||||
{{ trans(RENDERBOX_NO_DATA) }}
|
{{ trans(RENDERBOX_NO_DATA) }}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="person.phonenumber">
|
<li v-if="person.phonenumber">
|
||||||
<i class="fa fa-li fa-phone" />
|
<i class="fa fa-li fa-phone"/>
|
||||||
<a :href="'tel: ' + person.phonenumber">
|
<a :href="'tel: ' + person.phonenumber">
|
||||||
{{ person.phonenumber }}
|
{{ person.phonenumber }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="options.addNoData">
|
<li v-else-if="options.addNoData">
|
||||||
<i class="fa fa-li fa-phone" />
|
<i class="fa fa-li fa-phone"/>
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">
|
||||||
{{ trans(RENDERBOX_NO_DATA) }}
|
{{ trans(RENDERBOX_NO_DATA) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -246,25 +187,25 @@
|
|||||||
options.addCenter
|
options.addCenter
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<i class="fa fa-li fa-long-arrow-right" />
|
<i class="fa fa-li fa-long-arrow-right"/>
|
||||||
<template v-for="c in person.centers">
|
<template v-for="c in person.centers">
|
||||||
{{ c.name }}
|
{{ c.name }}
|
||||||
</template>
|
</template>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="options.addNoData">
|
<li v-else-if="options.addNoData">
|
||||||
<i class="fa fa-li fa-long-arrow-right" />
|
<i class="fa fa-li fa-long-arrow-right"/>
|
||||||
<p class="chill-no-data-statement">
|
<p class="chill-no-data-statement">
|
||||||
{{ trans(RENDERBOX_NO_DATA) }}
|
{{ trans(RENDERBOX_NO_DATA) }}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<slot name="custom-zone" />
|
<slot name="custom-zone"/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot name="end-bloc" />
|
<slot name="end-bloc"/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -278,11 +219,11 @@
|
|||||||
class="fa-stack fa-holder"
|
class="fa-stack fa-holder"
|
||||||
:title="trans(RENDERBOX_HOLDER)"
|
:title="trans(RENDERBOX_HOLDER)"
|
||||||
>
|
>
|
||||||
<i class="fa fa-circle fa-stack-1x text-success" />
|
<i class="fa fa-circle fa-stack-1x text-success"/>
|
||||||
<i class="fa fa-stack-1x">T</i>
|
<i class="fa fa-stack-1x">T</i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<person-text :person="person" />
|
<person-text :person="person"/>
|
||||||
</a>
|
</a>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<span
|
<span
|
||||||
@@ -290,18 +231,18 @@
|
|||||||
class="fa-stack fa-holder"
|
class="fa-stack fa-holder"
|
||||||
:title="trans(RENDERBOX_HOLDER)"
|
:title="trans(RENDERBOX_HOLDER)"
|
||||||
>
|
>
|
||||||
<i class="fa fa-circle fa-stack-1x text-success" />
|
<i class="fa fa-circle fa-stack-1x text-success"/>
|
||||||
<i class="fa fa-stack-1x">T</i>
|
<i class="fa fa-stack-1x">T</i>
|
||||||
</span>
|
</span>
|
||||||
<person-text :person="person" />
|
<person-text :person="person"/>
|
||||||
</span>
|
</span>
|
||||||
<slot name="post-badge" />
|
<slot name="post-badge"/>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import {computed} from "vue";
|
||||||
import { ISOToDate } from "ChillMainAssets/chill/js/date";
|
import {ISOToDate} from "ChillMainAssets/chill/js/date";
|
||||||
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
|
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
|
||||||
import GenderIconRenderBox from "ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue";
|
import GenderIconRenderBox from "ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue";
|
||||||
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
||||||
@@ -311,108 +252,69 @@ import {
|
|||||||
trans,
|
trans,
|
||||||
RENDERBOX_HOLDER,
|
RENDERBOX_HOLDER,
|
||||||
RENDERBOX_NO_DATA,
|
RENDERBOX_NO_DATA,
|
||||||
RENDERBOX_DEATHDATE,
|
RENDERBOX_DEATHDATE_STATEMENT,
|
||||||
RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS,
|
RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS,
|
||||||
RENDERBOX_RESIDENTIAL_ADDRESS,
|
RENDERBOX_RESIDENTIAL_ADDRESS,
|
||||||
RENDERBOX_LOCATED_AT,
|
RENDERBOX_LOCATED_AT,
|
||||||
RENDERBOX_BIRTHDAY_MAN,
|
RENDERBOX_BIRTHDAY_STATEMENT,
|
||||||
RENDERBOX_BIRTHDAY_WOMAN,
|
// PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
|
||||||
RENDERBOX_BIRTHDAY_UNKNOWN,
|
|
||||||
RENDERBOX_BIRTHDAY_NEUTRAL,
|
|
||||||
PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
|
|
||||||
RENDERBOX_YEARS_OLD,
|
RENDERBOX_YEARS_OLD,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
import {Person} from "ChillPersonAssets/types";
|
||||||
|
import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
|
||||||
|
|
||||||
const props = defineProps({
|
interface RenderOptions {
|
||||||
person: {
|
addInfo?: boolean;
|
||||||
required: true,
|
addEntity?: boolean;
|
||||||
},
|
addAltNames?: boolean;
|
||||||
options: {
|
addAge?: boolean;
|
||||||
type: Object,
|
addId?: boolean;
|
||||||
required: false,
|
addLink?: boolean;
|
||||||
},
|
hLevel?: number;
|
||||||
render: {
|
entityDisplayLong?: boolean;
|
||||||
type: String,
|
addCenter?: boolean;
|
||||||
},
|
addNoData?: boolean;
|
||||||
returnPath: {
|
isMultiline?: boolean;
|
||||||
type: String,
|
isHolder?: boolean;
|
||||||
},
|
addHouseholdLink?: boolean;
|
||||||
showResidentialAddresses: {
|
}
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const birthdateTranslation = computed(() => {
|
interface Props {
|
||||||
if (props.person.gender) {
|
person: Person;
|
||||||
const { genderTranslation } = props.person.gender;
|
options?: RenderOptions;
|
||||||
switch (genderTranslation) {
|
render?: "bloc" | "badge";
|
||||||
case "man":
|
returnPath?: string;
|
||||||
return RENDERBOX_BIRTHDAY_MAN;
|
showResidentialAddresses?: boolean;
|
||||||
case "woman":
|
}
|
||||||
return RENDERBOX_BIRTHDAY_WOMAN;
|
|
||||||
case "neutral":
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
return RENDERBOX_BIRTHDAY_NEUTRAL;
|
render: "bloc", options: {
|
||||||
case "unknown":
|
addInfo: true,
|
||||||
return RENDERBOX_BIRTHDAY_UNKNOWN;
|
addEntity: false,
|
||||||
default:
|
addAltNames: true,
|
||||||
return RENDERBOX_BIRTHDAY_UNKNOWN;
|
addAge: true,
|
||||||
}
|
addId: true,
|
||||||
} else {
|
addLink: false,
|
||||||
return RENDERBOX_BIRTHDAY_UNKNOWN;
|
hLevel: 3,
|
||||||
|
entityDisplayingLong: true,
|
||||||
|
addCenter: true,
|
||||||
|
addNoData: true,
|
||||||
|
isMultiline: true,
|
||||||
|
isHolder: false,
|
||||||
|
addHouseholdLink: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMultiline = computed(() => {
|
const isMultiline = computed<boolean>(() => {
|
||||||
return props.options?.isMultiline || false;
|
return props.options?.isMultiline || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const birthdate = computed(() => {
|
const getUrl = computed<string>(() => {
|
||||||
if (
|
|
||||||
props.person.birthdate !== null &&
|
|
||||||
props.person.birthdate !== undefined &&
|
|
||||||
props.person.birthdate.datetime
|
|
||||||
) {
|
|
||||||
return ISOToDate(props.person.birthdate.datetime);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const deathdate = computed(() => {
|
|
||||||
if (
|
|
||||||
props.person.deathdate !== null &&
|
|
||||||
props.person.deathdate !== undefined &&
|
|
||||||
props.person.deathdate.datetime
|
|
||||||
) {
|
|
||||||
return new Date(props.person.deathdate.datetime);
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const altNameLabel = computed(() => {
|
|
||||||
let altNameLabel = "";
|
|
||||||
(props.person.altNames || []).forEach(
|
|
||||||
(altName) => (altNameLabel += altName.label),
|
|
||||||
);
|
|
||||||
return altNameLabel;
|
|
||||||
});
|
|
||||||
|
|
||||||
const altNameKey = computed(() => {
|
|
||||||
let altNameKey = "";
|
|
||||||
(props.person.altNames || []).forEach(
|
|
||||||
(altName) => (altNameKey += altName.key),
|
|
||||||
);
|
|
||||||
return altNameKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getUrl = computed(() => {
|
|
||||||
return `/fr/person/${props.person.id}/general`;
|
return `/fr/person/${props.person.id}/general`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getCurrentHouseholdUrl = computed(() => {
|
const getCurrentHouseholdUrl = computed<string>(() => {
|
||||||
let returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
|
const returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
|
||||||
return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`;
|
return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,13 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span v-if="isCut">{{ cutText }}</span>
|
<span v-if="isCut">{{ cutText }}</span>
|
||||||
<span v-else class="person-text">
|
<span v-else class="person-text">
|
||||||
<span class="firstname">{{ person.firstName }}</span>
|
<span>{{ person.text }}</span>
|
||||||
<span class="lastname"> {{ person.lastName }}</span>
|
|
||||||
<span v-if="person.altNames && person.altNames.length > 0" class="altnames">
|
|
||||||
<span :class="'altname altname-' + altNameKey"
|
|
||||||
> ({{ altNameLabel }})</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span v-if="person.suffixText" class="suffixtext"
|
<span v-if="person.suffixText" class="suffixtext"
|
||||||
> {{ person.suffixText }}</span
|
> {{ person.suffixText }}</span
|
||||||
>
|
>
|
||||||
@@ -33,16 +27,6 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const { person, isCut = false, addAge = true } = toRefs(props);
|
const { person, isCut = false, addAge = true } = toRefs(props);
|
||||||
|
|
||||||
const altNameLabel = computed(() => {
|
|
||||||
if (!person.value.altNames) return "";
|
|
||||||
return person.value.altNames.map((a: AltName) => a.label).join("");
|
|
||||||
});
|
|
||||||
|
|
||||||
const altNameKey = computed(() => {
|
|
||||||
if (!person.value.altNames) return "";
|
|
||||||
return person.value.altNames.map((a: AltName) => a.key).join("");
|
|
||||||
});
|
|
||||||
|
|
||||||
const cutText = computed(() => {
|
const cutText = computed(() => {
|
||||||
if (!person.value.text) return "";
|
if (!person.value.text) return "";
|
||||||
const more = person.value.text.length > 15 ? "…" : "";
|
const more = person.value.text.length > 15 ? "…" : "";
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="action === 'show'">
|
<div v-if="action === 'show' && person !== null">
|
||||||
<div class="flex-table">
|
<div class="flex-table">
|
||||||
<person-render-box
|
<person-render-box
|
||||||
render="bloc"
|
render="bloc"
|
||||||
@@ -22,445 +22,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="action === 'edit' || action === 'create'">
|
<div v-else-if="action === 'edit' || action === 'create'">
|
||||||
<div class="form-floating mb-3">
|
<PersonEdit
|
||||||
<input
|
:id="props.id"
|
||||||
class="form-control form-control-lg"
|
:type="props.type"
|
||||||
id="lastname"
|
:action="props.action"
|
||||||
v-model="lastName"
|
:query="props.query"
|
||||||
:placeholder="trans(PERSON_MESSAGES_PERSON_LASTNAME)"
|
|
||||||
@change="checkErrors"
|
|
||||||
/>
|
/>
|
||||||
<label for="lastname">{{ trans(PERSON_MESSAGES_PERSON_LASTNAME) }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="queryItems">
|
|
||||||
<ul class="list-suggest add-items inline">
|
|
||||||
<li
|
|
||||||
v-for="(qi, i) in queryItems"
|
|
||||||
:key="i"
|
|
||||||
@click="addQueryItem('lastName', qi)"
|
|
||||||
>
|
|
||||||
<span class="person-text">{{ qi }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<input
|
|
||||||
class="form-control form-control-lg"
|
|
||||||
id="firstname"
|
|
||||||
v-model="firstName"
|
|
||||||
:placeholder="trans(PERSON_MESSAGES_PERSON_FIRSTNAME)"
|
|
||||||
@change="checkErrors"
|
|
||||||
/>
|
|
||||||
<label for="firstname">{{
|
|
||||||
trans(PERSON_MESSAGES_PERSON_FIRSTNAME)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="queryItems">
|
|
||||||
<ul class="list-suggest add-items inline">
|
|
||||||
<li
|
|
||||||
v-for="(qi, i) in queryItems"
|
|
||||||
:key="i"
|
|
||||||
@click="addQueryItem('firstName', qi)"
|
|
||||||
>
|
|
||||||
<span class="person-text">{{ qi }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="(a, i) in config.altNames"
|
|
||||||
:key="a.key"
|
|
||||||
class="form-floating mb-3"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="form-control form-control-lg"
|
|
||||||
:id="a.key"
|
|
||||||
:value="personAltNamesLabels[i]"
|
|
||||||
@input="onAltNameInput"
|
|
||||||
/>
|
|
||||||
<label :for="a.key">{{ localizeString(a.labels) }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODO fix placeholder if undefined
|
|
||||||
-->
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<select class="form-select form-select-lg" id="gender" v-model="gender">
|
|
||||||
<option selected disabled>
|
|
||||||
{{ trans(PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER) }}
|
|
||||||
</option>
|
|
||||||
<option v-for="g in config.genders" :value="g.id" :key="g.id">
|
|
||||||
{{ g.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label>{{ trans(PERSON_MESSAGES_PERSON_GENDER_TITLE) }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="form-floating mb-3"
|
|
||||||
v-if="showCenters && config.centers.length > 1"
|
|
||||||
>
|
|
||||||
<select class="form-select form-select-lg" id="center" v-model="center">
|
|
||||||
<option selected disabled>
|
|
||||||
{{ trans(PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER) }}
|
|
||||||
</option>
|
|
||||||
<option v-for="c in config.centers" :value="c" :key="c.id">
|
|
||||||
{{ c.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label>{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
<select
|
|
||||||
class="form-select form-select-lg"
|
|
||||||
id="civility"
|
|
||||||
v-model="civility"
|
|
||||||
>
|
|
||||||
<option selected disabled>
|
|
||||||
{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER) }}
|
|
||||||
</option>
|
|
||||||
<option v-for="c in config.civilities" :value="c.id" :key="c.id">
|
|
||||||
{{ localizeString(c.name) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label>{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text" id="phonenumber">
|
|
||||||
<i class="fa fa-fw fa-phone"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
class="form-control form-control-lg"
|
|
||||||
v-model="phonenumber"
|
|
||||||
:placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
|
|
||||||
:aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
|
|
||||||
aria-describedby="phonenumber"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text" id="mobilenumber">
|
|
||||||
<i class="fa fa-fw fa-mobile"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
class="form-control form-control-lg"
|
|
||||||
v-model="mobilenumber"
|
|
||||||
:placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
|
|
||||||
:aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
|
|
||||||
aria-describedby="mobilenumber"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text" id="email">
|
|
||||||
<i class="fa fa-fw fa-at"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
class="form-control form-control-lg"
|
|
||||||
v-model="email"
|
|
||||||
:placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
|
|
||||||
:aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
|
|
||||||
aria-describedby="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="action === 'create'" class="input-group mb-3 form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
v-model="showAddressForm"
|
|
||||||
name="showAddressForm"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label">
|
|
||||||
{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM) }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="action === 'create' && showAddressFormValue"
|
|
||||||
class="form-floating mb-3"
|
|
||||||
>
|
|
||||||
<p>{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_WARNING) }}</p>
|
|
||||||
<AddAddress
|
|
||||||
:context="addAddress.context"
|
|
||||||
:options="addAddress.options"
|
|
||||||
:addressChangedCallback="submitNewAddress"
|
|
||||||
ref="addAddress"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" v-if="errors.length">
|
|
||||||
<ul>
|
|
||||||
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import {
|
import { getPerson } from "../../_api/OnTheFly";
|
||||||
getCentersForPersonCreation,
|
|
||||||
getCivilities,
|
|
||||||
getGenders,
|
|
||||||
getPerson,
|
|
||||||
getPersonAltNames,
|
|
||||||
} from "../../_api/OnTheFly";
|
|
||||||
import PersonRenderBox from "../Entity/PersonRenderBox.vue";
|
import PersonRenderBox from "../Entity/PersonRenderBox.vue";
|
||||||
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
|
import PersonEdit from "./PersonEdit.vue";
|
||||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
import type { Person } from "ChillPersonAssets/types";
|
||||||
import {
|
|
||||||
trans,
|
|
||||||
PERSON_MESSAGES_PERSON_LASTNAME,
|
|
||||||
PERSON_MESSAGES_PERSON_FIRSTNAME,
|
|
||||||
PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER,
|
|
||||||
PERSON_MESSAGES_PERSON_GENDER_TITLE,
|
|
||||||
PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER,
|
|
||||||
PERSON_MESSAGES_PERSON_CENTER_TITLE,
|
|
||||||
PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER,
|
|
||||||
PERSON_MESSAGES_PERSON_CIVILITY_TITLE,
|
|
||||||
PERSON_MESSAGES_PERSON_PHONENUMBER,
|
|
||||||
PERSON_MESSAGES_PERSON_MOBILENUMBER,
|
|
||||||
PERSON_MESSAGES_PERSON_EMAIL,
|
|
||||||
PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM,
|
|
||||||
PERSON_MESSAGES_PERSON_ADDRESS_WARNING,
|
|
||||||
} from "translator";
|
|
||||||
|
|
||||||
const props = defineProps({
|
interface Props {
|
||||||
id: [String, Number],
|
id: string | number;
|
||||||
type: String,
|
type?: string;
|
||||||
action: String,
|
action: "show" | "edit" | "create";
|
||||||
query: String,
|
query?: string;
|
||||||
});
|
|
||||||
|
|
||||||
const person = reactive({
|
|
||||||
type: "person",
|
|
||||||
lastName: "",
|
|
||||||
firstName: "",
|
|
||||||
altNames: [],
|
|
||||||
addressId: null,
|
|
||||||
center: null,
|
|
||||||
gender: null,
|
|
||||||
civility: null,
|
|
||||||
birthdate: null,
|
|
||||||
phonenumber: "",
|
|
||||||
mobilenumber: "",
|
|
||||||
email: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = reactive({
|
|
||||||
altNames: [],
|
|
||||||
civilities: [],
|
|
||||||
centers: [],
|
|
||||||
genders: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const showCenters = ref(false);
|
|
||||||
const showAddressFormValue = ref(false);
|
|
||||||
const errors = ref([]);
|
|
||||||
|
|
||||||
const addAddress = reactive({
|
|
||||||
options: {
|
|
||||||
button: {
|
|
||||||
text: { create: "person.address.create_address" },
|
|
||||||
size: "btn-sm",
|
|
||||||
},
|
|
||||||
title: { create: "person.address.create_address" },
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
target: {},
|
|
||||||
edit: false,
|
|
||||||
addressId: null,
|
|
||||||
defaults: window.addaddress,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstName = computed({
|
|
||||||
get: () => person.firstName,
|
|
||||||
set: (value) => {
|
|
||||||
person.firstName = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const lastName = computed({
|
|
||||||
get: () => person.lastName,
|
|
||||||
set: (value) => {
|
|
||||||
person.lastName = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const gender = computed({
|
|
||||||
get: () => (person.gender ? person.gender.id : null),
|
|
||||||
set: (value) => {
|
|
||||||
person.gender = { id: value, type: "chill_main_gender" };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const civility = computed({
|
|
||||||
get: () => (person.civility ? person.civility.id : null),
|
|
||||||
set: (value) => {
|
|
||||||
person.civility = { id: value, type: "chill_main_civility" };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const birthDate = computed({
|
|
||||||
get: () => (person.birthdate ? person.birthdate.datetime.split("T")[0] : ""),
|
|
||||||
set: (value) => {
|
|
||||||
if (person.birthdate) {
|
|
||||||
person.birthdate.datetime = value + "T00:00:00+0100";
|
|
||||||
} else {
|
|
||||||
person.birthdate = { datetime: value + "T00:00:00+0100" };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const phonenumber = computed({
|
|
||||||
get: () => person.phonenumber,
|
|
||||||
set: (value) => {
|
|
||||||
person.phonenumber = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const mobilenumber = computed({
|
|
||||||
get: () => person.mobilenumber,
|
|
||||||
set: (value) => {
|
|
||||||
person.mobilenumber = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const email = computed({
|
|
||||||
get: () => person.email,
|
|
||||||
set: (value) => {
|
|
||||||
person.email = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const showAddressForm = computed({
|
|
||||||
get: () => showAddressFormValue.value,
|
|
||||||
set: (value) => {
|
|
||||||
showAddressFormValue.value = value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const center = computed({
|
|
||||||
get: () => {
|
|
||||||
const c = config.centers.find(
|
|
||||||
(c) => person.center !== null && person.center.id === c.id,
|
|
||||||
);
|
|
||||||
return typeof c === "undefined" ? null : c;
|
|
||||||
},
|
|
||||||
set: (value) => {
|
|
||||||
person.center = { id: value.id, type: value.type };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const genderClass = computed(() => {
|
|
||||||
switch (person.gender && person.gender.id) {
|
|
||||||
case "woman":
|
|
||||||
return "fa-venus";
|
|
||||||
case "man":
|
|
||||||
return "fa-mars";
|
|
||||||
case "both":
|
|
||||||
return "fa-neuter";
|
|
||||||
case "unknown":
|
|
||||||
return "fa-genderless";
|
|
||||||
default:
|
|
||||||
return "fa-genderless";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const genderTranslation = computed(() => {
|
|
||||||
switch (person.gender && person.gender.genderTranslation) {
|
|
||||||
case "woman":
|
|
||||||
return PERSON_MESSAGES_PERSON_GENDER_WOMAN;
|
|
||||||
case "man":
|
|
||||||
return PERSON_MESSAGES_PERSON_GENDER_MAN;
|
|
||||||
case "neutral":
|
|
||||||
return PERSON_MESSAGES_PERSON_GENDER_NEUTRAL;
|
|
||||||
case "unknown":
|
|
||||||
return PERSON_MESSAGES_PERSON_GENDER_UNKNOWN;
|
|
||||||
default:
|
|
||||||
return PERSON_MESSAGES_PERSON_GENDER_UNKNOWN;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const feminized = computed(() =>
|
|
||||||
person.gender && person.gender.id === "woman" ? "e" : "",
|
|
||||||
);
|
|
||||||
const personAltNamesLabels = computed(() =>
|
|
||||||
person.altNames.map((a) => (a ? a.label : "")),
|
|
||||||
);
|
|
||||||
const queryItems = computed(() =>
|
|
||||||
props.query ? props.query.split(" ") : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
function checkErrors() {
|
|
||||||
errors.value = [];
|
|
||||||
if (person.lastName === "") {
|
|
||||||
errors.value.push("Le nom ne doit pas être vide.");
|
|
||||||
}
|
|
||||||
if (person.firstName === "") {
|
|
||||||
errors.value.push("Le prénom ne doit pas être vide.");
|
|
||||||
}
|
|
||||||
if (!person.gender) {
|
|
||||||
errors.value.push("Le genre doit être renseigné");
|
|
||||||
}
|
|
||||||
if (showCenters.value && person.center === null) {
|
|
||||||
errors.value.push("Le centre doit être renseigné");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadData() {
|
const props = defineProps<Props>();
|
||||||
getPerson(props.id).then((p) => {
|
|
||||||
Object.assign(person, p);
|
const person = ref<Person | null>(null);
|
||||||
|
|
||||||
|
function loadData(): void {
|
||||||
|
if (props.id === undefined || props.id === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idNum = typeof props.id === "string" ? Number(props.id) : props.id;
|
||||||
|
if (!Number.isFinite(idNum)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getPerson(idNum as number).then((p) => {
|
||||||
|
person.value = p;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAltNameInput(event) {
|
|
||||||
const key = event.target.id;
|
|
||||||
const label = event.target.value;
|
|
||||||
let updateAltNames = person.altNames.filter((a) => a.key !== key);
|
|
||||||
updateAltNames.push({ key: key, label: label });
|
|
||||||
person.altNames = updateAltNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addQueryItem(field, queryItem) {
|
|
||||||
switch (field) {
|
|
||||||
case "lastName":
|
|
||||||
person.lastName = person.lastName
|
|
||||||
? (person.lastName += ` ${queryItem}`)
|
|
||||||
: queryItem;
|
|
||||||
break;
|
|
||||||
case "firstName":
|
|
||||||
person.firstName = person.firstName
|
|
||||||
? (person.firstName += ` ${queryItem}`)
|
|
||||||
: queryItem;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitNewAddress(payload) {
|
|
||||||
person.addressId = payload.addressId;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getPersonAltNames().then((altNames) => {
|
|
||||||
config.altNames = altNames;
|
|
||||||
});
|
|
||||||
getCivilities().then((civilities) => {
|
|
||||||
if ("results" in civilities) {
|
|
||||||
config.civilities = civilities.results;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
getGenders().then((genders) => {
|
|
||||||
if ("results" in genders) {
|
|
||||||
config.genders = genders.results;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (props.action !== "create") {
|
if (props.action !== "create") {
|
||||||
loadData();
|
loadData();
|
||||||
} else {
|
|
||||||
getCentersForPersonCreation().then((params) => {
|
|
||||||
config.centers = params.centers.filter((c) => c.isActive);
|
|
||||||
showCenters.value = params.showCenters;
|
|
||||||
if (showCenters.value && config.centers.length === 1) {
|
|
||||||
person.center = config.centers[0];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose(genderClass, genderTranslation, feminized, birthDate);
|
|
||||||
</script>
|
</script>
|
||||||
|
@@ -0,0 +1,707 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('lastName') }"
|
||||||
|
id="lastname"
|
||||||
|
v-model="lastName"
|
||||||
|
:placeholder="trans(PERSON_MESSAGES_PERSON_LASTNAME)"
|
||||||
|
/>
|
||||||
|
<label for="lastname">{{
|
||||||
|
trans(PERSON_MESSAGES_PERSON_LASTNAME)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('lastName')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="queryItems">
|
||||||
|
<ul class="list-suggest add-items inline">
|
||||||
|
<li
|
||||||
|
v-for="(qi, i) in queryItems"
|
||||||
|
:key="i"
|
||||||
|
@click="addQueryItem('lastName', qi)"
|
||||||
|
>
|
||||||
|
<span class="person-text">{{ qi }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('firstName') }"
|
||||||
|
id="firstname"
|
||||||
|
v-model="firstName"
|
||||||
|
:placeholder="trans(PERSON_MESSAGES_PERSON_FIRSTNAME)"
|
||||||
|
/>
|
||||||
|
<label for="firstname">{{
|
||||||
|
trans(PERSON_MESSAGES_PERSON_FIRSTNAME)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('firstName')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="queryItems">
|
||||||
|
<ul class="list-suggest add-items inline">
|
||||||
|
<li
|
||||||
|
v-for="(qi, i) in queryItems"
|
||||||
|
:key="i"
|
||||||
|
@click="addQueryItem('firstName', qi)"
|
||||||
|
>
|
||||||
|
<span class="person-text">{{ qi }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(a, i) in config.altNames" :key="a.key" class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:id="a.key"
|
||||||
|
:name="'label_' + a.key"
|
||||||
|
value=""
|
||||||
|
@input="onAltNameInput($event, a.key)"
|
||||||
|
/>
|
||||||
|
<label :for="'label_' + a.key">{{ localizeString(a.labels) }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="worker in config.identifiers"
|
||||||
|
:key="worker.definition_id"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{'is-invalid': hasViolationWithParameter('identifiers', 'definition_id', worker.definition_id.toString())}"
|
||||||
|
type="text"
|
||||||
|
:name="'worker_' + worker.definition_id"
|
||||||
|
:placeholder="localizeString(worker.label)"
|
||||||
|
@input="onIdentifierInput($event, worker.definition_id)"
|
||||||
|
/>
|
||||||
|
<label :for="'worker_' + worker.definition_id">{{
|
||||||
|
localizeString(worker.label)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitlesWithParameter('identifiers', 'definition_id', worker.definition_id.toString())"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="form-floating">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('gender') }"
|
||||||
|
id="gender"
|
||||||
|
v-model="gender"
|
||||||
|
>
|
||||||
|
<option selected disabled>
|
||||||
|
{{ trans(PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER) }}
|
||||||
|
</option>
|
||||||
|
<option v-for="g in config.genders" :value="g.id" :key="g.id">
|
||||||
|
{{ g.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label for="gender" class="form-label">{{
|
||||||
|
trans(PERSON_MESSAGES_PERSON_GENDER_TITLE)
|
||||||
|
}}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('gender')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3" v-if="showCenters && config.centers.length > 1">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="form-floating">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('center') }"
|
||||||
|
id="center"
|
||||||
|
v-model="center"
|
||||||
|
>
|
||||||
|
<option selected disabled>
|
||||||
|
{{ trans(PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER) }}
|
||||||
|
</option>
|
||||||
|
<option v-for="c in config.centers" :value="c" :key="c.id">
|
||||||
|
{{ c.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label for="center">{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('center')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<div class="form-floating">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('civility') }"
|
||||||
|
id="civility"
|
||||||
|
v-model="civility"
|
||||||
|
>
|
||||||
|
<option selected disabled>
|
||||||
|
{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER) }}
|
||||||
|
</option>
|
||||||
|
<option v-for="c in config.civilities" :value="c.id" :key="c.id">
|
||||||
|
{{ localizeString(c.name) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label for="civility">{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('civility')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-cake2-fill"></i>
|
||||||
|
</span>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('birthdate') }"
|
||||||
|
name="birthdate"
|
||||||
|
type="date"
|
||||||
|
v-model="birthDate"
|
||||||
|
:placeholder="trans(BIRTHDATE)"
|
||||||
|
:aria-label="trans(BIRTHDATE)"
|
||||||
|
/>
|
||||||
|
<label for="birthdate">{{ trans(BIRTHDATE) }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('birthdate')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text" id="phonenumber">
|
||||||
|
<i class="fa fa-fw fa-phone"></i>
|
||||||
|
</span>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('phonenumber') }"
|
||||||
|
v-model="phonenumber"
|
||||||
|
:placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
|
||||||
|
:aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
|
||||||
|
aria-describedby="phonenumber"
|
||||||
|
/>
|
||||||
|
<label for="phonenumber">{{ trans(PERSON_MESSAGES_PERSON_PHONENUMBER) }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('phonenumber')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text" id="mobilenumber">
|
||||||
|
<i class="fa fa-fw fa-mobile"></i>
|
||||||
|
</span>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('mobilenumber') }"
|
||||||
|
v-model="mobilenumber"
|
||||||
|
:placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
|
||||||
|
:aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
|
||||||
|
aria-describedby="mobilenumber"
|
||||||
|
/>
|
||||||
|
<label for="mobilenumber">{{ trans(PERSON_MESSAGES_PERSON_MOBILENUMBER) }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('mobilenumber')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="input-group has-validation">
|
||||||
|
<span class="input-group-text" id="email">
|
||||||
|
<i class="fa fa-fw fa-at"></i>
|
||||||
|
</span>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
class="form-control form-control-lg"
|
||||||
|
:class="{ 'is-invalid': hasViolation('email') }"
|
||||||
|
v-model="email"
|
||||||
|
:placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
|
||||||
|
:aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
|
||||||
|
aria-describedby="email"
|
||||||
|
/>
|
||||||
|
<label for="email">{{ trans(PERSON_MESSAGES_PERSON_EMAIL) }}</label>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="err in violationTitles('email')"
|
||||||
|
class="invalid-feedback was-validated-force"
|
||||||
|
>
|
||||||
|
{{ err }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="action === 'create'" class="input-group mb-3 form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="showAddressForm"
|
||||||
|
name="showAddressForm"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label">
|
||||||
|
{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM) }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="action === 'create' && showAddressFormValue"
|
||||||
|
class="form-floating mb-3"
|
||||||
|
>
|
||||||
|
<p>{{ trans(PERSON_MESSAGES_PERSON_ADDRESS_WARNING) }}</p>
|
||||||
|
<AddAddress
|
||||||
|
:context="addAddress.context"
|
||||||
|
:options="addAddress.options"
|
||||||
|
:addressChangedCallback="submitNewAddress"
|
||||||
|
ref="addAddress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning" v-if="errors.length">
|
||||||
|
<ul>
|
||||||
|
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
|
||||||
|
import {
|
||||||
|
createPerson,
|
||||||
|
getCentersForPersonCreation,
|
||||||
|
getCivilities,
|
||||||
|
getGenders,
|
||||||
|
getPerson,
|
||||||
|
getPersonAltNames,
|
||||||
|
getPersonIdentifiers,
|
||||||
|
WritePersonViolationMap,
|
||||||
|
} from "../../_api/OnTheFly";
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
BIRTHDATE,
|
||||||
|
PERSON_EDIT_ERROR_WHILE_SAVING,
|
||||||
|
PERSON_MESSAGES_PERSON_LASTNAME,
|
||||||
|
PERSON_MESSAGES_PERSON_FIRSTNAME,
|
||||||
|
PERSON_MESSAGES_PERSON_GENDER_PLACEHOLDER,
|
||||||
|
PERSON_MESSAGES_PERSON_GENDER_TITLE,
|
||||||
|
PERSON_MESSAGES_PERSON_CENTER_PLACEHOLDER,
|
||||||
|
PERSON_MESSAGES_PERSON_CENTER_TITLE,
|
||||||
|
PERSON_MESSAGES_PERSON_CIVILITY_PLACEHOLDER,
|
||||||
|
PERSON_MESSAGES_PERSON_CIVILITY_TITLE,
|
||||||
|
PERSON_MESSAGES_PERSON_PHONENUMBER,
|
||||||
|
PERSON_MESSAGES_PERSON_MOBILENUMBER,
|
||||||
|
PERSON_MESSAGES_PERSON_EMAIL,
|
||||||
|
PERSON_MESSAGES_PERSON_ADDRESS_SHOW_ADDRESS_FORM,
|
||||||
|
PERSON_MESSAGES_PERSON_ADDRESS_WARNING,
|
||||||
|
} from "translator";
|
||||||
|
import {
|
||||||
|
Center,
|
||||||
|
Civility,
|
||||||
|
Gender,
|
||||||
|
DateTimeWrite, ValidationExceptionInterface,
|
||||||
|
} from "ChillMainAssets/types";
|
||||||
|
import {
|
||||||
|
AltName,
|
||||||
|
Person,
|
||||||
|
PersonWrite,
|
||||||
|
PersonIdentifierWorker,
|
||||||
|
type Suggestion,
|
||||||
|
type EntitiesOrMe,
|
||||||
|
} from "ChillPersonAssets/types";
|
||||||
|
import {
|
||||||
|
isValidationException,
|
||||||
|
} from "ChillMainAssets/lib/api/apiMethods";
|
||||||
|
import {useToast} from "vue-toast-notification";
|
||||||
|
import {getTimezoneOffsetString, ISOToDate} from "ChillMainAssets/chill/js/date";
|
||||||
|
|
||||||
|
interface PersonEditComponentConfig {
|
||||||
|
id?: number | null;
|
||||||
|
type?: string;
|
||||||
|
action: "edit" | "create";
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<PersonEditComponentConfig>(), {
|
||||||
|
id: null,
|
||||||
|
type: "TODO",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit =
|
||||||
|
defineEmits<(e: "onPersonCreated", payload: { person: Person }) => void>();
|
||||||
|
|
||||||
|
defineExpose({ postPerson });
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const person = reactive<PersonWrite>({
|
||||||
|
type: "person",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
altNames: [],
|
||||||
|
// address: null,
|
||||||
|
birthdate: null,
|
||||||
|
deathdate: null,
|
||||||
|
phonenumber: "",
|
||||||
|
mobilenumber: "",
|
||||||
|
email: "",
|
||||||
|
gender: null,
|
||||||
|
center: null,
|
||||||
|
civility: null,
|
||||||
|
identifiers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = reactive<{
|
||||||
|
altNames: AltName[];
|
||||||
|
civilities: Civility[];
|
||||||
|
centers: Center[];
|
||||||
|
genders: Gender[];
|
||||||
|
identifiers: PersonIdentifierWorker[];
|
||||||
|
}>({
|
||||||
|
altNames: [],
|
||||||
|
civilities: [],
|
||||||
|
centers: [],
|
||||||
|
genders: [],
|
||||||
|
identifiers: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const showCenters = ref(false);
|
||||||
|
const showAddressFormValue = ref(false);
|
||||||
|
const errors = ref<string[]>([]);
|
||||||
|
|
||||||
|
const addAddress = reactive({
|
||||||
|
options: {
|
||||||
|
button: {
|
||||||
|
text: { create: "person.address.create_address" },
|
||||||
|
size: "btn-sm",
|
||||||
|
},
|
||||||
|
title: { create: "person.address.create_address" },
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
target: {},
|
||||||
|
edit: false,
|
||||||
|
addressId: null as number | null,
|
||||||
|
defaults: (window as any).addaddress,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstName = computed({
|
||||||
|
get: () => person.firstName,
|
||||||
|
set: (value: string) => {
|
||||||
|
person.firstName = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const lastName = computed({
|
||||||
|
get: () => person.lastName,
|
||||||
|
set: (value: string) => {
|
||||||
|
person.lastName = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gender = computed({
|
||||||
|
get: () => (person.gender ? person.gender.id : null),
|
||||||
|
set: (value: string | null) => {
|
||||||
|
person.gender = value
|
||||||
|
? { id: Number.parseInt(value), type: "chill_main_gender" }
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const civility = computed({
|
||||||
|
get: () => (person.civility ? person.civility.id : null),
|
||||||
|
set: (value: number | null) => {
|
||||||
|
person.civility =
|
||||||
|
value !== null ? { id: value, type: "chill_main_civility" } : null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const birthDate = computed({
|
||||||
|
get: () => (person.birthdate ? person.birthdate.datetime.split("T")[0] : ""),
|
||||||
|
set: (value: string) => {
|
||||||
|
const date = ISOToDate(value);
|
||||||
|
if (null === date) {
|
||||||
|
person.birthdate = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const offset = getTimezoneOffsetString(date, Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
|
if (person.birthdate) {
|
||||||
|
person.birthdate.datetime = value + "T00:00:00" + offset;
|
||||||
|
} else {
|
||||||
|
person.birthdate = { datetime: value + "T00:00:00" + offset };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const phonenumber = computed({
|
||||||
|
get: () => person.phonenumber,
|
||||||
|
set: (value: string) => {
|
||||||
|
person.phonenumber = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mobilenumber = computed({
|
||||||
|
get: () => person.mobilenumber,
|
||||||
|
set: (value: string) => {
|
||||||
|
person.mobilenumber = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const email = computed({
|
||||||
|
get: () => person.email,
|
||||||
|
set: (value: string) => {
|
||||||
|
person.email = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const showAddressForm = computed({
|
||||||
|
get: () => showAddressFormValue.value,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
showAddressFormValue.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const center = computed({
|
||||||
|
get: () => {
|
||||||
|
const c = config.centers.find(
|
||||||
|
(c) => person.center !== null && person.center.id === c.id,
|
||||||
|
);
|
||||||
|
return typeof c === "undefined" ? null : c;
|
||||||
|
},
|
||||||
|
set: (value: Center | null) => {
|
||||||
|
if (null !== value) {
|
||||||
|
person.center = {
|
||||||
|
id: value.id,
|
||||||
|
type: value.type,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
person.center = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the query items to display for suggestion
|
||||||
|
*/
|
||||||
|
const queryItems = computed(() => {
|
||||||
|
const words: null | string[] = props.query ? props.query.split(" ") : null;
|
||||||
|
|
||||||
|
if (null === words) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstNameWords = (person.firstName || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(" ");
|
||||||
|
const lastNameWords = (person.lastName || "").trim().toLowerCase().split(" ");
|
||||||
|
|
||||||
|
return words
|
||||||
|
.filter((word) => !firstNameWords.includes(word.toLowerCase()))
|
||||||
|
.filter((word) => !lastNameWords.includes(word.toLowerCase()));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
if (props.id !== undefined && props.id !== null) {
|
||||||
|
const person = await getPerson(props.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAltNameInput(event: Event, key: string): void {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const value = target.value;
|
||||||
|
const updateAltNamesKey = person.altNames.findIndex((a) => a.key === key);
|
||||||
|
if (-1 === updateAltNamesKey) {
|
||||||
|
person.altNames.push({ key, value });
|
||||||
|
} else {
|
||||||
|
person.altNames[updateAltNamesKey].value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIdentifierInput(event: Event, definition_id: number): void {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const value = target.value;
|
||||||
|
const updateIdentifierKey = person.identifiers.findIndex(
|
||||||
|
(w) => w.definition_id === definition_id,
|
||||||
|
);
|
||||||
|
if (-1 === updateIdentifierKey) {
|
||||||
|
person.identifiers.push({
|
||||||
|
type: "person_identifier",
|
||||||
|
definition_id,
|
||||||
|
value: { content: value },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
person.identifiers[updateIdentifierKey].value = { content: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQueryItem(field: "lastName" | "firstName", queryItem: string) {
|
||||||
|
switch (field) {
|
||||||
|
case "lastName":
|
||||||
|
person.lastName = person.lastName
|
||||||
|
? (person.lastName += ` ${queryItem}`)
|
||||||
|
: queryItem;
|
||||||
|
break;
|
||||||
|
case "firstName":
|
||||||
|
person.firstName = person.firstName
|
||||||
|
? (person.firstName += ` ${queryItem}`)
|
||||||
|
: queryItem;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WritePersonViolationKey = Extract<keyof WritePersonViolationMap, string>;
|
||||||
|
const violationsList = ref<ValidationExceptionInterface<WritePersonViolationMap>|null>(null);
|
||||||
|
|
||||||
|
function violationTitles<P extends WritePersonViolationKey>(property: P): string[] {
|
||||||
|
if (null === violationsList.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return violationsList.value.violationsByNormalizedProperty(property).map((v) => v.title);
|
||||||
|
|
||||||
|
}
|
||||||
|
function violationTitlesWithParameter<
|
||||||
|
P extends WritePersonViolationKey,
|
||||||
|
Param extends Extract<keyof WritePersonViolationMap[P], string>
|
||||||
|
>(
|
||||||
|
property: P,
|
||||||
|
with_parameter: Param,
|
||||||
|
with_parameter_value: WritePersonViolationMap[P][Param],
|
||||||
|
): string[] {
|
||||||
|
if (violationsList.value === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return violationsList.value.violationsByNormalizedPropertyAndParams(property, with_parameter, with_parameter_value)
|
||||||
|
.map((v) => v.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hasViolation<P extends WritePersonViolationKey>(property: P): boolean {
|
||||||
|
return violationTitles(property).length > 0;
|
||||||
|
}
|
||||||
|
function hasViolationWithParameter<
|
||||||
|
P extends WritePersonViolationKey,
|
||||||
|
Param extends Extract<keyof WritePersonViolationMap[P], string>
|
||||||
|
>(
|
||||||
|
property: P,
|
||||||
|
with_parameter: Param,
|
||||||
|
with_parameter_value: WritePersonViolationMap[P][Param],
|
||||||
|
): boolean {
|
||||||
|
return violationTitlesWithParameter(property, with_parameter, with_parameter_value).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitNewAddress(payload: { addressId: number }) {
|
||||||
|
// person.addressId = payload.addressId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postPerson(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const createdPerson = await createPerson(person);
|
||||||
|
|
||||||
|
emit("onPersonCreated", { person: createdPerson });
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (isValidationException<WritePersonViolationMap>(e)) {
|
||||||
|
violationsList.value = e;
|
||||||
|
} else {
|
||||||
|
toast.error(trans(PERSON_EDIT_ERROR_WHILE_SAVING));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPersonAltNames().then((altNames) => {
|
||||||
|
config.altNames = altNames;
|
||||||
|
});
|
||||||
|
getCivilities().then((civilities) => {
|
||||||
|
config.civilities = civilities;
|
||||||
|
});
|
||||||
|
getGenders().then((genders) => {
|
||||||
|
config.genders = genders;
|
||||||
|
});
|
||||||
|
getPersonIdentifiers().then((identifiers) => {
|
||||||
|
config.identifiers = identifiers.filter(
|
||||||
|
(w: PersonIdentifierWorker) =>
|
||||||
|
w.presence === 'ON_CREATION' || w.presence === 'REQUIRED'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (props.action !== "create") {
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
getCentersForPersonCreation().then((params) => {
|
||||||
|
config.centers = params.centers.filter((c: Center) => c.isActive);
|
||||||
|
showCenters.value = params.showCenters;
|
||||||
|
if (showCenters.value && config.centers.length === 1) {
|
||||||
|
// if there is only one center, preselect it
|
||||||
|
person.center = {
|
||||||
|
id: config.centers[0].id,
|
||||||
|
type: config.centers[0].type ?? "center",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.was-validated-force {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -88,6 +88,15 @@
|
|||||||
<div data-suggest-container="{{ altName.vars.full_name|e('html_attr') }}" class="col-sm-8" style="margin-left: auto;"></div>
|
<div data-suggest-container="{{ altName.vars.full_name|e('html_attr') }}" class="col-sm-8" style="margin-left: auto;"></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if form.identifiers|length > 0 %}
|
||||||
|
{% for f in form.identifiers %}
|
||||||
|
<div class="row mb-1" style="display:flex;">
|
||||||
|
{{ form_row(f) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{{ form_widget(form.identifiers) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ form_row(form.gender, { 'label' : 'Gender'|trans }) }}
|
{{ form_row(form.gender, { 'label' : 'Gender'|trans }) }}
|
||||||
|
|
||||||
|
@@ -0,0 +1,155 @@
|
|||||||
|
<?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\PersonBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
|
use Chill\MainBundle\Entity\Civility;
|
||||||
|
use Chill\MainBundle\Entity\Gender;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Entity\PersonAltName;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denormalize a Person entity from a JSON-like array structure, creating or updating an existing instance.
|
||||||
|
*
|
||||||
|
* To find an existing instance by his id, see the @see{PersonJsonReadDenormalizer}.
|
||||||
|
*/
|
||||||
|
final class PersonJsonDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
|
||||||
|
{
|
||||||
|
use DenormalizerAwareTrait;
|
||||||
|
use ObjectToPopulateTrait;
|
||||||
|
|
||||||
|
public function __construct(private readonly PersonIdentifierManagerInterface $personIdentifierManager) {}
|
||||||
|
|
||||||
|
public function denormalize($data, string $type, ?string $format = null, array $context = []): Person
|
||||||
|
{
|
||||||
|
$person = $this->extractObjectToPopulate($type, $context);
|
||||||
|
|
||||||
|
if (null === $person) {
|
||||||
|
$person = new Person();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setters applied directly per known field for readability
|
||||||
|
if (\array_key_exists('firstName', $data)) {
|
||||||
|
$person->setFirstName($data['firstName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('lastName', $data)) {
|
||||||
|
$person->setLastName($data['lastName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('phonenumber', $data)) {
|
||||||
|
$person->setPhonenumber($this->denormalizer->denormalize($data['phonenumber'], PhoneNumber::class, $format, $context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('mobilenumber', $data)) {
|
||||||
|
$person->setMobilenumber($this->denormalizer->denormalize($data['mobilenumber'], PhoneNumber::class, $format, $context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('gender', $data) && null !== $data['gender']) {
|
||||||
|
$gender = $this->denormalizer->denormalize($data['gender'], Gender::class, $format, []);
|
||||||
|
$person->setGender($gender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('birthdate', $data)) {
|
||||||
|
$object = $this->denormalizer->denormalize($data['birthdate'], \DateTime::class, $format, $context);
|
||||||
|
$person->setBirthdate($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('deathdate', $data)) {
|
||||||
|
$object = $this->denormalizer->denormalize($data['deathdate'], \DateTimeImmutable::class, $format, $context);
|
||||||
|
$person->setDeathdate($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('center', $data)) {
|
||||||
|
$object = $this->denormalizer->denormalize($data['center'], Center::class, $format, $context);
|
||||||
|
$person->setCenter($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('altNames', $data)) {
|
||||||
|
foreach ($data['altNames'] as $altNameData) {
|
||||||
|
if (!array_key_exists('key', $altNameData)
|
||||||
|
|| !array_key_exists('value', $altNameData)
|
||||||
|
|| '' === trim($altNameData['key'])
|
||||||
|
) {
|
||||||
|
throw new UnexpectedValueException('format for alt name is not correct');
|
||||||
|
}
|
||||||
|
$altNameKey = $altNameData['key'];
|
||||||
|
$altNameValue = $altNameData['value'];
|
||||||
|
|
||||||
|
$altName = $person->getAltNames()->findFirst(fn (int $key, PersonAltName $personAltName) => $personAltName->getKey() === $altNameKey);
|
||||||
|
if (null === $altName) {
|
||||||
|
$altName = new PersonAltName();
|
||||||
|
$person->addAltName($altName);
|
||||||
|
}
|
||||||
|
$altName->setKey($altNameKey)->setLabel($altNameValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('identifiers', $data)) {
|
||||||
|
foreach ($data['identifiers'] as $identifierData) {
|
||||||
|
if (!array_key_exists('definition_id', $identifierData)
|
||||||
|
|| !array_key_exists('value', $identifierData)
|
||||||
|
|| !is_int($identifierData['definition_id'])
|
||||||
|
|| !is_array($identifierData['value'])
|
||||||
|
) {
|
||||||
|
throw new UnexpectedValueException('format for identifiers is not correct');
|
||||||
|
}
|
||||||
|
|
||||||
|
$definitionId = $identifierData['definition_id'];
|
||||||
|
$value = $identifierData['value'];
|
||||||
|
|
||||||
|
$worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definitionId);
|
||||||
|
|
||||||
|
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$personIdentifier = $person->getIdentifiers()->findFirst(fn (int $key, PersonIdentifier $personIdentifier) => $personIdentifier->getId() === $definitionId);
|
||||||
|
if (null === $personIdentifier) {
|
||||||
|
$personIdentifier = new PersonIdentifier($worker->getDefinition());
|
||||||
|
$person->addIdentifier($personIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
$personIdentifier->setValue($value);
|
||||||
|
$personIdentifier->setCanonical($worker->canonicalizeValue($value));
|
||||||
|
|
||||||
|
if ($worker->isEmpty($personIdentifier)) {
|
||||||
|
$person->removeIdentifier($personIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('email', $data)) {
|
||||||
|
$person->setEmail($data['email']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('civility', $data) && null !== $data['civility']) {
|
||||||
|
$civility = $this->denormalizer->denormalize($data['civility'], Civility::class, $format, []);
|
||||||
|
$person->setCivility($civility);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $person;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization($data, $type, $format = null): bool
|
||||||
|
{
|
||||||
|
return Person::class === $type && 'person' === ($data['type'] ?? null) && !isset($data['id']);
|
||||||
|
}
|
||||||
|
}
|
@@ -11,169 +11,33 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\PersonBundle\Serializer\Normalizer;
|
namespace Chill\PersonBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Center;
|
|
||||||
use Chill\MainBundle\Entity\Civility;
|
|
||||||
use Chill\MainBundle\Entity\Gender;
|
|
||||||
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Entity\PersonAltName;
|
use Chill\PersonBundle\Entity\PersonAltName;
|
||||||
use Chill\PersonBundle\Repository\PersonRepository;
|
|
||||||
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
|
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use libphonenumber\PhoneNumber;
|
|
||||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a Person entity.
|
* Serialize a Person entity.
|
||||||
*/
|
*/
|
||||||
class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwareInterface, PersonJsonNormalizerInterface
|
class PersonJsonNormalizer implements NormalizerAwareInterface, NormalizerInterface
|
||||||
{
|
{
|
||||||
use DenormalizerAwareTrait;
|
|
||||||
|
|
||||||
use NormalizerAwareTrait;
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
use ObjectToPopulateTrait;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ChillEntityRenderExtension $render,
|
private readonly ChillEntityRenderExtension $render,
|
||||||
/* TODO: replace by PersonRenderInterface, as sthis is the only one required */
|
|
||||||
private readonly PersonRepository $repository,
|
|
||||||
private readonly CenterResolverManagerInterface $centerResolverManager,
|
private readonly CenterResolverManagerInterface $centerResolverManager,
|
||||||
private readonly ResidentialAddressRepository $residentialAddressRepository,
|
private readonly ResidentialAddressRepository $residentialAddressRepository,
|
||||||
private readonly PhoneNumberHelperInterface $phoneNumberHelper,
|
private readonly PhoneNumberHelperInterface $phoneNumberHelper,
|
||||||
|
private readonly \Chill\PersonBundle\PersonIdentifier\Rendering\PersonIdRenderingInterface $personIdRendering,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function denormalize($data, $type, $format = null, array $context = [])
|
|
||||||
{
|
|
||||||
$person = $this->extractObjectToPopulate($type, $context);
|
|
||||||
|
|
||||||
if (\array_key_exists('id', $data) && null === $person) {
|
|
||||||
$person = $this->repository->find($data['id']);
|
|
||||||
|
|
||||||
if (null === $person) {
|
|
||||||
throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
// currently, not allowed to update a person through api
|
|
||||||
// if instantiated with id
|
|
||||||
return $person;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $person) {
|
|
||||||
$person = new Person();
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields = [
|
|
||||||
'firstName',
|
|
||||||
'lastName',
|
|
||||||
'phonenumber',
|
|
||||||
'mobilenumber',
|
|
||||||
'gender',
|
|
||||||
'birthdate',
|
|
||||||
'deathdate',
|
|
||||||
'center',
|
|
||||||
'altNames',
|
|
||||||
'email',
|
|
||||||
'civility',
|
|
||||||
];
|
|
||||||
|
|
||||||
$fields = array_filter(
|
|
||||||
$fields,
|
|
||||||
static fn (string $field): bool => \array_key_exists($field, $data)
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($fields as $item) {
|
|
||||||
switch ($item) {
|
|
||||||
case 'firstName':
|
|
||||||
$person->setFirstName($data[$item]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'lastName':
|
|
||||||
$person->setLastName($data[$item]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'phonenumber':
|
|
||||||
$person->setPhonenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'mobilenumber':
|
|
||||||
$person->setMobilenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'gender':
|
|
||||||
$gender = $this->denormalizer->denormalize($data[$item], Gender::class, $format, []);
|
|
||||||
|
|
||||||
$person->setGender($gender);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'birthdate':
|
|
||||||
$object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
|
|
||||||
|
|
||||||
$person->setBirthdate($object);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'deathdate':
|
|
||||||
$object = $this->denormalizer->denormalize($data[$item], \DateTimeImmutable::class, $format, $context);
|
|
||||||
|
|
||||||
$person->setDeathdate($object);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'center':
|
|
||||||
$object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context);
|
|
||||||
$person->setCenter($object);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'altNames':
|
|
||||||
foreach ($data[$item] as $altName) {
|
|
||||||
$oldAltName = $person
|
|
||||||
->getAltNames()
|
|
||||||
->filter(static fn (PersonAltName $n): bool => $n->getKey() === $altName['key'])->first();
|
|
||||||
|
|
||||||
if (false === $oldAltName) {
|
|
||||||
$newAltName = new PersonAltName();
|
|
||||||
$newAltName->setKey($altName['key']);
|
|
||||||
$newAltName->setLabel($altName['label']);
|
|
||||||
$person->addAltName($newAltName);
|
|
||||||
} else {
|
|
||||||
$oldAltName->setLabel($altName['label']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'email':
|
|
||||||
$person->setEmail($data[$item]);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'civility':
|
|
||||||
$civility = $this->denormalizer->denormalize($data[$item], Civility::class, $format, []);
|
|
||||||
|
|
||||||
$person->setCivility($civility);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $person;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Person $person
|
* @param Person $person
|
||||||
* @param string|null $format
|
* @param string|null $format
|
||||||
@@ -204,6 +68,7 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
|
|||||||
'email' => $person->getEmail(),
|
'email' => $person->getEmail(),
|
||||||
'gender' => $this->normalizer->normalize($person->getGender(), $format, $context),
|
'gender' => $this->normalizer->normalize($person->getGender(), $format, $context),
|
||||||
'civility' => $this->normalizer->normalize($person->getCivility(), $format, $context),
|
'civility' => $this->normalizer->normalize($person->getCivility(), $format, $context),
|
||||||
|
'personId' => $this->personIdRendering->renderPersonId($person),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (\in_array('minimal', $groups, true) && 1 === \count($groups)) {
|
if (\in_array('minimal', $groups, true) && 1 === \count($groups)) {
|
||||||
@@ -215,11 +80,6 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
|
|||||||
null];
|
null];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supportsDenormalization($data, $type, $format = null)
|
|
||||||
{
|
|
||||||
return Person::class === $type && 'person' === ($data['type'] ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function supportsNormalization($data, $format = null): bool
|
public function supportsNormalization($data, $format = null): bool
|
||||||
{
|
{
|
||||||
return $data instanceof Person && 'json' === $format;
|
return $data instanceof Person && 'json' === $format;
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
<?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\PersonBundle\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
|
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Serializer\Exception\LogicException;
|
||||||
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a Person entity by his id during the denormalization process.
|
||||||
|
*/
|
||||||
|
readonly class PersonJsonReadDenormalizer implements DenormalizerInterface
|
||||||
|
{
|
||||||
|
public function __construct(private PersonRepository $repository) {}
|
||||||
|
|
||||||
|
public function denormalize($data, string $type, ?string $format = null, array $context = []): Person
|
||||||
|
{
|
||||||
|
if (!is_array($data)) {
|
||||||
|
throw new InvalidArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\array_key_exists('id', $data)) {
|
||||||
|
$person = $this->repository->find($data['id']);
|
||||||
|
|
||||||
|
if (null === $person) {
|
||||||
|
throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $person;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization($data, string $type, ?string $format = null)
|
||||||
|
{
|
||||||
|
return is_array($data) && Person::class === $type && 'person' === ($data['type'] ?? null) && isset($data['id']);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,141 @@
|
|||||||
|
<?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\PersonBundle\Tests\Controller;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||||
|
use Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer;
|
||||||
|
use Chill\PersonBundle\Controller\PersonIdentifierListApiController;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Normalizer\PersonIdentifierWorkerNormalizer;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||||
|
use Symfony\Component\Serializer\Serializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PersonIdentifierListApiControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testListAccessDenied(): void
|
||||||
|
{
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted('ROLE_USER')->willReturn(false)->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$serializer = new Serializer([new PersonIdentifierWorkerNormalizer(), new CollectionNormalizer()], [new JsonEncoder()]);
|
||||||
|
|
||||||
|
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
|
||||||
|
$paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
|
||||||
|
|
||||||
|
$controller = new PersonIdentifierListApiController(
|
||||||
|
$security->reveal(),
|
||||||
|
$serializer,
|
||||||
|
$personIdentifierManager->reveal(),
|
||||||
|
$paginatorFactory->reveal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
|
$controller->list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListSuccess(): void
|
||||||
|
{
|
||||||
|
// Build 3 workers
|
||||||
|
$engine = new class () implements PersonIdentifierEngineInterface {
|
||||||
|
public static function getName(): string
|
||||||
|
{
|
||||||
|
return 'dummy';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
|
||||||
|
|
||||||
|
public function renderAsString(?\Chill\PersonBundle\Entity\Identifier\PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$definition1 = new PersonIdentifierDefinition(['en' => 'Label 1'], 'dummy');
|
||||||
|
$definition2 = new PersonIdentifierDefinition(['en' => 'Label 2'], 'dummy');
|
||||||
|
$definition3 = new PersonIdentifierDefinition(['en' => 'Label 3'], 'dummy');
|
||||||
|
// simulate persisted ids
|
||||||
|
$r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
|
||||||
|
$r->setAccessible(true);
|
||||||
|
$r->setValue($definition1, 1);
|
||||||
|
$r->setValue($definition2, 2);
|
||||||
|
$r->setValue($definition3, 3);
|
||||||
|
|
||||||
|
$workers = [
|
||||||
|
new PersonIdentifierWorker($engine, $definition1),
|
||||||
|
new PersonIdentifierWorker($engine, $definition2),
|
||||||
|
new PersonIdentifierWorker($engine, $definition3),
|
||||||
|
];
|
||||||
|
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->isGranted('ROLE_USER')->willReturn(true)->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$personIdentifierManager = $this->prophesize(PersonIdentifierManagerInterface::class);
|
||||||
|
$personIdentifierManager->getWorkers()->willReturn($workers)->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$paginator = $this->prophesize(\Chill\MainBundle\Pagination\PaginatorInterface::class);
|
||||||
|
$paginator->setItemsPerPage(3)->shouldBeCalledOnce();
|
||||||
|
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||||
|
$paginator->getItemsPerPage()->willReturn(count($workers));
|
||||||
|
$paginator->getTotalItems()->willReturn(count($workers));
|
||||||
|
$paginator->hasNextPage()->willReturn(false);
|
||||||
|
$paginator->hasPreviousPage()->willReturn(false);
|
||||||
|
|
||||||
|
$paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
|
||||||
|
$paginatorFactory->create(3)->willReturn($paginator->reveal())->shouldBeCalledOnce();
|
||||||
|
|
||||||
|
$serializer = new Serializer([
|
||||||
|
new PersonIdentifierWorkerNormalizer(),
|
||||||
|
new CollectionNormalizer(),
|
||||||
|
], [new JsonEncoder()]);
|
||||||
|
|
||||||
|
$controller = new PersonIdentifierListApiController(
|
||||||
|
$security->reveal(),
|
||||||
|
$serializer,
|
||||||
|
$personIdentifierManager->reveal(),
|
||||||
|
$paginatorFactory->reveal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $controller->list();
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
$body = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
self::assertIsArray($body);
|
||||||
|
self::assertArrayHasKey('count', $body);
|
||||||
|
self::assertArrayHasKey('pagination', $body);
|
||||||
|
self::assertArrayHasKey('results', $body);
|
||||||
|
self::assertSame(3, $body['count']);
|
||||||
|
self::assertCount(3, $body['results']);
|
||||||
|
// spot check one item
|
||||||
|
self::assertSame('person_identifier_worker', $body['results'][0]['type']);
|
||||||
|
self::assertSame(1, $body['results'][0]['id']);
|
||||||
|
self::assertSame('dummy', $body['results'][0]['engine']);
|
||||||
|
self::assertSame(['en' => 'Label 1'], $body['results'][0]['label']);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,101 @@
|
|||||||
|
<?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\PersonBundle\Tests\PersonIdentifier\Normalizer;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Normalizer\PersonIdentifierWorkerNormalizer;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PersonIdentifierWorkerNormalizerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSupportsNormalization(): void
|
||||||
|
{
|
||||||
|
$engine = new class () implements PersonIdentifierEngineInterface {
|
||||||
|
public static function getName(): string
|
||||||
|
{
|
||||||
|
return 'dummy';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
|
||||||
|
|
||||||
|
public function renderAsString(?\Chill\PersonBundle\Entity\Identifier\PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$definition = new PersonIdentifierDefinition(label: ['en' => 'SSN'], engine: 'string');
|
||||||
|
$worker = new PersonIdentifierWorker($engine, $definition);
|
||||||
|
|
||||||
|
$normalizer = new PersonIdentifierWorkerNormalizer();
|
||||||
|
|
||||||
|
self::assertTrue($normalizer->supportsNormalization($worker));
|
||||||
|
self::assertFalse($normalizer->supportsNormalization(new \stdClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeReturnsExpectedArray(): void
|
||||||
|
{
|
||||||
|
$engine = new class () implements PersonIdentifierEngineInterface {
|
||||||
|
public static function getName(): string
|
||||||
|
{
|
||||||
|
return 'dummy';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
|
||||||
|
|
||||||
|
public function renderAsString(?\Chill\PersonBundle\Entity\Identifier\PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$definition = new PersonIdentifierDefinition(label: ['en' => 'SSN'], engine: 'string');
|
||||||
|
$definition->setActive(false);
|
||||||
|
$worker = new PersonIdentifierWorker($engine, $definition);
|
||||||
|
|
||||||
|
$normalizer = new PersonIdentifierWorkerNormalizer();
|
||||||
|
$normalized = $normalizer->normalize($worker);
|
||||||
|
|
||||||
|
self::assertSame([
|
||||||
|
'type' => 'person_identifier_worker',
|
||||||
|
'id' => null,
|
||||||
|
'engine' => 'string',
|
||||||
|
'label' => ['en' => 'SSN'],
|
||||||
|
'isActive' => false,
|
||||||
|
], $normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeThrowsOnInvalidObject(): void
|
||||||
|
{
|
||||||
|
$normalizer = new PersonIdentifierWorkerNormalizer();
|
||||||
|
$this->expectException(UnexpectedValueException::class);
|
||||||
|
$normalizer->normalize(new \stdClass());
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,131 @@
|
|||||||
|
<?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\PersonBundle\Tests\PersonIdentifier\Validator;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\IdentifierPresenceEnum;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Validator\RequiredIdentifierConstraint;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Validator\RequiredIdentifierConstraintValidator;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
#[CoversClass(RequiredIdentifierConstraintValidator::class)]
|
||||||
|
final class RequiredIdentifierConstraintValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private PersonIdentifierDefinition $requiredDefinition;
|
||||||
|
|
||||||
|
protected function createValidator(): RequiredIdentifierConstraintValidator
|
||||||
|
{
|
||||||
|
$this->requiredDefinition = new PersonIdentifierDefinition(
|
||||||
|
label: ['fr' => 'Identifiant requis'],
|
||||||
|
engine: 'test.engine',
|
||||||
|
);
|
||||||
|
$this->requiredDefinition->setPresence(IdentifierPresenceEnum::REQUIRED);
|
||||||
|
$reflection = new \ReflectionClass($this->requiredDefinition);
|
||||||
|
$id = $reflection->getProperty('id');
|
||||||
|
$id->setValue($this->requiredDefinition, 1);
|
||||||
|
|
||||||
|
// Mock only the required methods of the engine used by the validator through the worker
|
||||||
|
$engineProphecy = $this->prophesize(PersonIdentifierEngineInterface::class);
|
||||||
|
$engineProphecy->isEmpty(Argument::type(PersonIdentifier::class))
|
||||||
|
->will(function (array $args): bool {
|
||||||
|
/** @var PersonIdentifier $identifier */
|
||||||
|
$identifier = $args[0];
|
||||||
|
|
||||||
|
return '' === trim($identifier->getValue()['content'] ?? '');
|
||||||
|
});
|
||||||
|
$engineProphecy->renderAsString(Argument::any(), Argument::any())
|
||||||
|
->will(function (array $args): string {
|
||||||
|
/** @var PersonIdentifier|null $identifier */
|
||||||
|
$identifier = $args[0] ?? null;
|
||||||
|
|
||||||
|
return $identifier?->getValue()['content'] ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
$worker = new PersonIdentifierWorker($engineProphecy->reveal(), $this->requiredDefinition);
|
||||||
|
|
||||||
|
// Mock only the required method used by the validator
|
||||||
|
$managerProphecy = $this->prophesize(PersonIdentifierManagerInterface::class);
|
||||||
|
$managerProphecy->getWorkers()->willReturn([$worker]);
|
||||||
|
|
||||||
|
return new RequiredIdentifierConstraintValidator($managerProphecy->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnNonCollectionValue(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedValueException::class);
|
||||||
|
$this->validator->validate(new \stdClass(), new RequiredIdentifierConstraint());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnInvalidConstraintType(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedTypeException::class);
|
||||||
|
// Provide a valid Collection value so the type check reaches the constraint check
|
||||||
|
$this->validator->validate(new ArrayCollection(), new NotBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoViolationWhenRequiredIdentifierPresentAndNotEmpty(): void
|
||||||
|
{
|
||||||
|
$identifier = new PersonIdentifier($this->requiredDefinition);
|
||||||
|
$identifier->setValue(['content' => 'ABC']);
|
||||||
|
|
||||||
|
$collection = new ArrayCollection([$identifier]);
|
||||||
|
|
||||||
|
$this->validator->validate($collection, new RequiredIdentifierConstraint());
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViolationWhenRequiredIdentifierMissing(): void
|
||||||
|
{
|
||||||
|
$collection = new ArrayCollection();
|
||||||
|
|
||||||
|
$this->validator->validate($collection, new RequiredIdentifierConstraint());
|
||||||
|
|
||||||
|
$this->buildViolation('person_identifier.This identifier must be set')
|
||||||
|
->setParameter('{{ value }}', '')
|
||||||
|
->setParameter('definition_id', '1')
|
||||||
|
->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViolationWhenRequiredIdentifierIsEmpty(): void
|
||||||
|
{
|
||||||
|
$identifier = new PersonIdentifier($this->requiredDefinition);
|
||||||
|
$identifier->setValue(['content' => ' ']);
|
||||||
|
|
||||||
|
$collection = new ArrayCollection([$identifier]);
|
||||||
|
|
||||||
|
$this->validator->validate($collection, new RequiredIdentifierConstraint());
|
||||||
|
|
||||||
|
$this->buildViolation('person_identifier.This identifier must be set')
|
||||||
|
->setParameter('{{ value }}', ' ')
|
||||||
|
->setParameter('definition_id', '1')
|
||||||
|
->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,158 @@
|
|||||||
|
<?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\PersonBundle\Tests\PersonIdentifier\Validator;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraint;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraintValidator;
|
||||||
|
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository;
|
||||||
|
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
#[CoversClass(UniqueIdentifierConstraintValidator::class)]
|
||||||
|
final class UniqueIdentifierConstraintValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy|PersonIdentifierRepository
|
||||||
|
*/
|
||||||
|
private ObjectProphecy $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy|PersonRenderInterface
|
||||||
|
*/
|
||||||
|
private ObjectProphecy $personRender;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->repository = $this->prophesize(PersonIdentifierRepository::class);
|
||||||
|
$this->personRender = $this->prophesize(PersonRenderInterface::class);
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createValidator(): UniqueIdentifierConstraintValidator
|
||||||
|
{
|
||||||
|
return new UniqueIdentifierConstraintValidator($this->repository->reveal(), $this->personRender->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnInvalidConstraintType(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedTypeException::class);
|
||||||
|
|
||||||
|
// Provide a valid value so execution reaches the constraint type check
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'Test'], 'string');
|
||||||
|
$identifier = new PersonIdentifier($definition);
|
||||||
|
$identifier->setValue(['value' => 'ABC']);
|
||||||
|
|
||||||
|
$this->validator->validate($identifier, new NotBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThrowsOnInvalidValueType(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedValueException::class);
|
||||||
|
$this->validator->validate(new \stdClass(), new UniqueIdentifierConstraint());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoViolationWhenNoDuplicate(): void
|
||||||
|
{
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'Test'], 'string');
|
||||||
|
$identifier = new PersonIdentifier($definition);
|
||||||
|
$identifier->setValue(['value' => 'UNIQ']);
|
||||||
|
|
||||||
|
// Configure repository mock to return empty array
|
||||||
|
$this->repository->findByDefinitionAndCanonical($definition, ['value' => 'UNIQ'])->willReturn([]);
|
||||||
|
|
||||||
|
$this->validator->validate($identifier, new UniqueIdentifierConstraint());
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViolationWhenDuplicateFound(): void
|
||||||
|
{
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
|
||||||
|
$reflectionClass = new \ReflectionClass($definition);
|
||||||
|
$reflectionId = $reflectionClass->getProperty('id');
|
||||||
|
$reflectionId->setValue($definition, 1);
|
||||||
|
|
||||||
|
$personA = new Person();
|
||||||
|
$personA->setFirstName('Alice')->setLastName('Anderson');
|
||||||
|
$personB = new Person();
|
||||||
|
$personB->setFirstName('Bob')->setLastName('Brown');
|
||||||
|
|
||||||
|
$dup1 = new PersonIdentifier($definition);
|
||||||
|
$dup1->setPerson($personA);
|
||||||
|
$dup1->setValue(['value' => '123']);
|
||||||
|
$dup2 = new PersonIdentifier($definition);
|
||||||
|
$dup2->setPerson($personB);
|
||||||
|
$dup2->setValue(['value' => '123']);
|
||||||
|
|
||||||
|
// Repository returns duplicates
|
||||||
|
$this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1, $dup2]);
|
||||||
|
|
||||||
|
// Person renderer returns names
|
||||||
|
$this->personRender->renderString($personA, Argument::type('array'))->willReturn('Alice Anderson');
|
||||||
|
$this->personRender->renderString($personB, Argument::type('array'))->willReturn('Bob Brown');
|
||||||
|
|
||||||
|
$identifier = new PersonIdentifier($definition);
|
||||||
|
$identifier->setPerson(new Person());
|
||||||
|
$identifier->setValue(['value' => '123']);
|
||||||
|
|
||||||
|
$constraint = new UniqueIdentifierConstraint();
|
||||||
|
|
||||||
|
$this->validator->validate($identifier, $constraint);
|
||||||
|
|
||||||
|
$this->buildViolation($constraint->message)
|
||||||
|
->setParameter('{{ persons }}', 'Alice Anderson, Bob Brown')
|
||||||
|
->setParameter('definition_id', '1')
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViolationWhenDuplicateFoundButForSamePerson(): void
|
||||||
|
{
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'SSN'], 'string');
|
||||||
|
$reflectionClass = new \ReflectionClass($definition);
|
||||||
|
$reflectionId = $reflectionClass->getProperty('id');
|
||||||
|
$reflectionId->setValue($definition, 1);
|
||||||
|
|
||||||
|
$personA = new Person();
|
||||||
|
$personA->setFirstName('Alice')->setLastName('Anderson');
|
||||||
|
|
||||||
|
$dup1 = new PersonIdentifier($definition);
|
||||||
|
$dup1->setPerson($personA);
|
||||||
|
$dup1->setValue(['value' => '123']);
|
||||||
|
|
||||||
|
// Repository returns duplicates
|
||||||
|
$this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1]);
|
||||||
|
|
||||||
|
$identifier = new PersonIdentifier($definition);
|
||||||
|
$identifier->setPerson($personA);
|
||||||
|
$identifier->setValue(['value' => '123']);
|
||||||
|
|
||||||
|
$constraint = new UniqueIdentifierConstraint();
|
||||||
|
|
||||||
|
$this->validator->validate($identifier, $constraint);
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,76 @@
|
|||||||
|
<?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\PersonBundle\Tests\Repository\Identifier;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Identifier\StringIdentifier;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PersonIdentifierRepositoryTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
public function testFindByDefinitionAndCanonical(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$container = self::getContainer();
|
||||||
|
/** @var PersonIdentifierManagerInterface $personIdentifierManager */
|
||||||
|
$personIdentifierManager = $container->get(PersonIdentifierManagerInterface::class);
|
||||||
|
|
||||||
|
/** @var EntityManagerInterface $em */
|
||||||
|
$em = $container->get(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
// Get a random existing person from fixtures
|
||||||
|
/** @var Person|null $person */
|
||||||
|
$person = $em->getRepository(Person::class)->findOneBy([]);
|
||||||
|
self::assertNotNull($person, 'An existing Person is required for this integration test.');
|
||||||
|
|
||||||
|
// Create a definition
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'Test Identifier'], StringIdentifier::NAME);
|
||||||
|
$em->persist($definition);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
// Create an identifier attached to the person
|
||||||
|
$value = ['content' => 'ABC-'.bin2hex(random_bytes(4))];
|
||||||
|
$identifier = new PersonIdentifier($definition);
|
||||||
|
$identifier->setPerson($person);
|
||||||
|
$identifier->setValue($value);
|
||||||
|
$identifier->setCanonical($personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($identifier->getValue()));
|
||||||
|
$em->persist($identifier);
|
||||||
|
$em->flush();
|
||||||
|
|
||||||
|
// Use the repository to find by definition and value
|
||||||
|
/** @var PersonIdentifierRepository $repo */
|
||||||
|
$repo = $container->get(PersonIdentifierRepository::class);
|
||||||
|
$results = $repo->findByDefinitionAndCanonical($definition, $value);
|
||||||
|
|
||||||
|
self::assertNotEmpty($results, 'Repository should return at least one result.');
|
||||||
|
self::assertContainsOnlyInstancesOf(PersonIdentifier::class, $results);
|
||||||
|
self::assertTrue(in_array($identifier->getId(), array_map(static fn (PersonIdentifier $pi) => $pi->getId(), $results), true));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
foreach ($results as $res) {
|
||||||
|
$em->remove($res);
|
||||||
|
}
|
||||||
|
$em->flush();
|
||||||
|
$em->remove($definition);
|
||||||
|
$em->flush();
|
||||||
|
}
|
||||||
|
}
|
@@ -18,6 +18,7 @@ use Chill\MainBundle\Repository\CountryRepository;
|
|||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Entity\PersonPhone;
|
use Chill\PersonBundle\Entity\PersonPhone;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
use Chill\PersonBundle\Repository\PersonACLAwareRepository;
|
use Chill\PersonBundle\Repository\PersonACLAwareRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@@ -42,6 +43,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
|
|||||||
|
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private PersonIdentifierManagerInterface $personIdentifierManager;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
self::bootKernel();
|
self::bootKernel();
|
||||||
@@ -49,6 +52,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
|
|||||||
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
$this->countryRepository = self::getContainer()->get(CountryRepository::class);
|
$this->countryRepository = self::getContainer()->get(CountryRepository::class);
|
||||||
$this->centerRepository = self::getContainer()->get(CenterRepositoryInterface::class);
|
$this->centerRepository = self::getContainer()->get(CenterRepositoryInterface::class);
|
||||||
|
$this->personIdentifierManager = self::getContainer()->get(PersonIdentifierManagerInterface::class);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCountByCriteria()
|
public function testCountByCriteria()
|
||||||
@@ -66,7 +71,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
|
|||||||
$security->reveal(),
|
$security->reveal(),
|
||||||
$this->entityManager,
|
$this->entityManager,
|
||||||
$this->countryRepository,
|
$this->countryRepository,
|
||||||
$authorizationHelper->reveal()
|
$authorizationHelper->reveal(),
|
||||||
|
$this->personIdentifierManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
$number = $repository->countBySearchCriteria('diallo');
|
$number = $repository->countBySearchCriteria('diallo');
|
||||||
@@ -89,7 +95,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
|
|||||||
$security->reveal(),
|
$security->reveal(),
|
||||||
$this->entityManager,
|
$this->entityManager,
|
||||||
$this->countryRepository,
|
$this->countryRepository,
|
||||||
$authorizationHelper->reveal()
|
$authorizationHelper->reveal(),
|
||||||
|
$this->personIdentifierManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
$results = $repository->findBySearchCriteria(0, 5, false, 'diallo');
|
$results = $repository->findBySearchCriteria(0, 5, false, 'diallo');
|
||||||
@@ -120,7 +127,8 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
|
|||||||
$security->reveal(),
|
$security->reveal(),
|
||||||
$this->entityManager,
|
$this->entityManager,
|
||||||
$this->countryRepository,
|
$this->countryRepository,
|
||||||
$authorizationHelper->reveal()
|
$authorizationHelper->reveal(),
|
||||||
|
$this->personIdentifierManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
$actual = $repository->findByPhone($phoneNumber, 0, 10);
|
$actual = $repository->findByPhone($phoneNumber, 0, 10);
|
||||||
|
@@ -0,0 +1,302 @@
|
|||||||
|
<?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\PersonBundle\Tests\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
|
use Chill\MainBundle\Entity\Civility;
|
||||||
|
use Chill\MainBundle\Entity\Gender;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||||
|
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||||
|
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonDenormalizer;
|
||||||
|
use libphonenumber\PhoneNumber;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonDenormalizer
|
||||||
|
*/
|
||||||
|
final class PersonJsonDenormalizerTest extends TestCase
|
||||||
|
{
|
||||||
|
private function createIdentifierManager(): PersonIdentifierManagerInterface
|
||||||
|
{
|
||||||
|
return new class () implements PersonIdentifierManagerInterface {
|
||||||
|
public function getWorkers(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker
|
||||||
|
{
|
||||||
|
if (is_int($personIdentifierDefinition)) {
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
|
||||||
|
// Force the id for testing purposes
|
||||||
|
$r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
|
||||||
|
$r->setAccessible(true);
|
||||||
|
$r->setValue($definition, $personIdentifierDefinition);
|
||||||
|
} else {
|
||||||
|
$definition = $personIdentifierDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = new class () implements PersonIdentifierEngineInterface {
|
||||||
|
public static function getName(): string
|
||||||
|
{
|
||||||
|
return 'dummy';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||||
|
{
|
||||||
|
// trivial canonicalization for tests
|
||||||
|
return isset($value['content']) ? (string) $value['content'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
|
||||||
|
|
||||||
|
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmpty(PersonIdentifier $identifier): bool
|
||||||
|
{
|
||||||
|
$value = $identifier->getValue();
|
||||||
|
$content = isset($value['content']) ? trim((string) $value['content']) : '';
|
||||||
|
|
||||||
|
return '' === $content;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new PersonIdentifierWorker($engine, $definition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSupportsDenormalizationReturnsTrueForValidData(): void
|
||||||
|
{
|
||||||
|
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'type' => 'person',
|
||||||
|
// important: new Person (creation) must not contain an id
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
|
||||||
|
{
|
||||||
|
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
|
||||||
|
|
||||||
|
// not an array
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));
|
||||||
|
|
||||||
|
// missing type
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization([], Person::class));
|
||||||
|
|
||||||
|
// wrong type value
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person'], Person::class));
|
||||||
|
|
||||||
|
// id present means it's not a create payload for this denormalizer
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 123], Person::class));
|
||||||
|
|
||||||
|
// wrong target class
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], \stdClass::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDenormalizeMapsPayloadToPersonProperties(): void
|
||||||
|
{
|
||||||
|
$json = <<<'JSON'
|
||||||
|
{
|
||||||
|
"type": "person",
|
||||||
|
"firstName": "Jérome",
|
||||||
|
"lastName": "diallo",
|
||||||
|
"altNames": [
|
||||||
|
{
|
||||||
|
"key": "jeune_fille",
|
||||||
|
"value": "FJ"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"birthdate": null,
|
||||||
|
"deathdate": null,
|
||||||
|
"phonenumber": "",
|
||||||
|
"mobilenumber": "",
|
||||||
|
"email": "",
|
||||||
|
"gender": {
|
||||||
|
"id": 5,
|
||||||
|
"type": "chill_main_gender"
|
||||||
|
},
|
||||||
|
"center": {
|
||||||
|
"id": 1,
|
||||||
|
"type": "center"
|
||||||
|
},
|
||||||
|
"civility": null,
|
||||||
|
"identifiers": [
|
||||||
|
{
|
||||||
|
"type": "person_identifier",
|
||||||
|
"value": {
|
||||||
|
"content": "789456"
|
||||||
|
},
|
||||||
|
"definition_id": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
$inner = new class () implements DenormalizerInterface {
|
||||||
|
public ?Gender $gender = null;
|
||||||
|
public ?Center $center = null;
|
||||||
|
|
||||||
|
public function denormalize($data, $type, $format = null, array $context = [])
|
||||||
|
{
|
||||||
|
if (PhoneNumber::class === $type) {
|
||||||
|
return '' === $data ? null : new PhoneNumber();
|
||||||
|
}
|
||||||
|
if (\DateTime::class === $type || \DateTimeImmutable::class === $type) {
|
||||||
|
return null === $data ? null : new \DateTimeImmutable((string) $data);
|
||||||
|
}
|
||||||
|
if (Gender::class === $type) {
|
||||||
|
return $this->gender ??= new Gender();
|
||||||
|
}
|
||||||
|
if (Center::class === $type) {
|
||||||
|
return $this->center ??= new Center();
|
||||||
|
}
|
||||||
|
if (Civility::class === $type) {
|
||||||
|
return null; // input is null in our payload
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization($data, $type, $format = null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
|
||||||
|
$denormalizer->setDenormalizer($inner);
|
||||||
|
|
||||||
|
$person = $denormalizer->denormalize($data, Person::class);
|
||||||
|
|
||||||
|
self::assertInstanceOf(Person::class, $person);
|
||||||
|
self::assertSame('Jérome', $person->getFirstName());
|
||||||
|
self::assertSame('diallo', $person->getLastName());
|
||||||
|
|
||||||
|
// phone numbers: empty strings map to null via the inner denormalizer stub
|
||||||
|
self::assertNull($person->getPhonenumber());
|
||||||
|
self::assertNull($person->getMobilenumber());
|
||||||
|
|
||||||
|
// email passes through as is
|
||||||
|
self::assertSame('', $person->getEmail());
|
||||||
|
|
||||||
|
// nested objects are provided by our inner denormalizer and must be set back on the Person
|
||||||
|
self::assertSame($inner->gender, $person->getGender());
|
||||||
|
self::assertSame($inner->center, $person->getCenter());
|
||||||
|
|
||||||
|
// dates are null in the provided payload
|
||||||
|
self::assertNull($person->getBirthdate());
|
||||||
|
self::assertNull($person->getDeathdate());
|
||||||
|
|
||||||
|
// civility is null as provided
|
||||||
|
self::assertNull($person->getCivility());
|
||||||
|
|
||||||
|
// altNames: make sure the alt name with key jeune_fille has label FJ
|
||||||
|
$found = false;
|
||||||
|
foreach ($person->getAltNames() as $altName) {
|
||||||
|
if ('jeune_fille' === $altName->getKey()) {
|
||||||
|
$found = true;
|
||||||
|
self::assertSame('FJ', $altName->getLabel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::assertTrue($found, 'Expected altNames to contain key "jeune_fille"');
|
||||||
|
|
||||||
|
$found = false;
|
||||||
|
foreach ($person->getIdentifiers() as $identifier) {
|
||||||
|
if (5 === $identifier->getDefinition()->getId()) {
|
||||||
|
$found = true;
|
||||||
|
self::assertSame(['content' => '789456'], $identifier->getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::assertTrue($found, 'Expected identifiers with definition id 5');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDenormalizeRemovesEmptyIdentifier(): void
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'type' => 'person',
|
||||||
|
'firstName' => 'Alice',
|
||||||
|
'lastName' => 'Smith',
|
||||||
|
'identifiers' => [
|
||||||
|
[
|
||||||
|
'type' => 'person_identifier',
|
||||||
|
'value' => ['content' => ''],
|
||||||
|
'definition_id' => 7,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
|
||||||
|
|
||||||
|
$person = $denormalizer->denormalize($data, Person::class);
|
||||||
|
|
||||||
|
// The identifier with empty content must be considered empty and removed
|
||||||
|
self::assertSame(0, $person->getIdentifiers()->count(), 'Expected no identifiers to remain on the person');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDenormalizeRemovesPreviouslyExistingIdentifierWhenIncomingValueIsEmpty(): void
|
||||||
|
{
|
||||||
|
// Prepare an existing Person with a pre-existing identifier (definition id = 9)
|
||||||
|
$definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy');
|
||||||
|
$ref = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id');
|
||||||
|
$ref->setValue($definition, 9);
|
||||||
|
|
||||||
|
$existingIdentifier = new PersonIdentifier($definition);
|
||||||
|
$existingIdentifier->setValue(['content' => 'ABC']);
|
||||||
|
|
||||||
|
$person = new Person();
|
||||||
|
$person->addIdentifier($existingIdentifier);
|
||||||
|
|
||||||
|
// Also set the identifier's own id = 9 so that the denormalizer logic matches it
|
||||||
|
// (the current denormalizer matches by PersonIdentifier->getId() === definition_id)
|
||||||
|
$refId = new \ReflectionProperty(PersonIdentifier::class, 'id');
|
||||||
|
$refId->setValue($existingIdentifier, 9);
|
||||||
|
|
||||||
|
// Incoming payload sets the same definition id with an empty value
|
||||||
|
$data = [
|
||||||
|
'type' => 'person',
|
||||||
|
'identifiers' => [
|
||||||
|
[
|
||||||
|
'type' => 'person_identifier',
|
||||||
|
'value' => ['content' => ''],
|
||||||
|
'definition_id' => 9,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager());
|
||||||
|
|
||||||
|
// Use AbstractNormalizer::OBJECT_TO_POPULATE to update the existing person
|
||||||
|
$result = $denormalizer->denormalize($data, Person::class, null, [
|
||||||
|
AbstractNormalizer::OBJECT_TO_POPULATE => $person,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame($person, $result, 'Denormalizer should update and return the provided Person instance');
|
||||||
|
self::assertSame(0, $person->getIdentifiers()->count(), 'The previously existing identifier should be removed when incoming value is empty');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,63 @@
|
|||||||
|
<?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 Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
|
use PHPUnit\Framework\Assert;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
final class PersonJsonNormalizerIntegrationTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
public function testNormalizeExistingPersonFromDatabase(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$container = self::getContainer();
|
||||||
|
|
||||||
|
/** @var PersonRepository $repo */
|
||||||
|
$repo = $container->get(PersonRepository::class);
|
||||||
|
$person = $repo->findOneBy([]);
|
||||||
|
|
||||||
|
if (!$person instanceof Person) {
|
||||||
|
self::markTestSkipped('No person found in test database. Load fixtures to enable this test.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var SerializerInterface $serializer */
|
||||||
|
$serializer = $container->get(SerializerInterface::class);
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
$data = $serializer->normalize($person, 'json');
|
||||||
|
Assert::assertIsArray($data);
|
||||||
|
|
||||||
|
// Spot check some expected keys exist
|
||||||
|
foreach ([
|
||||||
|
'type', 'id', 'text', 'textAge', 'firstName', 'lastName', 'birthdate', 'age', 'gender', 'civility',
|
||||||
|
] as $key) {
|
||||||
|
Assert::assertArrayHasKey($key, $data, sprintf('Expected key %s in normalized payload', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal group should also work
|
||||||
|
$minimal = $serializer->normalize($person, 'json', ['groups' => 'minimal']);
|
||||||
|
Assert::assertIsArray($minimal);
|
||||||
|
foreach ([
|
||||||
|
'type', 'id', 'text', 'textAge', 'firstName', 'lastName',
|
||||||
|
] as $key) {
|
||||||
|
Assert::assertArrayHasKey($key, $minimal, sprintf('Expected key %s in minimal normalized payload', $key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -11,74 +11,186 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Serializer\Normalizer;
|
namespace Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
||||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Repository\PersonRepository;
|
use Chill\PersonBundle\Entity\PersonAltName;
|
||||||
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
|
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
|
||||||
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer;
|
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer;
|
||||||
|
use Chill\PersonBundle\PersonIdentifier\Rendering\PersonIdRenderingInterface;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*
|
*
|
||||||
* @coversNothing
|
* @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer
|
||||||
*/
|
*/
|
||||||
final class PersonJsonNormalizerTest extends KernelTestCase
|
final class PersonJsonNormalizerTest extends TestCase
|
||||||
{
|
{
|
||||||
use ProphecyTrait;
|
use ProphecyTrait;
|
||||||
|
|
||||||
private PersonJsonNormalizer $normalizer;
|
public function testSupportsNormalization(): void
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
{
|
||||||
self::bootKernel();
|
$normalizer = $this->createNormalizer();
|
||||||
|
|
||||||
$residentialAddressRepository = $this->prophesize(ResidentialAddressRepository::class);
|
self::assertTrue($normalizer->supportsNormalization(new Person(), 'json'));
|
||||||
$residentialAddressRepository
|
self::assertFalse($normalizer->supportsNormalization(new \stdClass(), 'json'));
|
||||||
->findCurrentResidentialAddressByPerson(Argument::type(Person::class), Argument::any())
|
self::assertFalse($normalizer->supportsNormalization(new Person(), 'xml'));
|
||||||
->willReturn([]);
|
|
||||||
|
|
||||||
$this->normalizer = $this->buildPersonJsonNormalizer(
|
|
||||||
self::getContainer()->get(ChillEntityRenderExtension::class),
|
|
||||||
self::getContainer()->get(PersonRepository::class),
|
|
||||||
self::getContainer()->get(CenterResolverManagerInterface::class),
|
|
||||||
$residentialAddressRepository->reveal(),
|
|
||||||
self::getContainer()->get(PhoneNumberHelperInterface::class),
|
|
||||||
self::getContainer()->get(NormalizerInterface::class)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testNormalization()
|
public function testNormalizeWithMinimalGroupReturnsOnlyBaseKeys(): void
|
||||||
{
|
{
|
||||||
$person = new Person();
|
$person = $this->createSamplePerson();
|
||||||
$result = $this->normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => ['read']]);
|
|
||||||
|
|
||||||
$this->assertIsArray($result);
|
$normalizer = $this->createNormalizer();
|
||||||
|
$data = $normalizer->normalize($person, 'json', [AbstractNormalizer::GROUPS => 'minimal']);
|
||||||
|
|
||||||
|
// Expected base keys
|
||||||
|
$expectedKeys = [
|
||||||
|
'type',
|
||||||
|
'id',
|
||||||
|
'text',
|
||||||
|
'textAge',
|
||||||
|
'firstName',
|
||||||
|
'lastName',
|
||||||
|
'current_household_address',
|
||||||
|
'birthdate',
|
||||||
|
'deathdate',
|
||||||
|
'age',
|
||||||
|
'phonenumber',
|
||||||
|
'mobilenumber',
|
||||||
|
'email',
|
||||||
|
'gender',
|
||||||
|
'civility',
|
||||||
|
'personId',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($expectedKeys as $key) {
|
||||||
|
self::assertArrayHasKey($key, $data, sprintf('Key %s should be present', $key));
|
||||||
|
}
|
||||||
|
self::assertSame('PERSON-ID-RENDER', $data['personId']);
|
||||||
|
|
||||||
|
// Ensure extended keys are not present in minimal mode
|
||||||
|
foreach (['centers', 'altNames', 'current_household_id', 'current_residential_addresses'] as $key) {
|
||||||
|
self::assertArrayNotHasKey($key, $data, sprintf('Key %s should NOT be present in minimal group', $key));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildPersonJsonNormalizer(
|
public function testNormalizeWithoutGroupsIncludesExtendedKeys(): void
|
||||||
ChillEntityRenderExtension $render,
|
{
|
||||||
PersonRepository $repository,
|
$person = $this->createSamplePerson(withAltNames: true);
|
||||||
CenterResolverManagerInterface $centerResolverManager,
|
|
||||||
ResidentialAddressRepository $residentialAddressRepository,
|
|
||||||
PhoneNumberHelperInterface $phoneNumberHelper,
|
|
||||||
NormalizerInterface $normalizer,
|
|
||||||
): PersonJsonNormalizer {
|
|
||||||
$personJsonNormalizer = new PersonJsonNormalizer(
|
|
||||||
$render,
|
|
||||||
$repository,
|
|
||||||
$centerResolverManager,
|
|
||||||
$residentialAddressRepository,
|
|
||||||
$phoneNumberHelper
|
|
||||||
);
|
|
||||||
$personJsonNormalizer->setNormalizer($normalizer);
|
|
||||||
|
|
||||||
return $personJsonNormalizer;
|
$center1 = (new Center())->setName('c1');
|
||||||
|
$center2 = (new Center())->setName('c2');
|
||||||
|
|
||||||
|
|
||||||
|
$normalizer = $this->createNormalizer(
|
||||||
|
centers: [$center1, $center2],
|
||||||
|
currentResidentialAddresses: [['addr' => 1]],
|
||||||
|
);
|
||||||
|
|
||||||
|
$data = $normalizer->normalize($person, 'json');
|
||||||
|
|
||||||
|
// Base keys
|
||||||
|
$baseKeys = [
|
||||||
|
'type', 'id', 'text', 'textAge', 'firstName', 'lastName', 'current_household_address', 'birthdate', 'deathdate', 'age', 'phonenumber', 'mobilenumber', 'email', 'gender', 'civility', 'personId',
|
||||||
|
];
|
||||||
|
foreach ($baseKeys as $key) {
|
||||||
|
self::assertArrayHasKey($key, $data, sprintf('Key %s should be present', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended keys
|
||||||
|
foreach (['centers', 'altNames', 'current_household_id', 'current_residential_addresses'] as $key) {
|
||||||
|
self::assertArrayHasKey($key, $data, sprintf('Key %s should be present', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame(['c1', 'c2'], $data['centers']);
|
||||||
|
self::assertIsArray($data['altNames']);
|
||||||
|
self::assertSame([['key' => 'aka', 'label' => 'Johnny']], $data['altNames']);
|
||||||
|
self::assertNull($data['current_household_id'], 'No household set so id should be null');
|
||||||
|
self::assertSame([['addr' => 1]], $data['current_residential_addresses']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createNormalizer(array $centers = [], array $currentResidentialAddresses = []): PersonJsonNormalizer
|
||||||
|
{
|
||||||
|
$render = $this->prophesize(ChillEntityRenderExtension::class);
|
||||||
|
$render->renderString(Argument::type(Person::class), ['addAge' => false])->willReturn('John Doe');
|
||||||
|
$render->renderString(Argument::type(Person::class), ['addAge' => true])->willReturn('John Doe (25)');
|
||||||
|
|
||||||
|
$centerResolver = $this->prophesize(CenterResolverManagerInterface::class);
|
||||||
|
$centerResolver->resolveCenters(Argument::type(Person::class))->willReturn($centers);
|
||||||
|
|
||||||
|
$raRepo = $this->prophesize(ResidentialAddressRepository::class);
|
||||||
|
$raRepo->findCurrentResidentialAddressByPerson(Argument::type(Person::class))->willReturn($currentResidentialAddresses);
|
||||||
|
|
||||||
|
$phoneHelper = $this->prophesize(PhoneNumberHelperInterface::class);
|
||||||
|
|
||||||
|
$personIdRendering = $this->prophesize(PersonIdRenderingInterface::class);
|
||||||
|
$personIdRendering->renderPersonId(Argument::type(Person::class))->willReturn('PERSON-ID-RENDER');
|
||||||
|
|
||||||
|
$normalizer = new PersonJsonNormalizer(
|
||||||
|
$render->reveal(),
|
||||||
|
$centerResolver->reveal(),
|
||||||
|
$raRepo->reveal(),
|
||||||
|
$phoneHelper->reveal(),
|
||||||
|
$personIdRendering->reveal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inner normalizer that echoes values or simple conversions
|
||||||
|
$inner = new class () implements NormalizerInterface {
|
||||||
|
public function supportsNormalization($data, $format = null): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalize($object, $format = null, array $context = [])
|
||||||
|
{
|
||||||
|
// For scalars and arrays, return as-is; for objects, return string or id when possible
|
||||||
|
if (\is_scalar($object) || null === $object) {
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
if ($object instanceof \DateTimeInterface) {
|
||||||
|
return $object->format('Y-m-d');
|
||||||
|
}
|
||||||
|
if ($object instanceof Center) {
|
||||||
|
return $object->getName();
|
||||||
|
}
|
||||||
|
if (is_array($object)) {
|
||||||
|
return array_map(fn ($o) => $this->normalize($o, $format, $context), $object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// default stub
|
||||||
|
return (string) (method_exists($object, 'getId') ? $object->getId() : 'normalized');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$normalizer->setNormalizer($inner);
|
||||||
|
|
||||||
|
return $normalizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSamplePerson(bool $withAltNames = false): Person
|
||||||
|
{
|
||||||
|
$p = new Person();
|
||||||
|
$p->setFirstName('John');
|
||||||
|
$p->setLastName('Doe');
|
||||||
|
$p->setBirthdate(new \DateTime('2000-01-01'));
|
||||||
|
$p->setEmail('john@example.test');
|
||||||
|
|
||||||
|
if ($withAltNames) {
|
||||||
|
$alt = new PersonAltName();
|
||||||
|
$alt->setKey('aka');
|
||||||
|
$alt->setLabel('Johnny');
|
||||||
|
$p->setAltNames(new ArrayCollection([$alt]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,86 @@
|
|||||||
|
<?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\PersonBundle\Tests\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Chill\PersonBundle\Repository\PersonRepository;
|
||||||
|
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonReadDenormalizer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonReadDenormalizer
|
||||||
|
*/
|
||||||
|
final class PersonJsonReadDenormalizerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSupportsDenormalizationReturnsTrueForValidData(): void
|
||||||
|
{
|
||||||
|
$repository = $this->getMockBuilder(PersonRepository::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$denormalizer = new PersonJsonReadDenormalizer($repository);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'type' => 'person',
|
||||||
|
'id' => 123,
|
||||||
|
];
|
||||||
|
|
||||||
|
self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
|
||||||
|
{
|
||||||
|
$repository = $this->getMockBuilder(PersonRepository::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$denormalizer = new PersonJsonReadDenormalizer($repository);
|
||||||
|
|
||||||
|
// not an array
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));
|
||||||
|
|
||||||
|
// missing type
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['id' => 1], Person::class));
|
||||||
|
|
||||||
|
// wrong type value
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person', 'id' => 1], Person::class));
|
||||||
|
|
||||||
|
// missing id
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], Person::class));
|
||||||
|
|
||||||
|
// wrong target class
|
||||||
|
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 1], \stdClass::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDenormalizeReturnsPersonFromRepository(): void
|
||||||
|
{
|
||||||
|
$person = new Person();
|
||||||
|
|
||||||
|
$repository = $this->getMockBuilder(PersonRepository::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->onlyMethods(['find'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$repository->expects(self::once())
|
||||||
|
->method('find')
|
||||||
|
->with(123)
|
||||||
|
->willReturn($person);
|
||||||
|
|
||||||
|
$denormalizer = new PersonJsonReadDenormalizer($repository);
|
||||||
|
|
||||||
|
$result = $denormalizer->denormalize(['id' => 123], Person::class);
|
||||||
|
|
||||||
|
self::assertSame($person, $result);
|
||||||
|
}
|
||||||
|
}
|
@@ -1993,3 +1993,16 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
|
||||||
|
/1.0/person/identifiers/workers:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- person
|
||||||
|
summary: List the person identifiers
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: "OK"
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
@@ -108,3 +108,9 @@ services:
|
|||||||
|
|
||||||
Chill\PersonBundle\PersonIdentifier\Rendering\:
|
Chill\PersonBundle\PersonIdentifier\Rendering\:
|
||||||
resource: '../PersonIdentifier/Rendering'
|
resource: '../PersonIdentifier/Rendering'
|
||||||
|
|
||||||
|
Chill\PersonBundle\PersonIdentifier\Normalizer\:
|
||||||
|
resource: '../PersonIdentifier/Normalizer'
|
||||||
|
|
||||||
|
Chill\PersonBundle\PersonIdentifier\Validator\:
|
||||||
|
resource: '../PersonIdentifier/Validator'
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
services:
|
|
||||||
# note: normalizers are loaded from ../services.yaml
|
|
||||||
|
|
||||||
Chill\PersonBundle\Serializer\Normalizer\:
|
|
||||||
autowire: true
|
|
||||||
autoconfigure: true
|
|
||||||
resource: '../../Serializer/Normalizer'
|
|
||||||
tags:
|
|
||||||
- { name: 'serializer.normalizer', priority: 64 }
|
|
||||||
|
|
@@ -0,0 +1,37 @@
|
|||||||
|
<?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\Migrations\Person;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250918095044 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add more details about presence in PersonIdentifier';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier_definition ADD presence VARCHAR(255) DEFAULT \'ON_EDIT\' NOT NULL');
|
||||||
|
$this->addSql('UPDATE chill_person_identifier_definition SET presence = \'NOT_EDITABLE\' WHERE is_editable_by_users IS FALSE');
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier_definition DROP is_editable_by_users');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier_definition ADD is_editable_by_users BOOLEAN DEFAULT false NOT NULL');
|
||||||
|
$this->addSql('UPDATE chill_person_identifier_definition SET is_editable_by_users = true WHERE presence <> \'NOT_EDITABLE\' ');
|
||||||
|
$this->addSql('ALTER TABLE chill_person_identifier_definition DROP presence');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
<?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\Migrations\Person;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250922151020 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add unique constraint for person identifiers';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX chill_person_identifier_unique ON chill_person_identifier (definition_id, canonical)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP INDEX chill_person_identifier_unique');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,53 @@
|
|||||||
|
<?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\Migrations\Person;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restrict the deletion of identifier_definition to avoid risk of error.
|
||||||
|
*
|
||||||
|
* An identifier definition can only be removed if there aren't any identifier defined.
|
||||||
|
*/
|
||||||
|
final class Version20250924101621 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Restrict the deletion of identifier_definition';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE chill_person_identifier DROP CONSTRAINT fk_bca5a36bd11ea911
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE chill_person_identifier ADD CONSTRAINT fk_bca5a36bd11ea911
|
||||||
|
FOREIGN KEY (definition_id) REFERENCES chill_person_identifier_definition (id)
|
||||||
|
on delete restrict
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE chill_person_identifier DROP CONSTRAINT fk_bca5a36bd11ea911
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE chill_person_identifier ADD CONSTRAINT fk_bca5a36bd11ea911
|
||||||
|
FOREIGN KEY (definition_id) REFERENCES chill_person_identifier_definition (id)
|
||||||
|
on delete cascade
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
@@ -265,3 +265,38 @@ add_persons:
|
|||||||
title: "Centre"
|
title: "Centre"
|
||||||
|
|
||||||
error_only_one_person: "Une seule personne peut être sélectionnée !"
|
error_only_one_person: "Une seule personne peut être sélectionnée !"
|
||||||
|
|
||||||
|
renderbox:
|
||||||
|
person: "Usager"
|
||||||
|
birthday_statement: >-
|
||||||
|
{gender, select,
|
||||||
|
man {Né le {birthdate, date}}
|
||||||
|
woman {Née le {birthdate, date}}
|
||||||
|
other {Né·e le {birthdate, date}}
|
||||||
|
}
|
||||||
|
deathdate_statement: >-
|
||||||
|
{gender, select,
|
||||||
|
man {Décédé le {deathdate, date}}
|
||||||
|
woman {Décédée le {deathdate, date}}
|
||||||
|
other {Décédé·e le {deathdate, date}}
|
||||||
|
}
|
||||||
|
household_without_address: "Le ménage de l'usager est sans adresse"
|
||||||
|
no_data: "Aucune information renseignée"
|
||||||
|
type:
|
||||||
|
thirdparty: "Tiers"
|
||||||
|
person: "Usager"
|
||||||
|
holder: "Titulaire"
|
||||||
|
years_old: >-
|
||||||
|
{n, plural,
|
||||||
|
=0 {0 an}
|
||||||
|
one {1 an}
|
||||||
|
other {# ans}
|
||||||
|
}
|
||||||
|
residential_address: "Adresse de résidence"
|
||||||
|
located_at: "réside chez"
|
||||||
|
household_number: "Ménage n°{number}"
|
||||||
|
current_members: "Membres actuels"
|
||||||
|
no_current_address: "Sans adresse actuellement"
|
||||||
|
new_household: "Nouveau ménage"
|
||||||
|
no_members_yet: "Aucun membre actuellement"
|
||||||
|
|
||||||
|
@@ -105,6 +105,8 @@ Administrative status: Situation administrative
|
|||||||
person:
|
person:
|
||||||
Identifiers: Identifiants
|
Identifiers: Identifiants
|
||||||
|
|
||||||
|
person_edit:
|
||||||
|
Error while saving: Erreur lors de l'enregistrement
|
||||||
|
|
||||||
# dédoublonnage
|
# dédoublonnage
|
||||||
Old person: Doublon
|
Old person: Doublon
|
||||||
@@ -1547,7 +1549,7 @@ person_messages:
|
|||||||
center_id: "Identifiant du centre"
|
center_id: "Identifiant du centre"
|
||||||
center_type: "Type de centre"
|
center_type: "Type de centre"
|
||||||
center_name: "Territoire"
|
center_name: "Territoire"
|
||||||
phonenumber: "Téléphone"
|
phonenumber: "Téléphone fixe"
|
||||||
mobilenumber: "Mobile"
|
mobilenumber: "Mobile"
|
||||||
altnames: "Autres noms"
|
altnames: "Autres noms"
|
||||||
email: "Courriel"
|
email: "Courriel"
|
||||||
|
@@ -73,5 +73,9 @@ relationship:
|
|||||||
person_creation:
|
person_creation:
|
||||||
If you want to create an household, an address is required: Pour la création d'un ménage, une adresse est requise
|
If you want to create an household, an address is required: Pour la création d'un ménage, une adresse est requise
|
||||||
|
|
||||||
|
person_identifier:
|
||||||
|
This identifier must be set: Cet identifiant doit être présent.
|
||||||
|
Identifier must be unique. The same identifier already exists for {{ persons }}: Identifiant déjà utilisé pour {{ persons }}
|
||||||
|
|
||||||
accompanying_course_work:
|
accompanying_course_work:
|
||||||
The endDate should be greater or equal than the start date: La date de fin doit être égale ou supérieure à la date de début
|
The endDate should be greater or equal than the start date: La date de fin doit être égale ou supérieure à la date de début
|
||||||
|
@@ -17,7 +17,6 @@ use Chill\MainBundle\Entity\Address;
|
|||||||
use Chill\MainBundle\Entity\Center;
|
use Chill\MainBundle\Entity\Center;
|
||||||
use Chill\MainBundle\Entity\Civility;
|
use Chill\MainBundle\Entity\Civility;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\Collections\ReadableCollection;
|
use Doctrine\Common\Collections\ReadableCollection;
|
||||||
@@ -206,12 +205,12 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
|
|||||||
|
|
||||||
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
|
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
|
||||||
#[ORM\Column(name: 'telephone', type: 'phone_number', nullable: true)]
|
#[ORM\Column(name: 'telephone', type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $telephone = null;
|
private ?PhoneNumber $telephone = null;
|
||||||
|
|
||||||
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
|
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
|
||||||
#[ORM\Column(name: 'telephone2', type: 'phone_number', nullable: true)]
|
#[ORM\Column(name: 'telephone2', type: 'phone_number', nullable: true)]
|
||||||
#[PhonenumberConstraint(type: 'any')]
|
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||||
private ?PhoneNumber $telephone2 = null;
|
private ?PhoneNumber $telephone2 = null;
|
||||||
|
|
||||||
#[ORM\Column(name: 'types', type: \Doctrine\DBAL\Types\Types::JSON, nullable: true)]
|
#[ORM\Column(name: 'types', type: \Doctrine\DBAL\Types\Types::JSON, nullable: true)]
|
||||||
|
Reference in New Issue
Block a user