@communecter/cocolight-api-client 1.0.142 → 1.0.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api/Poi.ts CHANGED
@@ -3,8 +3,38 @@ import { BaseEntity } from "./BaseEntity.js";
3
3
  import { PoiItemNormalized } from "./serverDataType/Poi.js";
4
4
  import { transformEntityRefs } from "../types/transforms.js";
5
5
 
6
+ import type { SetTypeValue } from "./BaseEntity.js";
6
7
  import type { AddPoiData } from "./EndpointApi.types.js";
7
8
 
9
+ /**
10
+ * Source unique des champs « équipement sportif » (RES) éditables sur le POI :
11
+ * - `schema` : type JSON → alimente le VIRTUAL_SCHEMA (draft-allow + cohérence des types)
12
+ * - `setType` : coercion backend pour `UPDATE_PATH_VALUE` (absent = string/array)
13
+ * On en dérive `Poi.VIRTUAL_SCHEMAS`, `Poi.CUSTOM_FIELD_HANDLERS` et `Poi.EQUIPMENT_SETTYPE`.
14
+ */
15
+ const POI_EQUIPMENT_FIELDS: ReadonlyArray<{ name: string; schema: Record<string, unknown>; setType?: SetTypeValue }> = [
16
+ // Nombres (dimensions)
17
+ ...["equip_surf", "equip_larg", "equip_long"].map((name) => ({ name, schema: { type: "number" }, setType: "float" as SetTypeValue })),
18
+ // Dates (string côté schéma, normalisées en Date à la lecture ; isoDate à l'écriture)
19
+ ...["equip_maj_date", "inst_date_creation", "inst_enqu_date"].map((name) => ({ name, schema: { type: "string" }, setType: "isoDate" as SetTypeValue })),
20
+ // Booléens
21
+ ...[
22
+ "inst_acc_handi_bool", "inst_trans_bool", "equip_eclair", "equip_acc_libre", "inst_part_bool",
23
+ "equip_pmr_acc", "equip_pmr_chem", "equip_pmr_douche", "equip_pmr_sanit", "equip_pmr_trib", "equip_pmr_vest",
24
+ "equip_pshs_aire", "equip_pshs_chem", "equip_pshs_sanit", "equip_pshs_trib", "equip_pshs_vest", "equip_pshs_sign",
25
+ "equip_douche"
26
+ ].map((name) => ({ name, schema: { type: "boolean" }, setType: "bool" as SetTypeValue })),
27
+ // Strings
28
+ ...[
29
+ "categorie", "equip_type_name", "inst_nom", "equip_type_famille", "equip_nature", "equip_sol",
30
+ "inst_acc_handi_type", "inst_trans_type", "equip_prop_nom", "equip_prop_type", "equip_gest_type"
31
+ ].map((name) => ({ name, schema: { type: "string" } })),
32
+ // Arrays de strings
33
+ ...[
34
+ "aps_name", "inst_part_type", "equip_loc_type", "equip_utilisateur"
35
+ ].map((name) => ({ name, schema: { type: "array", items: { type: "string" } } }))
36
+ ];
37
+
8
38
  export class Poi extends BaseEntity<PoiItemNormalized> {
9
39
  static override entityType = "poi";
10
40
 
@@ -16,7 +46,8 @@ export class Poi extends BaseEntity<PoiItemNormalized> {
16
46
  "UPDATE_BLOCK_INFO",
17
47
  "UPDATE_BLOCK_LOCALITY",
18
48
  "UPDATE_BLOCK_SLUG",
19
- "PROFIL_IMAGE"
49
+ "PROFIL_IMAGE",
50
+ "VIRTUAL_POI_EQUIPMENT"
20
51
  ];
21
52
 
22
53
  static ADD_BLOCKS = new Map([
@@ -32,6 +63,30 @@ export class Poi extends BaseEntity<PoiItemNormalized> {
32
63
  ["PROFIL_IMAGE", "updateImageProfil"]
33
64
  ] as const);
34
65
 
66
+ /**
67
+ * Champs « équipement sportif » (RES) éditables hors `UPDATE_BLOCK_*` : déclarés en
68
+ * VIRTUAL_SCHEMA (autorise l'écriture sur le draft + fixe les types) et persistés à
69
+ * l'update via `CUSTOM_FIELD_HANDLERS` → `updateEquipmentField` → `UPDATE_PATH_VALUE`.
70
+ * Même pattern qu'`Organization`/`Action`. (Ils restent aussi dans `ADD_POI` pour la création.)
71
+ */
72
+ static override VIRTUAL_SCHEMAS = {
73
+ VIRTUAL_POI_EQUIPMENT: {
74
+ type: "object",
75
+ properties: Object.fromEntries(POI_EQUIPMENT_FIELDS.map((f) => [f.name, f.schema]))
76
+ }
77
+ };
78
+
79
+ static override CUSTOM_FIELD_HANDLERS = new Map(
80
+ POI_EQUIPMENT_FIELDS.map((f) =>
81
+ [f.name, { updateMethod: "updateEquipmentField", schemaConstant: "VIRTUAL_POI_EQUIPMENT" }] as [string, { updateMethod: string; schemaConstant?: string }]
82
+ )
83
+ );
84
+
85
+ /** setType backend par champ équipement (consommé par `updateEquipmentField`). */
86
+ private static readonly EQUIPMENT_SETTYPE = new Map<string, SetTypeValue | undefined>(
87
+ POI_EQUIPMENT_FIELDS.map((f) => [f.name, f.setType])
88
+ );
89
+
35
90
  override defaultFields: Record<string, any> = {
36
91
  typeElement: this.getEntityType()
37
92
  };
@@ -83,6 +138,23 @@ export class Poi extends BaseEntity<PoiItemNormalized> {
83
138
  if (payload.id) delete payload.id;
84
139
  let hasChanged = false;
85
140
 
141
+ // 1. Champs équipement (RES) : non couverts par les UPDATE_BLOCK_*. Persistés
142
+ // individuellement via CUSTOM_FIELD_HANDLERS -> updateEquipmentField -> UPDATE_PATH_VALUE
143
+ // (même mécanisme qu'Organization/Action).
144
+ const customHandlers = Poi.CUSTOM_FIELD_HANDLERS;
145
+ if (customHandlers) {
146
+ const processedFields = new Set<string>();
147
+ for (const [fieldName, config] of customHandlers) {
148
+ if (fieldName in payload && this._hasFieldChanged(fieldName)) {
149
+ await this._invokeCustomFieldHandler(config.updateMethod, payload[fieldName], fieldName);
150
+ processedFields.add(fieldName);
151
+ hasChanged = true;
152
+ }
153
+ }
154
+ processedFields.forEach((f) => delete payload[f]);
155
+ }
156
+
157
+ // 2. Blocs standards (description, info, locality, slug, image).
86
158
  for (const [constant, methodName] of Array.from(Poi.UPDATE_BLOCKS)) {
87
159
  const blockData = this._extractChangedFieldsFromSchema(
88
160
  this.apiClient,
@@ -100,6 +172,34 @@ export class Poi extends BaseEntity<PoiItemNormalized> {
100
172
  return hasChanged;
101
173
  };
102
174
 
175
+ /**
176
+ * Handler générique des champs « équipement sportif » (RES), enregistré dans
177
+ * `CUSTOM_FIELD_HANDLERS`. Persiste un champ via `UPDATE_PATH_VALUE` avec le `setType`
178
+ * adéquat (lu dans `EQUIPMENT_SETTYPE`). `fieldName` est fourni par `_invokeCustomFieldHandler`.
179
+ *
180
+ * Effacement façon `Action` : `null`/`""` → on envoie `""` SANS `setType` → le backend fait
181
+ * `!empty($value)` → `$unset` (sinon `floatval("")=0` écraserait au lieu d'effacer).
182
+ */
183
+ async updateEquipmentField(value: unknown, fieldName: string): Promise<unknown> {
184
+ const setType = Poi.EQUIPMENT_SETTYPE.get(fieldName);
185
+ const isClear = value === null || value === "";
186
+ return this.updateField(fieldName, isClear ? "" : value, isClear || !setType ? {} : { setType });
187
+ }
188
+
189
+ /**
190
+ * Override confiné à `Poi` : forwarde `fieldName` au handler (2e argument) pour permettre
191
+ * un handler générique unique (`updateEquipmentField`) couvrant les ~39 champs équipement,
192
+ * sans toucher `BaseEntity._invokeCustomFieldHandler` (utilisé par les autres entités, qui
193
+ * ne passent que `value`). Copie volontaire du corps de base + le forward du `fieldName`.
194
+ */
195
+ protected override async _invokeCustomFieldHandler(methodName: string, value: any, fieldName: string): Promise<any> {
196
+ const method = (this as any)[methodName];
197
+ if (typeof method !== "function") {
198
+ throw new ApiError(`Custom handler "${methodName}" not found for field "${fieldName}"`, 500);
199
+ }
200
+ return await method.call(this, value, fieldName);
201
+ }
202
+
103
203
  async addPoi(data: Partial<AddPoiData> = {}): Promise<unknown> {
104
204
 
105
205
  // Si le parent direct n'est pas l'utilisateur connecté (ex: création via une org),
@@ -74,13 +74,16 @@ export class Project extends BaseEntity<ProjectItemNormalized> {
74
74
  if (rawAnswers && typeof rawAnswers === "object") {
75
75
  const linkedAnswers: Record<string, unknown[]> = {};
76
76
  for (const [formId, docs] of Object.entries(rawAnswers)) {
77
- if (Array.isArray(docs)) {
78
- linkedAnswers[formId] = docs.map((doc) =>
79
- doc && typeof doc === "object"
80
- ? this._linkEntity("answers", { ...(doc as Record<string, unknown>), collection: "answers" })
81
- : doc
82
- );
83
- }
77
+ // Le backend renvoie tantôt un tableau `[doc, ...]`, tantôt une map indexée
78
+ // par id `{ answerId: doc }`. On normalise vers un tableau dans les deux cas.
79
+ const list = Array.isArray(docs)
80
+ ? docs
81
+ : (docs && typeof docs === "object" ? Object.values(docs) : []);
82
+ linkedAnswers[formId] = list.map((doc) =>
83
+ doc && typeof doc === "object"
84
+ ? this._linkEntity("answers", { ...(doc as Record<string, unknown>), collection: "answers" })
85
+ : doc
86
+ );
84
87
  }
85
88
  (data as Record<string, unknown>).answers = linkedAnswers;
86
89
  }
package/src/api/User.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  GetSubscriptionsData,
18
18
  GetOrganizationsNoAdminData,
19
19
  GetOrganizationsAdminData,
20
+ GetUserEligiblePlacesData,
20
21
  GetFriendsAdminData,
21
22
  DemoteAdminData,
22
23
  GetNotificationsData,
@@ -410,6 +411,59 @@ export class User extends BaseEntity<UserItemNormalized> {
410
411
  return paginator.next() as Promise<PaginatorPage<Organization>>;
411
412
  }
412
413
 
414
+ /**
415
+ * Récupère les lieux (organizations) memberOf de l'utilisateur, filtrés
416
+ * côté serveur par des filters arbitraires (tags, source.key, etc.) et
417
+ * un flag `notSourceKey` configurables.
418
+ *
419
+ * Mirroir de `getOrganizations` mais permet de passer dynamiquement les
420
+ * filtres du finder du formulaire (vue collaborative coform/place), pour
421
+ * que la pagination soit correcte (filtrage côté serveur, pas côté client).
422
+ *
423
+ * Constant : GET_USER_ELIGIBLE_PLACES
424
+ */
425
+ async getEligiblePlaces(
426
+ data: Partial<GetUserEligiblePlacesData> = {},
427
+ options?: { restoredState?: PaginatorState }
428
+ ) {
429
+ // Force le default `searchType` ([NGO, Cooperative, ...]) — le PHP ne
430
+ // retourne rien si le tableau est vide / absent. Pattern aligné avec
431
+ // `getOrganizations`.
432
+ data.searchType = this._getDefaultFromEndpoint(
433
+ "GET_USER_ELIGIBLE_PLACES",
434
+ "searchType"
435
+ ) as GetUserEligiblePlacesData["searchType"];
436
+
437
+ const paginator = this._createPaginatorEngine({
438
+ initialData: data,
439
+ methodName: "getEligiblePlaces",
440
+ restoredState: options?.restoredState,
441
+ finalizer: async (finalData) => {
442
+ delete finalData?.pathParams;
443
+
444
+ // Auto-merge du filtre memberOf par défaut si l'appelant n'en pousse
445
+ // que des filters "métier" (tags, etc.). On garde la sémantique
446
+ // "uniquement les orgs où l'user est membre validé".
447
+ const userFilters = finalData.filters ?? {};
448
+ const memberOfDefaults: Record<string, unknown> = {
449
+ [`links.members.${this.id}`]: { "$exists": true },
450
+ [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
451
+ [`links.members.${this.id}.isInviting`]: { "$exists": false },
452
+ };
453
+ finalData.filters = { ...memberOfDefaults, ...userFilters };
454
+
455
+ // Cast nécessaire : callIsMe est typé Promise<unknown>, mais l'endpoint
456
+ // retourne bien la shape `{ results, count }` attendue par le finalizer.
457
+ // Pattern aligné avec getOrganizations (cf. la branche ternaire au-dessus).
458
+ return this.callIsMe(() => this.endpointApi.getUserEligiblePlaces(finalData)) as ReturnType<
459
+ typeof this.endpointApi.getUserEligiblePlaces
460
+ >;
461
+ },
462
+ });
463
+
464
+ return paginator.next() as Promise<PaginatorPage<Organization>>;
465
+ }
466
+
413
467
  /**
414
468
  * {@inheritDoc BaseEntity#getProjects}
415
469
  *
@@ -134,3 +134,24 @@ export interface FormItemNormalized {
134
134
  useBannerImg?: boolean;
135
135
  [key: string]: unknown;
136
136
  }
137
+
138
+ /**
139
+ * Une contribution individuelle à une table commune (commonTable) d'un coform.
140
+ * Retournée par `Form.getCommonTableContributors()` (endpoint GET_COFORM_COMMONTABLE_CONTRIBUTORS).
141
+ * Plusieurs entrées possibles pour un même `userId` (plusieurs solutions au même usage).
142
+ */
143
+ export interface CoformCommonTableContributor {
144
+ userId: string;
145
+ userName?: string;
146
+ userSlug?: string;
147
+ criteriaId: string;
148
+ /** Nom de la solution saisie. */
149
+ criteria: string;
150
+ /** love | happySmile | neutral | sad | cry | "" */
151
+ happiness?: string;
152
+ /** Note de 0 à 5. */
153
+ note?: number;
154
+ comment?: string;
155
+ fromAnswerId?: string;
156
+ [k: string]: unknown;
157
+ }