@communecter/cocolight-api-client 1.0.133 → 1.0.134

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@communecter/cocolight-api-client",
3
- "version": "1.0.133",
3
+ "version": "1.0.134",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
package/src/api/Answer.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BaseEntity } from "./BaseEntity.js";
2
2
  import { ApiError } from "../error.js";
3
3
 
4
- import type { CoformUploadAnswerFileData, DeleteElementData, SaveCoformAnswerData } from "./EndpointApi.types.js";
4
+ import type { CoformAnswersByIdData, CoformGetAnswerFilesData, CoformUploadAnswerFileData, DeleteElementData, SaveCoformAnswerData } from "./EndpointApi.types.js";
5
5
  import type { AnswerItemNormalized } from "./serverDataType/Answer.js";
6
6
  import type { FormItemNormalized } from "./serverDataType/Form.js";
7
7
 
@@ -45,6 +45,26 @@ export interface ProcessUploadsOptions {
45
45
  batchSize?: number;
46
46
  }
47
47
 
48
+ /**
49
+ * Options de `Answer.get()` — params optionnels du endpoint `COFORM_ANSWERS_BY_ID`.
50
+ */
51
+ export interface AnswerGetOptions {
52
+ /**
53
+ * Liste de champs à retourner côté backend (filtrage). Si absent, tous les
54
+ * champs sont renvoyés. Utile pour optimiser la taille de la réponse.
55
+ */
56
+ fields?: string[];
57
+ /**
58
+ * ID du formulaire parent. Utilisé côté backend pour calculer `canEdit` et
59
+ * `editDeniedReason` (droits d'édition contextuels). Si absent, **auto-injecté
60
+ * via `this.parent.id`** quand le parent est un `Form` (cas typique
61
+ * `form.answer({id})`).
62
+ */
63
+ formId?: string;
64
+ /** Chemin Finder (rarement utilisé). */
65
+ finderPath?: string;
66
+ }
67
+
48
68
  /**
49
69
  * Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
50
70
  * défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
@@ -83,6 +103,40 @@ export interface UploadAnswerFileResult {
83
103
  answerId: string;
84
104
  }
85
105
 
106
+ /**
107
+ * Options de `Answer.getFiles()`.
108
+ */
109
+ export interface GetFilesOptions {
110
+ /** Clé d'identification de l'input (ex: `"subFormId.inputId"`). Requise. */
111
+ subKey: string;
112
+ /** Type de document attendu. Défaut backend : `"file"`. */
113
+ docType?: "image" | "file";
114
+ /** Clé de contenu utilisée lors de l'upload. Défaut backend : `"presentation"`. */
115
+ contentKey?: string;
116
+ }
117
+
118
+ /**
119
+ * Élément renvoyé par `Answer.getFiles()` — un document attaché à l'Answer.
120
+ * Les champs `imagePath`/`imageThumbPath`/`imageMediumPath` ne sont présents
121
+ * que pour `docType: "image"`.
122
+ */
123
+ export interface AnswerFileItem {
124
+ /** ID MongoDB du document (utilisable pour suppression via `deleteDocumentById`). */
125
+ docId: string;
126
+ /** Chemin relatif du document côté backend. */
127
+ docPath: string;
128
+ /** Nom original du fichier. */
129
+ name?: string;
130
+ /** Taille en octets. */
131
+ size?: number;
132
+ /** Chemin de l'image (uniquement si `docType: "image"`). */
133
+ imagePath?: string | null;
134
+ /** Chemin du thumbnail (uniquement si `docType: "image"`). */
135
+ imageThumbPath?: string | null;
136
+ /** Chemin de la version medium (uniquement si `docType: "image"`). */
137
+ imageMediumPath?: string | null;
138
+ }
139
+
86
140
  export class Answer extends BaseEntity<AnswerItemNormalized> {
87
141
  static override entityType = "answers";
88
142
 
@@ -126,33 +180,61 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
126
180
  data.user = this._linkNestedEntity({ type: "citoyens", collection: "citoyens", ...data.user });
127
181
  }
128
182
 
129
- // Transformer context en instance Organization/Project
130
- // if (data.context && typeof data.context === "object" && !("id" in data.context)) {
131
- // // context est un ParentsMap: { "id1": {type: "organizations", name: "..."}, "id2": {...} }
132
- // // On transforme chaque entrée en instance d'entité
133
- // const transformedContext: Record<string, any> = {};
134
- // for (const [id, parentRef] of Object.entries(data.context)) {
135
- // if (parentRef && typeof parentRef === "object" && "type" in parentRef) {
136
- // transformedContext[id] = this._linkNestedEntity({ id, type: parentRef.type });
137
- // }
138
- // }
139
- // data.context = transformedContext;
140
- // }
183
+ // Normalisation legacy uploader : si parent Form connu, convertit les valeurs
184
+ // d'inputs `*.uploader` du format Array `[{docId, docPath}]` vers le format
185
+ // canonique `{updateDate, files: {docId: docPath}}`. Symétrique de ce que
186
+ // `processUploads()` écrit. Si parent pas Form → skip (pas de schéma pour
187
+ // identifier les inputs uploader).
188
+ const formData = this._resolveFormData();
189
+ if (formData && data.answers && typeof data.answers === "object") {
190
+ data.answers = this._normalizeLegacyUploaderInAnswers(
191
+ data.answers as Record<string, Record<string, unknown>>,
192
+ formData,
193
+ );
194
+ }
141
195
 
142
196
  return data;
143
197
  }
144
198
 
145
199
  /**
146
- * Rafraîchit les données de l'entité depuis l'API.
147
- * Constant : COFORM_ANSWERS_BY_ID
148
- */
149
- override async get(): Promise<Record<string, any>> {
200
+ * Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
201
+ *
202
+ * **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
203
+ * `form.answer({id})`), le param `formId` est automatiquement envoyé pour
204
+ * que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
205
+ * Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
206
+ *
207
+ * @param opts - Params optionnels :
208
+ * - `fields` : filtrage des champs côté backend
209
+ * - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
210
+ * - `finderPath` : chemin Finder (rare)
211
+ *
212
+ * @example
213
+ * // Cas commun : form.answer({id}) → get() auto avec formId
214
+ * const answer = await form.answer({ id: answerId });
215
+ * answer.serverData.canEdit; // true/false calculé backend
216
+ * answer.serverData.editDeniedReason; // raison si false
217
+ *
218
+ * @example
219
+ * // Cas avancé : filtrage de champs pour optim perf
220
+ * const answer = await form.answer();
221
+ * answer._id(answerId);
222
+ * await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
223
+ */
224
+ override async get(opts: AnswerGetOptions = {}): Promise<Record<string, any>> {
150
225
  if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.", 400);
151
226
  const answerId = this.id; // Type narrowing
152
227
 
153
- const answer = await this.endpointApi.coformAnswersById({
154
- answerId
155
- });
228
+ // Auto-injection du formId via parent Form (sauf si caller a explicitement
229
+ // passé opts.formId — même chaîne vide pour disable).
230
+ const formId = "formId" in opts ? opts.formId : this._autoFormIdFromParent();
231
+
232
+ const payload: CoformAnswersByIdData = { answerId };
233
+ if (opts.fields && opts.fields.length > 0) payload.fields = opts.fields;
234
+ if (typeof formId === "string" && formId.length > 0) payload.formId = formId;
235
+ if (opts.finderPath) payload.finderPath = opts.finderPath;
236
+
237
+ const answer = await this.endpointApi.coformAnswersById(payload);
156
238
 
157
239
  if (answer?.data?.id) {
158
240
  this._setData(answer.data as AnswerItemNormalized, { forceInitialDraftReset: true });
@@ -161,6 +243,20 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
161
243
  throw new ApiError(`Aucune réponse trouvée pour l'ID ${this.id}`, 404);
162
244
  }
163
245
 
246
+ /**
247
+ * Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
248
+ * @private
249
+ */
250
+ private _autoFormIdFromParent(): string | undefined {
251
+ const parent = this.parent;
252
+ if (parent && typeof (parent as any).getEntityType === "function"
253
+ && (parent as any).getEntityType() === "forms"
254
+ && typeof (parent as any).id === "string") {
255
+ return (parent as any).id as string;
256
+ }
257
+ return undefined;
258
+ }
259
+
164
260
  override async form(): Promise<never> {
165
261
  throw new ApiError(`form n'existe pas dans ${this.constructor.name}`, 501);
166
262
  }
@@ -200,6 +296,83 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
200
296
  this._isDeleted = true;
201
297
  }
202
298
 
299
+ /**
300
+ * Récupère la liste des fichiers attachés à un input précis de cette Answer
301
+ * via `COFORM_GET_ANSWER_FILES`.
302
+ *
303
+ * **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
304
+ * en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
305
+ * recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
306
+ *
307
+ * Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
308
+ * les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
309
+ * — pas besoin d'appel réseau.
310
+ *
311
+ * @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
312
+ * @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
313
+ * Tableau vide si aucun fichier trouvé.
314
+ * @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
315
+ *
316
+ * @example
317
+ * const form = await org.form({ id: formId });
318
+ * const answer = await form.answer({ id: answerId });
319
+ * const files = await answer.getFiles({
320
+ * subKey: `${subFormId}.${inputId}`,
321
+ * docType: "image",
322
+ * });
323
+ * files[0].docId; // ID du document (pour suppression future)
324
+ * files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
325
+ */
326
+ async getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]> {
327
+ if (!this.id) {
328
+ throw new ApiError("Answer sans id, impossible de lister ses fichiers.", 400);
329
+ }
330
+ if (!opts.subKey || opts.subKey.length === 0) {
331
+ throw new ApiError("`subKey` requis pour lister les fichiers d'un input.", 400);
332
+ }
333
+
334
+ const payload: CoformGetAnswerFilesData = {
335
+ answerId: this.id,
336
+ subKey: opts.subKey,
337
+ };
338
+ if (opts.docType) payload.docType = opts.docType;
339
+ if (opts.contentKey) payload.contentKey = opts.contentKey;
340
+
341
+ // Forme observée du backend (cf. site-json useCoFormAnswerFiles.tsx
342
+ // et schema COFORM_GET_ANSWER_FILES) :
343
+ // { result, answerId, subKey, contentKey, count, files: [{ id, name, size,
344
+ // docPath, imagePath, imageThumbPath, imageMediumPath }] }
345
+ const response = await this.callIsConnected(() =>
346
+ this.endpointApi.coformGetAnswerFiles(payload)
347
+ ) as {
348
+ result?: boolean;
349
+ msg?: string;
350
+ count?: number;
351
+ files?: Array<{
352
+ id?: string | null;
353
+ name?: string | null;
354
+ size?: number | null;
355
+ docPath?: string;
356
+ imagePath?: string | null;
357
+ imageThumbPath?: string | null;
358
+ imageMediumPath?: string | null;
359
+ }>;
360
+ };
361
+
362
+ // Filtre les entrées invalides (id + docPath requis) et normalise.
363
+ return (response.files ?? [])
364
+ .filter(f => typeof f.id === "string" && typeof f.docPath === "string")
365
+ .map(f => ({
366
+ docId: f.id!,
367
+ docPath: f.docPath!,
368
+ name: f.name ?? undefined,
369
+ size: f.size ?? undefined,
370
+ imagePath: f.imagePath ?? undefined,
371
+ imageThumbPath: f.imageThumbPath ?? undefined,
372
+ imageMediumPath: f.imageMediumPath ?? undefined,
373
+ }));
374
+ }
375
+
203
376
  /**
204
377
  * Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
205
378
  *
@@ -738,6 +911,52 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
738
911
  return obj;
739
912
  }
740
913
 
914
+ /**
915
+ * Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
916
+ * inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
917
+ *
918
+ * Utilisé par `_transformServerData()` pour harmoniser les Answer en format
919
+ * legacy Array `[{docId, docPath}]` avec le format moderne (ce que
920
+ * `processUploads()` écrit).
921
+ *
922
+ * Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
923
+ * @private
924
+ */
925
+ private _normalizeLegacyUploaderInAnswers(
926
+ answers: Record<string, Record<string, unknown>>,
927
+ formData: FormItemNormalized,
928
+ ): Record<string, Record<string, unknown>> {
929
+ const normalized: Record<string, Record<string, unknown>> = {};
930
+
931
+ for (const [subFormId, subFormData] of Object.entries(answers)) {
932
+ if (!this._isObjectRecord(subFormData)) {
933
+ normalized[subFormId] = subFormData as any;
934
+ continue;
935
+ }
936
+
937
+ const subForm = formData.inputs?.[subFormId];
938
+ if (!subForm) {
939
+ normalized[subFormId] = subFormData;
940
+ continue;
941
+ }
942
+
943
+ const cleanedSubForm: Record<string, unknown> = {};
944
+ for (const [inputId, inputValue] of Object.entries(subFormData)) {
945
+ const input = subForm.inputs?.[inputId];
946
+ const inputType = input && typeof input === "object" && "type" in input
947
+ ? (input.type as string | undefined)
948
+ : undefined;
949
+
950
+ cleanedSubForm[inputId] = inputType?.endsWith(".uploader")
951
+ ? this._normalizeUploaderValue(inputValue)
952
+ : inputValue;
953
+ }
954
+ normalized[subFormId] = cleanedSubForm;
955
+ }
956
+
957
+ return normalized;
958
+ }
959
+
741
960
  /**
742
961
  * Normalise une valeur d'input uploader vers le format legacy backend :
743
962
  * `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
@@ -64,6 +64,17 @@ export interface AnswerItemJson {
64
64
  [key: string]: unknown;
65
65
  }
66
66
 
67
+ /**
68
+ * Raisons de refus d'édition côté backend, calculées par `coformAnswersById`
69
+ * et présentes dans la réponse normalisée. Utilisé par les UI pour distinguer
70
+ * les modes readonly/edit et afficher des messages contextuels.
71
+ */
72
+ export type AnswerEditDeniedReason =
73
+ | "not_logged_in"
74
+ | "not_owner"
75
+ | "form_inactive"
76
+ | "form_closed";
77
+
67
78
  export interface AnswerItemNormalized {
68
79
  id: string;
69
80
  _id: ObjectID;
@@ -73,6 +84,7 @@ export interface AnswerItemNormalized {
73
84
  user?: string | User;
74
85
  links?: AnswerLinksBlock;
75
86
  draft?: boolean;
87
+ finished?: boolean;
76
88
  answers?: Record<string, unknown>;
77
89
  context?: ParentsMap | Record<string, Organization | Project>;
78
90
  form?: string;
@@ -83,5 +95,12 @@ export interface AnswerItemNormalized {
83
95
  date: Date;
84
96
  }>;
85
97
  project?: AnswerProjectRef;
98
+ /**
99
+ * L'utilisateur courant peut-il éditer cette réponse ? Calculé côté backend.
100
+ * Présent par défaut, ou enrichi avec le contexte si `formId` est fourni.
101
+ */
102
+ canEdit?: boolean;
103
+ /** Raison si `canEdit === false`. `null` si `canEdit === true`. */
104
+ editDeniedReason?: AnswerEditDeniedReason | null;
86
105
  [key: string]: unknown;
87
106
  }
@@ -36,6 +36,25 @@ export interface ProcessUploadsOptions {
36
36
  /** Nombre d'uploads parallèles par batch. Défaut : 4. */
37
37
  batchSize?: number;
38
38
  }
39
+ /**
40
+ * Options de `Answer.get()` — params optionnels du endpoint `COFORM_ANSWERS_BY_ID`.
41
+ */
42
+ export interface AnswerGetOptions {
43
+ /**
44
+ * Liste de champs à retourner côté backend (filtrage). Si absent, tous les
45
+ * champs sont renvoyés. Utile pour optimiser la taille de la réponse.
46
+ */
47
+ fields?: string[];
48
+ /**
49
+ * ID du formulaire parent. Utilisé côté backend pour calculer `canEdit` et
50
+ * `editDeniedReason` (droits d'édition contextuels). Si absent, **auto-injecté
51
+ * via `this.parent.id`** quand le parent est un `Form` (cas typique
52
+ * `form.answer({id})`).
53
+ */
54
+ formId?: string;
55
+ /** Chemin Finder (rarement utilisé). */
56
+ finderPath?: string;
57
+ }
39
58
  /**
40
59
  * Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
41
60
  * défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
@@ -72,6 +91,38 @@ export interface UploadAnswerFileResult {
72
91
  /** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
73
92
  answerId: string;
74
93
  }
94
+ /**
95
+ * Options de `Answer.getFiles()`.
96
+ */
97
+ export interface GetFilesOptions {
98
+ /** Clé d'identification de l'input (ex: `"subFormId.inputId"`). Requise. */
99
+ subKey: string;
100
+ /** Type de document attendu. Défaut backend : `"file"`. */
101
+ docType?: "image" | "file";
102
+ /** Clé de contenu utilisée lors de l'upload. Défaut backend : `"presentation"`. */
103
+ contentKey?: string;
104
+ }
105
+ /**
106
+ * Élément renvoyé par `Answer.getFiles()` — un document attaché à l'Answer.
107
+ * Les champs `imagePath`/`imageThumbPath`/`imageMediumPath` ne sont présents
108
+ * que pour `docType: "image"`.
109
+ */
110
+ export interface AnswerFileItem {
111
+ /** ID MongoDB du document (utilisable pour suppression via `deleteDocumentById`). */
112
+ docId: string;
113
+ /** Chemin relatif du document côté backend. */
114
+ docPath: string;
115
+ /** Nom original du fichier. */
116
+ name?: string;
117
+ /** Taille en octets. */
118
+ size?: number;
119
+ /** Chemin de l'image (uniquement si `docType: "image"`). */
120
+ imagePath?: string | null;
121
+ /** Chemin du thumbnail (uniquement si `docType: "image"`). */
122
+ imageThumbPath?: string | null;
123
+ /** Chemin de la version medium (uniquement si `docType: "image"`). */
124
+ imageMediumPath?: string | null;
125
+ }
75
126
  export declare class Answer extends BaseEntity<AnswerItemNormalized> {
76
127
  static entityType: string;
77
128
  static entityTag: string;
@@ -93,10 +144,36 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
93
144
  */
94
145
  protected _transformServerData(data: AnswerItemNormalized): AnswerItemNormalized;
95
146
  /**
96
- * Rafraîchit les données de l'entité depuis l'API.
97
- * Constant : COFORM_ANSWERS_BY_ID
98
- */
99
- get(): Promise<Record<string, any>>;
147
+ * Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
148
+ *
149
+ * **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
150
+ * `form.answer({id})`), le param `formId` est automatiquement envoyé pour
151
+ * que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
152
+ * Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
153
+ *
154
+ * @param opts - Params optionnels :
155
+ * - `fields` : filtrage des champs côté backend
156
+ * - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
157
+ * - `finderPath` : chemin Finder (rare)
158
+ *
159
+ * @example
160
+ * // Cas commun : form.answer({id}) → get() auto avec formId
161
+ * const answer = await form.answer({ id: answerId });
162
+ * answer.serverData.canEdit; // true/false calculé backend
163
+ * answer.serverData.editDeniedReason; // raison si false
164
+ *
165
+ * @example
166
+ * // Cas avancé : filtrage de champs pour optim perf
167
+ * const answer = await form.answer();
168
+ * answer._id(answerId);
169
+ * await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
170
+ */
171
+ get(opts?: AnswerGetOptions): Promise<Record<string, any>>;
172
+ /**
173
+ * Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
174
+ * @private
175
+ */
176
+ private _autoFormIdFromParent;
100
177
  form(): Promise<never>;
101
178
  /**
102
179
  * Supprime cette Answer côté serveur via `DELETE_ELEMENT`
@@ -115,6 +192,34 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
115
192
  * answer._isDeleted; // true
116
193
  */
117
194
  delete(reason?: string): Promise<void>;
195
+ /**
196
+ * Récupère la liste des fichiers attachés à un input précis de cette Answer
197
+ * via `COFORM_GET_ANSWER_FILES`.
198
+ *
199
+ * **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
200
+ * en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
201
+ * recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
202
+ *
203
+ * Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
204
+ * les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
205
+ * — pas besoin d'appel réseau.
206
+ *
207
+ * @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
208
+ * @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
209
+ * Tableau vide si aucun fichier trouvé.
210
+ * @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
211
+ *
212
+ * @example
213
+ * const form = await org.form({ id: formId });
214
+ * const answer = await form.answer({ id: answerId });
215
+ * const files = await answer.getFiles({
216
+ * subKey: `${subFormId}.${inputId}`,
217
+ * docType: "image",
218
+ * });
219
+ * files[0].docId; // ID du document (pour suppression future)
220
+ * files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
221
+ */
222
+ getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]>;
118
223
  /**
119
224
  * Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
120
225
  *
@@ -276,6 +381,18 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
276
381
  private _getUploadKeys;
277
382
  private _getValueAtPath;
278
383
  private _setValueAtPath;
384
+ /**
385
+ * Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
386
+ * inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
387
+ *
388
+ * Utilisé par `_transformServerData()` pour harmoniser les Answer en format
389
+ * legacy Array `[{docId, docPath}]` avec le format moderne (ce que
390
+ * `processUploads()` écrit).
391
+ *
392
+ * Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
393
+ * @private
394
+ */
395
+ private _normalizeLegacyUploaderInAnswers;
279
396
  /**
280
397
  * Normalise une valeur d'input uploader vers le format legacy backend :
281
398
  * `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
@@ -55,6 +55,12 @@ export interface AnswerItemJson {
55
55
  project?: AnswerProjectRef;
56
56
  [key: string]: unknown;
57
57
  }
58
+ /**
59
+ * Raisons de refus d'édition côté backend, calculées par `coformAnswersById`
60
+ * et présentes dans la réponse normalisée. Utilisé par les UI pour distinguer
61
+ * les modes readonly/edit et afficher des messages contextuels.
62
+ */
63
+ export type AnswerEditDeniedReason = "not_logged_in" | "not_owner" | "form_inactive" | "form_closed";
58
64
  export interface AnswerItemNormalized {
59
65
  id: string;
60
66
  _id: ObjectID;
@@ -64,6 +70,7 @@ export interface AnswerItemNormalized {
64
70
  user?: string | User;
65
71
  links?: AnswerLinksBlock;
66
72
  draft?: boolean;
73
+ finished?: boolean;
67
74
  answers?: Record<string, unknown>;
68
75
  context?: ParentsMap | Record<string, Organization | Project>;
69
76
  form?: string;
@@ -74,6 +81,13 @@ export interface AnswerItemNormalized {
74
81
  date: Date;
75
82
  }>;
76
83
  project?: AnswerProjectRef;
84
+ /**
85
+ * L'utilisateur courant peut-il éditer cette réponse ? Calculé côté backend.
86
+ * Présent par défaut, ou enrichi avec le contexte si `formId` est fourni.
87
+ */
88
+ canEdit?: boolean;
89
+ /** Raison si `canEdit === false`. `null` si `canEdit === true`. */
90
+ editDeniedReason?: AnswerEditDeniedReason | null;
77
91
  [key: string]: unknown;
78
92
  }
79
93
  export {};