@airoom/nextmin-react 0.1.0

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.
Files changed (95) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +133 -0
  3. package/dist/auth/AuthPage.d.ts +1 -0
  4. package/dist/auth/AuthPage.js +23 -0
  5. package/dist/auth/ForgotPasswordForm.d.ts +1 -0
  6. package/dist/auth/ForgotPasswordForm.js +28 -0
  7. package/dist/auth/SignInForm.d.ts +6 -0
  8. package/dist/auth/SignInForm.js +38 -0
  9. package/dist/auth/SignUpForm.d.ts +3 -0
  10. package/dist/auth/SignUpForm.js +30 -0
  11. package/dist/components/AddressAutocomplete.d.ts +21 -0
  12. package/dist/components/AddressAutocomplete.js +182 -0
  13. package/dist/components/AdminApp.d.ts +1 -0
  14. package/dist/components/AdminApp.js +134 -0
  15. package/dist/components/ConfirmDialog.d.ts +12 -0
  16. package/dist/components/ConfirmDialog.js +6 -0
  17. package/dist/components/FileUploader.d.ts +32 -0
  18. package/dist/components/FileUploader.js +480 -0
  19. package/dist/components/NoAccess.d.ts +3 -0
  20. package/dist/components/NoAccess.js +5 -0
  21. package/dist/components/PasswordInput.d.ts +19 -0
  22. package/dist/components/PasswordInput.js +11 -0
  23. package/dist/components/PhoneInput.d.ts +23 -0
  24. package/dist/components/PhoneInput.js +147 -0
  25. package/dist/components/RefMultiSelect.d.ts +14 -0
  26. package/dist/components/RefMultiSelect.js +76 -0
  27. package/dist/components/RefSingleSelect.d.ts +17 -0
  28. package/dist/components/RefSingleSelect.js +52 -0
  29. package/dist/components/SchemaForm.d.ts +13 -0
  30. package/dist/components/SchemaForm.js +592 -0
  31. package/dist/components/SectionLoader.d.ts +3 -0
  32. package/dist/components/SectionLoader.js +7 -0
  33. package/dist/components/Sidebar.d.ts +1 -0
  34. package/dist/components/Sidebar.js +87 -0
  35. package/dist/components/TableFilters.d.ts +16 -0
  36. package/dist/components/TableFilters.js +69 -0
  37. package/dist/components/TableSkeleton.d.ts +7 -0
  38. package/dist/components/TableSkeleton.js +5 -0
  39. package/dist/hooks/useGoogleMapsKey.d.ts +5 -0
  40. package/dist/hooks/useGoogleMapsKey.js +16 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/lib/api.d.ts +31 -0
  44. package/dist/lib/api.js +94 -0
  45. package/dist/lib/auth.d.ts +23 -0
  46. package/dist/lib/auth.js +51 -0
  47. package/dist/lib/googleLoader.d.ts +1 -0
  48. package/dist/lib/googleLoader.js +25 -0
  49. package/dist/lib/schemaService.d.ts +2 -0
  50. package/dist/lib/schemaService.js +39 -0
  51. package/dist/lib/schemaUtils.d.ts +4 -0
  52. package/dist/lib/schemaUtils.js +18 -0
  53. package/dist/lib/types.d.ts +50 -0
  54. package/dist/lib/types.js +1 -0
  55. package/dist/nextmin.css +1 -0
  56. package/dist/providers/NextMinProvider.d.ts +5 -0
  57. package/dist/providers/NextMinProvider.js +30 -0
  58. package/dist/router/AdminRouteNormalizer.d.ts +1 -0
  59. package/dist/router/AdminRouteNormalizer.js +32 -0
  60. package/dist/router/NextMinRouter.d.ts +1 -0
  61. package/dist/router/NextMinRouter.js +99 -0
  62. package/dist/state/nextMinSlice.d.ts +14 -0
  63. package/dist/state/nextMinSlice.js +34 -0
  64. package/dist/state/schemaLive.d.ts +2 -0
  65. package/dist/state/schemaLive.js +19 -0
  66. package/dist/state/schemasSlice.d.ts +20 -0
  67. package/dist/state/schemasSlice.js +43 -0
  68. package/dist/state/sessionSlice.d.ts +10 -0
  69. package/dist/state/sessionSlice.js +18 -0
  70. package/dist/state/store.d.ts +28 -0
  71. package/dist/state/store.js +7 -0
  72. package/dist/views/CreateEditPage.d.ts +4 -0
  73. package/dist/views/CreateEditPage.js +64 -0
  74. package/dist/views/DashboardPage.d.ts +1 -0
  75. package/dist/views/DashboardPage.js +107 -0
  76. package/dist/views/ListPage.d.ts +5 -0
  77. package/dist/views/ListPage.js +76 -0
  78. package/dist/views/NextNotFound.d.ts +1 -0
  79. package/dist/views/NextNotFound.js +6 -0
  80. package/dist/views/ProfilePage.d.ts +1 -0
  81. package/dist/views/ProfilePage.js +193 -0
  82. package/dist/views/SettingsEdit.d.ts +2 -0
  83. package/dist/views/SettingsEdit.js +87 -0
  84. package/dist/views/list/DataTableHero.d.ts +22 -0
  85. package/dist/views/list/DataTableHero.js +350 -0
  86. package/dist/views/list/ListHeader.d.ts +8 -0
  87. package/dist/views/list/ListHeader.js +7 -0
  88. package/dist/views/list/Pagination.d.ts +8 -0
  89. package/dist/views/list/Pagination.js +5 -0
  90. package/dist/views/list/formatters.d.ts +2 -0
  91. package/dist/views/list/formatters.js +62 -0
  92. package/dist/views/list/useListData.d.ts +10 -0
  93. package/dist/views/list/useListData.js +79 -0
  94. package/package.json +51 -0
  95. package/tsconfig.json +18 -0
@@ -0,0 +1,592 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState, useId } from 'react';
4
+ import { Form, Input, Textarea, Checkbox, RadioGroup, Radio, Select, SelectItem, Button, DatePicker, DateRangePicker, TimeInput, } from '@heroui/react';
5
+ import { useSelector } from 'react-redux';
6
+ import { inputTypeFor } from '../lib/schemaUtils';
7
+ import { RefMultiSelect } from './RefMultiSelect';
8
+ import { RefSingleSelect } from './RefSingleSelect';
9
+ import { PasswordInput } from './PasswordInput';
10
+ import { PhoneInput } from './PhoneInput';
11
+ import { FileUploader } from './FileUploader';
12
+ import AddressAutocompleteGoogle from './AddressAutocomplete';
13
+ import { useGoogleMapsKey } from '../hooks/useGoogleMapsKey';
14
+ import { parseDate, parseDateTime, parseTime, } from '@internationalized/date';
15
+ const inputClassNames = { inputWrapper: 'bg-transparent shadow-none' };
16
+ const selectClassNames = { trigger: 'bg-transparent shadow-none' };
17
+ const AUDIT_FIELDS = new Set(['createdAt', 'updatedAt']);
18
+ const digitsOnly = (s) => String(s ?? '').replace(/\D/g, '');
19
+ function readLocalUser() {
20
+ if (typeof window === 'undefined')
21
+ return null;
22
+ try {
23
+ const raw = localStorage.getItem('nextmin.user');
24
+ return raw ? JSON.parse(raw) : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /** ---------------------------- NEW HELPERS (DATE/TIME/RANGE) ---------------------------- **/
31
+ // Decide if an attribute is a date, time, or range, plus the withTime flag
32
+ function isDateType(attr) {
33
+ const t = String(attr?.type ?? '').toLowerCase();
34
+ const f = String(attr?.format ?? '').toLowerCase();
35
+ const it = String(inputTypeFor(attr) ?? '').toLowerCase(); // <-- force string
36
+ return (t === 'date' ||
37
+ f === 'date' ||
38
+ it === 'date' ||
39
+ it === 'datetime-local' ||
40
+ f === 'datetime' ||
41
+ t === 'datetime');
42
+ }
43
+ function isTimeType(attr) {
44
+ const t = String(attr?.type ?? '').toLowerCase();
45
+ const f = String(attr?.format ?? '').toLowerCase();
46
+ const it = String(inputTypeFor(attr) ?? '').toLowerCase(); // <-- force string
47
+ return t === 'time' || f === 'time' || it === 'time';
48
+ }
49
+ function isRangeType(attr) {
50
+ const t = String(attr?.type ?? '').toLowerCase();
51
+ const f = String(attr?.format ?? '').toLowerCase();
52
+ // support { type: 'range' }, { type: 'date-range' }, or { format: 'date-range' }
53
+ return (t === 'range' || t === 'date-range' || f === 'date-range' || f === 'range');
54
+ }
55
+ function withTimeEnabled(attr) {
56
+ return Boolean(attr?.withTime);
57
+ }
58
+ function parseTimeString(value) {
59
+ if (value == null)
60
+ return null;
61
+ let s = String(value).trim();
62
+ // accept 930 / 1530 → "09:30" / "15:30"
63
+ if (/^\d{3,4}$/.test(s)) {
64
+ const pad = s.padStart(4, '0');
65
+ s = `${pad.slice(0, 2)}:${pad.slice(2)}`;
66
+ }
67
+ // AM/PM path (e.g., "3:05 PM", "12:00 am")
68
+ const ampm = s.match(/\s*([AaPp][Mm])\s*$/);
69
+ if (ampm) {
70
+ const core = s.replace(/\s*[AaPp][Mm]\s*$/, '').trim();
71
+ const m = core.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
72
+ if (!m)
73
+ return null;
74
+ let h = Number(m[1]);
75
+ const min = Number(m[2]);
76
+ const sec = m[3] ? Number(m[3]) : 0;
77
+ const isPM = /^[Pp]/.test(ampm[1]);
78
+ if (isPM && h < 12)
79
+ h += 12; // 1..11 PM → 13..23
80
+ if (!isPM && h === 12)
81
+ h = 0; // 12 AM → 00
82
+ const hh = String(h).padStart(2, '0');
83
+ const mm = String(min).padStart(2, '0');
84
+ const ss = String(sec).padStart(2, '0');
85
+ try {
86
+ return parseTime(`${hh}:${mm}:${ss}`);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ // 24h path: "H:mm" / "HH:mm" / "HH:mm:ss"
93
+ const m24 = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
94
+ if (m24) {
95
+ const hh = String(Number(m24[1])).padStart(2, '0');
96
+ const mm = String(Number(m24[2])).padStart(2, '0');
97
+ const ss = String(m24[3] ? Number(m24[3]) : 0).padStart(2, '0');
98
+ try {
99
+ return parseTime(`${hh}:${mm}:${ss}`);
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+ function serializeTime(t) {
108
+ // store canonical 24h "HH:mm"
109
+ return t ? t.toString().slice(0, 5) : '';
110
+ }
111
+ function parseDateString(v, withTime) {
112
+ if (!v || typeof v !== 'string')
113
+ return null;
114
+ try {
115
+ return withTime ? parseDateTime(v) : parseDate(v);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ function normalizeRangeIn(value) {
122
+ if (!value)
123
+ return null;
124
+ if (Array.isArray(value)) {
125
+ const [s, e] = value;
126
+ return s && e ? { start: String(s), end: String(e) } : null;
127
+ }
128
+ if (typeof value === 'object') {
129
+ const anyv = value;
130
+ const s = anyv?.start ?? anyv?.from ?? anyv?.begin;
131
+ const e = anyv?.end ?? anyv?.to ?? anyv?.finish;
132
+ return s && e ? { start: String(s), end: String(e) } : null;
133
+ }
134
+ if (typeof value === 'string') {
135
+ const m = value.split('..');
136
+ return m.length === 2 && m[0] && m[1] ? { start: m[0], end: m[1] } : null;
137
+ }
138
+ return null;
139
+ }
140
+ function serializeDate(v, withTime) {
141
+ const raw = v.toString(); // "YYYY-MM-DD" or "YYYY-MM-DDThh:mm"
142
+ return withTime ? raw : raw.slice(0, 10);
143
+ }
144
+ function serializeRangeOut(start, end, withTime) {
145
+ if (!start || !end)
146
+ return null;
147
+ return {
148
+ start: serializeDate(start, withTime),
149
+ end: serializeDate(end, withTime),
150
+ };
151
+ }
152
+ function normalizeTimeRangeLoose(value) {
153
+ if (!value)
154
+ return {};
155
+ if (Array.isArray(value)) {
156
+ const [s, e] = value;
157
+ return {
158
+ start: s != null && s !== '' ? String(s) : undefined,
159
+ end: e != null && e !== '' ? String(e) : undefined,
160
+ };
161
+ }
162
+ if (typeof value === 'object') {
163
+ const anyv = value;
164
+ const s = anyv?.start ?? anyv?.from ?? anyv?.begin ?? '';
165
+ const e = anyv?.end ?? anyv?.to ?? anyv?.finish ?? '';
166
+ return {
167
+ start: s != null && s !== '' ? String(s) : undefined,
168
+ end: e != null && e !== '' ? String(e) : undefined,
169
+ };
170
+ }
171
+ if (typeof value === 'string') {
172
+ const parts = value.split('..');
173
+ if (parts.length === 2) {
174
+ const [s, e] = parts;
175
+ return {
176
+ start: s != null && s !== '' ? s : undefined,
177
+ end: e != null && e !== '' ? e : undefined,
178
+ };
179
+ }
180
+ }
181
+ return {};
182
+ }
183
+ /** --------------------------------------------------------------------------------------- **/
184
+ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel = 'Save', busy = false, showReset = true, onSubmit, }) {
185
+ const formUid = useId();
186
+ const { items } = useSelector((s) => s.schemas);
187
+ const reduxUser = useSelector((s) => s?.session?.user ?? null) ??
188
+ null;
189
+ const effectiveUser = useMemo(() => reduxUser ?? readLocalUser(), [reduxUser]);
190
+ const sessionRole = useMemo(() => {
191
+ if (!effectiveUser)
192
+ return [];
193
+ const raw = effectiveUser.roles ?? effectiveUser.role;
194
+ const toName = (r) => {
195
+ if (!r)
196
+ return null;
197
+ if (typeof r === 'string')
198
+ return r;
199
+ if (typeof r?.name === 'string')
200
+ return r.name;
201
+ return null;
202
+ };
203
+ if (Array.isArray(raw)) {
204
+ return raw.map(toName).filter((s) => !!s);
205
+ }
206
+ const single = toName(raw);
207
+ return single ? [single] : [];
208
+ }, [effectiveUser]);
209
+ const schema = useMemo(() => schemaOverride ??
210
+ items.find((s) => s.modelName.toLowerCase() === model.toLowerCase()), [items, model, schemaOverride]);
211
+ const [form, setForm] = useState(initialValues ?? {});
212
+ const [error, setError] = useState();
213
+ const isEdit = useMemo(() => !!(initialValues &&
214
+ (initialValues.id || initialValues._id)), [initialValues]);
215
+ useEffect(() => {
216
+ const next = { ...(initialValues ?? {}) };
217
+ if ('password' in next)
218
+ next.password = '';
219
+ if (schema) {
220
+ for (const [name, attr] of Object.entries(schema.attributes)) {
221
+ if (isPhoneAttr(name, attr) &&
222
+ typeof next[name] === 'string') {
223
+ next[name] = digitsOnly(next[name]);
224
+ }
225
+ }
226
+ }
227
+ setForm(next);
228
+ }, [initialValues, schema]);
229
+ if (!schema)
230
+ return _jsx("div", { className: "text-danger text-sm", children: "Schema not found" });
231
+ const bypassRoles = useMemo(() => {
232
+ const raw = schema?.access?.bypassPrivacy?.roles ?? [];
233
+ const toName = (r) => typeof r === 'string' ? r : typeof r?.name === 'string' ? r.name : null;
234
+ const configured = raw
235
+ .map(toName)
236
+ .filter((s) => !!s)
237
+ .map((s) => s.toLowerCase());
238
+ return configured.length > 0 ? configured : ['admin', 'superadmin'];
239
+ }, [schema]);
240
+ const canBypassPrivacy = useMemo(() => {
241
+ const userRolesLC = sessionRole.map((r) => r.toLowerCase());
242
+ return userRolesLC.some((r) => bypassRoles.includes(r));
243
+ }, [sessionRole, bypassRoles]);
244
+ const fields = useMemo(() => Object.entries(schema.attributes)
245
+ .filter(([name]) => !AUDIT_FIELDS.has(name))
246
+ .filter(([, attr]) => canBypassPrivacy ? true : !attr?.private)
247
+ .filter(([, attr]) => !isHiddenAttr(attr, isEdit))
248
+ .filter(([name, attr]) => !(isEdit && isPasswordAttr(name, attr)))
249
+ .map(([name, attr]) => ({ name, attr })), [schema, canBypassPrivacy, isEdit]);
250
+ const gridClass = 'grid gap-4 grid-cols-2';
251
+ const handleChange = (name, value) => setForm((f) => ({ ...f, [name]: value }));
252
+ const handleSubmit = async (e) => {
253
+ e.preventDefault();
254
+ setError(undefined);
255
+ try {
256
+ const { createdAt, updatedAt, ...payload } = form;
257
+ await onSubmit(payload);
258
+ }
259
+ catch (err) {
260
+ setError(err?.message || 'Submission failed');
261
+ }
262
+ };
263
+ const handleReset = () => {
264
+ const next = { ...(initialValues ?? {}) };
265
+ if ('password' in next)
266
+ next.password = '';
267
+ if (schema) {
268
+ for (const [name, attr] of Object.entries(schema.attributes)) {
269
+ if (isPhoneAttr(name, attr) &&
270
+ typeof next[name] === 'string') {
271
+ next[name] = digitsOnly(next[name]);
272
+ }
273
+ }
274
+ }
275
+ setForm(next);
276
+ };
277
+ return (_jsxs(Form, { className: gridClass, onSubmit: handleSubmit, onReset: handleReset, validationBehavior: "native", encType: "multipart/form-data", children: [fields.map(({ name, attr }) => {
278
+ const colClass = 'col-span-1';
279
+ // --- 1) Array of references → multi select (1 column)
280
+ const refArray = getRefArraySpec(attr);
281
+ if (refArray) {
282
+ return (_jsx("div", { className: colClass, children: _jsx(RefMultiSelect, { name: name, label: attr?.label ?? formatLabel(name), refModel: refArray.ref, showKey: refArray.show ?? 'name', description: attr?.description, value: normalizeIdsArray(form[name]), onChange: (ids) => handleChange(name, ids), disabled: busy, required: !!attr?.required, pageSize: attr?.pageSize }) }, name));
283
+ }
284
+ // --- 2) Single reference → single select (1 column)
285
+ const refSingle = getRefSingleSpec(attr);
286
+ if (refSingle) {
287
+ return (_jsx("div", { className: colClass, children: _jsx(RefSingleSelect, { name: name, label: attr?.label ?? formatLabel(name), refModel: refSingle.ref, showKey: refSingle.show ?? 'name', description: attr?.description, value: normalizeId(form[name]), onChange: (id) => handleChange(name, id), disabled: busy, required: !!attr?.required, classNames: selectClassNames, pageSize: attr?.pageSize }) }, name));
288
+ }
289
+ // let password reflect local state, but never prefill
290
+ const baseValue = name.toLowerCase() === 'password'
291
+ ? typeof form[name] === 'string'
292
+ ? form[name]
293
+ : ''
294
+ : (form[name] ?? (Array.isArray(attr) ? [] : ''));
295
+ return (_jsx("div", { className: colClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, value: baseValue, onChange: handleChange, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames }) }, name));
296
+ }), error && (_jsx("div", { className: "col-span-2", children: _jsx("div", { className: "text-danger text-sm", children: error }) })), _jsxs("div", { className: "flex gap-2 col-span-2", children: [_jsx(Button, { type: "submit", color: "primary", isLoading: busy, children: submitLabel }), showReset && (_jsx(Button, { type: "reset", variant: "flat", isDisabled: busy, children: "Reset" }))] })] }));
297
+ }
298
+ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, }) {
299
+ const id = `${uid}-${name}`;
300
+ const label = attr?.label ?? formatLabel(name);
301
+ const required = !!attr?.required;
302
+ const description = attr?.description;
303
+ // Prefer ref over enum. Only use enum when there is NO ref.
304
+ const enumVals = attr?.ref
305
+ ? undefined
306
+ : attr?.enum;
307
+ const isPasswordField = name.toLowerCase() === 'password' ||
308
+ attr?.format === 'password' ||
309
+ attr?.writeOnly === true;
310
+ const isPhoneField = isPhoneAttr(name, attr);
311
+ const rawMask = typeof attr?.mask === 'string' ? attr.mask : '';
312
+ const hasSlots = /[Xx9#_]/.test(rawMask);
313
+ const phoneMask = hasSlots ? rawMask : 'xxx-xxxx-xxxx';
314
+ const isFileField = String(attr?.format || '').toLowerCase() === 'file' ||
315
+ /^(file|image|avatar|photo|picture|logo|siteLogo|sitelogo|profilepicture|bannerImage)$/i.test(name);
316
+ if (isFileField) {
317
+ const head = Array.isArray(attr) ? attr[0] : attr;
318
+ const multiple = Array.isArray(attr) || Boolean(head?.multiple);
319
+ const fileTypes = head?.fileTypes;
320
+ const maxFileSize = typeof head?.maxFileSize === 'number' ? head.maxFileSize : undefined;
321
+ const maxFilesCount = multiple && typeof head?.maxFilesCount === 'number'
322
+ ? Math.max(1, head.maxFilesCount)
323
+ : undefined;
324
+ const isRequired = !!attr?.required ||
325
+ (Array.isArray(attr) ? !!head?.required : false);
326
+ const controlledValue = multiple
327
+ ? Array.isArray(value)
328
+ ? value
329
+ : typeof value === 'string' && value
330
+ ? [value]
331
+ : []
332
+ : typeof value === 'string'
333
+ ? value
334
+ : null;
335
+ return (_jsx(FileUploader, { name: name, label: label, value: controlledValue, onChange: (v) => onChange(name, v), required: isRequired, disabled: disabled, accept: fileTypes, maxSizeBytes: maxFileSize, multiple: multiple, maxFilesCount: maxFilesCount, description: attr?.description, className: "w-full" }));
336
+ }
337
+ if (isPasswordField) {
338
+ return (_jsx(PasswordInput, { id: id, name: name, label: label, value: typeof value === 'string' ? value : '', onChange: (v) => onChange(name, v), disabled: disabled, required: required, description: description, className: "w-full", classNames: inputClassNames, autoComplete: "new-password" }));
339
+ }
340
+ if (isPhoneField) {
341
+ const rawDigits = typeof value === 'string' || typeof value === 'number'
342
+ ? String(value).replace(/\D/g, '')
343
+ : '';
344
+ return (_jsx(PhoneInput, { id: id, name: name, label: label, mask: phoneMask, value: rawDigits, onChange: (raw) => onChange(name, raw), disabled: disabled, required: required, description: description, className: "w-full", classNames: inputClassNames }));
345
+ }
346
+ const mapsKey = useGoogleMapsKey();
347
+ const populateField = getPopulateTarget(attr);
348
+ // Address → Autocomplete
349
+ if (isAddressAttr(name, attr)) {
350
+ if (!mapsKey) {
351
+ return (_jsxs("div", { className: "text-danger text-xs", children: ["Google Maps API key is missing. Set ", _jsx("b", { children: "googleMapsKey" }), " in", ' ', _jsx("b", { children: "Settings" }), " or define ", _jsx("b", { children: "NEXT_PUBLIC_GOOGLE_MAPS_KEY" }), " in the environment file."] }));
352
+ }
353
+ return (_jsx(AddressAutocompleteGoogle, { apiKey: mapsKey, name: name, label: label, description: description, value: typeof value === 'string' ? value : '', onChange: (addr, latlng) => {
354
+ if (!latlng)
355
+ return;
356
+ onChange(name, addr);
357
+ if (populateField) {
358
+ onChange(populateField, `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`);
359
+ }
360
+ }, disabled: disabled, required: required, className: "w-full", classNames: inputClassNames, countryCodes: attr?.countryCodes, limit: attr?.limit ?? 8 }));
361
+ }
362
+ /** ----------------------- NEW: DATE / TIME / RANGE RENDERERS ----------------------- **/
363
+ // 1) TIME-ONLY RANGE → two TimeInput fields (AM/PM UI)
364
+ if (isRangeType(attr) && attr?.timeOnly === true) {
365
+ const raw = normalizeTimeRangeLoose(value);
366
+ const startT = parseTimeString(raw.start ?? null);
367
+ const endT = parseTimeString(raw.end ?? null);
368
+ const setStart = (t) => {
369
+ onChange(name, {
370
+ start: serializeTime(t),
371
+ end: endT ? serializeTime(endT) : '',
372
+ });
373
+ };
374
+ const setEnd = (t) => {
375
+ onChange(name, {
376
+ start: startT ? serializeTime(startT) : '00:00', // default start if user sets end first
377
+ end: serializeTime(t),
378
+ });
379
+ };
380
+ return (_jsx("div", { className: "w-full", children: _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(TimeInput, { variant: "bordered", label: `${label} (Start)`, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: startT ?? undefined, onChange: setStart, granularity: "minute", hourCycle: 12 }), _jsx(TimeInput, { variant: "bordered", label: `${label} (End)`, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: endT ?? undefined, onChange: setEnd, granularity: "minute", hourCycle: 12 })] }) }));
381
+ }
382
+ // 2) DATE RANGE (default; supports withTime)
383
+ if (isRangeType(attr)) {
384
+ const withTime = withTimeEnabled(attr);
385
+ const norm = normalizeRangeIn(value);
386
+ const startVal = norm?.start ? parseDateString(norm.start, withTime) : null;
387
+ const endVal = norm?.end ? parseDateString(norm.end, withTime) : null;
388
+ return (_jsx(DateRangePicker, { variant: "bordered", label: label, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: startVal && endVal ? { start: startVal, end: endVal } : null, onChange: (val) => {
389
+ if (!val)
390
+ return onChange(name, null);
391
+ const { start, end } = val;
392
+ onChange(name, serializeRangeOut(start, end, withTime));
393
+ }, granularity: withTime ? 'minute' : 'day', hourCycle: withTime ? 24 : undefined }));
394
+ }
395
+ // 3) SINGLE DATE (supports withTime)
396
+ if (isDateType(attr)) {
397
+ const withTime = withTimeEnabled(attr);
398
+ const parsed = parseDateString(value, withTime);
399
+ return (_jsx(DatePicker, { variant: "bordered", label: label, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: parsed ?? null, onChange: (v) => onChange(name, v
400
+ ? serializeDate(v, withTime)
401
+ : ''), granularity: withTime ? 'minute' : 'day', hourCycle: withTime ? 24 : undefined }));
402
+ }
403
+ // 4) SINGLE TIME (uses your existing isTimeType)
404
+ if (isTimeType(attr)) {
405
+ const tVal = parseTimeString(value);
406
+ return (_jsx(TimeInput, { variant: "bordered", label: label, labelPlacement: "outside", className: "w-full", isDisabled: disabled, isRequired: required, description: description, value: tVal ?? undefined, onChange: (t) => onChange(name, serializeTime(t)), granularity: "minute", hourCycle: 12 }));
407
+ }
408
+ const normalize = (s) => (s ?? '').toLowerCase().trim();
409
+ const canSeeOption = (opt) => {
410
+ const raw = typeof window !== 'undefined'
411
+ ? localStorage.getItem('nextmin.user')
412
+ : null;
413
+ const currentUser = raw ? JSON.parse(raw) : null;
414
+ if (!currentUser)
415
+ return false;
416
+ const o = normalize(opt);
417
+ const roles = currentUser.role ?? currentUser.roles ?? [];
418
+ const roleNames = new Set(roles.map((r) => normalize(r.name)));
419
+ if (roleNames.has('superadmin'))
420
+ return true;
421
+ if (o === 'system')
422
+ return false;
423
+ return true;
424
+ };
425
+ // Enum → RadioGroup (≤4) or Select (>4)
426
+ if (enumVals && enumVals.length > 0) {
427
+ if (enumVals.length <= 4) {
428
+ return (_jsx(RadioGroup, { id: id, name: name, label: label, orientation: "horizontal", value: (value ?? ''), onValueChange: (v) => onChange(name, v), isDisabled: disabled, description: description, isRequired: required, className: "w-full", children: enumVals.map((opt) => {
429
+ if (!canSeeOption(opt))
430
+ return null;
431
+ return (_jsx(Radio, { value: opt, children: opt }, opt));
432
+ }) }));
433
+ }
434
+ return (_jsx(Select, { variant: "bordered", classNames: selectClassNames, labelPlacement: "outside", id: id, name: name, label: label, selectedKeys: value ? new Set([String(value)]) : new Set(), onSelectionChange: (keys) => {
435
+ if (keys === 'all')
436
+ return;
437
+ const v = Array.from(keys)[0];
438
+ onChange(name, v != null ? String(v) : '');
439
+ }, isDisabled: disabled, description: description, className: "w-full", selectionMode: "single", isRequired: required, children: enumVals.map((opt) => (_jsx(SelectItem, { textValue: opt, children: opt }, opt))) }));
440
+ }
441
+ // Boolean → Checkbox
442
+ if (inputTypeFor(attr) === 'checkbox') {
443
+ return (_jsxs("div", { className: "w-full", children: [_jsx(Checkbox, { id: id, name: name, isSelected: !!value, onValueChange: (v) => onChange(name, v), isDisabled: disabled, isRequired: required, children: label }), description ? (_jsx("p", { className: "text-default-500 text-xs mt-1", children: description })) : null] }));
444
+ }
445
+ // Textarea
446
+ if (inputTypeFor(attr) === 'textarea') {
447
+ return (_jsx(Textarea, { variant: "bordered", classNames: inputClassNames, id: id, name: name, label: label, labelPlacement: "outside", value: value ?? '', onChange: (e) => onChange(name, e.target.value), isDisabled: disabled, minRows: 3, isReadOnly: attr.readOnly, description: description, className: "w-full", isRequired: required }));
448
+ }
449
+ function attrHead(attr) {
450
+ return Array.isArray(attr) ? attr[0] : attr;
451
+ }
452
+ function isAttrReadOnly(attr) {
453
+ const a = attrHead(attr);
454
+ return !!a?.readOnly;
455
+ }
456
+ // Array (non-ref) → comma-separated input
457
+ if (Array.isArray(attr)) {
458
+ return (_jsx(Input, { variant: "bordered", classNames: inputClassNames, id: id, name: name, label: label, isReadOnly: isAttrReadOnly(attr), labelPlacement: "outside-top", placeholder: "comma-separated values", value: Array.isArray(value) ? value.join(', ') : (value ?? ''), onChange: (e) => onChange(name, e.target.value
459
+ .split(',')
460
+ .map((s) => s.trim())
461
+ .filter(Boolean)), isDisabled: disabled, description: description ?? 'Enter values separated by commas.', className: "w-full", isRequired: required }));
462
+ }
463
+ function pickInputType(x, fieldName) {
464
+ if (fieldName.toLowerCase() === 'password')
465
+ return 'password';
466
+ switch (x) {
467
+ case 'number':
468
+ case 'email':
469
+ case 'password':
470
+ case 'date':
471
+ case 'time':
472
+ case 'datetime-local':
473
+ return x;
474
+ default:
475
+ return 'text';
476
+ }
477
+ }
478
+ const inputType = pickInputType(inputTypeFor(attr), name);
479
+ return (_jsx(Input, { variant: "bordered", classNames: inputClassNames, id: id, name: name, label: label, isReadOnly: attr.readOnly, labelPlacement: "outside-top", type: inputType, value: value ?? '', onChange: (e) => onChange(name, inputType === 'number'
480
+ ? numberOrEmpty(e.target.value)
481
+ : e.target.value), isDisabled: disabled, description: description, className: "w-full", isRequired: required, autoComplete: name.toLowerCase() === 'password' ? 'new-password' : undefined }));
482
+ }
483
+ function isHiddenAttr(attr, isEdit) {
484
+ const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
485
+ const rule = head?.hidden;
486
+ if (typeof rule === 'boolean')
487
+ return rule;
488
+ if (rule && typeof rule === 'object') {
489
+ return isEdit ? !!rule.edit : !!rule.create;
490
+ }
491
+ return false;
492
+ }
493
+ function isPhoneAttr(name, attr) {
494
+ const byName = /phone/i.test(name);
495
+ const byFormat = String(attr?.format || '').toLowerCase() === 'phone';
496
+ const byType = String(attr?.type || '').toLowerCase() === 'phone';
497
+ const byMask = typeof attr?.mask === 'string' && attr.mask.includes('x');
498
+ return byName || byFormat || byType || byMask;
499
+ }
500
+ function isPasswordAttr(name, attr) {
501
+ const byName = /^password$/i.test(name);
502
+ const byFormat = String(attr?.format || '').toLowerCase() === 'password';
503
+ const byWriteOnly = attr?.writeOnly === true;
504
+ return byName || byFormat || byWriteOnly;
505
+ }
506
+ function getRefArraySpec(attr) {
507
+ if (!Array.isArray(attr) || attr.length === 0)
508
+ return null;
509
+ const head = attr[0];
510
+ if (head &&
511
+ typeof head === 'object' &&
512
+ String(head.type).toLowerCase() === 'objectid' &&
513
+ typeof head.ref === 'string') {
514
+ return { ref: head.ref, show: head.show };
515
+ }
516
+ return null;
517
+ }
518
+ function getRefSingleSpec(attr) {
519
+ if (!attr || typeof attr !== 'object' || Array.isArray(attr))
520
+ return null;
521
+ const a = attr;
522
+ const t = String(a.type || '').toLowerCase();
523
+ if (t === 'objectid' && typeof a.ref === 'string' && a.ref.trim()) {
524
+ return { ref: a.ref, show: a.show };
525
+ }
526
+ return null;
527
+ }
528
+ function normalizeIdsArray(raw) {
529
+ if (!raw)
530
+ return [];
531
+ if (Array.isArray(raw)) {
532
+ return raw
533
+ .map((v) => {
534
+ if (typeof v === 'string')
535
+ return v;
536
+ if (v && typeof v === 'object') {
537
+ const anyv = v;
538
+ return ((typeof anyv.id === 'string' && anyv.id) ||
539
+ (typeof anyv._id === 'string' && anyv._id) ||
540
+ null);
541
+ }
542
+ return null;
543
+ })
544
+ .filter((x) => !!x);
545
+ }
546
+ return [];
547
+ }
548
+ function normalizeId(raw) {
549
+ if (!raw)
550
+ return null;
551
+ if (typeof raw === 'string')
552
+ return raw;
553
+ if (typeof raw === 'object') {
554
+ const anyv = raw;
555
+ const id = (typeof anyv.id === 'string' && anyv.id) ||
556
+ (typeof anyv._id === 'string' && anyv._id) ||
557
+ null;
558
+ return id;
559
+ }
560
+ return null;
561
+ }
562
+ function formatLabel(raw) {
563
+ const spaced = raw
564
+ .replace(/[_\-]+/g, ' ')
565
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
566
+ .trim();
567
+ return spaced
568
+ .split(/\s+/)
569
+ .map((w) => (w ? w[0].toUpperCase() + w.slice(1).toLowerCase() : w))
570
+ .join(' ');
571
+ }
572
+ function numberOrEmpty(v) {
573
+ if (v === '' || v === null || v === undefined)
574
+ return '';
575
+ const n = Number(v);
576
+ return Number.isNaN(n) ? '' : n;
577
+ }
578
+ function isAddressAttr(name, attr) {
579
+ const fname = String(name || '').toLowerCase();
580
+ const byName = /(address|location)/i.test(fname);
581
+ const byFormat = String(attr?.format || '').toLowerCase() === 'address';
582
+ const byPopulate = typeof attr?.populate === 'string' &&
583
+ attr.populate.length > 0;
584
+ const isStringType = String(attr?.type || '').toLowerCase() === 'string' ||
585
+ !('type' in attr);
586
+ return isStringType && (byName || byFormat || byPopulate);
587
+ }
588
+ function getPopulateTarget(attr) {
589
+ const head = Array.isArray(attr) ? (attr?.[0] ?? {}) : (attr ?? {});
590
+ const t = head?.populate ?? attr?.populate;
591
+ return typeof t === 'string' && t.trim() ? t.trim() : null;
592
+ }
@@ -0,0 +1,3 @@
1
+ export declare function SectionLoader({ label }: {
2
+ label?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Spinner } from '@heroui/react';
4
+ export function SectionLoader({ label = 'Loading…' }) {
5
+ // Fills only the parent area; parent should have `relative`
6
+ return (_jsx("div", { className: "absolute inset-0 grid place-items-center", children: _jsxs("div", { className: "flex items-center gap-3 rounded-xl border bg-background/70 backdrop-blur px-4 py-3 shadow-sm", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { className: "text-sm text-foreground/80", children: label })] }) }));
7
+ }
@@ -0,0 +1 @@
1
+ export declare function Sidebar(): import("react/jsx-runtime").JSX.Element;