next-recomponents 2.0.38 → 2.0.39

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.
@@ -1,9 +1,40 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { EnhancedEndpoints, ItemsRecord, Props, ShowOptions } from "./types";
3
3
  import useToken from "./get.token";
4
4
  import axios from "axios";
5
5
  import httpStatusCodes from "./http.codes";
6
6
 
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function getErrorMeaning(error: any): string | undefined {
12
+ return httpStatusCodes.find((s) => s.code === error?.status)?.meaning;
13
+ }
14
+
15
+ function mergeDataArray(existing: any[], incoming: any, matchId?: any): any[] {
16
+ const idx = existing.findIndex((d) => d?.id == matchId);
17
+ if (idx >= 0) {
18
+ return existing.map((d, i) => (i === idx ? incoming : d));
19
+ }
20
+ return [incoming, ...existing];
21
+ }
22
+
23
+ // Copia profunda de un solo key para evitar mutación de estado
24
+ function cloneKey<T extends Record<string, any>>(state: T, key: string): T {
25
+ return {
26
+ ...state,
27
+ [key]: {
28
+ ...state[key],
29
+ data: state[key]?.data ? [...state[key].data] : [],
30
+ },
31
+ };
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Hook
36
+ // ---------------------------------------------------------------------------
37
+
7
38
  export default function useResources<T extends Record<string, ItemsRecord>>({
8
39
  baseURI,
9
40
  endpoints,
@@ -11,517 +42,389 @@ export default function useResources<T extends Record<string, ItemsRecord>>({
11
42
  }: Props<T>) {
12
43
  const token = useToken();
13
44
 
14
- const [info, setInfo] = useState<EnhancedEndpoints<T>>(
45
+ const [info, setInfo] = useState<EnhancedEndpoints<T>>(() =>
15
46
  Object.keys(endpoints).reduce((acc: any, key) => {
16
- const newAcc: any = {
17
- ...acc,
18
- [key]: {
19
- loaded: false,
20
- id: endpoints[key]?.id,
21
- defaultParams: endpoints[key]?.defaultParams,
22
- data: [],
23
- selectedItem: {},
24
- },
47
+ acc[key] = {
48
+ loaded: false,
49
+ id: endpoints[key]?.id,
50
+ defaultParams: endpoints[key]?.defaultParams,
51
+ data: [],
52
+ selectedItem: {},
53
+ state: "success",
54
+ errorMessage: "",
25
55
  };
26
-
27
- return newAcc;
56
+ return acc;
28
57
  }, {} as any),
29
58
  );
30
59
 
31
- const results: EnhancedEndpoints<T> = Object.keys(endpoints).reduce(
32
- (acc: any, key) => {
33
- const endpoint = endpoints[key];
34
- const showFunc = async (
35
- { limit = 10, page = 1, merge = true, ...query }: ShowOptions,
36
- autoLoad: boolean = false,
37
- ) => {
38
- const options = {
39
- method: "GET",
40
- url: `${baseURI}/${key}`,
41
- params: { limit, page, ...query },
42
- headers: { Authorization: token },
43
- };
44
-
45
- const newInfo = { ...info };
46
- // newInfo[key] = newInfo[key] || {};
47
- newInfo[key].state = "loading";
48
- newInfo[key].errorMessage = "";
49
- newInfo[key].params = query;
50
- newInfo[key].loaded = true;
51
-
52
- setInfo(newInfo);
53
-
54
- try {
55
- const consulta = await axios.request(options);
56
- const d = consulta.data;
57
- newInfo[key].state = "success";
58
- newInfo[key].errorMessage = "";
59
-
60
- newInfo[key].data = merge
61
- ? page == 1
62
- ? d.data
63
- : [...d.data, ...(newInfo[key].data || [])]
64
- : d.data;
65
- newInfo[key].totalItems = d.totalItems;
66
- newInfo[key].totalPages = d.totalPages;
67
- newInfo[key].currentPage = d.currentPage;
68
-
69
- setInfo({ ...newInfo });
70
- return d.data;
71
- } catch (error: any) {
72
- const item = httpStatusCodes.find((s) => s.code == error.status);
73
-
74
- newInfo[key].state = "error";
75
- newInfo[key].errorMessage = item?.meaning;
76
- if (error.status == 403) {
77
- onError?.({ error, ...{ errorMessage: item?.meaning } });
78
- }
79
- setInfo({ ...newInfo });
80
- return error;
81
- }
82
- };
83
- const findFunc = async (id: number, query: any) => {
84
- const options = {
85
- method: "GET",
86
- url: `${baseURI}/${key}/${id}`,
87
- params: { ...query },
88
- headers: { Authorization: token },
89
- };
90
- const newInfo = { ...info };
91
- // newInfo[key] = newInfo[key] || {};
92
- newInfo[key].state = "loading";
93
- newInfo[key].errorMessage = "";
94
- newInfo[key].params = query;
95
- newInfo[key].loaded = true;
96
-
97
- setInfo(newInfo);
98
-
99
- try {
100
- const consulta = await axios.request(options);
101
- const d = consulta.data;
102
- newInfo[key].state = "success";
103
- newInfo[key].errorMessage = "";
104
-
105
- newInfo[key].selectedItem = d;
106
- const index = newInfo[key]?.data?.findIndex((d: any) => d?.id == id);
107
-
108
- if (index >= 0) {
109
- newInfo[key].data[index] = d;
110
- } else {
111
- if (newInfo[key]?.data) {
112
- newInfo[key].data.unshift(d);
113
- } else {
114
- newInfo[key].data = [d];
115
- }
116
- }
117
-
118
- setInfo({ ...newInfo });
119
- return d;
120
- } catch (error: any) {
121
- const item = httpStatusCodes.find((s) => s.code == error.status);
122
-
123
- newInfo[key].state = "error";
124
- newInfo[key].errorMessage = item?.meaning || error.message;
125
- if (error.status == 403) {
126
- onError?.({ error, ...{ errorMessage: item?.meaning } });
127
- }
128
- setInfo({ ...newInfo });
129
- return error;
130
- }
131
- };
132
- // Helper para actualizar el array sin mutar
133
- const mergeDataArray = (
134
- existingData: any[] | undefined,
135
- newItem: any,
136
- matchId?: any,
137
- ): any[] => {
138
- if (!existingData) return [newItem];
139
- const index = existingData.findIndex((d: any) => d?.id == matchId);
140
- if (index >= 0) {
141
- return existingData.map((d: any, i: number) =>
142
- i === index ? newItem : d,
143
- );
144
- }
145
- return [newItem, ...existingData];
146
- };
60
+ // Ref siempre actualizado → las callbacks leen estado fresco sin re-crearse
61
+ const infoRef = useRef(info);
62
+ useEffect(() => {
63
+ infoRef.current = info;
64
+ }, [info]);
65
+
66
+ // ------------------------------------------------------------------
67
+ // Funciones de estado auxiliares (no dependen de key en tiempo de cierre)
68
+ // ------------------------------------------------------------------
69
+
70
+ const setKeyLoading = useCallback((key: string) => {
71
+ setInfo((prev) => ({
72
+ ...cloneKey(prev, key),
73
+ [key]: {
74
+ ...cloneKey(prev, key)[key],
75
+ state: "loading",
76
+ errorMessage: "",
77
+ },
78
+ }));
79
+ }, []);
80
+
81
+ const setKeyError = useCallback(
82
+ (key: string, error: any) => {
83
+ const meaning = getErrorMeaning(error);
84
+ if (error?.status === 403) onError?.({ error, errorMessage: meaning });
85
+ setInfo((prev) => ({
86
+ ...cloneKey(prev, key),
87
+ [key]: {
88
+ ...cloneKey(prev, key)[key],
89
+ state: "error",
90
+ errorMessage: meaning || error?.message,
91
+ },
92
+ }));
93
+ },
94
+ [onError],
95
+ );
147
96
 
148
- const bodyCreateFunc = async (
149
- data: Record<string, any> | Record<string, any>[],
150
- ) => {
151
- const options = {
152
- method: "POST",
153
- url: `${baseURI}/${key}`,
154
- data: Array.isArray(data) ? [...data] : { ...data },
155
- headers: { Authorization: token },
156
- };
97
+ // ------------------------------------------------------------------
98
+ // show
99
+ // ------------------------------------------------------------------
100
+
101
+ const show = useCallback(
102
+ async (
103
+ key: string,
104
+ { limit = 10, page = 1, merge = true, ...query }: ShowOptions,
105
+ ) => {
106
+ setInfo((prev) => ({
107
+ ...cloneKey(prev, key),
108
+ [key]: {
109
+ ...cloneKey(prev, key)[key],
110
+ state: "loading",
111
+ errorMessage: "",
112
+ params: query,
113
+ loaded: true,
114
+ },
115
+ }));
157
116
 
158
- const newInfo = {
159
- ...info,
160
- [key]: {
161
- ...info[key],
162
- data: [...(info[key]?.data ?? [])],
163
- state: "loading",
164
- errorMessage: "",
165
- },
166
- };
167
-
168
- setInfo(newInfo);
169
-
170
- try {
171
- const consulta = await axios.request(options);
172
- const d = consulta.data;
173
-
174
- let updatedData: any[];
175
-
176
- if (Array.isArray(data)) {
177
- updatedData = [...(newInfo[key]?.data ?? [])];
178
- for (const datum of data) {
179
- updatedData = mergeDataArray(updatedData, d, datum?.id);
180
- }
181
- updatedData = updatedData.flat();
182
- } else {
183
- updatedData = mergeDataArray(
184
- newInfo[key]?.data,
185
- d,
186
- (data as any)?.id,
187
- );
188
- }
189
-
190
- setInfo({
191
- ...newInfo,
117
+ try {
118
+ const { data: res } = await axios.get(`${baseURI}/${key}`, {
119
+ params: { limit, page, ...query },
120
+ headers: { Authorization: token },
121
+ });
122
+
123
+ setInfo((prev) => {
124
+ const prevData = prev[key]?.data ?? [];
125
+ const merged = merge
126
+ ? page === 1
127
+ ? res.data
128
+ : [...prevData, ...res.data]
129
+ : res.data;
130
+ return {
131
+ ...prev,
192
132
  [key]: {
193
- ...newInfo[key],
133
+ ...prev[key],
194
134
  state: "success",
195
135
  errorMessage: "",
196
- selectedItem: Array.isArray(data) ? newInfo[key].selectedItem : d,
197
- data: updatedData,
136
+ data: merged,
137
+ totalItems: res.totalItems,
138
+ totalPages: res.totalPages,
139
+ currentPage: res.currentPage,
198
140
  },
199
- });
200
-
201
- return d;
202
- } catch (error: any) {
203
- const item = httpStatusCodes.find((s) => s.code == error.status);
141
+ };
142
+ });
204
143
 
205
- if (error.status == 403) {
206
- onError?.({ error, errorMessage: item?.meaning });
207
- }
144
+ return res.data as any[];
145
+ } catch (error: any) {
146
+ setKeyError(key, error);
147
+ return error;
148
+ }
149
+ },
150
+ [baseURI, token, setKeyError],
151
+ );
208
152
 
209
- setInfo({
210
- ...newInfo,
211
- [key]: {
212
- ...newInfo[key],
213
- state: "error",
214
- errorMessage: item?.meaning || error.message,
215
- },
216
- });
153
+ // ------------------------------------------------------------------
154
+ // find
155
+ // ------------------------------------------------------------------
217
156
 
218
- return error;
219
- }
220
- };
157
+ const find = useCallback(
158
+ async (key: string, id: number, query?: any) => {
159
+ setInfo((prev) => ({
160
+ ...cloneKey(prev, key),
161
+ [key]: {
162
+ ...cloneKey(prev, key)[key],
163
+ state: "loading",
164
+ errorMessage: "",
165
+ params: query,
166
+ loaded: true,
167
+ },
168
+ }));
221
169
 
222
- const formCreateFunc = async (
223
- data: Record<string, any> | Record<string, any>[],
224
- ) => {
225
- const formData = new FormData();
226
-
227
- if (Array.isArray(data)) {
228
- data.forEach((item, i) => {
229
- for (const [k, v] of Object.entries(item)) {
230
- formData.append(`${k}[${i}]`, v as any);
231
- }
232
- });
233
- } else {
234
- for (const [k, v] of Object.entries(data)) {
235
- formData.append(k, v);
236
- }
237
- }
238
-
239
- const options = {
240
- method: "POST",
241
- url: `${baseURI}/${key}`,
242
- data: formData,
243
- headers: {
244
- Authorization: token,
245
- "Content-Type": "multipart/form-data",
246
- },
247
- };
170
+ try {
171
+ const { data: d } = await axios.get(`${baseURI}/${key}/${id}`, {
172
+ params: query,
173
+ headers: { Authorization: token },
174
+ });
248
175
 
249
- const newInfo = {
250
- ...info,
176
+ setInfo((prev) => ({
177
+ ...prev,
251
178
  [key]: {
252
- ...info[key],
253
- data: [...(info[key]?.data ?? [])],
254
- state: "loading",
179
+ ...prev[key],
180
+ state: "success",
255
181
  errorMessage: "",
182
+ selectedItem: d,
183
+ data: mergeDataArray(prev[key]?.data ?? [], d, id),
256
184
  },
257
- };
258
-
259
- setInfo(newInfo);
260
-
261
- try {
262
- const consulta = await axios.request(options);
263
- const d = consulta.data;
264
-
265
- let updatedData: any[];
266
-
267
- if (Array.isArray(data)) {
268
- updatedData = [...(newInfo[key]?.data ?? [])];
269
- for (const datum of data) {
270
- updatedData = mergeDataArray(updatedData, d, datum?.id);
271
- }
272
- updatedData = updatedData.flat();
273
- } else {
274
- updatedData = mergeDataArray(
275
- newInfo[key]?.data,
276
- d,
277
- (data as any)?.id,
278
- );
279
- }
280
-
281
- setInfo({
282
- ...newInfo,
185
+ }));
186
+
187
+ return d;
188
+ } catch (error: any) {
189
+ setKeyError(key, error);
190
+ return error;
191
+ }
192
+ },
193
+ [baseURI, token, setKeyError],
194
+ );
195
+
196
+ // ------------------------------------------------------------------
197
+ // create (body o multipart según contenido)
198
+ // ------------------------------------------------------------------
199
+
200
+ const buildFormData = (
201
+ data: Record<string, any> | Record<string, any>[],
202
+ ): FormData => {
203
+ const fd = new FormData();
204
+ if (Array.isArray(data)) {
205
+ data.forEach((item, i) => {
206
+ Object.entries(item).forEach(([k, v]) =>
207
+ fd.append(`${k}[${i}]`, v as any),
208
+ );
209
+ });
210
+ } else {
211
+ Object.entries(data).forEach(([k, v]) => fd.append(k, v));
212
+ }
213
+ return fd;
214
+ };
215
+
216
+ const hasFiles = (
217
+ data: Record<string, any> | Record<string, any>[],
218
+ ): boolean =>
219
+ Array.isArray(data)
220
+ ? data.some((item) => Object.values(item).some((v) => v instanceof File))
221
+ : Object.values(data).some((v) => v instanceof File);
222
+
223
+ const create = useCallback(
224
+ async (key: string, data: Record<string, any> | Record<string, any>[]) => {
225
+ setKeyLoading(key);
226
+
227
+ const isForm = hasFiles(data);
228
+ const payload = isForm
229
+ ? buildFormData(data)
230
+ : Array.isArray(data)
231
+ ? [...data]
232
+ : { ...data };
233
+
234
+ const headers: Record<string, string> = { Authorization: token };
235
+ if (isForm) headers["Content-Type"] = "multipart/form-data";
236
+
237
+ try {
238
+ const { data: d } = await axios.post(`${baseURI}/${key}`, payload, {
239
+ headers,
240
+ });
241
+
242
+ setInfo((prev) => {
243
+ const prevData = prev[key]?.data ?? [];
244
+ // Para arrays el servidor debería devolver el array creado;
245
+ // si devuelve un solo objeto, lo envolvemos.
246
+ const incoming: any[] = Array.isArray(d) ? d : [d];
247
+ const updatedData = incoming.reduce(
248
+ (acc, item) => mergeDataArray(acc, item, item?.id),
249
+ [...prevData],
250
+ );
251
+
252
+ return {
253
+ ...prev,
283
254
  [key]: {
284
- ...newInfo[key],
255
+ ...prev[key],
285
256
  state: "success",
286
257
  errorMessage: "",
287
- selectedItem: Array.isArray(data) ? newInfo[key].selectedItem : d,
258
+ selectedItem: Array.isArray(data) ? prev[key].selectedItem : d,
288
259
  data: updatedData,
289
260
  },
290
- });
261
+ };
262
+ });
291
263
 
292
- return d;
293
- } catch (error: any) {
294
- const item = httpStatusCodes.find((s) => s.code == error.status);
264
+ return d;
265
+ } catch (error: any) {
266
+ setKeyError(key, error);
267
+ return error;
268
+ }
269
+ },
270
+ [baseURI, token, setKeyLoading, setKeyError],
271
+ );
295
272
 
296
- if (error.status == 403) {
297
- onError?.({ error, errorMessage: item?.meaning });
298
- }
273
+ // ------------------------------------------------------------------
274
+ // update
275
+ // ------------------------------------------------------------------
299
276
 
300
- setInfo({
301
- ...newInfo,
302
- [key]: {
303
- ...newInfo[key],
304
- state: "error",
305
- errorMessage: item?.meaning || error.message,
306
- },
307
- });
277
+ const update = useCallback(
278
+ async (key: string, id: number, data: any) => {
279
+ setKeyLoading(key);
308
280
 
309
- return error;
310
- }
311
- };
312
- acc[key] = {
313
- ...endpoint,
314
- loaded: false,
315
- show: showFunc,
316
- find: findFunc,
317
- create: async (data: Record<string, any> | Record<string, any>[]) => {
318
- const hasFile = Array.isArray(data)
319
- ? data.some((item) =>
320
- Object.values(item).some((value) => value instanceof File),
321
- )
322
- : Object.values(data).some((value) => value instanceof File);
323
-
324
- if (hasFile) {
325
- return await formCreateFunc(data);
326
- }
327
-
328
- return await bodyCreateFunc(data);
329
- },
330
- update: async (id: number, data: any) => {
331
- const options = {
332
- method: "PATCH",
333
- url: `${baseURI}/${key}/${id}`,
334
- data: Array.isArray(data) ? [...data] : { ...data },
335
- headers: { Authorization: token },
336
- };
281
+ try {
282
+ const { data: d } = await axios.patch(
283
+ `${baseURI}/${key}/${id}`,
284
+ Array.isArray(data) ? [...data] : { ...data },
285
+ { headers: { Authorization: token } },
286
+ );
337
287
 
338
- const newInfo = {
339
- ...info,
340
- [key]: {
341
- ...info[key], // ✅ copia superficial del key
342
- data: [...(info[key]?.data ?? [])], // ✅ copia del array
343
- state: "loading",
344
- errorMessage: "",
345
- },
346
- };
288
+ setInfo((prev) => ({
289
+ ...prev,
290
+ [key]: {
291
+ ...prev[key],
292
+ state: "success",
293
+ errorMessage: "",
294
+ selectedItem: { ...prev[key].selectedItem, ...d },
295
+ data: mergeDataArray(
296
+ prev[key]?.data ?? [],
297
+ { ...prev[key]?.data?.find((i: any) => i?.id == id), ...d },
298
+ id,
299
+ ),
300
+ },
301
+ }));
347
302
 
348
- setInfo(newInfo);
349
-
350
- try {
351
- const consulta = await axios.request(options);
352
- const d = consulta.data;
353
-
354
- const index = newInfo[key]?.data?.findIndex(
355
- (item: any) => item?.id == id,
356
- );
357
-
358
- const updatedData =
359
- index >= 0
360
- ? newInfo[key].data.map(
361
- (
362
- item: any,
363
- i: number, // ✅ map en lugar de mutación
364
- ) => (i === index ? { ...item, ...d } : item),
365
- )
366
- : newInfo[key]?.data
367
- ? [d, ...newInfo[key].data] // ✅ nuevo array en lugar de unshift
368
- : [d];
369
-
370
- const updatedInfo = {
371
- ...newInfo,
372
- [key]: {
373
- ...newInfo[key],
374
- state: "success",
375
- errorMessage: "",
376
- selectedItem: { ...newInfo[key].selectedItem, ...d },
377
- data: updatedData,
378
- },
379
- };
380
-
381
- setInfo(updatedInfo);
382
- return d;
383
- } catch (error: any) {
384
- const item = httpStatusCodes.find((s) => s.code == error.status);
385
-
386
- setInfo({
387
- ...newInfo,
388
- [key]: {
389
- ...newInfo[key],
390
- state: "error",
391
- errorMessage: item?.meaning || error.message,
392
- },
393
- });
394
-
395
- if (error.status == 403) {
396
- onError?.({ error, ...{ errorMessage: item?.meaning } });
397
- }
398
-
399
- return error;
400
- }
401
- },
402
- remove: async (id: number) => {
403
- const options = {
404
- method: "DELETE",
405
- url: `${baseURI}/${key}/${id}`,
406
- headers: { Authorization: token },
407
- };
303
+ return d;
304
+ } catch (error: any) {
305
+ setKeyError(key, error);
306
+ return error;
307
+ }
308
+ },
309
+ [baseURI, token, setKeyLoading, setKeyError],
310
+ );
408
311
 
409
- const newInfo = {
410
- ...info,
411
- [key]: {
412
- ...info[key],
413
- data: [...(info[key]?.data ?? [])],
414
- state: "loading",
415
- errorMessage: "",
416
- },
417
- };
312
+ // ------------------------------------------------------------------
313
+ // remove
314
+ // ------------------------------------------------------------------
418
315
 
419
- setInfo(newInfo);
420
-
421
- try {
422
- const consulta = await axios.request(options);
423
- const d = consulta.data;
424
-
425
- setInfo({
426
- ...newInfo,
427
- [key]: {
428
- ...newInfo[key],
429
- state: "success",
430
- errorMessage: "",
431
- selectedItem: d,
432
- data:
433
- newInfo[key]?.data?.filter((item: any) => item?.id != id) ??
434
- [], // ✅ filter en lugar de splice
435
- },
436
- });
437
-
438
- return d.data;
439
- } catch (error: any) {
440
- const item = httpStatusCodes.find((s) => s.code == error.status);
441
-
442
- if (error.status == 403) {
443
- onError?.({ error, errorMessage: item?.meaning });
444
- }
445
-
446
- setInfo({
447
- ...newInfo,
448
- [key]: {
449
- ...newInfo[key],
450
- state: "error",
451
- errorMessage: item?.meaning || error.message,
452
- },
453
- });
454
-
455
- return error;
456
- }
457
- },
458
- totalPages: info[key]?.totalPages,
459
- currentPage: info[key]?.currentPage,
460
- state: info[key]?.state || "success",
461
- errorMessage: info[key]?.errorMessage,
462
- params: info[key]?.params,
463
- setLoaded: () => {
464
- const newInfo = { ...info };
465
- newInfo[key].loaded = true;
466
- setInfo(newInfo);
467
- },
468
- getAllPages: async (limit = 100) => {
469
- const allData: any[] = [];
470
- let currentPage = 1;
471
- let totalPages = 1;
472
-
473
- while (currentPage <= totalPages) {
474
- const response = await results[key].show({
475
- limit,
476
- page: currentPage,
477
- });
478
-
479
- // Importante: `show` ya actualiza info[key], así que podemos leer desde ahí
480
- const currentInfo = info[key];
481
-
482
- if (Array.isArray(response)) {
483
- allData.push(...response);
484
- }
485
-
486
- currentPage = currentInfo.currentPage || currentPage + 1;
487
- totalPages = currentInfo.totalPages || currentPage;
488
- }
489
-
490
- const newInfo = { ...info };
491
- newInfo[key].data = allData;
492
- newInfo[key].currentPage = currentPage;
493
- newInfo[key].totalPages = totalPages;
494
- setInfo(newInfo);
495
-
496
- return allData;
497
- },
498
- data: info[key]?.data || [],
499
- selectedItem: info[key]?.selectedItem || {},
500
- };
316
+ const remove = useCallback(
317
+ async (key: string, id: number) => {
318
+ setKeyLoading(key);
501
319
 
502
- return acc;
320
+ try {
321
+ const { data: d } = await axios.delete(`${baseURI}/${key}/${id}`, {
322
+ headers: { Authorization: token },
323
+ });
324
+
325
+ setInfo((prev) => ({
326
+ ...prev,
327
+ [key]: {
328
+ ...prev[key],
329
+ state: "success",
330
+ errorMessage: "",
331
+ selectedItem: d,
332
+ data: (prev[key]?.data ?? []).filter((item: any) => item?.id != id),
333
+ },
334
+ }));
335
+
336
+ return d;
337
+ } catch (error: any) {
338
+ setKeyError(key, error);
339
+ return error;
340
+ }
503
341
  },
504
- {},
342
+ [baseURI, token, setKeyLoading, setKeyError],
505
343
  );
506
344
 
507
- async function doSome() {
508
- const key = Object.keys(info).find((k) => {
509
- return info[k].loaded === false;
510
- });
511
-
512
- if (key) {
513
- if (info[key]?.id) {
514
- await results[key].find(info[key]?.id, info[key]?.defaultParams);
515
- } else if (info[key]?.defaultParams) {
516
- await results[key].show(info[key]?.defaultParams);
517
- } else {
518
- results[key].setLoaded();
519
- }
520
- }
521
- }
345
+ // ------------------------------------------------------------------
346
+ // getAllPages ya no usa referencia circular a results
347
+ // ------------------------------------------------------------------
348
+
349
+ const getAllPages = useCallback(
350
+ async (key: string, limit = 100) => {
351
+ const allData: any[] = [];
352
+ let page = 1;
353
+ let totalPages = 1;
354
+
355
+ do {
356
+ const pageData = await show(key, { limit, page, merge: false });
357
+ if (Array.isArray(pageData)) allData.push(...pageData);
358
+ // Leer totalPages desde infoRef (siempre fresco)
359
+ totalPages = infoRef.current[key]?.totalPages ?? page;
360
+ page++;
361
+ } while (page <= totalPages);
362
+
363
+ setInfo((prev) => ({
364
+ ...prev,
365
+ [key]: { ...prev[key], data: allData },
366
+ }));
367
+
368
+ return allData;
369
+ },
370
+ [show],
371
+ );
372
+
373
+ // ------------------------------------------------------------------
374
+ // Auto-load al montar
375
+ // ------------------------------------------------------------------
376
+
522
377
  useEffect(() => {
523
- doSome();
524
- }, [JSON.stringify(Object.values(info).map((v) => v.loaded))]);
378
+ const key = Object.keys(info).find((k) => info[k].loaded === false);
379
+ if (!key) return;
380
+
381
+ if (info[key]?.id) {
382
+ find(key, info[key].id as any, info[key].defaultParams);
383
+ } else if (info[key]?.defaultParams) {
384
+ show(key, info[key].defaultParams);
385
+ } else {
386
+ setInfo((prev) => ({
387
+ ...prev,
388
+ [key]: { ...prev[key], loaded: true },
389
+ }));
390
+ }
391
+ // Solo se dispara cuando cambia cuáles keys están cargados
392
+ // eslint-disable-next-line react-hooks/exhaustive-deps
393
+ }, [
394
+ Object.keys(info)
395
+ .map((k) => info[k].loaded)
396
+ .join(","),
397
+ ]);
398
+
399
+ // ------------------------------------------------------------------
400
+ // Construir el objeto results que se expone al consumidor
401
+ // ------------------------------------------------------------------
402
+
403
+ const results = Object.keys(endpoints).reduce((acc: any, key) => {
404
+ const s = info[key];
405
+ acc[key] = {
406
+ ...endpoints[key],
407
+ data: s?.data ?? [],
408
+ selectedItem: s?.selectedItem ?? {},
409
+ state: s?.state ?? "success",
410
+ errorMessage: s?.errorMessage,
411
+ params: s?.params,
412
+ totalPages: s?.totalPages,
413
+ currentPage: s?.currentPage,
414
+ totalItems: s?.totalItems,
415
+ loaded: s?.loaded ?? false,
416
+ show: (opts: ShowOptions) => show(key, opts),
417
+ find: (id: number, query?: any) => find(key, id, query),
418
+ create: (data: Record<string, any> | Record<string, any>[]) =>
419
+ create(key, data),
420
+ update: (id: number, data: any) => update(key, id, data),
421
+ remove: (id: number) => remove(key, id),
422
+ getAllPages: (limit?: number) => getAllPages(key, limit),
423
+ setLoaded: () =>
424
+ setInfo((prev) => ({ ...prev, [key]: { ...prev[key], loaded: true } })),
425
+ };
426
+ return acc;
427
+ }, {} as EnhancedEndpoints<T>);
525
428
 
526
429
  return results;
527
430
  }