mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-01 01:38:23 +00:00
Compare commits
85 Commits
create-adm
...
ticket/64-
| Author | SHA1 | Date | |
|---|---|---|---|
|
3f6bbbc1b3
|
|||
|
31e57d7507
|
|||
|
5c098a336d
|
|||
|
e107d20bea
|
|||
|
491fd81f9b
|
|||
|
8c2acbd166
|
|||
|
e291c7abec
|
|||
|
1e186fab58
|
|||
|
83a2c04537
|
|||
|
e89b33bc1a
|
|||
|
6b208e9962
|
|||
|
b0c63fab91
|
|||
|
6d4c4d2c74
|
|||
|
df6087d468
|
|||
|
ffac143ab9
|
|||
|
f1bf6023ff
|
|||
|
71e146e4f0
|
|||
|
4234377b7e
|
|||
|
1be82b3049
|
|||
|
808954df42
|
|||
|
3f8bb6c5c0
|
|||
|
23e1a0d36a
|
|||
|
a594d86346
|
|||
|
870907804b
|
|||
|
e9e6c05e3d
|
|||
|
532f2dd842
|
|||
|
d14d4d4d8f
|
|||
| a22cbe0239 | |||
| 98902bdeb8 | |||
|
592a0f3698
|
|||
|
d469eb19ad
|
|||
| 4765d4fe28 | |||
|
|
30bcb85549 | ||
|
189a9337b4
|
|||
|
c030232a73
|
|||
|
d4f9726f90
|
|||
|
8740025dbd
|
|||
|
6d8ef035ea
|
|||
|
60eab628ee
|
|||
|
1fd559b722
|
|||
|
b526e802d7
|
|||
|
60937152c3
|
|||
|
e566f60a4a
|
|||
|
c06531cddb
|
|||
|
4a1da25fee
|
|||
|
02783e5391
|
|||
|
be3b9f0f56
|
|||
|
ee006f55d6
|
|||
|
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
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Admin interface for Motive entity
|
||||
time: 2025-10-07T15:59:45.597029709+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
||||
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Add an admin interface for Motive entity
|
||||
time: 2025-10-22T11:15:52.13937955+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: Add columns or tables
|
||||
@@ -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).
|
||||
|
||||
Tests must be run using the `symfony` command:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
vendor/bin/phpunit --filter methodName path/to/TestFile.php
|
||||
symfony composer exec phpunit -- --filter methodName path/to/TestFile.php
|
||||
```
|
||||
|
||||
#### Test Structure
|
||||
|
||||
@@ -66,6 +66,7 @@ framework:
|
||||
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
|
||||
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
|
||||
'Chill\MainBundle\Notification\Email\NotificationEmailMessages\ScheduleDailyNotificationDigestMessage': async
|
||||
'Chill\TicketBundle\Messenger\PostTicketUpdateMessage': async
|
||||
# end of routes added by chill-bundles recipes
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-tsc": "^3.1.1",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<location />
|
||||
<location />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Location from "ChillActivityAssets/vuejs/Activity/components/Location.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: {
|
||||
Location,
|
||||
},
|
||||
name: "App",
|
||||
components: {
|
||||
Location,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,6 @@ use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
|
||||
/**
|
||||
* Immersion.
|
||||
@@ -86,14 +85,14 @@ class Immersion implements \Stringable
|
||||
* @Assert\NotBlank()
|
||||
*/
|
||||
#[ORM\Column(name: 'tuteurPhoneNumber', type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint(type: 'any')]
|
||||
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||
private ?PhoneNumber $tuteurPhoneNumber = null;
|
||||
|
||||
#[ORM\Column(name: 'structureAccName', type: \Doctrine\DBAL\Types\Types::TEXT, nullable: true)]
|
||||
private ?string $structureAccName = null;
|
||||
|
||||
#[ORM\Column(name: 'structureAccPhonenumber', type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint(type: 'any')]
|
||||
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||
private ?PhoneNumber $structureAccPhonenumber = null;
|
||||
|
||||
#[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\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Repository\LocationRepository;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
|
||||
@@ -67,12 +67,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint(type: 'any')]
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
|
||||
private ?PhoneNumber $phonenumber1 = null;
|
||||
|
||||
#[Serializer\Groups(['read', 'write', 'docgen:read'])]
|
||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint(type: 'any')]
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::ANY])]
|
||||
private ?PhoneNumber $phonenumber2 = null;
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
|
||||
@@ -23,7 +23,6 @@ use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
|
||||
/**
|
||||
* User.
|
||||
@@ -116,7 +115,7 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
||||
* The user's mobile phone number.
|
||||
*/
|
||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint]
|
||||
#[\Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber]
|
||||
private ?PhoneNumber $phonenumber = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,8 @@ interface PhoneNumberHelperInterface
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
||||
*/
|
||||
public function isValidPhonenumberAny($phonenumber): bool
|
||||
{
|
||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
||||
if (false === $this->isConfigured) {
|
||||
return true;
|
||||
}
|
||||
$validation = $this->performTwilioLookup($phonenumber);
|
||||
@@ -142,7 +142,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
||||
*/
|
||||
public function isValidPhonenumberLandOrVoip($phonenumber): bool
|
||||
{
|
||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
||||
if (false === $this->isConfigured) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
||||
*/
|
||||
public function isValidPhonenumberMobile($phonenumber): bool
|
||||
{
|
||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
||||
if (false === $this->isConfigured) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
|
||||
|
||||
private function performTwilioLookup($phonenumber)
|
||||
{
|
||||
if (false === $this->isPhonenumberValidationConfigured()) {
|
||||
if (false === $this->isConfigured) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -158,3 +158,18 @@ export const intervalISOToDays = (str: string | null): number | null => {
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ $chill-theme-buttons: (
|
||||
&.btn-unlink,
|
||||
&.btn-action,
|
||||
&.btn-edit,
|
||||
&.btn-tpchild,
|
||||
&.btn-wopilink,
|
||||
&.btn-update {
|
||||
&, &:hover {
|
||||
@@ -82,7 +81,6 @@ $chill-theme-buttons: (
|
||||
&.btn-remove::before,
|
||||
&.btn-choose::before,
|
||||
&.btn-notify::before,
|
||||
&.btn-tpchild::before,
|
||||
&.btn-download::before,
|
||||
&.btn-search::before,
|
||||
&.btn-cancel::before {
|
||||
@@ -112,7 +110,6 @@ $chill-theme-buttons: (
|
||||
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
|
||||
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken
|
||||
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
|
||||
&.btn-tpchild::before { content: "\f007"; } // fa-user
|
||||
&.btn-download::before { content: "\f019"; } // fa-download
|
||||
&.btn-search::before { content: "\f002"; } // fa-search
|
||||
}
|
||||
|
||||
@@ -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 fetchOption = Record<string, boolean | string | number | null>;
|
||||
|
||||
export type Primitive = string | number | boolean | null;
|
||||
export type Params = Record<string, number | string>;
|
||||
|
||||
export interface Pagination {
|
||||
@@ -25,20 +31,115 @@ export interface TransportExceptionInterface {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ValidationExceptionInterface
|
||||
extends TransportExceptionInterface {
|
||||
name: "ValidationException";
|
||||
error: object;
|
||||
violations: string[];
|
||||
titles: string[];
|
||||
propertyPaths: string[];
|
||||
export class ValidationException<
|
||||
M extends Record<string, Record<string, string|number>> = Record<
|
||||
string,
|
||||
Record<string, string|number>
|
||||
>,
|
||||
>
|
||||
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: {
|
||||
title: string;
|
||||
propertyPath: string;
|
||||
}[];
|
||||
/**
|
||||
* 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;
|
||||
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 {
|
||||
@@ -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
|
||||
* and use of the @link{fetchResults} method.
|
||||
* What this does
|
||||
* - 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",
|
||||
url: string,
|
||||
body?: body | Input | null,
|
||||
@@ -90,7 +330,8 @@ export const makeFetch = <Input, Output>(
|
||||
if (typeof options !== "undefined") {
|
||||
opts = Object.assign(opts, options);
|
||||
}
|
||||
return fetch(url, opts).then((response) => {
|
||||
|
||||
return fetch(url, opts).then(async (response) => {
|
||||
if (response.status === 204) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -100,9 +341,20 @@ export const makeFetch = <Input, Output>(
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
return response.json().then((response) => {
|
||||
throw ValidationException(response);
|
||||
});
|
||||
// Unprocessable Entity -> payload de validation Symfony
|
||||
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) {
|
||||
@@ -167,12 +419,6 @@ function _fetchAction<T>(
|
||||
throw NotFoundException(response);
|
||||
}
|
||||
|
||||
if (response.status === 422) {
|
||||
return response.json().then((response) => {
|
||||
throw ValidationException(response);
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw AccessException(response);
|
||||
}
|
||||
@@ -231,24 +477,6 @@ export const fetchScopes = (): Promise<Scope[]> => {
|
||||
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
|
||||
const AccessException = (response: Response): 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,65 @@
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import { CreatableEntityType } from "ChillPersonAssets/types";
|
||||
import {ThirdpartyCompany} from "../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
export interface DateTime {
|
||||
datetime: 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 {
|
||||
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: TranslatableString;
|
||||
genderTranslation: GenderTranslation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight reference to a Gender, used in POST / PUT requests.
|
||||
*/
|
||||
export interface SetGender {
|
||||
type: "chill_main_gender";
|
||||
id: number;
|
||||
// TODO
|
||||
}
|
||||
|
||||
export interface Household {
|
||||
@@ -28,6 +79,18 @@ export interface Center {
|
||||
id: number;
|
||||
type: "center";
|
||||
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 {
|
||||
@@ -119,6 +182,13 @@ export interface Address {
|
||||
isNoAddress: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate an existing address in write operations.
|
||||
*/
|
||||
export interface SetAddress {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface AddressWithPoint extends Address {
|
||||
point: Point;
|
||||
}
|
||||
@@ -226,13 +296,63 @@ export interface TransportExceptionInterface {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ValidationExceptionInterface
|
||||
extends TransportExceptionInterface {
|
||||
type IndexedKey<Base extends string> = `${Base}[${number}]`;
|
||||
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";
|
||||
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[];
|
||||
/** Only titles */
|
||||
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 {
|
||||
@@ -300,3 +420,23 @@ export interface TabDefinition {
|
||||
icon: string | null;
|
||||
counter: () => number;
|
||||
}
|
||||
|
||||
export type CreateComponentConfigGeneral = {
|
||||
action: 'create';
|
||||
allowedTypes: CreatableEntityType[];
|
||||
query: string;
|
||||
parent: null;
|
||||
}
|
||||
|
||||
export type CreateComponentThirdPartyAddContact = {
|
||||
action: 'addContact';
|
||||
allowedTypes: readonly ['thirdparty'];
|
||||
query: string;
|
||||
parent: ThirdpartyCompany;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for the CreateModal and Create component
|
||||
*/
|
||||
export type CreateComponentConfig = CreateComponentConfigGeneral | CreateComponentThirdPartyAddContact;
|
||||
|
||||
|
||||
@@ -12,24 +12,22 @@
|
||||
ref="showAddress"
|
||||
/>
|
||||
|
||||
<!-- step 1 -->
|
||||
<teleport to="body" v-if="inModal">
|
||||
<modal
|
||||
v-if="flag.suggestPane"
|
||||
modal-dialog-class="modal-dialog-scrollable modal-xl"
|
||||
@close="resetPane"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="modal-title">
|
||||
{{ trans(getTextTitle) }}
|
||||
<span v-if="flag.loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||
<span class="sr-only">{{
|
||||
trans(ADDRESS_LOADING)
|
||||
}}</span>
|
||||
</span>
|
||||
</h2>
|
||||
</template>
|
||||
<!-- step 1 -->
|
||||
<teleport to="body" v-if="inModal">
|
||||
<modal
|
||||
v-if="flag.suggestPane"
|
||||
modal-dialog-class="modal-dialog-scrollable modal-xl"
|
||||
@close="resetPane"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="modal-title">
|
||||
{{ trans(getTextTitle) }}
|
||||
<span v-if="flag.loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
|
||||
</span>
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<suggest-pane
|
||||
@@ -90,9 +88,7 @@
|
||||
{{ trans(getTextTitle) }}
|
||||
<span v-if="flag.loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||
<span class="sr-only">{{
|
||||
trans(ADDRESS_LOADING)
|
||||
}}</span>
|
||||
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
|
||||
</span>
|
||||
</h2>
|
||||
</template>
|
||||
@@ -175,9 +171,7 @@
|
||||
{{ trans(getTextTitle) }}
|
||||
<span v-if="flag.loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw" />
|
||||
<span class="sr-only">{{
|
||||
trans(ADDRESS_LOADING)
|
||||
}}</span>
|
||||
<span class="sr-only">{{ trans(ADDRESS_LOADING) }}</span>
|
||||
</span>
|
||||
</h2>
|
||||
</template>
|
||||
@@ -248,14 +242,14 @@ import {
|
||||
} from "../api";
|
||||
import {
|
||||
CREATE_A_NEW_ADDRESS,
|
||||
ADDRESS_LOADING,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ADDRESS_LOADING,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
CANCEL,
|
||||
SAVE,
|
||||
PREVIOUS,
|
||||
NEXT,
|
||||
trans,
|
||||
SAVE,
|
||||
PREVIOUS,
|
||||
NEXT,
|
||||
trans,
|
||||
} from "translator";
|
||||
import ShowPane from "./ShowPane.vue";
|
||||
import SuggestPane from "./SuggestPane.vue";
|
||||
@@ -265,16 +259,17 @@ import DatePane from "./DatePane.vue";
|
||||
export default {
|
||||
name: "AddAddress",
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
CREATE_A_NEW_ADDRESS,
|
||||
ADDRESS_LOADING,
|
||||
CANCEL,
|
||||
SAVE,
|
||||
PREVIOUS,
|
||||
NEXT,
|
||||
};
|
||||
},props: ["context", "options", "addressChangedCallback"],
|
||||
return {
|
||||
trans,
|
||||
CREATE_A_NEW_ADDRESS,
|
||||
ADDRESS_LOADING,
|
||||
CANCEL,
|
||||
SAVE,
|
||||
PREVIOUS,
|
||||
NEXT,
|
||||
};
|
||||
},
|
||||
props: ["context", "options", "addressChangedCallback"],
|
||||
components: {
|
||||
Modal,
|
||||
ShowPane,
|
||||
@@ -394,9 +389,9 @@ export default {
|
||||
) {
|
||||
console.log("this.options.title", this.options.title);
|
||||
|
||||
return this.context.edit
|
||||
? ACTIVITY_EDIT_ADDRESS
|
||||
: ACTIVITY_CREATE_ADDRESS;
|
||||
return this.context.edit
|
||||
? ACTIVITY_EDIT_ADDRESS
|
||||
: ACTIVITY_CREATE_ADDRESS;
|
||||
}
|
||||
return this.context.edit
|
||||
? this.defaultz.title.edit
|
||||
|
||||
@@ -55,9 +55,7 @@
|
||||
:placeholder="trans(ADDRESS_BUILDING_NAME)"
|
||||
v-model="buildingName"
|
||||
/>
|
||||
<label for="buildingName">{{
|
||||
trans(ADDRESS_BUILDING_NAME)
|
||||
}}</label>
|
||||
<label for="buildingName">{{ trans(ADDRESS_BUILDING_NAME) }}</label>
|
||||
</div>
|
||||
<div class="form-floating my-1">
|
||||
<input
|
||||
@@ -79,9 +77,7 @@
|
||||
:placeholder="trans(ADDRESS_DISTRIBUTION)"
|
||||
v-model="distribution"
|
||||
/>
|
||||
<label for="distribution">{{
|
||||
trans(ADDRESS_DISTRIBUTION)
|
||||
}}</label>
|
||||
<label for="distribution">{{ trans(ADDRESS_DISTRIBUTION) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,35 +85,36 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_FLOOR,
|
||||
ADDRESS_CORRIDOR,
|
||||
ADDRESS_STEPS,
|
||||
ADDRESS_FLAT,
|
||||
ADDRESS_BUILDING_NAME,
|
||||
ADDRESS_DISTRIBUTION,
|
||||
ADDRESS_EXTRA,
|
||||
ADDRESS_FILL_AN_ADDRESS,
|
||||
trans,
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_FLOOR,
|
||||
ADDRESS_CORRIDOR,
|
||||
ADDRESS_STEPS,
|
||||
ADDRESS_FLAT,
|
||||
ADDRESS_BUILDING_NAME,
|
||||
ADDRESS_DISTRIBUTION,
|
||||
ADDRESS_EXTRA,
|
||||
ADDRESS_FILL_AN_ADDRESS,
|
||||
trans,
|
||||
} from "translator";
|
||||
export default {
|
||||
name: "AddressMore",
|
||||
setup() {
|
||||
return {
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_FLOOR,
|
||||
ADDRESS_CORRIDOR,
|
||||
ADDRESS_STEPS,
|
||||
ADDRESS_FLAT,
|
||||
ADDRESS_BUILDING_NAME,
|
||||
ADDRESS_DISTRIBUTION,
|
||||
ADDRESS_EXTRA,
|
||||
ADDRESS_FILL_AN_ADDRESS,
|
||||
trans,
|
||||
};
|
||||
},props: ["entity", "isNoAddress"],
|
||||
return {
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_FLOOR,
|
||||
ADDRESS_CORRIDOR,
|
||||
ADDRESS_STEPS,
|
||||
ADDRESS_FLAT,
|
||||
ADDRESS_BUILDING_NAME,
|
||||
ADDRESS_DISTRIBUTION,
|
||||
ADDRESS_EXTRA,
|
||||
ADDRESS_FILL_AN_ADDRESS,
|
||||
trans,
|
||||
};
|
||||
},
|
||||
props: ["entity", "isNoAddress"],
|
||||
computed: {
|
||||
floor: {
|
||||
set(value) {
|
||||
|
||||
@@ -57,9 +57,7 @@
|
||||
:placeholder="trans(ADDRESS_STREET_NUMBER)"
|
||||
v-model="streetNumber"
|
||||
/>
|
||||
<label for="streetNumber">{{
|
||||
trans(ADDRESS_STREET_NUMBER)
|
||||
}}</label>
|
||||
<label for="streetNumber">{{ trans(ADDRESS_STREET_NUMBER) }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,31 +70,32 @@ import {
|
||||
fetchReferenceAddresses,
|
||||
} from "../../api.js";
|
||||
import {
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_ADDRESS,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_SELECT_ADDRESS,
|
||||
ADDRESS_CREATE_ADDRESS,
|
||||
trans,
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_ADDRESS,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_SELECT_ADDRESS,
|
||||
ADDRESS_CREATE_ADDRESS,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "AddressSelection",
|
||||
components: { VueMultiselect },
|
||||
setup() {
|
||||
return {
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_ADDRESS,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_SELECT_ADDRESS,
|
||||
ADDRESS_CREATE_ADDRESS,
|
||||
trans,
|
||||
};
|
||||
},props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
|
||||
return {
|
||||
ADDRESS_STREET,
|
||||
ADDRESS_STREET_NUMBER,
|
||||
ADDRESS_ADDRESS,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_SELECT_ADDRESS,
|
||||
ADDRESS_CREATE_ADDRESS,
|
||||
trans,
|
||||
};
|
||||
},
|
||||
props: ["entity", "context", "updateMapCenter", "flag", "checkErrors"],
|
||||
data() {
|
||||
return {
|
||||
value: this.context.edit ? this.entity.address.addressReference : null,
|
||||
|
||||
@@ -61,31 +61,32 @@
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { searchCities, fetchCities } from "../../api.js";
|
||||
import {
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_POSTAL_CODE_CODE,
|
||||
ADDRESS_POSTAL_CODE_NAME,
|
||||
ADDRESS_CREATE_POSTAL_CODE,
|
||||
ADDRESS_CITY,
|
||||
ADDRESS_SELECT_CITY,
|
||||
trans,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_POSTAL_CODE_CODE,
|
||||
ADDRESS_POSTAL_CODE_NAME,
|
||||
ADDRESS_CREATE_POSTAL_CODE,
|
||||
ADDRESS_CITY,
|
||||
ADDRESS_SELECT_CITY,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "CitySelection",
|
||||
components: { VueMultiselect },
|
||||
setup() {
|
||||
return {
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_CITY,
|
||||
ADDRESS_SELECT_CITY,
|
||||
ADDRESS_POSTAL_CODE_CODE,
|
||||
ADDRESS_POSTAL_CODE_NAME,
|
||||
ADDRESS_CREATE_POSTAL_CODE,
|
||||
trans,
|
||||
};
|
||||
},props: [
|
||||
return {
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
ADDRESS_CITY,
|
||||
ADDRESS_SELECT_CITY,
|
||||
ADDRESS_POSTAL_CODE_CODE,
|
||||
ADDRESS_POSTAL_CODE_NAME,
|
||||
ADDRESS_CREATE_POSTAL_CODE,
|
||||
trans,
|
||||
};
|
||||
},
|
||||
props: [
|
||||
"entity",
|
||||
"context",
|
||||
"focusOnAddress",
|
||||
|
||||
@@ -24,27 +24,28 @@
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
import {
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
ADDRESS_COUNTRY,
|
||||
ADDRESS_SELECT_COUNTRY,
|
||||
trans,
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
ADDRESS_COUNTRY,
|
||||
ADDRESS_SELECT_COUNTRY,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "CountrySelection",
|
||||
components: { VueMultiselect },
|
||||
setup() {
|
||||
return {
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
ADDRESS_COUNTRY,
|
||||
ADDRESS_SELECT_COUNTRY,
|
||||
trans,
|
||||
};
|
||||
},props: ["context", "entity", "flag", "checkErrors"],
|
||||
return {
|
||||
MULTISELECT_SELECTED_LABEL,
|
||||
MULTISELECT_SELECT_LABEL,
|
||||
MULTISELECT_DESELECT_LABEL,
|
||||
ADDRESS_COUNTRY,
|
||||
ADDRESS_SELECT_COUNTRY,
|
||||
trans,
|
||||
};
|
||||
},
|
||||
props: ["context", "entity", "flag", "checkErrors"],
|
||||
emits: ["getCities"],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -106,10 +106,10 @@ import AddressMap from "./AddAddress/AddressMap";
|
||||
import AddressMore from "./AddAddress/AddressMore";
|
||||
import ActionButtons from "./ActionButtons.vue";
|
||||
import {
|
||||
ADDRESS_SELECT_AN_ADDRESS_TITLE,
|
||||
ADDRESS_IS_CONFIDENTIAL,
|
||||
ADDRESS_IS_NO_ADDRESS,
|
||||
trans,
|
||||
ADDRESS_SELECT_AN_ADDRESS_TITLE,
|
||||
ADDRESS_IS_CONFIDENTIAL,
|
||||
ADDRESS_IS_NO_ADDRESS,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
@@ -123,13 +123,14 @@ export default {
|
||||
ActionButtons,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
ADDRESS_SELECT_AN_ADDRESS_TITLE,
|
||||
ADDRESS_IS_CONFIDENTIAL,
|
||||
ADDRESS_IS_NO_ADDRESS,
|
||||
};
|
||||
},props: [
|
||||
return {
|
||||
trans,
|
||||
ADDRESS_SELECT_AN_ADDRESS_TITLE,
|
||||
ADDRESS_IS_CONFIDENTIAL,
|
||||
ADDRESS_IS_NO_ADDRESS,
|
||||
};
|
||||
},
|
||||
props: [
|
||||
"context",
|
||||
"options",
|
||||
"defaultz",
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
|
||||
<div v-if="flag.success" class="alert alert-success">
|
||||
{{ trans(getSuccessText) }}
|
||||
<span v-if="forceRedirect">{{
|
||||
trans(ADDRESS_WAIT_REDIRECTION)
|
||||
}}</span>
|
||||
<span v-if="forceRedirect">{{ trans(ADDRESS_WAIT_REDIRECTION) }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -101,34 +99,36 @@
|
||||
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
|
||||
import ActionButtons from "./ActionButtons.vue";
|
||||
import {
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ADDRESS_NOT_YET_ADDRESS,
|
||||
ADDRESS_WAIT_REDIRECTION,
|
||||
ADDRESS_LOADING,
|
||||
ADDRESS_ADDRESS_EDIT_SUCCESS,
|
||||
ADDRESS_ADDRESS_NEW_SUCCESS,
|
||||
trans,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ADDRESS_NOT_YET_ADDRESS,
|
||||
ADDRESS_WAIT_REDIRECTION,
|
||||
ADDRESS_LOADING,
|
||||
ADDRESS_ADDRESS_EDIT_SUCCESS,
|
||||
ADDRESS_ADDRESS_NEW_SUCCESS,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "ShowPane",
|
||||
methods: {},components: {
|
||||
methods: {},
|
||||
components: {
|
||||
AddressRenderBox,
|
||||
ActionButtons,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ADDRESS_NOT_YET_ADDRESS,
|
||||
ADDRESS_WAIT_REDIRECTION,
|
||||
ADDRESS_LOADING,
|
||||
ADDRESS_ADDRESS_NEW_SUCCESS,
|
||||
ADDRESS_ADDRESS_EDIT_SUCCESS,
|
||||
};
|
||||
},props: [
|
||||
return {
|
||||
trans,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ADDRESS_NOT_YET_ADDRESS,
|
||||
ADDRESS_WAIT_REDIRECTION,
|
||||
ADDRESS_LOADING,
|
||||
ADDRESS_ADDRESS_NEW_SUCCESS,
|
||||
ADDRESS_ADDRESS_EDIT_SUCCESS,
|
||||
};
|
||||
},
|
||||
props: [
|
||||
"context",
|
||||
"defaultz",
|
||||
"options",
|
||||
@@ -169,17 +169,19 @@ export default {
|
||||
this.options.button.text.create !== null)
|
||||
) {
|
||||
// console.log('this.options.button.text', this.options.button.text)
|
||||
return this.context.edit
|
||||
? ACTIVITY_CREATE_ADDRESS
|
||||
: ACTIVITY_EDIT_ADDRESS;
|
||||
}
|
||||
console.log("defaultz", this.defaultz);
|
||||
return this.context.edit
|
||||
? ACTIVITY_CREATE_ADDRESS
|
||||
: ACTIVITY_EDIT_ADDRESS;
|
||||
}
|
||||
console.log("defaultz", this.defaultz);
|
||||
return this.context.edit
|
||||
? this.defaultz.button.text.edit
|
||||
: this.defaultz.button.text.create;
|
||||
},
|
||||
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() {
|
||||
return typeof this.options.onlyButton !== "undefined"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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') }">
|
||||
<label for="person">
|
||||
<input
|
||||
@@ -14,7 +14,7 @@
|
||||
</label>
|
||||
</a>
|
||||
</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') }">
|
||||
<label for="thirdparty">
|
||||
<input
|
||||
@@ -31,49 +31,65 @@
|
||||
</ul>
|
||||
|
||||
<div class="my-4">
|
||||
<on-the-fly-person
|
||||
<PersonEdit
|
||||
v-if="type === 'person'"
|
||||
:action="action"
|
||||
action="create"
|
||||
:query="query"
|
||||
ref="castPerson"
|
||||
@onPersonCreated="onPersonCreated"
|
||||
/>
|
||||
|
||||
<on-the-fly-thirdparty
|
||||
<ThirdPartyEdit
|
||||
v-if="type === 'thirdparty'"
|
||||
:action="action"
|
||||
:query="query"
|
||||
:parent="parent"
|
||||
ref="castThirdparty"
|
||||
@onThirdPartyCreated="onThirdPartyCreated"
|
||||
type=""
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
|
||||
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import {
|
||||
trans,
|
||||
ONTHEFLY_CREATE_PERSON,
|
||||
ONTHEFLY_CREATE_THIRDPARTY,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { CreatableEntityType, Person } from "ChillPersonAssets/types";
|
||||
import { CreateComponentConfig } from "ChillMainAssets/types";
|
||||
import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue";
|
||||
import ThirdPartyEdit from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdPartyEdit.vue";
|
||||
import {Thirdparty} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
const props = defineProps({
|
||||
action: String,
|
||||
allowedTypes: Array,
|
||||
query: String,
|
||||
const props = withDefaults(defineProps<CreateComponentConfig>(), {
|
||||
action: "create",
|
||||
query: "",
|
||||
parent: null,
|
||||
});
|
||||
|
||||
const type = ref(null);
|
||||
const emit =
|
||||
defineEmits<{
|
||||
(e: "onPersonCreated", payload: { person: Person }): void,
|
||||
(e: "onThirdPartyCreated", payload: { thirdParty: Thirdparty }): void,
|
||||
}>();
|
||||
|
||||
const radioType = computed({
|
||||
const type = ref<CreatableEntityType | null>(null);
|
||||
|
||||
const radioType = computed<CreatableEntityType | null>({
|
||||
get: () => type.value,
|
||||
set: (val) => {
|
||||
set: (val: CreatableEntityType | null) => {
|
||||
type.value = val;
|
||||
console.log("## type:", val, ", action:", props.action);
|
||||
},
|
||||
});
|
||||
|
||||
const castPerson = ref(null);
|
||||
const castThirdparty = ref(null);
|
||||
type PersonEditComponent = InstanceType<typeof PersonEdit>;
|
||||
type ThirdPartyEditComponent = InstanceType<typeof ThirdPartyEdit>;
|
||||
|
||||
const castPerson = ref<PersonEditComponent|null>(null);
|
||||
const castThirdparty = ref<ThirdPartyEditComponent|null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
type.value =
|
||||
@@ -82,30 +98,37 @@ onMounted(() => {
|
||||
: "person";
|
||||
});
|
||||
|
||||
function isActive(tab) {
|
||||
function isActive(tab: CreatableEntityType) {
|
||||
return type.value === tab;
|
||||
}
|
||||
|
||||
function castDataByType() {
|
||||
switch (radioType.value) {
|
||||
case "person":
|
||||
return castPerson.value.$data.person;
|
||||
case "thirdparty":
|
||||
let data = castThirdparty.value.$data.thirdparty;
|
||||
if (data.address !== undefined && data.address !== null) {
|
||||
data.address = { id: data.address.address_id };
|
||||
} else {
|
||||
data.address = null;
|
||||
}
|
||||
return data;
|
||||
default:
|
||||
throw Error("Invalid type of entity");
|
||||
const containsThirdParty = computed<boolean>(() =>
|
||||
props.allowedTypes.includes("thirdparty"),
|
||||
);
|
||||
const containsPerson = computed<boolean>(() => {
|
||||
if (props.action === 'addContact') {
|
||||
return false;
|
||||
}
|
||||
return props.allowedTypes.includes("person");
|
||||
});
|
||||
|
||||
function save(): void {
|
||||
if (radioType.value === "person" && castPerson.value !== null) {
|
||||
castPerson.value.postPerson();
|
||||
} else if (radioType.value === "thirdparty" && castThirdparty.value !== null) {
|
||||
castThirdparty.value.postThirdParty();
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
castDataByType,
|
||||
});
|
||||
function onThirdPartyCreated(payload: { thirdParty: Thirdparty }) {
|
||||
emit("onThirdPartyCreated", payload);
|
||||
}
|
||||
|
||||
function onPersonCreated(payload: { person: Person }) {
|
||||
emit("onPersonCreated", payload);
|
||||
}
|
||||
|
||||
defineExpose({ save });
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<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";
|
||||
import {Thirdparty} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onPersonCreated", payload: { person: Person }): void;
|
||||
(e: "onThirdPartyCreated", payload: { thirdParty: Thirdparty }): void;
|
||||
(e: "close"): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<CreateComponentConfig & {modalTitle: string}>();
|
||||
const modalDialogClass = { "modal-xl": true, "modal-scrollable": true };
|
||||
|
||||
type CreateComponentType = InstanceType<typeof Create>;
|
||||
|
||||
const create = useTemplateRef<CreateComponentType>("create");
|
||||
|
||||
const onPersonCreated = ({person}: {person: Person}): void => {
|
||||
emit("onPersonCreated", {person});
|
||||
};
|
||||
|
||||
const onThirdPartyCreated = ({thirdParty}: {thirdParty: Thirdparty}): void => {
|
||||
emit("onThirdPartyCreated", {thirdParty: thirdParty});
|
||||
}
|
||||
|
||||
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"
|
||||
:parent="props.parent"
|
||||
@onPersonCreated="onPersonCreated"
|
||||
@onThirdPartyCreated="onThirdPartyCreated"
|
||||
></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>
|
||||
@@ -9,7 +9,7 @@
|
||||
class="btn btn-sm"
|
||||
target="_blank"
|
||||
:class="classAction"
|
||||
:title="trans(titleAction)"
|
||||
:title="titleAction"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ buttonText }}<span v-if="isDead"> (‡)</span>
|
||||
@@ -23,14 +23,14 @@
|
||||
>
|
||||
<template #header>
|
||||
<h3 v-if="parent" class="modal-title">
|
||||
{{ trans(titleModal, { q: parent.text }) }}
|
||||
{{ titleModal }}
|
||||
</h3>
|
||||
<h3 v-else class="modal-title">
|
||||
{{ trans(titleModal) }}
|
||||
{{ titleModal }}
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<template #body v-if="type === 'person'">
|
||||
<template #body v-if="type === 'person' && action === 'show'">
|
||||
<on-the-fly-person
|
||||
:id="id"
|
||||
:type="type"
|
||||
@@ -40,12 +40,21 @@
|
||||
<div v-if="hasResourceComment">
|
||||
<h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3>
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ parent.comment }}
|
||||
{{ parent?.comment }}
|
||||
</blockquote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body v-else-if="type === 'thirdparty'">
|
||||
<template #body v-else-if="type === 'person' && action === 'edit'">
|
||||
<PersonEdit
|
||||
:id="id"
|
||||
:action="'edit'"
|
||||
:query="''"
|
||||
ref="castEditPerson"
|
||||
></PersonEdit>
|
||||
</template>
|
||||
|
||||
<template #body v-else-if="type === 'thirdparty' && action === 'show'">
|
||||
<on-the-fly-thirdparty
|
||||
:id="id"
|
||||
:type="type"
|
||||
@@ -55,11 +64,15 @@
|
||||
<div v-if="hasResourceComment">
|
||||
<h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3>
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ parent.comment }}
|
||||
{{ parent?.comment }}
|
||||
</blockquote>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #body v-else-if="type === 'thirdparty' && action === 'edit'">
|
||||
<ThirdPartyEdit ref="castEditThirdParty" action="edit" :id="id"></ThirdPartyEdit>
|
||||
</template>
|
||||
|
||||
<template #body v-else-if="parent">
|
||||
<on-the-fly-thirdparty
|
||||
:parent="parent"
|
||||
@@ -73,7 +86,7 @@
|
||||
<on-the-fly-create
|
||||
:action="action"
|
||||
:allowed-types="allowedTypes"
|
||||
:query="query"
|
||||
:query="query || ''"
|
||||
ref="castNew"
|
||||
/>
|
||||
</template>
|
||||
@@ -82,9 +95,9 @@
|
||||
<a
|
||||
v-if="action === 'show'"
|
||||
:href="buildLocation(id, type)"
|
||||
:title="trans(titleMessage)"
|
||||
:title="titleMessage"
|
||||
class="btn btn-show"
|
||||
>{{ trans(buttonMessage) }}
|
||||
>{{ buttonMessage }}
|
||||
</a>
|
||||
<a v-else class="btn btn-save" @click="saveAction">
|
||||
{{ trans(ACTION_SAVE) }}
|
||||
@@ -93,8 +106,8 @@
|
||||
</modal>
|
||||
</teleport>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, defineEmits, defineProps } from "vue";
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, defineEmits, defineProps, useTemplateRef} from "vue";
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import OnTheFlyCreate from "./Create.vue";
|
||||
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
|
||||
@@ -104,7 +117,7 @@ import {
|
||||
ACTION_SHOW,
|
||||
ACTION_EDIT,
|
||||
ACTION_CREATE,
|
||||
ACTION_ADDCONTACT,
|
||||
ACTION_SAVE,
|
||||
ONTHEFLY_CREATE_TITLE_DEFAULT,
|
||||
ONTHEFLY_CREATE_TITLE_PERSON,
|
||||
ONTHEFLY_CREATE_TITLE_THIRDPARTY,
|
||||
@@ -112,40 +125,66 @@ import {
|
||||
ONTHEFLY_SHOW_THIRDPARTY,
|
||||
ONTHEFLY_EDIT_PERSON,
|
||||
ONTHEFLY_EDIT_THIRDPARTY,
|
||||
ONTHEFLY_ADDCONTACT_TITLE,
|
||||
ACTION_REDIRECT_PERSON,
|
||||
ACTION_REDIRECT_THIRDPARTY,
|
||||
ONTHEFLY_SHOW_FILE_PERSON,
|
||||
ONTHEFLY_SHOW_FILE_THIRDPARTY,
|
||||
ONTHEFLY_SHOW_FILE_DEFAULT,
|
||||
ONTHEFLY_RESOURCE_COMMENT_TITLE,
|
||||
ACTION_SAVE,
|
||||
THIRDPARTY_ADDCONTACT,
|
||||
THIRDPARTY_ADDCONTACT_TITLE,
|
||||
} from "translator";
|
||||
import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue";
|
||||
import ThirdPartyEdit from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdPartyEdit.vue";
|
||||
import ThirdParty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
|
||||
|
||||
const props = defineProps({
|
||||
type: String,
|
||||
id: [String, Number],
|
||||
action: String,
|
||||
buttonText: String,
|
||||
displayBadge: Boolean,
|
||||
isDead: Boolean,
|
||||
parent: Object,
|
||||
allowedTypes: Array,
|
||||
query: String,
|
||||
// Types
|
||||
type EntityType = "person" | "thirdparty";
|
||||
type ActionType = "show" | "edit" | "create" | "addContact";
|
||||
|
||||
interface ParentRef {
|
||||
type: string;
|
||||
id: string | number;
|
||||
text?: string;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
interface OnTheFlyComponentProps {
|
||||
type: EntityType;
|
||||
id: number;
|
||||
action: ActionType;
|
||||
buttonText?: string | null;
|
||||
displayBadge?: boolean;
|
||||
isDead?: boolean;
|
||||
parent?: ParentRef | null;
|
||||
allowedTypes?: EntityType[];
|
||||
query?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<OnTheFlyComponentProps>(), {
|
||||
buttonText: null,
|
||||
displayBadge: false,
|
||||
isDead: false,
|
||||
parent: null,
|
||||
allowedTypes: () => ["person", "thirdparty"] as EntityType[],
|
||||
query: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits(["saveFormOnTheFly"]);
|
||||
const emit = defineEmits<{
|
||||
(e: "saveFormOnTheFly", payload: { type: string | undefined; data: any }): void;
|
||||
}>();
|
||||
|
||||
const modal = ref({
|
||||
type castEditPersonType = InstanceType<typeof PersonEdit>;
|
||||
type castEditThirdPartyType = InstanceType<typeof ThirdParty>;
|
||||
const castEditPerson = useTemplateRef<castEditPersonType>('castEditPerson')
|
||||
const castEditThirdParty = useTemplateRef<castEditThirdPartyType>('castEditThirdParty');
|
||||
|
||||
const modal = ref<{ showModal: boolean; modalDialogClass: string }>({
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
});
|
||||
|
||||
const castPerson = ref();
|
||||
const castThirdparty = ref();
|
||||
const castNew = ref();
|
||||
|
||||
const hasResourceComment = computed(() => {
|
||||
const hasResourceComment = computed<boolean>(() => {
|
||||
return (
|
||||
typeof props.parent !== "undefined" &&
|
||||
props.parent !== null &&
|
||||
@@ -156,7 +195,7 @@ const hasResourceComment = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const classAction = computed(() => {
|
||||
const classAction = computed<string>(() => {
|
||||
switch (props.action) {
|
||||
case "show":
|
||||
return "btn-show";
|
||||
@@ -171,174 +210,127 @@ const classAction = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const titleAction = computed(() => {
|
||||
const titleAction = computed<string>(() => {
|
||||
switch (props.action) {
|
||||
case "show":
|
||||
return ACTION_SHOW;
|
||||
return ACTION_SHOW as unknown as string;
|
||||
case "edit":
|
||||
return ACTION_EDIT;
|
||||
return ACTION_EDIT as unknown as string;
|
||||
case "create":
|
||||
return ACTION_CREATE;
|
||||
return ACTION_CREATE as unknown as string;
|
||||
case "addContact":
|
||||
return ACTION_ADDCONTACT;
|
||||
return THIRDPARTY_ADDCONTACT as unknown as string;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const titleCreate = computed(() => {
|
||||
const titleCreate = computed<string>(() => {
|
||||
if (typeof props.allowedTypes === "undefined") {
|
||||
return ONTHEFLY_CREATE_TITLE_DEFAULT;
|
||||
return trans(ONTHEFLY_CREATE_TITLE_DEFAULT)
|
||||
}
|
||||
return props.allowedTypes.every((t) => t === "person")
|
||||
? ONTHEFLY_CREATE_TITLE_PERSON
|
||||
: props.allowedTypes.every((t) => t === "thirdparty")
|
||||
? ONTHEFLY_CREATE_TITLE_THIRDPARTY
|
||||
: ONTHEFLY_CREATE_TITLE_DEFAULT;
|
||||
return props.allowedTypes.every((t: EntityType) => t === "person")
|
||||
? (trans(ONTHEFLY_CREATE_TITLE_PERSON))
|
||||
: props.allowedTypes.every((t: EntityType) => t === "thirdparty")
|
||||
? (trans(ONTHEFLY_CREATE_TITLE_THIRDPARTY))
|
||||
: (trans(ONTHEFLY_CREATE_TITLE_DEFAULT));
|
||||
});
|
||||
|
||||
const titleModal = computed(() => {
|
||||
const titleModal = computed<string>(() => {
|
||||
switch (props.action) {
|
||||
case "show":
|
||||
if (props.type == "person") {
|
||||
return ONTHEFLY_SHOW_PERSON;
|
||||
return trans(ONTHEFLY_SHOW_PERSON)
|
||||
} else if (props.type == "thirdparty") {
|
||||
return ONTHEFLY_SHOW_THIRDPARTY;
|
||||
return trans(ONTHEFLY_SHOW_THIRDPARTY)
|
||||
}
|
||||
break;
|
||||
case "edit":
|
||||
if (props.type == "person") {
|
||||
return ONTHEFLY_EDIT_PERSON;
|
||||
return trans(ONTHEFLY_EDIT_PERSON)
|
||||
} else if (props.type == "thirdparty") {
|
||||
return ONTHEFLY_EDIT_THIRDPARTY;
|
||||
return trans(ONTHEFLY_EDIT_THIRDPARTY)
|
||||
}
|
||||
break;
|
||||
case "create":
|
||||
return titleCreate.value;
|
||||
case "addContact":
|
||||
return ONTHEFLY_ADDCONTACT_TITLE;
|
||||
return trans(THIRDPARTY_ADDCONTACT_TITLE)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const titleMessage = computed(() => {
|
||||
const titleMessage = computed<string>(() => {
|
||||
switch (props.type) {
|
||||
case "person":
|
||||
return ACTION_REDIRECT_PERSON;
|
||||
return trans(ACTION_REDIRECT_PERSON);
|
||||
case "thirdparty":
|
||||
return ACTION_REDIRECT_THIRDPARTY;
|
||||
return trans(ACTION_REDIRECT_THIRDPARTY);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const buttonMessage = computed(() => {
|
||||
const buttonMessage = computed<string>(() => {
|
||||
switch (props.type) {
|
||||
case "person":
|
||||
return ONTHEFLY_SHOW_FILE_PERSON;
|
||||
return trans(ONTHEFLY_SHOW_FILE_PERSON);
|
||||
case "thirdparty":
|
||||
return ONTHEFLY_SHOW_FILE_THIRDPARTY;
|
||||
return trans(ONTHEFLY_SHOW_FILE_THIRDPARTY);
|
||||
default:
|
||||
return ONTHEFLY_SHOW_FILE_DEFAULT;
|
||||
return trans(ONTHEFLY_SHOW_FILE_DEFAULT);
|
||||
}
|
||||
});
|
||||
|
||||
const isDisplayBadge = computed(() => {
|
||||
return props.displayBadge === true && props.buttonText !== null;
|
||||
const isDisplayBadge = computed<boolean>(() => {
|
||||
return props.displayBadge && props.buttonText !== null;
|
||||
});
|
||||
|
||||
const badgeType = computed(() => {
|
||||
return "entity-" + props.type + " badge-" + props.type;
|
||||
const badgeType = computed<string>(() => {
|
||||
return "entity-" + (props.type ?? "") + " badge-" + (props.type ?? "");
|
||||
});
|
||||
|
||||
const getReturnPath = computed(() => {
|
||||
const getReturnPath = computed<string>(() => {
|
||||
return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
});
|
||||
|
||||
function closeModal() {
|
||||
function closeModal(): void {
|
||||
modal.value.showModal = false;
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
function openModal(): void {
|
||||
modal.value.showModal = true;
|
||||
}
|
||||
|
||||
function changeActionTo(action) {
|
||||
console.log(action);
|
||||
// Not reactive in setup, but you can emit or use a ref if needed
|
||||
}
|
||||
|
||||
function saveAction() {
|
||||
let type = props.type,
|
||||
data = {};
|
||||
switch (type) {
|
||||
case "person":
|
||||
data = castPerson.value?.$data.person;
|
||||
break;
|
||||
case "thirdparty":
|
||||
data = castThirdparty.value?.$data.thirdparty;
|
||||
break;
|
||||
default:
|
||||
if (typeof props.type === "undefined") {
|
||||
if (props.action === "addContact") {
|
||||
type = "thirdparty";
|
||||
data = castThirdparty.value?.$data.thirdparty;
|
||||
data.parent = {
|
||||
type: "thirdparty",
|
||||
id: props.parent.id,
|
||||
};
|
||||
data.civility =
|
||||
data.civility !== null
|
||||
? {
|
||||
type: "chill_main_civility",
|
||||
id: data.civility.id,
|
||||
}
|
||||
: null;
|
||||
data.profession = data.profession !== "" ? data.profession : "";
|
||||
} else {
|
||||
type = castNew.value.radioType;
|
||||
data = castNew.value.castDataByType();
|
||||
if (typeof data.civility !== "undefined" && null !== data.civility) {
|
||||
data.civility =
|
||||
data.civility !== null
|
||||
? {
|
||||
type: "chill_main_civility",
|
||||
id: data.civility.id,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
if (
|
||||
typeof data.profession !== "undefined" &&
|
||||
"" !== data.profession
|
||||
) {
|
||||
data.profession = data.profession !== "" ? data.profession : "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw "error with object type";
|
||||
}
|
||||
}
|
||||
emit("saveFormOnTheFly", { type: type, data: data });
|
||||
}
|
||||
|
||||
function buildLocation(id, type) {
|
||||
function buildLocation(id: string | number | undefined, type: EntityType | undefined): string | undefined {
|
||||
if (type === "person") {
|
||||
return encodeURI(`/fr/person/${id}/general${getReturnPath.value}`);
|
||||
} else if (type === "thirdparty") {
|
||||
return encodeURI(`/fr/3party/3party/${id}/view${getReturnPath.value}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function saveAction() {
|
||||
if (props.type === "person") {
|
||||
const person = await castEditPerson.value?.postPerson();
|
||||
if (null !== person) {
|
||||
emit("saveFormOnTheFly", {type: props.type, data: person})
|
||||
}
|
||||
} else if (props.type === 'thirdparty') {
|
||||
const thirdParty = await castEditThirdParty.value?.postThirdParty();
|
||||
if (null !== thirdParty) {
|
||||
emit("saveFormOnTheFly", {type: props.type, data: thirdParty })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
closeModal,
|
||||
changeActionTo,
|
||||
saveAction,
|
||||
castPerson,
|
||||
castThirdparty,
|
||||
castNew,
|
||||
hasResourceComment,
|
||||
modal,
|
||||
isDisplayBadge,
|
||||
|
||||
@@ -12,13 +12,20 @@
|
||||
>{{ trans(USER_CURRENT_USER) }}</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
v-else-if="!isEntityHousehold(p)"
|
||||
:class="getBadgeClass(p)"
|
||||
class="chill_denomination"
|
||||
:style="getBadgeStyle(p)"
|
||||
>
|
||||
{{ p.text }}
|
||||
</span>
|
||||
<span v-else
|
||||
:class="getBadgeClass(p)"
|
||||
class="chill_denomination"
|
||||
:style="getBadgeStyle(p)"
|
||||
>
|
||||
Ménage n°{{ p.id }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="record_actions mb-0">
|
||||
@@ -40,6 +47,7 @@
|
||||
:key="uniqid"
|
||||
:buttonTitle="translatedListOfTypes"
|
||||
:modalTitle="translatedListOfTypes"
|
||||
:allowCreate="true"
|
||||
@addNewPersons="addNewEntity"
|
||||
>
|
||||
</add-persons>
|
||||
@@ -53,9 +61,12 @@
|
||||
@click="addNewSuggested(s)"
|
||||
style="margin: 0"
|
||||
>
|
||||
<span :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
|
||||
<span v-if="!isEntityHousehold(s)" :class="getBadgeClass(s)" :style="getBadgeStyle(s)">
|
||||
{{ s.text }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Ménage n°{{ s.id }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -74,8 +85,9 @@ import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
|
||||
import {
|
||||
Entities,
|
||||
EntitiesOrMe,
|
||||
EntityType,
|
||||
EntityType, isEntityHousehold,
|
||||
SearchOptions,
|
||||
Suggestion,
|
||||
} from "ChillPersonAssets/types";
|
||||
import {
|
||||
PICK_ENTITY_MODAL_TITLE,
|
||||
@@ -182,7 +194,7 @@ function addNewSuggested(entity: EntitiesOrMe) {
|
||||
emits("addNewEntity", { entity });
|
||||
}
|
||||
|
||||
function addNewEntity({ selected }: addNewEntities) {
|
||||
function addNewEntity({ selected }: { selected: Suggestion[] }) {
|
||||
Object.values(selected).forEach((item) => {
|
||||
emits("addNewEntity", { entity: item.result });
|
||||
});
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<i :class="['fa', genderClass, 'px-1']" />
|
||||
<i :class="['bi', genderClass]"></i>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
const props = defineProps({
|
||||
gender: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
import type { Gender } from "ChillMainAssets/types";
|
||||
import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
|
||||
|
||||
const genderClass = computed(() => {
|
||||
switch (props.gender.genderTranslation) {
|
||||
case "woman":
|
||||
return "fa-venus";
|
||||
case "man":
|
||||
return "fa-mars";
|
||||
case "both":
|
||||
return "fa-neuter";
|
||||
interface GenderIconRenderBoxProps {
|
||||
gender: Gender;
|
||||
}
|
||||
|
||||
const props = defineProps<GenderIconRenderBoxProps>();
|
||||
|
||||
const genderClass = computed<string>(() => {
|
||||
switch (toGenderTranslation(props.gender)) {
|
||||
case "female":
|
||||
return "bi-gender-female";
|
||||
case "male":
|
||||
return "bi-gender-male";
|
||||
case "neutral":
|
||||
case "unknown":
|
||||
return "fa-genderless";
|
||||
default:
|
||||
return "fa-genderless";
|
||||
return "bi-gender-neuter";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -52,23 +52,15 @@ import { trans, MODAL_ACTION_CLOSE } from "translator";
|
||||
import { defineProps } from "vue";
|
||||
|
||||
export interface ModalProps {
|
||||
modalDialogClass: string;
|
||||
hideFooter: boolean;
|
||||
modalDialogClass?: string | Record<string, boolean>;
|
||||
hideFooter?: boolean;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
defineProps({
|
||||
modalDialogClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = withDefaults(defineProps<ModalProps>(), {
|
||||
modalDialogClass: "",
|
||||
hideFooter: false,
|
||||
show: true,
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {ref} from "vue";
|
||||
import {ValidationExceptionInterface} from "ChillMainAssets/types";
|
||||
|
||||
export function useViolationList<T extends Record<string, Record<string, string>>>() {
|
||||
type ViolationKey = Extract<keyof T, string>;
|
||||
const violationsList = ref<ValidationExceptionInterface<T>|null>(null);
|
||||
|
||||
function violationTitles<P extends ViolationKey>(property: P): string[] {
|
||||
if (null === violationsList.value) {
|
||||
return [];
|
||||
}
|
||||
const r = violationsList.value.violationsByNormalizedProperty(property).map((v) => v.title);
|
||||
|
||||
|
||||
return r;
|
||||
|
||||
}
|
||||
function violationTitlesWithParameter<
|
||||
P extends ViolationKey,
|
||||
Param extends Extract<keyof T[P], string>
|
||||
>(
|
||||
property: P,
|
||||
with_parameter: Param,
|
||||
with_parameter_value: T[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 ViolationKey>(property: P): boolean {
|
||||
return violationTitles(property).length > 0;
|
||||
}
|
||||
function hasViolationWithParameter<
|
||||
P extends ViolationKey,
|
||||
Param extends Extract<keyof T[P], string>
|
||||
>(
|
||||
property: P,
|
||||
with_parameter: Param,
|
||||
with_parameter_value: T[P][Param],
|
||||
): boolean {
|
||||
return violationTitlesWithParameter(property, with_parameter, with_parameter_value).length > 0;
|
||||
}
|
||||
|
||||
function setValidationException<V extends ValidationExceptionInterface<T>>(validationException: V): void {
|
||||
violationsList.value = validationException;
|
||||
}
|
||||
|
||||
function cleanException(): void {
|
||||
violationsList.value = null;
|
||||
}
|
||||
|
||||
return {violationTitles, violationTitlesWithParameter, setValidationException, cleanException, hasViolationWithParameter, hasViolation};
|
||||
}
|
||||
@@ -80,9 +80,7 @@ class ExtractPhonenumberFromPattern
|
||||
}
|
||||
|
||||
if (5 < $length) {
|
||||
$filtered = \trim(\strtr($subject, [$matches[0] => '']));
|
||||
|
||||
return new SearchExtractionResult($filtered, [\implode('', $phonenumber)]);
|
||||
return new SearchExtractionResult($subject, [\implode('', $phonenumber)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
|
||||
{
|
||||
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) {
|
||||
return null;
|
||||
@@ -51,7 +51,7 @@ class DateNormalizer implements ContextAwareNormalizerInterface, DenormalizerInt
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function normalize($date, $format = null, array $context = [])
|
||||
public function normalize($date, $format = null, array $context = []): array
|
||||
{
|
||||
/* @var DateTimeInterface $date */
|
||||
switch ($format) {
|
||||
|
||||
@@ -45,8 +45,11 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
|
||||
|
||||
try {
|
||||
return $this->phoneNumberUtil->parse($data, $this->defaultCarrierCode);
|
||||
} catch (NumberParseException $e) {
|
||||
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
|
||||
} catch (NumberParseException) {
|
||||
$phonenumber = new PhoneNumber();
|
||||
$phonenumber->setRawInput($data);
|
||||
|
||||
return $phonenumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,20 +43,20 @@ final class ExtractPhonenumberFromPatternTest extends KernelTestCase
|
||||
|
||||
yield ['BE', 'Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', 'no phonenumber and a date'];
|
||||
|
||||
yield ['BE', 'Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
|
||||
yield ['BE', 'Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo 0486 123 456', 'a phonenumber and a name'];
|
||||
|
||||
yield ['BE', 'Diallo 123 456', 1, ['123456'], 'Diallo', 'a number and a name, without leadiing 0'];
|
||||
yield ['BE', 'Diallo 123 456', 1, ['123456'], 'Diallo 123 456', 'a number and a name, without leadiing 0'];
|
||||
|
||||
yield ['BE', '123 456', 1, ['123456'], '', 'only phonenumber'];
|
||||
yield ['BE', '123 456', 1, ['123456'], '123 456', 'only phonenumber'];
|
||||
|
||||
yield ['BE', '0123 456', 1, ['+32123456'], '', 'only phonenumber with a leading 0'];
|
||||
yield ['BE', '0123 456', 1, ['+32123456'], '0123 456', 'only phonenumber with a leading 0'];
|
||||
|
||||
yield ['FR', '123 456', 1, ['123456'], '', 'only phonenumber'];
|
||||
yield ['FR', '123 456', 1, ['123456'], '123 456', 'only phonenumber'];
|
||||
|
||||
yield ['FR', '0123 456', 1, ['+33123456'], '', 'only phonenumber with a leading 0'];
|
||||
yield ['FR', '0123 456', 1, ['+33123456'], '0123 456', 'only phonenumber with a leading 0'];
|
||||
|
||||
yield ['FR', 'Diallo 0486 123 456', 1, ['+33486123456'], 'Diallo', 'a phonenumber and a name'];
|
||||
yield ['FR', 'Diallo 0486 123 456', 1, ['+33486123456'], 'Diallo 0486 123 456', 'a phonenumber and a name'];
|
||||
|
||||
yield ['FR', 'Diallo +32486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
|
||||
yield ['FR', 'Diallo +32486 123 456', 1, ['+32486123456'], 'Diallo +32486 123 456', 'a phonenumber and a name'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace Chill\MainBundle\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @deprecated use odolbeau/phonenumber validator instead
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
|
||||
class PhonenumberConstraint extends Constraint
|
||||
{
|
||||
|
||||
@@ -16,6 +16,9 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
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
|
||||
{
|
||||
public function __construct(private readonly LoggerInterface $logger, private readonly PhoneNumberHelperInterface $phonenumberHelper) {}
|
||||
|
||||
@@ -136,34 +136,6 @@ filter_order:
|
||||
Search: Chercher dans la liste
|
||||
By date: Filtrer par date
|
||||
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:
|
||||
add: "Ajouter"
|
||||
modal_title: >-
|
||||
|
||||
@@ -935,11 +935,12 @@ onthefly:
|
||||
thirdparty: Détails du tiers
|
||||
file_person: Ouvrir la fiche de l'usager
|
||||
file_thirdparty: Voir le Tiers
|
||||
file_default: Voir
|
||||
edit:
|
||||
person: Modifier un usager
|
||||
thirdparty: Modifier un tiers
|
||||
create:
|
||||
button: Créer {q}
|
||||
button: Créer "q"
|
||||
title:
|
||||
default: Création d'un nouvel usager ou d'un tiers professionnel
|
||||
person: Création d'un nouvel usager
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Actions\PersonCreate;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
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\PersonAltName;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||
|
||||
class PersonCreateDTO
|
||||
{
|
||||
#[Assert\NotBlank(message: 'The firstname cannot be empty')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public string $firstName;
|
||||
|
||||
#[Assert\NotBlank(message: 'The lastname cannot be empty')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public string $lastName;
|
||||
|
||||
#[Birthdate]
|
||||
public ?\DateTime $birthdate = null;
|
||||
|
||||
#[Assert\NotNull(message: 'The gender must be set')]
|
||||
public ?Gender $gender = null;
|
||||
|
||||
public ?Civility $civility = null;
|
||||
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::FIXED_LINE, MisdPhoneNumberConstraint::VOIP, MisdPhoneNumberConstraint::PERSONAL_NUMBER])]
|
||||
public ?PhoneNumber $phonenumber = null;
|
||||
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::MOBILE])]
|
||||
public ?PhoneNumber $mobilenumber = null;
|
||||
|
||||
#[Assert\Email]
|
||||
public ?string $email = '';
|
||||
|
||||
// Checkbox that indicates whether the address form was checked in creation form
|
||||
public bool $addressForm = false;
|
||||
|
||||
// Selected address value (unmapped in Person entity during creation)
|
||||
public ?Address $address = null;
|
||||
|
||||
public ?Center $center = null;
|
||||
|
||||
/**
|
||||
* @var array<string, PersonAltName> where the key is the altname's key
|
||||
*/
|
||||
public array $altNames = [];
|
||||
|
||||
/**
|
||||
* @var array<string, PersonIdentifier>
|
||||
*/
|
||||
#[Assert\Valid(traverse: true)]
|
||||
public array $identifiers = [];
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Actions\PersonCreate\Service;
|
||||
|
||||
use Chill\PersonBundle\Actions\PersonCreate\PersonCreateDTO;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
|
||||
class PersonCreateDTOFactory
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
|
||||
private readonly PersonIdentifierManagerInterface $personIdentifierManager,
|
||||
) {}
|
||||
|
||||
public function createPersonCreateDTO(Person $person): PersonCreateDTO
|
||||
{
|
||||
$dto = new PersonCreateDTO();
|
||||
$dto->firstName = $person->getFirstName();
|
||||
$dto->lastName = $person->getLastName();
|
||||
$dto->birthdate = $person->getBirthdate();
|
||||
$dto->gender = $person->getGender();
|
||||
$dto->civility = $person->getCivility();
|
||||
$dto->phonenumber = $person->getPhonenumber();
|
||||
$dto->mobilenumber = $person->getMobilenumber();
|
||||
$dto->email = $person->getEmail();
|
||||
$dto->center = $person->getCenter();
|
||||
// address/addressForm are not mapped on Person entity; left to defaults
|
||||
|
||||
foreach ($this->configPersonAltNamesHelper->getChoices() as $key => $labels) {
|
||||
$altName = $person->getAltNames()->findFirst(fn (int $k, PersonAltName $altName) => $altName->getKey() === $key);
|
||||
if (null === $altName) {
|
||||
$altName = new PersonAltName();
|
||||
$altName->setKey($key);
|
||||
}
|
||||
$dto->altNames[$key] = $altName;
|
||||
}
|
||||
|
||||
foreach ($this->personIdentifierManager->getWorkers() as $worker) {
|
||||
$identifier = $person
|
||||
->getIdentifiers()
|
||||
->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition() === $identifier->getDefinition());
|
||||
if (null === $identifier) {
|
||||
$identifier = new PersonIdentifier($worker->getDefinition());
|
||||
$person->addIdentifier($identifier);
|
||||
}
|
||||
$dto->identifiers['identifier_'.$worker->getDefinition()->getId()] = $identifier;
|
||||
}
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
public function mapPersonCreateDTOtoPerson(PersonCreateDTO $personCreateDTO, Person $person): void
|
||||
{
|
||||
$person
|
||||
->setFirstName($personCreateDTO->firstName)
|
||||
->setLastName($personCreateDTO->lastName)
|
||||
->setBirthdate($personCreateDTO->birthdate)
|
||||
->setGender($personCreateDTO->gender)
|
||||
->setCivility($personCreateDTO->civility)
|
||||
->setPhonenumber($personCreateDTO->phonenumber)
|
||||
->setMobilenumber($personCreateDTO->mobilenumber)
|
||||
->setEmail($personCreateDTO->email)
|
||||
->setCenter($personCreateDTO->center);
|
||||
|
||||
foreach ($personCreateDTO->altNames as $altName) {
|
||||
if ('' === $altName->getLabel()) {
|
||||
$person->removeAltName($altName);
|
||||
} else {
|
||||
$person->addAltName($altName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($personCreateDTO->identifiers as $identifier) {
|
||||
$worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition());
|
||||
if ($worker->isEmpty($identifier)) {
|
||||
$person->removeIdentifier($identifier);
|
||||
} else {
|
||||
$person->addIdentifier($identifier);
|
||||
}
|
||||
}
|
||||
// Note: address and addressForm are handled by controller/form during creation, not mapped here
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?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\Actions\PersonEdit;
|
||||
|
||||
use Chill\MainBundle\Entity\Civility;
|
||||
use Chill\MainBundle\Entity\Country;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Chill\MainBundle\Entity\Language;
|
||||
use Chill\PersonBundle\Entity\AdministrativeStatus;
|
||||
use Chill\PersonBundle\Entity\EmploymentStatus;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\Entity\MaritalStatus;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class PersonEditDTO
|
||||
{
|
||||
#[Assert\NotBlank(message: 'The firstname cannot be empty')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public string $firstName;
|
||||
|
||||
#[Assert\NotBlank(message: 'The lastname cannot be empty')]
|
||||
#[Assert\Length(max: 255)]
|
||||
public string $lastName;
|
||||
|
||||
#[Birthdate]
|
||||
public ?\DateTime $birthdate = null;
|
||||
|
||||
#[Assert\GreaterThanOrEqual(propertyPath: 'birthdate')]
|
||||
#[Assert\LessThanOrEqual('today')]
|
||||
public ?\DateTimeImmutable $deathdate = null;
|
||||
|
||||
#[Assert\NotNull(message: 'The gender must be set')]
|
||||
public ?Gender $gender = null;
|
||||
|
||||
#[Assert\Valid]
|
||||
public CommentEmbeddable $genderComment;
|
||||
|
||||
public ?int $numberOfChildren = null;
|
||||
|
||||
/**
|
||||
* @var array<string, PersonAltName> where the key is the altname's key
|
||||
*/
|
||||
public array $altNames = [];
|
||||
|
||||
public string $memo = '';
|
||||
|
||||
public ?EmploymentStatus $employmentStatus = null;
|
||||
|
||||
public ?AdministrativeStatus $administrativeStatus = null;
|
||||
|
||||
public string $placeOfBirth = '';
|
||||
|
||||
public ?string $contactInfo = '';
|
||||
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::FIXED_LINE, MisdPhoneNumberConstraint::VOIP, MisdPhoneNumberConstraint::PERSONAL_NUMBER])]
|
||||
public ?PhoneNumber $phonenumber = null;
|
||||
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::MOBILE])]
|
||||
public ?PhoneNumber $mobilenumber = null;
|
||||
|
||||
public ?bool $acceptSms = false;
|
||||
|
||||
#[Assert\Valid(traverse: true)]
|
||||
public Collection $otherPhonenumbers; // Collection<int, \Chill\PersonBundle\Entity\PersonPhone>
|
||||
|
||||
#[Assert\Email]
|
||||
public ?string $email = '';
|
||||
|
||||
public ?bool $acceptEmail = false;
|
||||
|
||||
public ?Country $countryOfBirth = null;
|
||||
|
||||
public ?Country $nationality = null;
|
||||
|
||||
public Collection $spokenLanguages; // Collection<int, Language>
|
||||
|
||||
public ?Civility $civility = null;
|
||||
|
||||
public ?MaritalStatus $maritalStatus = null;
|
||||
|
||||
public ?\DateTimeInterface $maritalStatusDate = null;
|
||||
|
||||
#[Assert\Valid]
|
||||
public CommentEmbeddable $maritalStatusComment;
|
||||
|
||||
public ?array $cFData = null;
|
||||
|
||||
/**
|
||||
* @var array<string, PersonIdentifier>
|
||||
*/
|
||||
#[Assert\Valid(traverse: true)]
|
||||
public array $identifiers = [];
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?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\Actions\PersonEdit\Service;
|
||||
|
||||
use Chill\PersonBundle\Actions\PersonEdit\PersonEditDTO;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
|
||||
class PersonEditDTOFactory
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
|
||||
private readonly PersonIdentifierManagerInterface $personIdentifierManager,
|
||||
) {}
|
||||
|
||||
public function createPersonEditDTO(Person $person): PersonEditDTO
|
||||
{
|
||||
$dto = new PersonEditDTO();
|
||||
$dto->firstName = $person->getFirstName();
|
||||
$dto->lastName = $person->getLastName();
|
||||
$dto->birthdate = $person->getBirthdate();
|
||||
$dto->deathdate = (null !== $deathDate = $person->getDeathdate()) ? \DateTimeImmutable::createFromInterface($deathDate) : null;
|
||||
$dto->gender = $person->getGender();
|
||||
$dto->genderComment = $person->getGenderComment();
|
||||
$dto->numberOfChildren = $person->getNumberOfChildren();
|
||||
$dto->memo = $person->getMemo() ?? '';
|
||||
$dto->employmentStatus = $person->getEmploymentStatus();
|
||||
$dto->administrativeStatus = $person->getAdministrativeStatus();
|
||||
$dto->placeOfBirth = $person->getPlaceOfBirth() ?? '';
|
||||
$dto->contactInfo = $person->getcontactInfo();
|
||||
$dto->phonenumber = $person->getPhonenumber();
|
||||
$dto->mobilenumber = $person->getMobilenumber();
|
||||
$dto->acceptSms = $person->getAcceptSMS();
|
||||
$dto->otherPhonenumbers = $person->getOtherPhoneNumbers();
|
||||
$dto->email = $person->getEmail();
|
||||
$dto->acceptEmail = $person->getAcceptEmail();
|
||||
$dto->countryOfBirth = $person->getCountryOfBirth();
|
||||
$dto->nationality = $person->getNationality();
|
||||
$dto->spokenLanguages = $person->getSpokenLanguages();
|
||||
$dto->civility = $person->getCivility();
|
||||
$dto->maritalStatus = $person->getMaritalStatus();
|
||||
$dto->maritalStatusDate = $person->getMaritalStatusDate();
|
||||
$dto->maritalStatusComment = $person->getMaritalStatusComment();
|
||||
$dto->cFData = $person->getCFData();
|
||||
|
||||
|
||||
foreach ($this->configPersonAltNamesHelper->getChoices() as $key => $labels) {
|
||||
$altName = $person->getAltNames()->findFirst(fn (int $k, PersonAltName $altName) => $altName->getKey() === $key);
|
||||
if (null === $altName) {
|
||||
$altName = new PersonAltName();
|
||||
$altName->setKey($key);
|
||||
}
|
||||
$dto->altNames[$key] = $altName;
|
||||
}
|
||||
|
||||
foreach ($this->personIdentifierManager->getWorkers() as $worker) {
|
||||
$identifier = $person
|
||||
->getIdentifiers()
|
||||
->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition() === $identifier->getDefinition());
|
||||
if (null === $identifier) {
|
||||
$identifier = new PersonIdentifier($worker->getDefinition());
|
||||
$person->addIdentifier($identifier);
|
||||
}
|
||||
$dto->identifiers['identifier_'.$worker->getDefinition()->getId()] = $identifier;
|
||||
}
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
public function mapPersonEditDTOtoPerson(PersonEditDTO $personEditDTO, Person $person): void
|
||||
{
|
||||
// Copy all editable fields from the DTO back to the Person entity
|
||||
$person
|
||||
->setFirstName($personEditDTO->firstName)
|
||||
->setLastName($personEditDTO->lastName)
|
||||
->setBirthdate($personEditDTO->birthdate)
|
||||
->setDeathdate($personEditDTO->deathdate)
|
||||
->setGender($personEditDTO->gender)
|
||||
->setGenderComment($personEditDTO->genderComment)
|
||||
->setNumberOfChildren($personEditDTO->numberOfChildren)
|
||||
->setMemo($personEditDTO->memo)
|
||||
->setEmploymentStatus($personEditDTO->employmentStatus)
|
||||
->setAdministrativeStatus($personEditDTO->administrativeStatus)
|
||||
->setPlaceOfBirth($personEditDTO->placeOfBirth)
|
||||
->setcontactInfo($personEditDTO->contactInfo)
|
||||
->setPhonenumber($personEditDTO->phonenumber)
|
||||
->setMobilenumber($personEditDTO->mobilenumber)
|
||||
->setAcceptSMS($personEditDTO->acceptSms ?? false)
|
||||
->setOtherPhoneNumbers($personEditDTO->otherPhonenumbers)
|
||||
->setEmail($personEditDTO->email)
|
||||
->setAcceptEmail($personEditDTO->acceptEmail ?? false)
|
||||
->setCountryOfBirth($personEditDTO->countryOfBirth)
|
||||
->setNationality($personEditDTO->nationality)
|
||||
->setSpokenLanguages($personEditDTO->spokenLanguages)
|
||||
->setCivility($personEditDTO->civility)
|
||||
->setMaritalStatus($personEditDTO->maritalStatus)
|
||||
->setMaritalStatusDate($personEditDTO->maritalStatusDate)
|
||||
->setMaritalStatusComment($personEditDTO->maritalStatusComment)
|
||||
->setCFData($personEditDTO->cFData);
|
||||
|
||||
foreach ($personEditDTO->altNames as $altName) {
|
||||
if ('' === $altName->getLabel()) {
|
||||
$person->removeAltName($altName);
|
||||
} else {
|
||||
$person->addAltName($altName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($personEditDTO->identifiers as $identifier) {
|
||||
$worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($identifier->getDefinition());
|
||||
if ($worker->isEmpty($identifier)) {
|
||||
$person->removeIdentifier($identifier);
|
||||
} else {
|
||||
$person->addIdentifier($identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ class ConfigPersonAltNamesHelper
|
||||
|
||||
/**
|
||||
* get the choices as key => values.
|
||||
*
|
||||
* @return array<string, array<string, string>> where the key is the altName's key, and the value is an array of TranslatableString
|
||||
*/
|
||||
public function getChoices(): array
|
||||
{
|
||||
|
||||
@@ -11,36 +11,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Form\CreationPersonType;
|
||||
use Chill\PersonBundle\Privacy\PrivacyEvent;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Search\SimilarPersonMatcher;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function hash;
|
||||
|
||||
final class PersonController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||
private readonly SimilarPersonMatcher $similarPersonMatcher,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly PersonRepository $personRepository,
|
||||
private readonly ConfigPersonAltNamesHelper $configPersonAltNameHelper,
|
||||
@@ -85,110 +71,6 @@ final class PersonController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for creating a new person.
|
||||
*
|
||||
* The controller register data from a previous post on the form, and
|
||||
* register it in the session.
|
||||
*
|
||||
* The next post compare the data with previous one and, if yes, show a
|
||||
* review page if there are "alternate persons".
|
||||
*/
|
||||
#[Route(path: '/{_locale}/person/new', name: 'chill_person_new')]
|
||||
public function newAction(Request $request): Response
|
||||
{
|
||||
$person = new Person();
|
||||
|
||||
$authorizedCenters = $this->authorizationHelper->getReachableCenters($this->getUser(), PersonVoter::CREATE);
|
||||
|
||||
if (1 === \count($authorizedCenters)) {
|
||||
$person->setCenter($authorizedCenters[0]);
|
||||
}
|
||||
|
||||
$form = $this->createForm(CreationPersonType::class, $person)
|
||||
->add('editPerson', SubmitType::class, [
|
||||
'label' => 'Add the person',
|
||||
])->add('createPeriod', SubmitType::class, [
|
||||
'label' => 'Add the person and create an accompanying period',
|
||||
])->add('createHousehold', SubmitType::class, [
|
||||
'label' => 'Add the person and create a household',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (Request::METHOD_GET === $request->getMethod()) {
|
||||
$this->lastPostDataReset();
|
||||
} elseif (
|
||||
Request::METHOD_POST === $request->getMethod()
|
||||
&& $form->isValid()
|
||||
) {
|
||||
$alternatePersons = $this->similarPersonMatcher
|
||||
->matchPerson($person);
|
||||
|
||||
if (
|
||||
false === $this->isLastPostDataChanges($form, $request, true)
|
||||
|| 0 === \count($alternatePersons)
|
||||
) {
|
||||
$this->em->persist($person);
|
||||
|
||||
$this->em->flush();
|
||||
$this->lastPostDataReset();
|
||||
|
||||
$address = $form->get('address')->getData();
|
||||
$addressForm = (bool) $form->get('addressForm')->getData();
|
||||
|
||||
if (null !== $address && $addressForm) {
|
||||
$household = new Household();
|
||||
|
||||
$member = new HouseholdMember();
|
||||
$member->setPerson($person);
|
||||
$member->setStartDate(new \DateTimeImmutable());
|
||||
|
||||
$household->addMember($member);
|
||||
$household->setForceAddress($address);
|
||||
|
||||
$this->em->persist($member);
|
||||
$this->em->persist($household);
|
||||
$this->em->flush();
|
||||
|
||||
if ($form->get('createHousehold')->isClicked()) {
|
||||
return $this->redirectToRoute('chill_person_household_members_editor', [
|
||||
'persons' => [$person->getId()],
|
||||
'household' => $household->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($form->get('createPeriod')->isClicked()) {
|
||||
return $this->redirectToRoute('chill_person_accompanying_course_new', [
|
||||
'person_id' => [$person->getId()],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($form->get('createHousehold')->isClicked()) {
|
||||
return $this->redirectToRoute('chill_person_household_members_editor', [
|
||||
'persons' => [$person->getId()],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'chill_person_general_edit',
|
||||
['person_id' => $person->getId()]
|
||||
);
|
||||
}
|
||||
} elseif (Request::METHOD_POST === $request->getMethod() && !$form->isValid()) {
|
||||
$this->addFlash('error', $this->translator->trans('This form contains errors'));
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'@ChillPerson/Person/create.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'alternatePersons' => $alternatePersons ?? [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/person/{person_id}/general', name: 'chill_person_view')]
|
||||
public function viewAction(int $person_id)
|
||||
{
|
||||
@@ -250,51 +132,4 @@ final class PersonController extends AbstractController
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function isLastPostDataChanges(Form $form, Request $request, bool $replace = false): bool
|
||||
{
|
||||
/** @var SessionInterface $session */
|
||||
$session = $this->get('session');
|
||||
|
||||
if (!$session->has('last_person_data')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$newPost = $this->lastPostDataBuildHash($form, $request);
|
||||
|
||||
$isChanged = $session->get('last_person_data') !== $newPost;
|
||||
|
||||
if ($replace) {
|
||||
$session->set('last_person_data', $newPost);
|
||||
}
|
||||
|
||||
return $isChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* build the hash for posted data.
|
||||
*
|
||||
* For privacy reasons, the data are hashed using sha512
|
||||
*/
|
||||
private function lastPostDataBuildHash(Form $form, Request $request): string
|
||||
{
|
||||
$fields = [];
|
||||
$ignoredFields = ['form_status', '_token'];
|
||||
|
||||
foreach ($request->request->all()[$form->getName()] as $field => $value) {
|
||||
if (\in_array($field, $ignoredFields, true)) {
|
||||
continue;
|
||||
}
|
||||
$fields[$field] = \is_array($value) ?
|
||||
\implode(',', $value) : $value;
|
||||
}
|
||||
ksort($fields);
|
||||
|
||||
return \hash('sha512', \implode('&', $fields));
|
||||
}
|
||||
|
||||
private function lastPostDataReset(): void
|
||||
{
|
||||
$this->get('session')->set('last_person_data', '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<?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\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Actions\PersonCreate\Service\PersonCreateDTOFactory;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Form\CreationPersonType;
|
||||
use Chill\PersonBundle\Search\SimilarPersonMatcher;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\ClickableInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class PersonCreateController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthorizationHelperInterface $authorizationHelper,
|
||||
private readonly SimilarPersonMatcher $similarPersonMatcher,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly PersonCreateDTOFactory $personCreateDTOFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Method for creating a new person.
|
||||
*
|
||||
* The controller registers data from a previous post on the form and
|
||||
* registers it in the session.
|
||||
*
|
||||
* The next post compares the data with the previous one and, if yes, shows a
|
||||
* review page if there are "alternate persons".
|
||||
*/
|
||||
#[Route(path: '/{_locale}/person/new', name: 'chill_person_new')]
|
||||
public function newAction(Request $request, SessionInterface $session): Response
|
||||
{
|
||||
$person = new Person();
|
||||
|
||||
$authorizedCenters = $this->authorizationHelper->getReachableCenters($this->getUser(), PersonVoter::CREATE);
|
||||
|
||||
$dto = $this->personCreateDTOFactory->createPersonCreateDTO($person);
|
||||
|
||||
if (1 === \count($authorizedCenters)) {
|
||||
$dto->center = $authorizedCenters[0];
|
||||
}
|
||||
|
||||
$form = $this->createForm(CreationPersonType::class, $dto)
|
||||
->add('editPerson', SubmitType::class, [
|
||||
'label' => 'Add the person',
|
||||
])->add('createPeriod', SubmitType::class, [
|
||||
'label' => 'Add the person and create an accompanying period',
|
||||
])->add('createHousehold', SubmitType::class, [
|
||||
'label' => 'Add the person and create a household',
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (Request::METHOD_GET === $request->getMethod()) {
|
||||
$this->lastPostDataReset($session);
|
||||
} elseif (
|
||||
Request::METHOD_POST === $request->getMethod()
|
||||
&& $form->isValid()
|
||||
) {
|
||||
$alternatePersons = $this->similarPersonMatcher
|
||||
->matchPerson($person);
|
||||
|
||||
$createHouseholdButton = $form->get('createHousehold');
|
||||
$createPeriodButton = $form->get('createPeriod');
|
||||
$editPersonButton = $form->get('editPerson');
|
||||
|
||||
if (!$createHouseholdButton instanceof ClickableInterface) {
|
||||
throw new \UnexpectedValueException();
|
||||
}
|
||||
if (!$createPeriodButton instanceof ClickableInterface) {
|
||||
throw new \UnexpectedValueException();
|
||||
}
|
||||
if (!$editPersonButton instanceof ClickableInterface) {
|
||||
throw new \UnexpectedValueException();
|
||||
}
|
||||
|
||||
if (
|
||||
false === $this->isLastPostDataChanges($form, $request, $session)
|
||||
|| 0 === \count($alternatePersons)
|
||||
) {
|
||||
$this->personCreateDTOFactory->mapPersonCreateDTOtoPerson($dto, $person);
|
||||
$this->em->persist($person);
|
||||
|
||||
$this->em->flush();
|
||||
$this->lastPostDataReset($session);
|
||||
|
||||
$address = $dto->address;
|
||||
$addressForm = $dto->addressForm;
|
||||
|
||||
if (null !== $address && $addressForm) {
|
||||
$household = new Household();
|
||||
|
||||
$member = new HouseholdMember();
|
||||
$member->setPerson($person);
|
||||
$member->setStartDate(new \DateTimeImmutable());
|
||||
|
||||
$household->addMember($member);
|
||||
$household->setForceAddress($address);
|
||||
|
||||
$this->em->persist($member);
|
||||
$this->em->persist($household);
|
||||
$this->em->flush();
|
||||
|
||||
if ($createHouseholdButton->isClicked()) {
|
||||
return $this->redirectToRoute('chill_person_household_members_editor', [
|
||||
'persons' => [$person->getId()],
|
||||
'household' => $household->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($createPeriodButton->isClicked()) {
|
||||
return $this->redirectToRoute('chill_person_accompanying_course_new', [
|
||||
'person_id' => [$person->getId()],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($createHouseholdButton->isClicked()) {
|
||||
return $this->redirectToRoute('chill_person_household_members_editor', [
|
||||
'persons' => [$person->getId()],
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'chill_person_general_edit',
|
||||
['person_id' => $person->getId()]
|
||||
);
|
||||
}
|
||||
} elseif (Request::METHOD_POST === $request->getMethod() && !$form->isValid()) {
|
||||
$this->addFlash('error', $this->translator->trans('This form contains errors'));
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'@ChillPerson/Person/create.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'alternatePersons' => $alternatePersons ?? [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function isLastPostDataChanges(FormInterface $form, Request $request, SessionInterface $session): bool
|
||||
{
|
||||
if (!$session->has('last_person_data')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$newPost = $this->lastPostDataBuildHash($form, $request);
|
||||
|
||||
$isChanged = $session->get('last_person_data') !== $newPost;
|
||||
$session->set('last_person_data', $newPost);
|
||||
|
||||
return $isChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* build the hash for posted data.
|
||||
*
|
||||
* For privacy reasons, the data are hashed using sha512
|
||||
*/
|
||||
private function lastPostDataBuildHash(FormInterface $form, Request $request): string
|
||||
{
|
||||
$fields = [];
|
||||
$ignoredFields = ['form_status', '_token', 'identifiers'];
|
||||
|
||||
foreach ($request->request->all()[$form->getName()] as $field => $value) {
|
||||
if (\in_array($field, $ignoredFields, true)) {
|
||||
continue;
|
||||
}
|
||||
$fields[$field] = \is_array($value) ?
|
||||
\implode(',', $value) : $value;
|
||||
}
|
||||
ksort($fields);
|
||||
|
||||
return \hash('sha512', \implode('&', $fields));
|
||||
}
|
||||
|
||||
private function lastPostDataReset(SessionInterface $session): void
|
||||
{
|
||||
$session->set('last_person_data', '');
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\CustomFieldsBundle\EntityRepository\CustomFieldsDefaultGroupRepository;
|
||||
use Chill\PersonBundle\Actions\PersonEdit\Service\PersonEditDTOFactory;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Form\PersonType;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
@@ -38,6 +39,7 @@ final readonly class PersonEditController
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private Environment $twig,
|
||||
private PersonEditDTOFactory $personEditDTOFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -50,9 +52,11 @@ final readonly class PersonEditController
|
||||
throw new AccessDeniedHttpException('You are not allowed to edit this person.');
|
||||
}
|
||||
|
||||
$dto = $this->personEditDTOFactory->createPersonEditDTO($person);
|
||||
|
||||
$form = $this->formFactory->create(
|
||||
PersonType::class,
|
||||
$person,
|
||||
$dto,
|
||||
['cFGroup' => $this->customFieldsDefaultGroupRepository->findOneByEntity(Person::class)?->getCustomFieldsGroup()]
|
||||
);
|
||||
|
||||
@@ -62,6 +66,7 @@ final readonly class PersonEditController
|
||||
$session
|
||||
->getFlashBag()->add('error', new TranslatableMessage('This form contains errors'));
|
||||
} elseif ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->personEditDTOFactory->mapPersonEditDTOtoPerson($dto, $person);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('The person data has been updated'));
|
||||
|
||||
@@ -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 should use the PersonRepository service instead of a custom service name.
|
||||
$loader->load('services/repository.yaml');
|
||||
$loader->load('services/serializer.yaml');
|
||||
$loader->load('services/security.yaml');
|
||||
$loader->load('services/doctrineEventListener.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,15 +12,24 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Entity\Identifier;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraint;
|
||||
use Chill\PersonBundle\PersonIdentifier\Validator\ValidIdentifierConstraint;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_person_identifier')]
|
||||
#[ORM\UniqueConstraint(name: 'chill_person_identifier_unique', columns: ['definition_id', 'canonical'])]
|
||||
#[ORM\UniqueConstraint(name: 'chill_person_identifier_unique_person_definition', columns: ['definition_id', 'person_id'])]
|
||||
#[UniqueIdentifierConstraint]
|
||||
#[ValidIdentifierConstraint]
|
||||
#[Serializer\DiscriminatorMap('type', ['person_identifier' => PersonIdentifier::class])]
|
||||
class PersonIdentifier
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
#[ORM\GeneratedValue]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Person::class)]
|
||||
@@ -28,14 +37,16 @@ class PersonIdentifier
|
||||
private ?Person $person = null;
|
||||
|
||||
#[ORM\Column(name: 'value', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
#[Serializer\Groups(['read'])]
|
||||
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 = '';
|
||||
|
||||
public function __construct(
|
||||
#[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')]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private PersonIdentifierDefinition $definition,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -11,30 +11,35 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Entity\Identifier;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_person_identifier_definition')]
|
||||
#[Serializer\DiscriminatorMap('type', ['person_identifier_definition' => PersonIdentifierDefinition::class])]
|
||||
class PersonIdentifierDefinition
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(name: 'id', type: \Doctrine\DBAL\Types\Types::INTEGER)]
|
||||
#[ORM\Column(name: 'id', type: Types::INTEGER)]
|
||||
#[ORM\GeneratedValue]
|
||||
#[Serializer\Groups(['read'])]
|
||||
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;
|
||||
|
||||
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,
|
||||
#[ORM\Column(name: 'engine', type: \Doctrine\DBAL\Types\Types::STRING, length: 100)]
|
||||
#[ORM\Column(name: 'engine', type: Types::STRING, length: 100)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
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,
|
||||
#[ORM\Column(name: 'is_editable_by_users', type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => false])]
|
||||
private bool $isEditableByUsers = false,
|
||||
#[ORM\Column(name: 'data', type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
#[ORM\Column(name: 'presence', type: Types::STRING, nullable: false, enumType: IdentifierPresenceEnum::class, options: ['default' => IdentifierPresenceEnum::ON_EDIT])]
|
||||
private IdentifierPresenceEnum $presence = IdentifierPresenceEnum::ON_EDIT,
|
||||
#[ORM\Column(name: 'data', type: Types::JSON, nullable: false, options: ['default' => '[]', 'jsonb' => true])]
|
||||
private array $data = [],
|
||||
) {}
|
||||
|
||||
@@ -58,11 +63,6 @@ class PersonIdentifierDefinition
|
||||
return $this->engine;
|
||||
}
|
||||
|
||||
public function setEngine(string $engine): void
|
||||
{
|
||||
$this->engine = $engine;
|
||||
}
|
||||
|
||||
public function isSearchable(): bool
|
||||
{
|
||||
return $this->isSearchable;
|
||||
@@ -75,12 +75,7 @@ class PersonIdentifierDefinition
|
||||
|
||||
public function isEditableByUsers(): bool
|
||||
{
|
||||
return $this->isEditableByUsers;
|
||||
}
|
||||
|
||||
public function setIsEditableByUsers(bool $isEditableByUsers): void
|
||||
{
|
||||
$this->isEditableByUsers = $isEditableByUsers;
|
||||
return $this->presence->isEditableByUser();
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
@@ -104,4 +99,16 @@ class PersonIdentifierDefinition
|
||||
{
|
||||
$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\Workflow\EntityWorkflowSignatureStateEnum;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
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\PersonCurrentAddress;
|
||||
use Chill\PersonBundle\Entity\Person\PersonResource;
|
||||
use Chill\PersonBundle\PersonIdentifier\Validator\RequiredIdentifierConstraint;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
|
||||
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
|
||||
@@ -47,6 +47,7 @@ use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use Misd\PhoneNumberBundle\Validator\Constraints\PhoneNumber as MisdPhoneNumberConstraint;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
@@ -273,6 +274,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[RequiredIdentifierConstraint]
|
||||
#[Assert\Valid]
|
||||
private Collection $identifiers;
|
||||
|
||||
/**
|
||||
@@ -319,7 +322,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* The person's mobile phone number.
|
||||
*/
|
||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint(type: 'mobile')]
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::MOBILE])]
|
||||
private ?PhoneNumber $mobilenumber = null;
|
||||
|
||||
/**
|
||||
@@ -359,7 +362,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
* The person's phonenumber.
|
||||
*/
|
||||
#[ORM\Column(type: 'phone_number', nullable: true)]
|
||||
#[PhonenumberConstraint(type: 'landline')]
|
||||
#[MisdPhoneNumberConstraint(type: [MisdPhoneNumberConstraint::FIXED_LINE, MisdPhoneNumberConstraint::VOIP, MisdPhoneNumberConstraint::PERSONAL_NUMBER])]
|
||||
private ?PhoneNumber $phonenumber = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,15 +11,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
|
||||
use Chill\MainBundle\Form\Type\PickAddressType;
|
||||
use Chill\MainBundle\Form\Type\PickCenterType;
|
||||
use Chill\MainBundle\Form\Type\PickCivilityType;
|
||||
use Chill\PersonBundle\Actions\PersonCreate\PersonCreateDTO;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Form\Type\PersonAltNameType;
|
||||
use Chill\PersonBundle\Form\Type\PickGenderType;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
@@ -33,6 +32,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class CreationPersonType extends AbstractType
|
||||
{
|
||||
@@ -80,15 +80,18 @@ final class CreationPersonType extends AbstractType
|
||||
->add('addressForm', CheckboxType::class, [
|
||||
'label' => 'Create a household and add an address',
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'help' => 'A new household will be created. The person will be member of this household.',
|
||||
])
|
||||
->add('address', PickAddressType::class, [
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'label' => false,
|
||||
]);
|
||||
|
||||
$builder->add('identifiers', PersonIdentifiersType::class, [
|
||||
'by_reference' => false,
|
||||
'step' => 'on_create',
|
||||
]);
|
||||
|
||||
if ($this->askCenters) {
|
||||
$builder
|
||||
->add('center', PickCenterType::class, [
|
||||
@@ -112,7 +115,7 @@ final class CreationPersonType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Person::class,
|
||||
'data_class' => PersonCreateDTO::class,
|
||||
'constraints' => [
|
||||
new Callback($this->validateCheckedAddress(...)),
|
||||
],
|
||||
@@ -129,10 +132,12 @@ final class CreationPersonType extends AbstractType
|
||||
|
||||
public function validateCheckedAddress($data, ExecutionContextInterface $context, $payload): void
|
||||
{
|
||||
/** @var bool $addressFrom */
|
||||
$addressFrom = $context->getObject()->get('addressForm')->getData();
|
||||
/** @var ?Address $address */
|
||||
$address = $context->getObject()->get('address')->getData();
|
||||
if (!$data instanceof PersonCreateDTO) {
|
||||
throw new UnexpectedTypeException($data, PersonCreateDTO::class);
|
||||
}
|
||||
|
||||
$addressFrom = $data->addressForm;
|
||||
$address = $data->address;
|
||||
|
||||
if ($addressFrom && null === $address) {
|
||||
$context->buildViolation('person_creation.If you want to create an household, an address is required')
|
||||
|
||||
@@ -12,10 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Form\DataMapper;
|
||||
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
use Symfony\Component\Form\Exception\UnexpectedTypeException;
|
||||
|
||||
class PersonAltNameDataMapper implements DataMapperInterface
|
||||
{
|
||||
@@ -25,62 +22,24 @@ class PersonAltNameDataMapper implements DataMapperInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$viewData instanceof Collection) {
|
||||
throw new UnexpectedTypeException($viewData, Collection::class);
|
||||
}
|
||||
|
||||
$mapIndexToKey = [];
|
||||
|
||||
foreach ($viewData->getIterator() as $key => $altName) {
|
||||
/* @var PersonAltName $altName */
|
||||
$mapIndexToKey[$altName->getKey()] = $key;
|
||||
if (!is_array($viewData)) {
|
||||
throw new \InvalidArgumentException('View data must be an array');
|
||||
}
|
||||
|
||||
foreach ($forms as $key => $form) {
|
||||
if (\array_key_exists($key, $mapIndexToKey)) {
|
||||
$form->setData($viewData->get($mapIndexToKey[$key])->getLabel());
|
||||
$personAltName = $viewData[$key];
|
||||
if (!$personAltName instanceof PersonAltName) {
|
||||
throw new \InvalidArgumentException('PersonAltName must be an instance of PersonAltName');
|
||||
}
|
||||
$form->setData($personAltName->getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
public function mapFormsToData(\Traversable $forms, &$viewData): void
|
||||
{
|
||||
$mapIndexToKey = [];
|
||||
|
||||
if (\is_array($viewData)) {
|
||||
$dataIterator = $viewData;
|
||||
} else {
|
||||
$dataIterator = $viewData instanceof ArrayCollection ?
|
||||
$viewData->toArray() : $viewData->getIterator();
|
||||
}
|
||||
|
||||
foreach ($dataIterator as $key => $altName) {
|
||||
/* @var PersonAltName $altName */
|
||||
$mapIndexToKey[$altName->getKey()] = $key;
|
||||
}
|
||||
|
||||
foreach ($forms as $key => $form) {
|
||||
$isEmpty = empty($form->getData());
|
||||
|
||||
if (\array_key_exists($key, $mapIndexToKey)) {
|
||||
if ($isEmpty) {
|
||||
$viewData->remove($mapIndexToKey[$key]);
|
||||
} else {
|
||||
$viewData->get($mapIndexToKey[$key])->setLabel($form->getData());
|
||||
}
|
||||
} else {
|
||||
if (!$isEmpty) {
|
||||
$altName = (new PersonAltName())
|
||||
->setKey($key)
|
||||
->setLabel($form->getData());
|
||||
|
||||
if (\is_array($viewData)) {
|
||||
$viewData[] = $altName;
|
||||
} else {
|
||||
$viewData->add($altName);
|
||||
}
|
||||
}
|
||||
}
|
||||
$personAltName = array_find($viewData, fn (PersonAltName $altName) => $altName->getKey() === $key);
|
||||
$personAltName->setLabel($form->getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,60 +14,51 @@ namespace Chill\PersonBundle\Form\DataMapper;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\PersonIdentifier\Exception\UnexpectedTypeException;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
final readonly class PersonIdentifiersDataMapper implements DataMapperInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PersonIdentifierManagerInterface $identifierManager,
|
||||
private PersonIdentifierDefinitionRepository $identifierDefinitionRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public function mapDataToForms($viewData, \Traversable $forms): void
|
||||
{
|
||||
if (!$viewData instanceof Collection) {
|
||||
throw new UnexpectedTypeException($viewData, Collection::class);
|
||||
if (!$viewData instanceof PersonIdentifier) {
|
||||
throw new UnexpectedTypeException($viewData, PersonIdentifier::class);
|
||||
}
|
||||
/** @var array<string, FormInterface> $formsByKey */
|
||||
$formsByKey = iterator_to_array($forms);
|
||||
|
||||
foreach ($this->identifierManager->getWorkers() as $worker) {
|
||||
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||
continue;
|
||||
}
|
||||
$form = $formsByKey['identifier_'.$worker->getDefinition()->getId()];
|
||||
$identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $worker->getDefinition()->getId() === $identifier->getId());
|
||||
if (null === $identifier) {
|
||||
$identifier = new PersonIdentifier($worker->getDefinition());
|
||||
}
|
||||
$form->setData($identifier->getValue());
|
||||
$worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($viewData->getDefinition());
|
||||
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||
return;
|
||||
}
|
||||
foreach ($forms as $key => $form) {
|
||||
$form->setData($viewData->getValue()[$key] ?? $worker->getDefaultValue()[$key] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public function mapFormsToData(\Traversable $forms, &$viewData): void
|
||||
{
|
||||
if (!$viewData instanceof Collection) {
|
||||
throw new UnexpectedTypeException($viewData, Collection::class);
|
||||
if (!$viewData instanceof PersonIdentifier) {
|
||||
throw new UnexpectedTypeException($viewData, PersonIdentifier::class);
|
||||
}
|
||||
$worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($viewData->getDefinition());
|
||||
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
foreach ($forms as $name => $form) {
|
||||
$identifierId = (int) substr((string) $name, 11);
|
||||
$identifier = $viewData->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getId() === $identifierId);
|
||||
$definition = $this->identifierDefinitionRepository->find($identifierId);
|
||||
if (null === $identifier) {
|
||||
$identifier = new PersonIdentifier($definition);
|
||||
$viewData->add($identifier);
|
||||
}
|
||||
if (!$identifier->getDefinition()->isEditableByUsers()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$worker = $this->identifierManager->buildWorkerByPersonIdentifierDefinition($definition);
|
||||
$identifier->setValue($form->getData());
|
||||
$identifier->setCanonical($worker->canonicalizeValue($identifier->getValue()));
|
||||
$values[$name] = $form->getData();
|
||||
}
|
||||
|
||||
$viewData->setValue($values);
|
||||
$viewData->setCanonical($worker->canonicalizeValue($viewData->getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,12 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Identifier\IdentifierPresenceEnum;
|
||||
use Chill\PersonBundle\Form\DataMapper\PersonIdentifiersDataMapper;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class PersonIdentifiersType extends AbstractType
|
||||
{
|
||||
@@ -27,22 +29,34 @@ final class PersonIdentifiersType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
foreach ($this->identifierManager->getWorkers() as $worker) {
|
||||
foreach ($this->identifierManager->getWorkers() as $k => $worker) {
|
||||
if (!$worker->getDefinition()->isEditableByUsers()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip some on creation
|
||||
if ('on_create' === $options['step']
|
||||
&& IdentifierPresenceEnum::ON_EDIT === $worker->getDefinition()->getPresence()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subBuilder = $builder->create(
|
||||
'identifier_'.$worker->getDefinition()->getId(),
|
||||
options: [
|
||||
'compound' => true,
|
||||
'label' => $this->translatableStringHelper->localize($worker->getDefinition()->getLabel()),
|
||||
'error_bubbling' => false,
|
||||
]
|
||||
);
|
||||
$subBuilder->setDataMapper($this->identifiersDataMapper);
|
||||
$worker->buildForm($subBuilder);
|
||||
$builder->add($subBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
$builder->setDataMapper($this->identifiersDataMapper);
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefault('step', 'on_edit')
|
||||
->setAllowedValues('step', ['on_edit', 'on_create']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ use Chill\MainBundle\Form\Type\PickCivilityType;
|
||||
use Chill\MainBundle\Form\Type\Select2CountryType;
|
||||
use Chill\MainBundle\Form\Type\Select2LanguageType;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Actions\PersonEdit\PersonEditDTO;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonPhone;
|
||||
use Chill\PersonBundle\Form\Type\PersonAltNameType;
|
||||
use Chill\PersonBundle\Form\Type\PersonPhoneType;
|
||||
@@ -242,7 +242,7 @@ class PersonType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Person::class,
|
||||
'data_class' => PersonEditDTO::class,
|
||||
]);
|
||||
|
||||
$resolver->setRequired([
|
||||
|
||||
@@ -32,6 +32,7 @@ class PersonAltNameType extends AbstractType
|
||||
[
|
||||
'label' => $label,
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,20 +13,26 @@ namespace Chill\PersonBundle\PersonIdentifier\Identifier;
|
||||
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||
use Chill\PersonBundle\PersonIdentifier\IdentifierViolationDTO;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class StringIdentifier implements PersonIdentifierEngineInterface
|
||||
{
|
||||
public const NAME = 'chill-person-bundle.string-identifier';
|
||||
|
||||
private const ONLY_NUMBERS = 'only_numbers';
|
||||
private const FIXED_LENGTH = 'fixed_length';
|
||||
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'chill-person-bundle.string-identifier';
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string
|
||||
{
|
||||
return $value['content'] ?? '';
|
||||
return trim($value['content'] ?? '');
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void
|
||||
@@ -36,6 +42,37 @@ final readonly class StringIdentifier implements PersonIdentifierEngineInterface
|
||||
|
||||
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'] ?? '');
|
||||
}
|
||||
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
$config = $definition->getData();
|
||||
$content = (string) ($identifier->getValue()['content'] ?? '');
|
||||
$violations = [];
|
||||
|
||||
if (($config[self::ONLY_NUMBERS] ?? false) && !preg_match('/^[0-9]+$/', $content)) {
|
||||
$violations[] = new IdentifierViolationDTO('person_identifier.only_number', '2a3352c0-a2b9-11f0-a767-b7a3f80e52f1');
|
||||
}
|
||||
|
||||
if (null !== ($config[self::FIXED_LENGTH] ?? null) && strlen($content) !== $config[self::FIXED_LENGTH]) {
|
||||
$violations[] = new IdentifierViolationDTO(
|
||||
'person_identifier.fixed_length',
|
||||
'2b02a8fe-a2b9-11f0-bfe5-033300972783',
|
||||
['limit' => (string) $config[self::FIXED_LENGTH]]
|
||||
);
|
||||
}
|
||||
|
||||
return $violations;
|
||||
}
|
||||
|
||||
public function getDefaultValue(PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return ['content' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Data Transfer Object to create a ConstraintViolationListInterface.
|
||||
*/
|
||||
class IdentifierViolationDTO
|
||||
{
|
||||
public function __construct(
|
||||
public string $message,
|
||||
/**
|
||||
* @var string an UUID
|
||||
*/
|
||||
public string $code,
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public array $parameters = [],
|
||||
public string $messageDomain = 'validators',
|
||||
) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,29 @@ interface PersonIdentifierEngineInterface
|
||||
{
|
||||
public static function getName(): string;
|
||||
|
||||
/**
|
||||
* @phpstan-pure
|
||||
*/
|
||||
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string;
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Return a list of @see{IdentifierViolationDTO} to generatie violation errors.
|
||||
*
|
||||
* @return list<IdentifierViolationDTO>
|
||||
*/
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array;
|
||||
|
||||
public function getDefaultValue(PersonIdentifierDefinition $definition): array;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\PersonIdentifier;
|
||||
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||
use Chill\PersonBundle\PersonIdentifier\Exception\EngineNotFoundException;
|
||||
use Chill\PersonBundle\PersonIdentifier\Exception\PersonIdentifierDefinitionNotFoundException;
|
||||
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierDefinitionRepository;
|
||||
|
||||
final readonly class PersonIdentifierManager implements PersonIdentifierManagerInterface
|
||||
@@ -44,8 +45,16 @@ final readonly class PersonIdentifierManager implements PersonIdentifierManagerI
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,16 @@ interface PersonIdentifierManagerInterface
|
||||
/**
|
||||
* Build PersonIdentifierWorker's for all active definition.
|
||||
*
|
||||
* Only active definition are returned.
|
||||
*
|
||||
* @return list<PersonIdentifierWorker>
|
||||
*/
|
||||
public function getWorkers(): array;
|
||||
|
||||
public function buildWorkerByPersonIdentifierDefinition(PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
|
||||
/**
|
||||
* @param int|PersonIdentifierDefinition $personIdentifierDefinition an instance of PersonIdentifierDefinition, or his id
|
||||
*
|
||||
* @throw PersonIdentifierNotFoundException
|
||||
*/
|
||||
public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
final readonly class PersonIdentifierWorker
|
||||
readonly class PersonIdentifierWorker
|
||||
{
|
||||
public function __construct(
|
||||
private PersonIdentifierEngineInterface $identifierEngine,
|
||||
@@ -46,4 +46,25 @@ final readonly class PersonIdentifierWorker
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<IdentifierViolationDTO>
|
||||
*/
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return $this->identifierEngine->validate($identifier, $definition);
|
||||
}
|
||||
|
||||
public function getDefaultValue(): array
|
||||
{
|
||||
return $this->identifierEngine->getDefaultValue($this->definition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,23 @@
|
||||
<?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 ValidIdentifierConstraint extends Constraint
|
||||
{
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
@@ -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\PersonIdentifier\Validator;
|
||||
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
|
||||
final class ValidIdentifierConstraintValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private readonly PersonIdentifierManagerInterface $personIdentifierManager) {}
|
||||
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof ValidIdentifierConstraint) {
|
||||
throw new UnexpectedTypeException($constraint, ValidIdentifierConstraint::class);
|
||||
}
|
||||
|
||||
if (!$value instanceof PersonIdentifier) {
|
||||
throw new UnexpectedValueException($value, PersonIdentifier::class);
|
||||
}
|
||||
|
||||
$worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($value->getDefinition());
|
||||
|
||||
$violations = $worker->validate($value, $value->getDefinition());
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$this->context->buildViolation($violation->message)
|
||||
->setParameters($violation->parameters)
|
||||
->setParameter('{{ code }}', $violation->code)
|
||||
->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\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
|
||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
@@ -27,7 +29,13 @@ use Symfony\Component\Security\Core\Security;
|
||||
|
||||
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(
|
||||
?string $default = null,
|
||||
@@ -107,6 +115,15 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
|
||||
$query
|
||||
->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 = [];
|
||||
$pertinenceArgs = [];
|
||||
$andWhereSearchClause = [];
|
||||
@@ -124,20 +141,53 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
|
||||
'(starts_with(LOWER(UNACCENT(lastname)), UNACCENT(LOWER(?))))::int';
|
||||
\array_push($pertinenceArgs, $str, $str, $str, $str);
|
||||
|
||||
$andWhereSearchClause[] =
|
||||
'(LOWER(UNACCENT(?)) <<% person.fullnamecanonical OR '.
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' )";
|
||||
\array_push($andWhereSearchClauseArgs, $str, $str);
|
||||
$q = [
|
||||
'LOWER(UNACCENT(?)) <<% person.fullnamecanonical',
|
||||
"person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ",
|
||||
];
|
||||
$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 * 1000000";
|
||||
$qArguments = [...$qArguments, $str, ...$idDefinitionWorkers];
|
||||
$pertinenceArgs = [...$pertinenceArgs, $str, ...$idDefinitionWorkers];
|
||||
}
|
||||
|
||||
$andWhereSearchClause[] = '('.implode(' OR ', $q).')';
|
||||
$andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, ...$qArguments];
|
||||
}
|
||||
|
||||
$query->andWhereClause(
|
||||
\implode(' AND ', $andWhereSearchClause),
|
||||
$andWhereSearchClauseArgs
|
||||
);
|
||||
} else {
|
||||
$pertinence = ['1'];
|
||||
$pertinenceArgs = [];
|
||||
}
|
||||
|
||||
if (null !== $phonenumber) {
|
||||
$personPhoneClause = "person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%')";
|
||||
if (count($andWhereSearchClauseArgs) > 0) {
|
||||
$initialSearchClause = '(('.\implode(' AND ', $andWhereSearchClause).') OR '.$personPhoneClause.')';
|
||||
}
|
||||
$andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, $phonenumber, $phonenumber, $phonenumber];
|
||||
|
||||
// drastically increase pertinence
|
||||
$pertinence[] = "(person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%'))::int * 1000000";
|
||||
$pertinenceArgs = [...$pertinenceArgs, $phonenumber, $phonenumber, $phonenumber];
|
||||
} else {
|
||||
$initialSearchClause = \implode(' AND ', $andWhereSearchClause);
|
||||
}
|
||||
|
||||
if (isset($initialSearchClause)) {
|
||||
$query->andWhereClause(
|
||||
$initialSearchClause,
|
||||
$andWhereSearchClauseArgs
|
||||
);
|
||||
}
|
||||
|
||||
$query
|
||||
->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs);
|
||||
|
||||
@@ -176,14 +226,6 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
|
||||
);
|
||||
}
|
||||
|
||||
if (null !== $phonenumber) {
|
||||
$query->andWhereClause(
|
||||
"person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR pp.phonenumber LIKE '%' || ? || '%'",
|
||||
[$phonenumber, $phonenumber, $phonenumber]
|
||||
);
|
||||
$query->setFromClause($query->getFromClause().' LEFT JOIN chill_person_phone pp ON pp.person_id = person.id');
|
||||
}
|
||||
|
||||
if (null !== $city) {
|
||||
$query->setFromClause($query->getFromClause().' '.
|
||||
'JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id '.
|
||||
|
||||
@@ -10,16 +10,40 @@ import {
|
||||
Scope,
|
||||
Job,
|
||||
PrivateCommentEmbeddable,
|
||||
TranslatableString,
|
||||
DateTimeWrite,
|
||||
SetGender,
|
||||
SetCenter,
|
||||
SetCivility, Gender,
|
||||
} from "ChillMainAssets/types";
|
||||
import { StoredObject } from "ChillDocStoreAssets/types";
|
||||
import { Thirdparty } from "../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
import { Calendar } from "../../../ChillCalendarBundle/Resources/public/types";
|
||||
import Person from "./vuejs/_components/OnTheFly/Person.vue";
|
||||
|
||||
/**
|
||||
* An alternative name, as configured locally
|
||||
*/
|
||||
export interface AltName {
|
||||
label: string;
|
||||
labels: TranslatableString;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface PersonAltNameWrite {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An altname for a person
|
||||
*/
|
||||
export interface PersonAltName {
|
||||
label: string;
|
||||
/**
|
||||
* will match a key in @link{AltName}
|
||||
*/
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
id: number;
|
||||
type: "person";
|
||||
@@ -27,7 +51,7 @@ export interface Person {
|
||||
textAge: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
altNames: AltName[];
|
||||
altNames: PersonAltName[];
|
||||
suffixText: string;
|
||||
current_household_address: Address | null;
|
||||
birthdate: DateTime | null;
|
||||
@@ -36,11 +60,63 @@ export interface Person {
|
||||
phonenumber: string;
|
||||
mobilenumber: string;
|
||||
email: string;
|
||||
gender: "woman" | "man" | "other";
|
||||
gender: Gender;
|
||||
centers: Center[];
|
||||
civility: Civility | null;
|
||||
current_household_id: number;
|
||||
current_residential_addresses: Address[];
|
||||
current_residential_addresses: ResidentialAddress[];
|
||||
/**
|
||||
* The person id as configured by the user
|
||||
*/
|
||||
personId: string;
|
||||
identifiers: PersonIdentifier[];
|
||||
}
|
||||
|
||||
export interface PersonIdentifier {
|
||||
id: number;
|
||||
type: "person_identifier";
|
||||
value: object;
|
||||
definition: PersonIdentifierDefinition;
|
||||
}
|
||||
|
||||
export interface PersonIdentifierDefinition {
|
||||
id: number;
|
||||
type: "person_identifier_definition";
|
||||
engine: string;
|
||||
}
|
||||
|
||||
export interface ResidentialAddress {
|
||||
address: Address | null;
|
||||
endDate: DateTime | null;
|
||||
hostPerson: Person | null;
|
||||
hostThirdParty: Thirdparty | null;
|
||||
startDate: DateTime | null;
|
||||
}
|
||||
|
||||
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: PersonAltNameWrite[];
|
||||
addressId: 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 {
|
||||
@@ -329,22 +405,50 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
|
||||
workflows: object[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity types that a user can create through AddPersons component
|
||||
*/
|
||||
export type CreatableEntityType = "person" | "thirdparty";
|
||||
|
||||
/**
|
||||
* Entities that can be search and selected by a user
|
||||
*/
|
||||
export type EntityType =
|
||||
| CreatableEntityType
|
||||
| "user_group"
|
||||
| "user"
|
||||
| "person"
|
||||
| "thirdparty"
|
||||
| "household";
|
||||
|
||||
export type Entities = (UserGroup | User | Person | Thirdparty | Household) & {
|
||||
address?: Address | null;
|
||||
kind?: string;
|
||||
text?: string;
|
||||
profession?: string;
|
||||
};
|
||||
export type Entities = (UserGroup | User | Person | Thirdparty | Household);
|
||||
|
||||
export function isEntityHousehold(e: Entities): e is Household {
|
||||
return e.type === "household";
|
||||
}
|
||||
|
||||
export type EntitiesOrMe = "me" | Entities;
|
||||
|
||||
|
||||
// Type guards to discriminate Suggestions by their result kind
|
||||
export function isSuggestionForUserGroup(s: Suggestion): s is Suggestion & { result: UserGroup } {
|
||||
return (s as any)?.result?.type === "user_group";
|
||||
}
|
||||
|
||||
export function isSuggestionForUser(s: Suggestion): s is Suggestion & { result: User } {
|
||||
return (s as any)?.result?.type === "user";
|
||||
}
|
||||
|
||||
export function isSuggestionForPerson(s: Suggestion): s is Suggestion & { result: Person } {
|
||||
return (s as any)?.result?.type === "person";
|
||||
}
|
||||
|
||||
export function isSuggestionForThirdParty(s: Suggestion): s is Suggestion & { result: Thirdparty } {
|
||||
return (s as any)?.result?.type === "thirdparty";
|
||||
}
|
||||
|
||||
export function isSuggestionForHousehold(s: Suggestion): s is Suggestion & { result: Household } {
|
||||
return (s as any)?.result?.type === "household";
|
||||
}
|
||||
|
||||
export type AddPersonResult = Entities & {
|
||||
parent?: Entities | null;
|
||||
};
|
||||
@@ -352,9 +456,8 @@ export type AddPersonResult = Entities & {
|
||||
export interface Suggestion {
|
||||
key: string;
|
||||
relevance: number;
|
||||
result: AddPersonResult;
|
||||
result: Entities;
|
||||
}
|
||||
|
||||
export interface SearchPagination {
|
||||
first: number;
|
||||
items_per_page: number;
|
||||
@@ -366,12 +469,13 @@ export interface SearchPagination {
|
||||
export interface Search {
|
||||
count: number;
|
||||
pagination: SearchPagination;
|
||||
results: Suggestion[];
|
||||
results: {relevance: number, result: Entities}[];
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
uniq: boolean;
|
||||
type: string[];
|
||||
/** @deprecated */
|
||||
type: EntityType[];
|
||||
priority: number | null;
|
||||
button: {
|
||||
size: string;
|
||||
@@ -381,6 +485,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 {
|
||||
sta: number;
|
||||
txt: string;
|
||||
|
||||
@@ -322,7 +322,7 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
addNewPersons({ selected, modal }) {
|
||||
addNewPersons({ selected }) {
|
||||
//console.log('@@@ CLICK button addNewPersons', selected);
|
||||
this.$store
|
||||
.dispatch("addRequestor", selected.shift())
|
||||
@@ -337,7 +337,6 @@ export default {
|
||||
});
|
||||
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
},
|
||||
saveFormOnTheFly(payload) {
|
||||
console.log(
|
||||
|
||||
@@ -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,116 @@
|
||||
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import {Center, Civility, Gender, SetCenter} from "ChillMainAssets/types";
|
||||
import {
|
||||
AltName,
|
||||
Person, PersonIdentifier,
|
||||
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 personToWritePerson = (person: Person): PersonWrite => {
|
||||
return {
|
||||
type: "person",
|
||||
firstName: person.firstName,
|
||||
lastName: person.lastName,
|
||||
altNames: person.altNames.map((altName) => ({key: altName.key, value: altName.label})),
|
||||
addressId: null,
|
||||
birthdate: null === person.birthdate ? null : {datetime: person.birthdate.datetime8601},
|
||||
deathdate: null === person.deathdate ? null : {datetime: person.deathdate.datetime8601},
|
||||
phonenumber: person.phonenumber,
|
||||
mobilenumber: person.mobilenumber,
|
||||
center: null === person.centers ? null : person.centers
|
||||
.map((center): SetCenter => ({id: center.id, type: "center"}))
|
||||
.find(() => true) || null,
|
||||
email: person.email,
|
||||
civility: null === person.civility ? null : {id: person.civility.id, type: "chill_main_civility"},
|
||||
gender: null === person.gender ? null : {id: person.gender.id, type: "chill_main_gender"},
|
||||
identifiers: person.identifiers.map((identifier: PersonIdentifier) => ({type: "person_identifier", definition_id: identifier.definition.id, value: identifier.value})),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
};
|
||||
|
||||
export const editPerson = async (person: PersonWrite, personId: number): Promise<Person> => {
|
||||
return makeFetch<PersonWrite, Person, WritePersonViolationMap>(
|
||||
"PATCH",
|
||||
`/api/1.0/person/person/${personId}.json`,
|
||||
person,
|
||||
);
|
||||
}
|
||||
@@ -3,487 +3,242 @@
|
||||
class="btn"
|
||||
:class="getClassButton"
|
||||
:title="buttonTitle"
|
||||
@click="openModal"
|
||||
@click="openModalChoose"
|
||||
>
|
||||
<span v-if="displayTextButton">{{ buttonTitle }}</span>
|
||||
</a>
|
||||
|
||||
<teleport to="body">
|
||||
<modal
|
||||
v-if="showModal"
|
||||
@close="closeModal"
|
||||
:modal-dialog-class="modalDialogClass"
|
||||
:show="showModal"
|
||||
:hide-footer="false"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="modal-title">
|
||||
{{ modalTitle }}
|
||||
</h3>
|
||||
</template>
|
||||
<person-choose-modal
|
||||
v-if="showModalChoose"
|
||||
ref="personChooseModal"
|
||||
:modal-title="modalTitle"
|
||||
:options="options"
|
||||
:selected="selected"
|
||||
:modal-dialog-class="'modal-dialog-scrollable modal-xl'"
|
||||
:allow-create="props.allowCreate"
|
||||
@close="closeModalChoose"
|
||||
@onPickEntities="onPickEntities"
|
||||
@onAskForCreate="onAskForCreate"
|
||||
@triggerAddContact="triggerAddContact"
|
||||
@updateSelected="updateSelected"
|
||||
@cleanSelected="emptySelected"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<CreateModal
|
||||
v-if="creatableEntityTypes.length > 0 && showModalCreate && null == thirdPartyParentAddContact"
|
||||
action="create"
|
||||
:allowed-types="creatableEntityTypes"
|
||||
:query="query"
|
||||
:parent="null"
|
||||
modalTitle="test"
|
||||
@close="closeModalCreate"
|
||||
@onPersonCreated="onPersonCreated"
|
||||
@onThirdPartyCreated="onThirdPartyCreated"
|
||||
></CreateModal>
|
||||
|
||||
<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">
|
||||
<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
|
||||
class="btn btn-create"
|
||||
@click.prevent="
|
||||
() => {
|
||||
$emit('addNewPersons', {
|
||||
selected: selectedComputed,
|
||||
});
|
||||
query = '';
|
||||
closeModal();
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ trans(ACTION_ADD) }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
<CreateModal
|
||||
v-if="showModalCreate && thirdPartyParentAddContact !== null"
|
||||
:allowed-types="['thirdparty']"
|
||||
action="addContact"
|
||||
modalTitle="test"
|
||||
:parent="thirdPartyParentAddContact"
|
||||
:query="''"
|
||||
@close="closeModalCreate"
|
||||
@onPersonCreated="onPersonCreated"
|
||||
@onThirdPartyCreated="onThirdPartyCreated"
|
||||
></CreateModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import { ref, reactive, computed, nextTick, watch, shallowRef } from "vue";
|
||||
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 {
|
||||
import {ref, computed, nextTick, useTemplateRef} from "vue";
|
||||
import PersonChooseModal from "./AddPersons/PersonChooseModal.vue";
|
||||
import type {
|
||||
Suggestion,
|
||||
Search,
|
||||
AddPersonResult as OriginalResult,
|
||||
SearchOptions,
|
||||
CreatableEntityType,
|
||||
EntityType,
|
||||
Person,
|
||||
} from "ChillPersonAssets/types";
|
||||
import { marked } from "marked";
|
||||
import options = marked.options;
|
||||
import CreateModal from "ChillMainAssets/vuejs/OnTheFly/components/CreateModal.vue";
|
||||
import {Thirdparty, ThirdpartyCompany} from "../../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
// Extend Result type to include optional addressId
|
||||
type Result = OriginalResult & { addressId?: number };
|
||||
interface AddPersonsConfig {
|
||||
suggested?: Suggestion[];
|
||||
buttonTitle: string;
|
||||
modalTitle: string;
|
||||
options: SearchOptions;
|
||||
allowCreate?: boolean;
|
||||
types?: EntityType[] | undefined;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
suggested: { type: Array as () => Suggestion[], default: () => [] },
|
||||
selected: { type: Array as () => Suggestion[], default: () => [] },
|
||||
buttonTitle: { type: String, required: true },
|
||||
modalTitle: { type: String, required: true },
|
||||
options: { type: Object as () => SearchOptions, required: true },
|
||||
const props = withDefaults(defineProps<AddPersonsConfig>(), {
|
||||
suggested: () => [],
|
||||
allowCreate: () => true,
|
||||
types: () => ["person"],
|
||||
});
|
||||
|
||||
defineEmits(["addNewPersons"]);
|
||||
const emit =
|
||||
defineEmits<{
|
||||
(e: "addNewPersons", payload: { selected: Suggestion[] }): void;
|
||||
}
|
||||
>();
|
||||
|
||||
const showModal = ref(false);
|
||||
const modalDialogClass = ref("modal-dialog-scrollable modal-xl");
|
||||
type PersonChooseModalType = InstanceType<typeof PersonChooseModal>;
|
||||
const personChooseModal = useTemplateRef<PersonChooseModalType>('personChooseModal');
|
||||
|
||||
const modal = shallowRef({
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
});
|
||||
/**
|
||||
* Flag to show/hide the modal "choose".
|
||||
*/
|
||||
const showModalChoose = ref(false);
|
||||
|
||||
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>,
|
||||
});
|
||||
/**
|
||||
* Flag to show/hide the modal "create".
|
||||
*/
|
||||
const showModalCreate = ref(false);
|
||||
|
||||
const searchRef = ref<HTMLInputElement | null>(null);
|
||||
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
|
||||
/**
|
||||
* Store the previous search query, stored while going from "search" state to "create"
|
||||
*/
|
||||
const query = ref("");
|
||||
|
||||
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);
|
||||
/**
|
||||
* Temporarily store the thirdparty company when calling "addContact"
|
||||
*/
|
||||
const thirdPartyParentAddContact = ref<ThirdpartyCompany|null>(null);
|
||||
|
||||
/**
|
||||
* Contains the selected elements.
|
||||
*
|
||||
* If the property option.uniq is true, this will contains only one element.
|
||||
*
|
||||
* Suggestion must be added/removed using the @link{addSuggestionToSelected} and @link{removeSuggestionFromSelected}
|
||||
* methods.
|
||||
*/
|
||||
const selected = ref<Map<string, Suggestion>>(new Map());
|
||||
|
||||
const getClassButton = computed(() => {
|
||||
let size = props.options?.button?.size ?? "";
|
||||
let type = props.options?.button?.type ?? "btn-create";
|
||||
return size ? size + " " + type : type;
|
||||
const size = props.options?.button?.size ?? "";
|
||||
const type = props.options?.button?.type ?? "btn-create";
|
||||
return size ? `${size} ${type}` : type;
|
||||
});
|
||||
|
||||
const displayTextButton = computed(() =>
|
||||
props.options?.button?.display !== undefined
|
||||
? props.options.button.display
|
||||
: true,
|
||||
);
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
const creatableEntityTypes = computed<CreatableEntityType[]>(() => {
|
||||
if (typeof props.options.type !== "undefined") {
|
||||
return props.options.type.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);
|
||||
return props.types.filter(
|
||||
(e: EntityType) => e === "thirdparty" || e === "person",
|
||||
);
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
showModal.value = true;
|
||||
nextTick(() => {
|
||||
if (searchRef.value) searchRef.value.focus();
|
||||
});
|
||||
}
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
function onAskForCreate(payload: { query: string }): void {
|
||||
query.value = payload.query;
|
||||
closeModalChoose();
|
||||
showModalCreate.value = true;
|
||||
}
|
||||
|
||||
function setQuery(q: string) {
|
||||
search.query = q;
|
||||
function openModalChoose(): void {
|
||||
showModalChoose.value = true;
|
||||
}
|
||||
|
||||
// Clear previous search if any
|
||||
if (search.currentSearchQueryController) {
|
||||
search.currentSearchQueryController.abort();
|
||||
search.currentSearchQueryController = null;
|
||||
function closeModalChoose(): void {
|
||||
showModalChoose.value = false;
|
||||
}
|
||||
|
||||
function closeModalCreate(): void {
|
||||
if (null !== thirdPartyParentAddContact) {
|
||||
thirdPartyParentAddContact.value = 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);
|
||||
showModalCreate.value = false;
|
||||
}
|
||||
|
||||
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) {
|
||||
let suggestion = {
|
||||
key: entity.type + entity.id,
|
||||
relevance: 0.5,
|
||||
result: entity,
|
||||
};
|
||||
search.priorSuggestion = suggestion;
|
||||
/**
|
||||
* Called by PersonSuggestion's updateSelection event, when an element is checked/unchecked
|
||||
*/
|
||||
function updateSelected(payload: {suggestion: Suggestion, isSelected: boolean}): void {
|
||||
if (payload.isSelected) {
|
||||
addSuggestionToSelected(payload.suggestion);
|
||||
} else {
|
||||
search.priorSuggestion = {};
|
||||
removeSuggestionFromSelected(payload.suggestion);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
function addSuggestionToSelected(suggestion: Suggestion): void {
|
||||
if (props.options.uniq) {
|
||||
selected.value.clear();
|
||||
}
|
||||
selected.value.set(suggestion.key, suggestion);
|
||||
}
|
||||
|
||||
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);
|
||||
function removeSuggestionFromSelected(suggestion: Suggestion): void {
|
||||
selected.value.delete(suggestion.key);
|
||||
}
|
||||
|
||||
function emptySelected(): void {
|
||||
selected.value = new Map();
|
||||
}
|
||||
|
||||
function onPickEntities(): void {
|
||||
const alls = Array.from(selected.value.values());
|
||||
emit("addNewPersons", { selected: alls });
|
||||
closeModalChoose();
|
||||
}
|
||||
|
||||
function triggerAddContact({parent}: {parent: ThirdpartyCompany}): void {
|
||||
closeModalChoose();
|
||||
openModalChoose();
|
||||
thirdPartyParentAddContact.value = parent;
|
||||
showModalCreate.value = true;
|
||||
}
|
||||
|
||||
function onPersonCreated(payload: { person: Person }): void {
|
||||
showModalCreate.value = false;
|
||||
const suggestion = {
|
||||
result: payload.person,
|
||||
relevance: 999999,
|
||||
key: "person",
|
||||
};
|
||||
addSuggestionToSelected(suggestion);
|
||||
if (props.options.uniq) {
|
||||
emit("addNewPersons", { selected: [suggestion] });
|
||||
} else {
|
||||
openModalChoose();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
(newSelected) => {
|
||||
search.selected = newSelected;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
function onThirdPartyCreated(payload: {thirdParty: Thirdparty}): void {
|
||||
showModalCreate.value = false;
|
||||
const suggestion = {
|
||||
result: payload.thirdParty,
|
||||
relevance: 999999,
|
||||
key: "thirdparty",
|
||||
};
|
||||
addSuggestionToSelected(suggestion);
|
||||
if (props.options.uniq) {
|
||||
emit("addNewPersons", { selected: [suggestion] });
|
||||
} else {
|
||||
openModalChoose();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.suggested,
|
||||
(newSuggested) => {
|
||||
search.suggested = newSuggested;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
function resetSearch(): void {
|
||||
selected.value = new Map();
|
||||
personChooseModal.value?.resetSearch();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => modal,
|
||||
(val) => {
|
||||
showModal.value = val.value.showModal;
|
||||
modalDialogClass.value = val.value.modalDialogClass;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
resetSearch,
|
||||
showModal,
|
||||
});
|
||||
defineExpose({resetSearch})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
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;
|
||||
//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 lang="scss" scoped>
|
||||
/* Button styles can remain here if needed */
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
<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"
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:isSelected="item.isSelected"
|
||||
:type="checkUniq"
|
||||
@update-selected="(payload) => emit('updateSelected', payload)"
|
||||
@trigger-add-contact="triggerAddContact"
|
||||
/>
|
||||
|
||||
<div v-if="hasNoResult">
|
||||
<div class="noResult chill-no-data-statement">
|
||||
{{
|
||||
trans(ADD_PERSONS_SUGGESTED_COUNTER, {
|
||||
count: suggestedCounter,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="props.allowCreate && query.length > 0"
|
||||
class="create-button"
|
||||
>
|
||||
<button type="button" class="btn btn-submit" @click="emit('onAskForCreate', { query })">
|
||||
{{ trans(ONTHEFLY_CREATE_BUTTON, { q: query }) }}
|
||||
</button>
|
||||
</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 { searchEntities } from "ChillPersonAssets/vuejs/_api/AddPersons";
|
||||
|
||||
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,
|
||||
SearchOptions,
|
||||
Entities,
|
||||
} from "ChillPersonAssets/types";
|
||||
import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
interface Props {
|
||||
modalTitle: string;
|
||||
options: SearchOptions;
|
||||
suggested?: Suggestion[];
|
||||
selected: Map<string, Suggestion>;
|
||||
modalDialogClass?: string;
|
||||
allowCreate?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
suggested: () => [],
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
allowCreate: () => true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "onPickEntities"): void;
|
||||
(e: "onAskForCreate", payload: { query: string }): void;
|
||||
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
|
||||
(e: "updateSelected", payload: {suggestion: Suggestion, isSelected: boolean}): void;
|
||||
(e: "cleanSelected"): void;
|
||||
}>();
|
||||
|
||||
const searchRef = ref<HTMLInputElement | 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,
|
||||
priorSuggestion: {} as Partial<Suggestion>,
|
||||
hasPreviousQuery: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Contains the suggested entities from the search results.
|
||||
*
|
||||
* In other words, those entities are displayed and selectable by the user
|
||||
*/
|
||||
const suggested = ref<Suggestion[]>([]);
|
||||
|
||||
const query = computed({
|
||||
get: () => search.query,
|
||||
set: (val: string) => setQuery(val),
|
||||
});
|
||||
const queryLength = computed(() => search.query.length);
|
||||
const suggestedCounter = computed(() => suggested.value.length);
|
||||
const selectedCounter = computed(() => props.selected.size);
|
||||
|
||||
const checkUniq = computed(() =>
|
||||
props.options.uniq ? "radio" : "checkbox",
|
||||
);
|
||||
|
||||
const selectedAndSuggested = computed<(Suggestion & {isSelected: boolean})[]>(() => {
|
||||
const selectedAndSuggested = [];
|
||||
|
||||
// add selected that are not in the search results
|
||||
for (const selected of props.selected.values()) {
|
||||
if (!suggested.value.some((s: Suggestion) => s.key === selected.key)) {
|
||||
selectedAndSuggested.push({...selected, isSelected: false});
|
||||
}
|
||||
}
|
||||
for (const suggestion of suggested.value) {
|
||||
selectedAndSuggested.push({...suggestion, isSelected: props.selected.has(suggestion.key)})
|
||||
}
|
||||
|
||||
return selectedAndSuggested;
|
||||
});
|
||||
|
||||
const hasNoResult = computed(() => search.hasPreviousQuery && suggested.value.length === 0);
|
||||
|
||||
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);
|
||||
search.hasPreviousQuery = true;
|
||||
})
|
||||
.catch((error: DOMException) => {
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
|
||||
|
||||
function loadSuggestions(suggestedArr: {relevance: number, result: Entities}[]): void {
|
||||
suggested.value = suggestedArr.map((item) => {
|
||||
return {
|
||||
key: item.result.type + item.result.id,
|
||||
relevance: item.relevance,
|
||||
result: item.result
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetSuggestion() {
|
||||
search.query = "";
|
||||
suggested.value = [];
|
||||
}
|
||||
|
||||
function resetSelection() {
|
||||
emit("cleanSelected");
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
resetSelection();
|
||||
resetSuggestion();
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
suggested.value.forEach((suggestion: Suggestion) => {
|
||||
emit("updateSelected", {suggestion, isSelected: true})
|
||||
});
|
||||
}
|
||||
|
||||
function triggerAddContact(payload: {parent: ThirdpartyCompany}) {
|
||||
emit("triggerAddContact", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when the user clicks on the "add" button.
|
||||
*/
|
||||
function pickEntities(): void {
|
||||
emit("onPickEntities", );
|
||||
search.query = "";
|
||||
emit("close");
|
||||
}
|
||||
|
||||
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 > button {
|
||||
margin-top: 0.5em;
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
.noResult {
|
||||
text-align: center;
|
||||
margin: 2em;
|
||||
font-size: large;
|
||||
}
|
||||
</style>
|
||||
@@ -1,39 +1,38 @@
|
||||
<template>
|
||||
<div class="list-item" :class="{ checked: isChecked }">
|
||||
<div class="list-item" :class="{ checked: props.isSelected }">
|
||||
<label>
|
||||
<div>
|
||||
<input
|
||||
:type="type"
|
||||
v-model="selected"
|
||||
:value="props.item.key"
|
||||
name="item"
|
||||
:id="item.key"
|
||||
:value="setValueByType(item, type)"
|
||||
:id="props.item.key"
|
||||
:checked="props.isSelected"
|
||||
@click="onUpdateValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<suggestion-person
|
||||
v-if="item.result.type === 'person'"
|
||||
v-if="isSuggestionForPerson(item)"
|
||||
:item="item"
|
||||
></suggestion-person>
|
||||
|
||||
<suggestion-third-party
|
||||
v-if="item.result.type === 'thirdparty'"
|
||||
@newPriorSuggestion="newPriorSuggestion"
|
||||
v-if="isSuggestionForThirdParty(item)"
|
||||
@trigger-add-contact="triggerAddContact"
|
||||
:item="item"
|
||||
></suggestion-third-party>
|
||||
|
||||
<suggestion-user
|
||||
v-if="item.result.type === 'user'"
|
||||
v-if="isSuggestionForUser(item)"
|
||||
:item="item"
|
||||
></suggestion-user>
|
||||
|
||||
<suggestion-user-group
|
||||
v-if="item.result.type === 'user_group'"
|
||||
v-if="isSuggestionForUserGroup(item)"
|
||||
:item="item"
|
||||
></suggestion-user-group>
|
||||
|
||||
<suggestion-household
|
||||
v-if="item.result.type === 'household'"
|
||||
v-if="isSuggestionForHousehold(item)"
|
||||
:item="item"
|
||||
></suggestion-household>
|
||||
</label>
|
||||
@@ -51,31 +50,38 @@ import SuggestionHousehold from "./TypeHousehold.vue";
|
||||
import SuggestionUserGroup from "./TypeUserGroup.vue";
|
||||
|
||||
// Types
|
||||
import { Result, Suggestion } from "ChillPersonAssets/types";
|
||||
import {
|
||||
isSuggestionForHousehold,
|
||||
isSuggestionForPerson,
|
||||
isSuggestionForThirdParty, isSuggestionForUser,
|
||||
isSuggestionForUserGroup,
|
||||
Suggestion
|
||||
} from "ChillPersonAssets/types";
|
||||
import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
const props = defineProps<{
|
||||
item: Suggestion;
|
||||
search: { selected: Suggestion[] };
|
||||
type: string;
|
||||
isSelected: boolean;
|
||||
type: "radio"|"checkbox";
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "updateSelected", payload: {suggestion: Suggestion, isSelected: boolean}): void;
|
||||
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
|
||||
}>();
|
||||
const emit = defineEmits(["updateSelected", "newPriorSuggestion"]);
|
||||
|
||||
// v-model for selected
|
||||
const selected = computed({
|
||||
get: () => props.search.selected,
|
||||
set: (value) => emit("updateSelected", value),
|
||||
});
|
||||
const isChecked = computed<boolean>(() => props.isSelected)
|
||||
|
||||
const isChecked = computed(
|
||||
() => props.search.selected.indexOf(props.item) !== -1,
|
||||
);
|
||||
|
||||
function setValueByType(value: Suggestion, type: string) {
|
||||
return type === "radio" ? [value] : value;
|
||||
const onUpdateValue = (event: Event) => {
|
||||
const target = event?.target;
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
console.error("the value of checked is not an HTMLInputElement");
|
||||
return;
|
||||
}
|
||||
emit("updateSelected", {suggestion: props.item, isSelected: props.type === "radio" ? true : target.checked});
|
||||
}
|
||||
|
||||
function newPriorSuggestion(response: Result) {
|
||||
emit("newPriorSuggestion", response);
|
||||
function triggerAddContact(payload: {parent: ThirdpartyCompany}) {
|
||||
emit("triggerAddContact", payload);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ import { defineProps } from "vue";
|
||||
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
||||
import HouseholdRenderBox from "ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue";
|
||||
import { Suggestion } from "ChillPersonAssets/types";
|
||||
import {Household} from "ChillMainAssets/types";
|
||||
|
||||
interface TypeHouseholdProps {
|
||||
item: Suggestion;
|
||||
item: Suggestion & {result: Household};
|
||||
}
|
||||
|
||||
defineProps<TypeHouseholdProps>();
|
||||
|
||||
@@ -23,7 +23,7 @@ import { computed, defineProps } from "vue";
|
||||
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
|
||||
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
||||
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
|
||||
import { Person } from "ChillPersonAssets/types";
|
||||
import {Person, Suggestion} from "ChillPersonAssets/types";
|
||||
|
||||
function formatDate(dateString: string | undefined, format: string) {
|
||||
if (!dateString) return "";
|
||||
@@ -36,9 +36,7 @@ function formatDate(dateString: string | undefined, format: string) {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
result: Person; // add other fields as needed
|
||||
};
|
||||
item: Suggestion & { result: Person },
|
||||
}>();
|
||||
|
||||
const hasBirthdate = computed(() => props.item.result.birthdate !== null);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="container tpartycontainer">
|
||||
<div class="tparty-identification">
|
||||
<span v-if="item.result.profession" class="profession">{{
|
||||
<span v-if="(isThirdpartyChild(item.result) || isThirdpartyContact(item.result)) && item.result.profession" class="profession">{{
|
||||
item.result.profession
|
||||
}}</span>
|
||||
<span class="name"> {{ item.result.text }} </span>
|
||||
@@ -12,20 +12,18 @@
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tpartyparent" v-if="hasParent">
|
||||
<span class="name"> > {{ item.result.parent?.text }} </span>
|
||||
<div class="tpartyparent" v-if="isThirdpartyChild(item.result) && null !== item.result.parent">
|
||||
<span class="name"> > {{ item.result.parent.text }} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right_actions">
|
||||
<badge-entity :entity="item.result" :options="{ displayLong: true }" />
|
||||
<on-the-fly
|
||||
v-if="item.result.kind === 'company'"
|
||||
:parent="item.result"
|
||||
@save-form-on-the-fly="saveFormOnTheFly"
|
||||
action="addContact"
|
||||
ref="onTheFly"
|
||||
/>
|
||||
<a
|
||||
v-if="item.result.type === 'thirdparty' && item.result.kind === 'company'"
|
||||
class="btn btn-tpchild"
|
||||
@click="emit('triggerAddContact', {parent: item.result})"
|
||||
><i class="bi bi-person-fill-add"></i></a>
|
||||
<on-the-fly type="thirdparty" :id="item.result.id" action="show" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -37,15 +35,19 @@ import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { Result, Suggestion } from "ChillPersonAssets/types";
|
||||
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
|
||||
import {
|
||||
isThirdpartyChild,
|
||||
isThirdpartyContact,
|
||||
Thirdparty, ThirdpartyCompany
|
||||
} from "./../../../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||
|
||||
interface TypeThirdPartyProps {
|
||||
item: Suggestion;
|
||||
item: Suggestion & {result: Thirdparty};
|
||||
}
|
||||
|
||||
const props = defineProps<TypeThirdPartyProps>();
|
||||
|
||||
const emit = defineEmits<(e: "newPriorSuggestion", payload: unknown) => void>();
|
||||
const emit = defineEmits<(e: "triggerAddContact", payload: {parent: ThirdpartyCompany}) => void>();
|
||||
|
||||
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
|
||||
const toast = useToast();
|
||||
@@ -54,47 +56,23 @@ const hasAddress = computed(() => {
|
||||
if (props.item.result.address !== null) {
|
||||
return true;
|
||||
}
|
||||
if (props.item.result.parent !== null) {
|
||||
if (props.item.result.parent) {
|
||||
return props.item.result.parent.address !== null;
|
||||
}
|
||||
if (isThirdpartyChild(props.item.result) && props.item.result.parent !== null) {
|
||||
return props.item.result.parent.address !== null;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const hasParent = computed(() => {
|
||||
return props.item.result.parent !== null;
|
||||
return false;
|
||||
});
|
||||
|
||||
const getAddress = computed(() => {
|
||||
if (props.item.result.address !== null) {
|
||||
return props.item.result.address;
|
||||
}
|
||||
if (props.item.result.parent && props.item.result.parent.address !== null) {
|
||||
if (isThirdpartyChild(props.item.result) && props.item.result.parent !== null && props.item.result.parent.address !== null) {
|
||||
return props.item.result.parent.address;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
function saveFormOnTheFly({ data }: { data: Thirdparty }) {
|
||||
makeFetch("POST", "/api/1.0/thirdparty/thirdparty.json", data)
|
||||
.then((response: unknown) => {
|
||||
const result = response as Result;
|
||||
emit("newPriorSuggestion", result);
|
||||
if (onTheFly.value) onTheFly.value.closeModal();
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const errorResponse = error as { name: string; violations: string[] };
|
||||
if (errorResponse.name === "ValidationException") {
|
||||
for (let v of errorResponse.violations) {
|
||||
if (toast) toast.open({ message: v });
|
||||
}
|
||||
} else {
|
||||
if (toast) toast.open({ message: "An error occurred" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// i18n config (if needed elsewhere)
|
||||
const i18n = {
|
||||
messages: {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="container usercontainer">
|
||||
<div class="user-identification">
|
||||
<UserRenderBoxBadge :user="item.result" />
|
||||
<UserRenderBoxBadge :user="props.item.result" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="right_actions">
|
||||
<BadgeEntity :entity="item.result" :options="{ displayLong: true }" />
|
||||
<BadgeEntity :entity="props.item.result" :options="{ displayLong: true }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -14,18 +14,14 @@ import { computed, defineProps } from "vue";
|
||||
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||
import { Suggestion } from "ChillPersonAssets/types";
|
||||
import {User} from "ChillMainAssets/types";
|
||||
|
||||
interface TypeUserProps {
|
||||
item: Suggestion;
|
||||
item: Suggestion & {result: User};
|
||||
}
|
||||
|
||||
const props = defineProps<TypeUserProps>();
|
||||
|
||||
const hasParent = computed(() => props.item.result.parent !== null);
|
||||
|
||||
defineExpose({
|
||||
hasParent,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -15,7 +15,7 @@ import UserGroupRenderBox from "ChillMainAssets/vuejs/_components/Entity/UserGro
|
||||
import { Suggestion } from "ChillPersonAssets/types";
|
||||
|
||||
interface TypeUserGroupProps {
|
||||
item: Suggestion;
|
||||
item: Suggestion & {result: UserGroup};
|
||||
}
|
||||
|
||||
const props = defineProps<TypeUserGroupProps>();
|
||||
|
||||
@@ -5,46 +5,16 @@
|
||||
<div class="item-col">
|
||||
<div class="entity-label">
|
||||
<div :class="'denomination h' + options.hLevel">
|
||||
<a v-if="options.addLink === true" :href="getUrl">
|
||||
<!-- 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.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.altNames && options.addAltNames == true"
|
||||
class="altnames"
|
||||
>
|
||||
<span :class="'altname altname-' + altNameKey">{{
|
||||
altNameLabel
|
||||
}}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="options.addId == true"
|
||||
class="id-number"
|
||||
:title="'n° ' + person.id"
|
||||
>{{ person.id }}</span
|
||||
>
|
||||
|
||||
<template v-if="options.addLink === true">
|
||||
<a v-if="options.addLink === true" :href="getUrl">
|
||||
<span>{{ person.text }}</span>
|
||||
<span v-if="person.deathdate" class="deathdate"> (‡)</span>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ person.text }}</span>
|
||||
<span v-if="person.deathdate" class="deathdate"> (‡)</span>
|
||||
</template>
|
||||
<badge-entity
|
||||
v-if="options.addEntity === true"
|
||||
:entity="person"
|
||||
@@ -52,61 +22,36 @@
|
||||
/>
|
||||
</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">
|
||||
<gender-icon-render-box
|
||||
v-if="person.gender"
|
||||
:gender="person.gender"
|
||||
/>
|
||||
<time
|
||||
v-if="person.birthdate && !person.deathdate"
|
||||
:datetime="person.birthdate"
|
||||
:title="birthdate"
|
||||
/> <span
|
||||
v-if="person.birthdate"
|
||||
>
|
||||
{{
|
||||
trans(birthdateTranslation) +
|
||||
" " +
|
||||
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>
|
||||
|
||||
{{ trans(RENDERBOX_BIRTHDAY_STATEMENT, {gender: toGenderTranslation(person.gender), birthdate: ISOToDate(person.birthdate?.datetime)}) }}
|
||||
</span>
|
||||
<span v-if="options.addAge && person.birthdate" class="age">
|
||||
({{ trans(RENDERBOX_YEARS_OLD, { n: person.age }) }})
|
||||
({{ trans(RENDERBOX_YEARS_OLD, {n: person.age}) }})
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span
|
||||
v-if="person.deathdate"
|
||||
>
|
||||
{{ trans(RENDERBOX_DEATHDATE_STATEMENT, {gender: toGenderTranslation(person.gender), deathdate: ISOToDate(person.deathdate?.datetime)}) }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,11 +59,11 @@
|
||||
<div class="float-button bottom">
|
||||
<div class="box">
|
||||
<div class="action">
|
||||
<slot name="record-actions" />
|
||||
<slot name="record-actions"/>
|
||||
</div>
|
||||
<ul class="list-content fa-ul">
|
||||
<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
|
||||
v-if="person.current_household_address"
|
||||
:address="person.current_household_address"
|
||||
@@ -130,11 +75,6 @@
|
||||
<a
|
||||
v-if="options.addHouseholdLink === true"
|
||||
:href="getCurrentHouseholdUrl"
|
||||
:title="
|
||||
trans(PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER, {
|
||||
id: person.current_household_id,
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="badge rounded-pill bg-chill-beige">
|
||||
<i
|
||||
@@ -144,7 +84,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<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">
|
||||
{{ trans(RENDERBOX_NO_DATA) }}
|
||||
</p>
|
||||
@@ -160,7 +100,7 @@
|
||||
v-for="(addr, i) in person.current_residential_addresses"
|
||||
:key="i"
|
||||
>
|
||||
<i class="fa fa-li fa-map-marker" />
|
||||
<i class="fa fa-li fa-map-marker"/>
|
||||
<div v-if="addr.address">
|
||||
<span class="item-key">
|
||||
{{ trans(RENDERBOX_RESIDENTIAL_ADDRESS) }}:
|
||||
@@ -180,9 +120,10 @@
|
||||
:person="addr.hostPerson"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<address-render-box
|
||||
v-if="addr.hostPerson.address"
|
||||
:address="addr.hostPerson.address"
|
||||
v-if="addr.hostPerson?.current_household_address"
|
||||
:address="addr.hostPerson.current_household_address"
|
||||
:is-multiline="isMultiline"
|
||||
/>
|
||||
</div>
|
||||
@@ -204,36 +145,36 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<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">
|
||||
{{ trans(RENDERBOX_NO_DATA) }}
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li v-if="person.mobilenumber">
|
||||
<i class="fa fa-li fa-mobile" />
|
||||
<i class="fa fa-li fa-mobile"/>
|
||||
<a :href="'tel: ' + person.mobilenumber">
|
||||
{{ person.mobilenumber }}
|
||||
</a>
|
||||
</li>
|
||||
<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">
|
||||
{{ trans(RENDERBOX_NO_DATA) }}
|
||||
</p>
|
||||
</li>
|
||||
<li v-if="person.phonenumber">
|
||||
<i class="fa fa-li fa-phone" />
|
||||
<i class="fa fa-li fa-phone"/>
|
||||
<a :href="'tel: ' + person.phonenumber">
|
||||
{{ person.phonenumber }}
|
||||
</a>
|
||||
</li>
|
||||
<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">
|
||||
{{ trans(RENDERBOX_NO_DATA) }}
|
||||
</p>
|
||||
@@ -246,25 +187,25 @@
|
||||
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">
|
||||
{{ c.name }}
|
||||
</template>
|
||||
</li>
|
||||
<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">
|
||||
{{ trans(RENDERBOX_NO_DATA) }}
|
||||
</p>
|
||||
</li>
|
||||
<slot name="custom-zone" />
|
||||
<slot name="custom-zone"/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="end-bloc" />
|
||||
<slot name="end-bloc"/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -278,11 +219,11 @@
|
||||
class="fa-stack fa-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>
|
||||
</span>
|
||||
|
||||
<person-text :person="person" />
|
||||
<person-text :person="person"/>
|
||||
</a>
|
||||
<span v-else>
|
||||
<span
|
||||
@@ -290,18 +231,18 @@
|
||||
class="fa-stack fa-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>
|
||||
</span>
|
||||
<person-text :person="person" />
|
||||
<person-text :person="person"/>
|
||||
</span>
|
||||
<slot name="post-badge" />
|
||||
<slot name="post-badge"/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { ISOToDate } from "ChillMainAssets/chill/js/date";
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
import {ISOToDate} from "ChillMainAssets/chill/js/date";
|
||||
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
|
||||
import GenderIconRenderBox from "ChillMainAssets/vuejs/_components/Entity/GenderIconRenderBox.vue";
|
||||
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
|
||||
@@ -311,108 +252,70 @@ import {
|
||||
trans,
|
||||
RENDERBOX_HOLDER,
|
||||
RENDERBOX_NO_DATA,
|
||||
RENDERBOX_DEATHDATE,
|
||||
RENDERBOX_DEATHDATE_STATEMENT,
|
||||
RENDERBOX_HOUSEHOLD_WITHOUT_ADDRESS,
|
||||
RENDERBOX_RESIDENTIAL_ADDRESS,
|
||||
RENDERBOX_LOCATED_AT,
|
||||
RENDERBOX_BIRTHDAY_MAN,
|
||||
RENDERBOX_BIRTHDAY_WOMAN,
|
||||
RENDERBOX_BIRTHDAY_UNKNOWN,
|
||||
RENDERBOX_BIRTHDAY_NEUTRAL,
|
||||
PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
|
||||
RENDERBOX_BIRTHDAY_STATEMENT,
|
||||
// PERSONS_ASSOCIATED_SHOW_HOUSEHOLD_NUMBER,
|
||||
RENDERBOX_YEARS_OLD,
|
||||
} from "translator";
|
||||
import {Person} from "ChillPersonAssets/types";
|
||||
import {toGenderTranslation} from "ChillMainAssets/lib/api/genderHelper";
|
||||
|
||||
const props = defineProps({
|
||||
person: {
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
render: {
|
||||
type: String,
|
||||
},
|
||||
returnPath: {
|
||||
type: String,
|
||||
},
|
||||
showResidentialAddresses: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
interface RenderOptions {
|
||||
addInfo?: boolean;
|
||||
addEntity?: boolean;
|
||||
addAltNames?: boolean;
|
||||
addAge?: boolean;
|
||||
addId?: boolean;
|
||||
addLink?: boolean;
|
||||
hLevel?: number;
|
||||
entityDisplayLong?: boolean;
|
||||
addCenter?: boolean;
|
||||
addNoData?: boolean;
|
||||
isMultiline?: boolean;
|
||||
isHolder?: boolean;
|
||||
addHouseholdLink?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
person: Person;
|
||||
options?: RenderOptions;
|
||||
render?: "bloc" | "badge";
|
||||
returnPath?: string;
|
||||
showResidentialAddresses?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
render: "bloc",
|
||||
options: () => ({
|
||||
addInfo: true,
|
||||
addEntity: false,
|
||||
addAltNames: true,
|
||||
addAge: true,
|
||||
addId: true,
|
||||
addLink: false,
|
||||
hLevel: 3,
|
||||
entityDisplayLong: true,
|
||||
addCenter: true,
|
||||
addNoData: true,
|
||||
isMultiline: true,
|
||||
isHolder: false,
|
||||
addHouseholdLink: true
|
||||
}),
|
||||
});
|
||||
|
||||
const birthdateTranslation = computed(() => {
|
||||
if (props.person.gender) {
|
||||
const { genderTranslation } = props.person.gender;
|
||||
switch (genderTranslation) {
|
||||
case "man":
|
||||
return RENDERBOX_BIRTHDAY_MAN;
|
||||
case "woman":
|
||||
return RENDERBOX_BIRTHDAY_WOMAN;
|
||||
case "neutral":
|
||||
return RENDERBOX_BIRTHDAY_NEUTRAL;
|
||||
case "unknown":
|
||||
return RENDERBOX_BIRTHDAY_UNKNOWN;
|
||||
default:
|
||||
return RENDERBOX_BIRTHDAY_UNKNOWN;
|
||||
}
|
||||
} else {
|
||||
return RENDERBOX_BIRTHDAY_UNKNOWN;
|
||||
}
|
||||
});
|
||||
|
||||
const isMultiline = computed(() => {
|
||||
const isMultiline = computed<boolean>(() => {
|
||||
return props.options?.isMultiline || false;
|
||||
});
|
||||
|
||||
const birthdate = computed(() => {
|
||||
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(() => {
|
||||
const getUrl = computed<string>(() => {
|
||||
return `/fr/person/${props.person.id}/general`;
|
||||
});
|
||||
|
||||
const getCurrentHouseholdUrl = computed(() => {
|
||||
let returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
|
||||
const getCurrentHouseholdUrl = computed<string>(() => {
|
||||
const returnPath = props.returnPath ? `?returnPath=${props.returnPath}` : ``;
|
||||
return `/fr/person/household/${props.person.current_household_id}/summary${returnPath}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<span v-if="isCut">{{ cutText }}</span>
|
||||
<span v-else class="person-text">
|
||||
<span class="firstname">{{ person.firstName }}</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>{{ person.text }}</span>
|
||||
<span v-if="person.suffixText" class="suffixtext"
|
||||
> {{ person.suffixText }}</span
|
||||
>
|
||||
@@ -33,16 +27,6 @@ const props = defineProps<{
|
||||
|
||||
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(() => {
|
||||
if (!person.value.text) return "";
|
||||
const more = person.value.text.length > 15 ? "…" : "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="action === 'show'">
|
||||
<div v-if="action === 'show' && person !== null">
|
||||
<div class="flex-table">
|
||||
<person-render-box
|
||||
render="bloc"
|
||||
@@ -21,446 +21,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="action === 'edit' || action === 'create'">
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="lastname"
|
||||
v-model="lastName"
|
||||
: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 v-else-if="props.action === 'edit' || props.action === 'create'">
|
||||
<PersonEdit
|
||||
:id="props.id"
|
||||
:type="props.type"
|
||||
:action="props.action"
|
||||
:query="props.query"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import {
|
||||
getCentersForPersonCreation,
|
||||
getCivilities,
|
||||
getGenders,
|
||||
getPerson,
|
||||
getPersonAltNames,
|
||||
} from "../../_api/OnTheFly";
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getPerson } from "../../_api/OnTheFly";
|
||||
import PersonRenderBox from "../Entity/PersonRenderBox.vue";
|
||||
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
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";
|
||||
import PersonEdit from "./PersonEdit.vue";
|
||||
import type { Person } from "ChillPersonAssets/types";
|
||||
|
||||
const props = defineProps({
|
||||
id: [String, Number],
|
||||
type: String,
|
||||
action: 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é");
|
||||
}
|
||||
interface Props {
|
||||
id: number;
|
||||
type?: string;
|
||||
action: "show" | "edit" | "create";
|
||||
query?: string;
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
getPerson(props.id).then((p) => {
|
||||
Object.assign(person, p);
|
||||
});
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {query: ""});
|
||||
|
||||
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;
|
||||
}
|
||||
const person = ref<Person | null>(null);
|
||||
|
||||
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(() => {
|
||||
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") {
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
<template>
|
||||
<div v-if="action === 'create' || (action === 'edit' && dataLoaded)">
|
||||
<div class="mb-3">
|
||||
<div class="input-group has-validation">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': violations.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 violations.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': violations.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 violations.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>
|
||||
|
||||
<template v-if="action === 'create'">
|
||||
<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>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-if="action === 'create'">
|
||||
<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': violations.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 violations.violationTitlesWithParameter('identifiers', 'definition_id', worker.definition_id.toString())"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="input-group has-validation">
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
:class="{ 'is-invalid': violations.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 violations.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': violations.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 violations.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': violations.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 violations.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': violations.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 violations.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': violations.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 violations.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': violations.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 violations.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': violations.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 violations.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>
|
||||
<div v-else>
|
||||
|
||||
</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, editPerson,
|
||||
getCentersForPersonCreation,
|
||||
getCivilities,
|
||||
getGenders,
|
||||
getPerson,
|
||||
getPersonAltNames,
|
||||
getPersonIdentifiers,
|
||||
personToWritePerson,
|
||||
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,
|
||||
} from "ChillMainAssets/types";
|
||||
import {
|
||||
AltName,
|
||||
Person,
|
||||
PersonWrite,
|
||||
PersonIdentifierWorker,
|
||||
} from "ChillPersonAssets/types";
|
||||
import {
|
||||
isValidationException,
|
||||
} from "ChillMainAssets/lib/api/apiMethods";
|
||||
import {useToast} from "vue-toast-notification";
|
||||
import {getTimezoneOffsetString, ISOToDate} from "ChillMainAssets/chill/js/date";
|
||||
import {useViolationList} from "ChillMainAssets/vuejs/_composables/violationList";
|
||||
|
||||
interface PersonEditComponentConfig {
|
||||
id?: number | null;
|
||||
action: "edit" | "create";
|
||||
query: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<PersonEditComponentConfig>(), {
|
||||
id: null,
|
||||
});
|
||||
|
||||
const emit =
|
||||
defineEmits<(e: "onPersonCreated", payload: { person: Person }) => void>();
|
||||
|
||||
defineExpose({ postPerson });
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const person = reactive<PersonWrite>({
|
||||
type: "person",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
altNames: [],
|
||||
addressId: 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 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()));
|
||||
});
|
||||
|
||||
const dataLoaded = ref<boolean>(false);
|
||||
|
||||
async function loadData() {
|
||||
if (props.id !== undefined && props.id !== null) {
|
||||
const p = await getPerson(props.id);
|
||||
const w = personToWritePerson(p);
|
||||
person.firstName = w.firstName;
|
||||
person.lastName = w.lastName;
|
||||
person.altNames.push(...w.altNames)
|
||||
person.civility = w.civility;
|
||||
person.addressId = w.addressId;
|
||||
person.birthdate = w.birthdate;
|
||||
person.deathdate = w.deathdate;
|
||||
person.phonenumber = w.phonenumber;
|
||||
person.mobilenumber = w.mobilenumber;
|
||||
person.email = w.email;
|
||||
person.gender = w.gender;
|
||||
person.center = w.center;
|
||||
person.civility = w.civility;
|
||||
person.identifiers.push(...w.identifiers);
|
||||
dataLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const violations = useViolationList<WritePersonViolationMap>();
|
||||
|
||||
function submitNewAddress(payload: { addressId: number }) {
|
||||
person.addressId = payload.addressId;
|
||||
}
|
||||
|
||||
async function postPerson(): Promise<Person> {
|
||||
try {
|
||||
if (props.action === 'create') {
|
||||
const createdPerson = await createPerson(person);
|
||||
emit("onPersonCreated", { person: createdPerson });
|
||||
|
||||
return Promise.resolve(createdPerson);
|
||||
} else if (props.id !== null) {
|
||||
const updatedPerson = await editPerson(person, props.id);
|
||||
emit("onPersonCreated", { person: updatedPerson });
|
||||
|
||||
return Promise.resolve(updatedPerson);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (isValidationException<WritePersonViolationMap>(e)) {
|
||||
violations.setValidationException(e);
|
||||
} else {
|
||||
toast.error(trans(PERSON_EDIT_ERROR_WHILE_SAVING));
|
||||
}
|
||||
}
|
||||
throw "'action' is not create, or edit with a not-null id";
|
||||
}
|
||||
|
||||
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>
|
||||
{% endfor %}
|
||||
{% 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 }) }}
|
||||
|
||||
|
||||
@@ -140,9 +140,7 @@
|
||||
<fieldset>
|
||||
<legend><h2>{{ 'person.Identifiers'|trans }}</h2></legend>
|
||||
<div>
|
||||
{% for f in form.identifiers %}
|
||||
{{ form_row(f) }}
|
||||
{% endfor %}
|
||||
{{ form_widget(form.identifiers) }}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
|
||||
@@ -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((string) $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->getDefinition()->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;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Civility;
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
|
||||
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
|
||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
|
||||
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\DenormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
/**
|
||||
* Serialize a Person entity.
|
||||
*/
|
||||
class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwareInterface, PersonJsonNormalizerInterface
|
||||
class PersonJsonNormalizer implements NormalizerAwareInterface, NormalizerInterface
|
||||
{
|
||||
use DenormalizerAwareTrait;
|
||||
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
use ObjectToPopulateTrait;
|
||||
|
||||
public function __construct(
|
||||
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 ResidentialAddressRepository $residentialAddressRepository,
|
||||
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 string|null $format
|
||||
@@ -204,6 +68,8 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
|
||||
'email' => $person->getEmail(),
|
||||
'gender' => $this->normalizer->normalize($person->getGender(), $format, $context),
|
||||
'civility' => $this->normalizer->normalize($person->getCivility(), $format, $context),
|
||||
'personId' => $this->personIdRendering->renderPersonId($person),
|
||||
'identifiers' => $this->normalizer->normalize($person->getIdentifiers(), $format, $context),
|
||||
];
|
||||
|
||||
if (\in_array('minimal', $groups, true) && 1 === \count($groups)) {
|
||||
@@ -215,11 +81,6 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
|
||||
null];
|
||||
}
|
||||
|
||||
public function supportsDenormalization($data, $type, $format = null)
|
||||
{
|
||||
return Person::class === $type && 'person' === ($data['type'] ?? null);
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, $format = null): bool
|
||||
{
|
||||
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,146 @@
|
||||
<?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 Action\PersonEdit\Service;
|
||||
|
||||
use Chill\MainBundle\Entity\Civility;
|
||||
use Chill\MainBundle\Entity\Country;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\Gender;
|
||||
use Chill\MainBundle\Entity\Language;
|
||||
use Chill\PersonBundle\Actions\PersonEdit\PersonEditDTO;
|
||||
use Chill\PersonBundle\Actions\PersonEdit\Service\PersonEditDTOFactory;
|
||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Entity\AdministrativeStatus;
|
||||
use Chill\PersonBundle\Entity\EmploymentStatus;
|
||||
use Chill\PersonBundle\Entity\MaritalStatus;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonAltName;
|
||||
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use libphonenumber\PhoneNumber;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class PersonEditDTOFactoryTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testMapPersonEditDTOtoPersonCopiesAllFields(): void
|
||||
{
|
||||
$configHelper = $this->createMock(ConfigPersonAltNamesHelper::class);
|
||||
$identifierManager = $this->createMock(PersonIdentifierManagerInterface::class);
|
||||
$factory = new PersonEditDTOFactory($configHelper, $identifierManager);
|
||||
|
||||
$dto = new PersonEditDTO();
|
||||
$dto->firstName = 'John';
|
||||
$dto->lastName = 'Doe';
|
||||
$dto->birthdate = new \DateTime('1980-05-10');
|
||||
$dto->deathdate = new \DateTimeImmutable('2050-01-01');
|
||||
$dto->gender = new Gender();
|
||||
$dto->genderComment = new CommentEmbeddable('gender comment');
|
||||
$dto->numberOfChildren = 2;
|
||||
$dto->memo = 'Some memo';
|
||||
$dto->employmentStatus = new EmploymentStatus();
|
||||
$dto->administrativeStatus = new AdministrativeStatus();
|
||||
$dto->placeOfBirth = 'Cityville';
|
||||
$dto->contactInfo = 'Some contact info';
|
||||
$phone = new PhoneNumber();
|
||||
$dto->phonenumber = $phone;
|
||||
$mobile = new PhoneNumber();
|
||||
$dto->mobilenumber = $mobile;
|
||||
$dto->acceptSms = true;
|
||||
$dto->otherPhonenumbers = new ArrayCollection();
|
||||
$dto->email = 'john.doe@example.org';
|
||||
$dto->acceptEmail = true;
|
||||
$dto->countryOfBirth = new Country();
|
||||
$dto->nationality = new Country();
|
||||
$dto->spokenLanguages = new ArrayCollection([new Language()]);
|
||||
$dto->civility = new Civility();
|
||||
$dto->maritalStatus = new MaritalStatus();
|
||||
$dto->maritalStatusDate = new \DateTime('2010-01-01');
|
||||
$dto->maritalStatusComment = new CommentEmbeddable('married');
|
||||
$dto->cFData = ['foo' => 'bar'];
|
||||
|
||||
$person = new Person();
|
||||
|
||||
$factory->mapPersonEditDTOtoPerson($dto, $person);
|
||||
|
||||
self::assertSame('John', $person->getFirstName());
|
||||
self::assertSame('Doe', $person->getLastName());
|
||||
self::assertSame($dto->birthdate, $person->getBirthdate());
|
||||
self::assertSame($dto->deathdate, $person->getDeathdate());
|
||||
self::assertSame($dto->gender, $person->getGender());
|
||||
self::assertSame($dto->genderComment, $person->getGenderComment());
|
||||
self::assertSame($dto->numberOfChildren, $person->getNumberOfChildren());
|
||||
self::assertSame('Some memo', $person->getMemo());
|
||||
self::assertSame($dto->employmentStatus, $person->getEmploymentStatus());
|
||||
self::assertSame($dto->administrativeStatus, $person->getAdministrativeStatus());
|
||||
self::assertSame('Cityville', $person->getPlaceOfBirth());
|
||||
self::assertSame('Some contact info', $person->getcontactInfo());
|
||||
self::assertSame($phone, $person->getPhonenumber());
|
||||
self::assertSame($mobile, $person->getMobilenumber());
|
||||
self::assertTrue($person->getAcceptSMS());
|
||||
self::assertSame($dto->otherPhonenumbers, $person->getOtherPhoneNumbers());
|
||||
self::assertSame('john.doe@example.org', $person->getEmail());
|
||||
self::assertTrue($person->getAcceptEmail());
|
||||
self::assertSame($dto->countryOfBirth, $person->getCountryOfBirth());
|
||||
self::assertSame($dto->nationality, $person->getNationality());
|
||||
self::assertSame($dto->spokenLanguages, $person->getSpokenLanguages());
|
||||
self::assertSame($dto->civility, $person->getCivility());
|
||||
self::assertSame($dto->maritalStatus, $person->getMaritalStatus());
|
||||
self::assertSame($dto->maritalStatusDate, $person->getMaritalStatusDate());
|
||||
self::assertSame($dto->maritalStatusComment, $person->getMaritalStatusComment());
|
||||
self::assertSame($dto->cFData, $person->getCFData());
|
||||
}
|
||||
|
||||
public function testAltNamesHandlingWithConfigHelper(): void
|
||||
{
|
||||
$configHelper = $this->createMock(ConfigPersonAltNamesHelper::class);
|
||||
$configHelper->method('getChoices')->willReturn([
|
||||
'aka' => ['en' => 'Also Known As'],
|
||||
'nickname' => ['en' => 'Nickname'],
|
||||
]);
|
||||
|
||||
$identifierManager = $this->createMock(PersonIdentifierManagerInterface::class);
|
||||
$identifierManager->method('getWorkers')->willReturn([]);
|
||||
|
||||
$factory = new PersonEditDTOFactory($configHelper, $identifierManager);
|
||||
$person = new Person();
|
||||
|
||||
$dto = $factory->createPersonEditDTO($person);
|
||||
|
||||
// Assert DTO has two altNames keys from helper
|
||||
self::assertCount(2, $dto->altNames);
|
||||
self::assertContainsOnlyInstancesOf(PersonAltName::class, $dto->altNames);
|
||||
self::assertSame(['aka', 'nickname'], array_keys($dto->altNames));
|
||||
self::assertSame(['aka' => 'aka', 'nickname' => 'nickname'], array_map(fn (PersonAltName $altName) => $altName->getKey(), $dto->altNames));
|
||||
|
||||
// Fill only one label and leave the other empty
|
||||
$dto->altNames['aka']->setLabel('The Boss');
|
||||
// 'nickname' remains empty by default
|
||||
|
||||
// Map DTO back to person
|
||||
$factory->mapPersonEditDTOtoPerson($dto, $person);
|
||||
|
||||
// Assert only the filled alt name is persisted on the person
|
||||
$altNames = $person->getAltNames();
|
||||
self::assertCount(1, $altNames);
|
||||
$altNameArray = $altNames->toArray();
|
||||
self::assertSame('aka', $altNameArray[0]->getKey());
|
||||
self::assertSame('The Boss', $altNameArray[0]->getLabel());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?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\PersonIdentifier;
|
||||
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(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isEmpty(PersonIdentifier $identifier): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDefaultValue(PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
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,111 @@
|
||||
<?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\Identifier;
|
||||
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
|
||||
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
|
||||
use Chill\PersonBundle\PersonIdentifier\Identifier\StringIdentifier;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StringIdentifierValidationTest extends TestCase
|
||||
{
|
||||
private function makeDefinition(array $data = []): PersonIdentifierDefinition
|
||||
{
|
||||
$definition = new PersonIdentifierDefinition(label: ['en' => 'Test'], engine: StringIdentifier::NAME);
|
||||
if ([] !== $data) {
|
||||
$definition->setData($data);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
private function makeIdentifier(PersonIdentifierDefinition $definition, ?string $content): PersonIdentifier
|
||||
{
|
||||
$identifier = new PersonIdentifier($definition);
|
||||
$identifier->setValue(['content' => $content]);
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
public function testValidateWithoutOptionsHasNoViolations(): void
|
||||
{
|
||||
$definition = $this->makeDefinition();
|
||||
$identifier = $this->makeIdentifier($definition, 'AB-123');
|
||||
|
||||
$engine = new StringIdentifier();
|
||||
$violations = $engine->validate($identifier, $definition);
|
||||
|
||||
self::assertIsArray($violations);
|
||||
self::assertCount(0, $violations);
|
||||
}
|
||||
|
||||
public function testValidateOnlyNumbersOption(): void
|
||||
{
|
||||
$definition = $this->makeDefinition(['only_numbers' => true]);
|
||||
$engine = new StringIdentifier();
|
||||
|
||||
// valid numeric content
|
||||
$identifierOk = $this->makeIdentifier($definition, '123456');
|
||||
$violationsOk = $engine->validate($identifierOk, $definition);
|
||||
self::assertCount(0, $violationsOk);
|
||||
|
||||
// invalid alphanumeric content
|
||||
$identifierBad = $this->makeIdentifier($definition, '12AB');
|
||||
$violationsBad = $engine->validate($identifierBad, $definition);
|
||||
self::assertCount(1, $violationsBad);
|
||||
self::assertSame('person_identifier.only_number', $violationsBad[0]->message);
|
||||
self::assertSame('2a3352c0-a2b9-11f0-a767-b7a3f80e52f1', $violationsBad[0]->code);
|
||||
}
|
||||
|
||||
public function testValidateFixedLengthOption(): void
|
||||
{
|
||||
$definition = $this->makeDefinition(['fixed_length' => 5]);
|
||||
$engine = new StringIdentifier();
|
||||
|
||||
// valid exact length
|
||||
$identifierOk = $this->makeIdentifier($definition, 'ABCDE');
|
||||
$violationsOk = $engine->validate($identifierOk, $definition);
|
||||
self::assertCount(0, $violationsOk);
|
||||
|
||||
// invalid length (too short)
|
||||
$identifierBad = $this->makeIdentifier($definition, 'AB');
|
||||
$violationsBad = $engine->validate($identifierBad, $definition);
|
||||
self::assertCount(1, $violationsBad);
|
||||
self::assertSame('person_identifier.fixed_length', $violationsBad[0]->message);
|
||||
self::assertSame('2b02a8fe-a2b9-11f0-bfe5-033300972783', $violationsBad[0]->code);
|
||||
self::assertSame(['limit' => '5'], $violationsBad[0]->parameters);
|
||||
}
|
||||
|
||||
public function testValidateOnlyNumbersAndFixedLengthTogether(): void
|
||||
{
|
||||
$definition = $this->makeDefinition(['only_numbers' => true, 'fixed_length' => 4]);
|
||||
$engine = new StringIdentifier();
|
||||
|
||||
// valid: numeric and correct length
|
||||
$identifierOk = $this->makeIdentifier($definition, '1234');
|
||||
$violationsOk = $engine->validate($identifierOk, $definition);
|
||||
self::assertCount(0, $violationsOk);
|
||||
|
||||
// invalid: non-numeric and wrong length -> two violations expected
|
||||
$identifierBad = $this->makeIdentifier($definition, 'AB');
|
||||
$violationsBad = $engine->validate($identifierBad, $definition);
|
||||
self::assertCount(2, $violationsBad);
|
||||
// Order is defined by implementation: numbers check first, then length
|
||||
self::assertSame('person_identifier.only_number', $violationsBad[0]->message);
|
||||
self::assertSame('person_identifier.fixed_length', $violationsBad[1]->message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?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\PersonIdentifier;
|
||||
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(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isEmpty(PersonIdentifier $identifier): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDefaultValue(PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
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(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isEmpty(PersonIdentifier $identifier): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getDefaultValue(PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
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',
|
||||
'definition_id' => null,
|
||||
'engine' => 'string',
|
||||
'label' => ['en' => 'SSN'],
|
||||
'isActive' => false,
|
||||
'presence' => 'ON_EDIT',
|
||||
], $normalized);
|
||||
}
|
||||
|
||||
public function testNormalizeThrowsOnInvalidObject(): void
|
||||
{
|
||||
$normalizer = new PersonIdentifierWorkerNormalizer();
|
||||
$this->expectException(UnexpectedValueException::class);
|
||||
$normalizer->normalize(new \stdClass());
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,21 @@ class PersonIdRenderingTest extends TestCase
|
||||
// same behavior as StringIdentifier::renderAsString
|
||||
return $identifier?->getValue()['content'] ?? '';
|
||||
}
|
||||
|
||||
public function isEmpty(PersonIdentifier $identifier): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function validate(PersonIdentifier $identifier, PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDefaultValue(PersonIdentifierDefinition $definition): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return new PersonIdentifierWorker($engine, $definition);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user