@fovestta2/web-react 1.2.1 → 1.2.3

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.
@@ -27,7 +27,7 @@ exports.FvDropdown = void 0;
27
27
  const react_1 = __importStar(require("react"));
28
28
  const validation_engine_1 = require("@fovestta2/validation-engine");
29
29
  const FvDropdown = ({ label = '', placeholder = 'Select an option', options = [], value, schema, disabled = false, className = '', inputClassName = '', labelClassName = '', errorClassName = '', onChange, onBlur, onFocus, }) => {
30
- const [errorMessage, setErrorMessage] = (0, react_1.useState)(null);
30
+ const [isTouched, setIsTouched] = (0, react_1.useState)(false);
31
31
  const [isOpen, setIsOpen] = (0, react_1.useState)(false);
32
32
  const [searchText, setSearchText] = (0, react_1.useState)('');
33
33
  const [filteredOptions, setFilteredOptions] = (0, react_1.useState)([]);
@@ -35,6 +35,12 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
35
35
  const containerRef = (0, react_1.useRef)(null);
36
36
  const inputRef = (0, react_1.useRef)(null);
37
37
  const listRef = (0, react_1.useRef)(null);
38
+ const errorMessage = (0, react_1.useMemo)(() => {
39
+ if (!isTouched || !schema)
40
+ return null;
41
+ const result = validation_engine_1.Validator.validate(value, schema);
42
+ return result.isValid ? null : (result.message || result.errorKey);
43
+ }, [value, schema, isTouched]);
38
44
  // Helper type guards
39
45
  const isGroup = (item) => {
40
46
  return 'items' in item && Array.isArray(item.items);
@@ -89,7 +95,6 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
89
95
  };
90
96
  // Sync initial and prop changes
91
97
  (0, react_1.useEffect)(() => {
92
- validateValue(value);
93
98
  const selected = findOption(value);
94
99
  if (selected) {
95
100
  setSearchText(selected.label);
@@ -104,6 +109,7 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
104
109
  if (containerRef.current && !containerRef.current.contains(event.target)) {
105
110
  if (isOpen) {
106
111
  closeDropdown();
112
+ setIsTouched(true);
107
113
  if (onBlur)
108
114
  onBlur();
109
115
  }
@@ -114,17 +120,6 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
114
120
  document.removeEventListener('mousedown', handleClickOutside);
115
121
  };
116
122
  }, [isOpen, value, options]);
117
- const validateValue = (val) => {
118
- if (!schema)
119
- return;
120
- const result = validation_engine_1.Validator.validate(val, schema);
121
- if (!result.isValid && result.errorKey) {
122
- setErrorMessage(result.errorKey);
123
- }
124
- else {
125
- setErrorMessage(null);
126
- }
127
- };
128
123
  const openDropdown = () => {
129
124
  if (disabled)
130
125
  return;
@@ -157,7 +152,6 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
157
152
  if (!value)
158
153
  setSearchText('');
159
154
  }
160
- validateValue(value);
161
155
  };
162
156
  const toggleDropdown = () => {
163
157
  var _a;
@@ -186,6 +180,7 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
186
180
  setSearchText(viewItem.label);
187
181
  setIsOpen(false);
188
182
  setHighlightedIndex(-1);
183
+ setIsTouched(true);
189
184
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
190
185
  if (onBlur)
191
186
  onBlur();
@@ -260,10 +255,13 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
260
255
  ERR_REQUIRED: 'This field is required',
261
256
  ERR_INVALID_VALUE: 'Invalid selection',
262
257
  ERR_DUPLICATE: 'This selection already exists',
258
+ ERR_MIN_VALUE: 'Value is too small',
259
+ ERR_MAX_VALUE: 'Value is too large',
260
+ ERR_CUSTOM_INVALID: 'Invalid value',
263
261
  };
264
262
  return errorMessages[errorMessage] || errorMessage;
265
263
  };
266
- return (react_1.default.createElement("div", { ref: containerRef, className: `fv-dropdown-container ${className}`, style: { marginBottom: '16px', display: 'flex', flexDirection: 'column', position: 'relative' }, onKeyDown: handleKeyDown },
264
+ return (react_1.default.createElement("div", { ref: containerRef, className: `fv-dropdown-container ${className}`, style: { marginBottom: '16px', display: 'flex', flexDirection: 'column', position: 'relative', width: '100%' }, onKeyDown: handleKeyDown },
267
265
  label && (react_1.default.createElement("label", { className: `fv-dropdown-label ${labelClassName}`, style: { marginBottom: '6px', fontSize: '14px', fontWeight: 500, color: disabled ? '#999' : '#333' } },
268
266
  label,
269
267
  isRequired() && react_1.default.createElement("span", { className: "fv-required", style: { color: '#dc3545', fontWeight: 'bold' } }, " *"))),
@@ -273,7 +271,8 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
273
271
  borderRadius: '4px',
274
272
  backgroundColor: disabled ? '#f5f5f5' : '#fff',
275
273
  cursor: disabled ? 'not-allowed' : 'text',
276
- position: 'relative'
274
+ position: 'relative',
275
+ width: '100%'
277
276
  }, onClick: (e) => {
278
277
  var _a;
279
278
  if (disabled)
@@ -287,6 +286,8 @@ const FvDropdown = ({ label = '', placeholder = 'Select an option', options = []
287
286
  } },
288
287
  react_1.default.createElement("input", { ref: inputRef, type: "text", className: `fv-dropdown-input ${inputClassName}`, value: searchText, onChange: handleSearchChange, onFocus: openDropdown, disabled: disabled, placeholder: placeholder, style: {
289
288
  flex: 1,
289
+ width: '100%',
290
+ minWidth: 0,
290
291
  padding: '10px',
291
292
  border: 'none',
292
293
  background: 'transparent',
@@ -27,7 +27,7 @@ exports.FvEmailField = void 0;
27
27
  const react_1 = __importStar(require("react"));
28
28
  const validation_engine_1 = require("@fovestta2/validation-engine");
29
29
  const FvEmailField = ({ label = 'Email Address', placeholder = 'e.g. john@example.com', value, schema, disabled = false, readonly = false, className = '', inputClassName = '', labelClassName = '', errorClassName = '', onChange, onBlur, onFocus, }) => {
30
- const [errorMessage, setErrorMessage] = (0, react_1.useState)(null);
30
+ const [isTouched, setIsTouched] = (0, react_1.useState)(false);
31
31
  const defaultSchema = schema || {
32
32
  controlType: 'EntryField',
33
33
  errorPriority: ['required', 'regex'],
@@ -41,23 +41,18 @@ const FvEmailField = ({ label = 'Email Address', placeholder = 'e.g. john@exampl
41
41
  },
42
42
  ],
43
43
  };
44
- (0, react_1.useEffect)(() => {
45
- validateValue(value);
46
- }, [value, schema]);
47
- const validateValue = (val) => {
48
- const result = validation_engine_1.Validator.validate(val, schema || defaultSchema);
49
- if (!result.isValid && result.errorKey) {
50
- setErrorMessage(result.errorKey);
51
- }
52
- else {
53
- setErrorMessage(null);
54
- }
55
- };
44
+ const errorMessage = (0, react_1.useMemo)(() => {
45
+ if (!isTouched)
46
+ return null;
47
+ const result = validation_engine_1.Validator.validate(value, schema || defaultSchema);
48
+ return result.isValid ? null : (result.message || result.errorKey);
49
+ }, [value, schema, isTouched, defaultSchema]);
50
+ // Sync logic removed in favor of useMemo
56
51
  const handleInput = (e) => {
57
52
  onChange(e.target.value);
58
53
  };
59
54
  const handleBlur = (e) => {
60
- validateValue(value);
55
+ setIsTouched(true);
61
56
  if (onBlur)
62
57
  onBlur();
63
58
  };
@@ -77,6 +72,9 @@ const FvEmailField = ({ label = 'Email Address', placeholder = 'e.g. john@exampl
77
72
  ERR_EMAIL_INVALID: 'Invalid email format',
78
73
  ERR_REGEX_MISMATCH: 'Invalid email format',
79
74
  ERR_DUPLICATE: 'Email already exists',
75
+ ERR_MIN_VALUE: 'Value is too small',
76
+ ERR_MAX_VALUE: 'Value is too large',
77
+ ERR_CUSTOM_INVALID: 'Invalid value',
80
78
  };
81
79
  return errorMessages[errorMessage] || errorMessage;
82
80
  };
@@ -27,22 +27,14 @@ exports.FvEntryField = void 0;
27
27
  const react_1 = __importStar(require("react"));
28
28
  const validation_engine_1 = require("@fovestta2/validation-engine");
29
29
  const FvEntryField = ({ label = '', placeholder = '', value, schema, disabled = false, readonly = false, type = 'text', allowAlphabetsOnly = false, maxLength, className = '', inputClassName = '', labelClassName = '', errorClassName = '', onChange, onBlur, onFocus, }) => {
30
- const [errorMessage, setErrorMessage] = (0, react_1.useState)(null);
31
- (0, react_1.useEffect)(() => {
32
- validateValue(value);
33
- }, [value, schema]);
34
- const validateValue = (val) => {
35
- if (!schema)
36
- return;
37
- const result = validation_engine_1.Validator.validate(val, schema);
38
- // In React this is fully controlled by state, without formControl injection
39
- if (!result.isValid && result.errorKey) {
40
- setErrorMessage(result.errorKey);
41
- }
42
- else {
43
- setErrorMessage(null);
44
- }
45
- };
30
+ const [isTouched, setIsTouched] = (0, react_1.useState)(false);
31
+ const errorMessage = (0, react_1.useMemo)(() => {
32
+ if (!isTouched || !schema)
33
+ return null;
34
+ const result = validation_engine_1.Validator.validate(value, schema);
35
+ return result.isValid ? null : (result.message || result.errorKey);
36
+ }, [value, schema, isTouched]);
37
+ // Sync logic removed in favor of useMemo
46
38
  const handleInput = (e) => {
47
39
  let newValue = e.target.value;
48
40
  if (allowAlphabetsOnly) {
@@ -54,7 +46,7 @@ const FvEntryField = ({ label = '', placeholder = '', value, schema, disabled =
54
46
  onChange(newValue);
55
47
  };
56
48
  const handleBlur = (e) => {
57
- validateValue(value);
49
+ setIsTouched(true);
58
50
  if (onBlur)
59
51
  onBlur();
60
52
  };
@@ -75,6 +67,9 @@ const FvEntryField = ({ label = '', placeholder = '', value, schema, disabled =
75
67
  ERR_MAX_LENGTH: 'Value is too long',
76
68
  ERR_REGEX_MISMATCH: 'Invalid format',
77
69
  ERR_DUPLICATE: 'This value already exists',
70
+ ERR_MIN_VALUE: 'Value is too small',
71
+ ERR_MAX_VALUE: 'Value is too large',
72
+ ERR_CUSTOM_INVALID: 'Invalid value',
78
73
  };
79
74
  return errorMessages[errorMessage] || errorMessage;
80
75
  };
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+ export interface FvMasterAction {
3
+ label: string;
4
+ onClick: () => void;
5
+ icon?: React.ReactNode;
6
+ variant?: 'primary' | 'secondary' | 'danger';
7
+ className?: string;
8
+ }
9
+ export interface FvMasterHeaderConfig {
10
+ title?: string;
11
+ subtitle?: string;
12
+ addAction?: FvMasterAction;
13
+ addOptions?: FvMasterAction[];
14
+ className?: string;
15
+ }
16
+ export interface FvMasterCardConfig {
17
+ label: string;
18
+ value: string | number;
19
+ icon?: React.ReactNode;
20
+ colSpan?: number;
21
+ color?: string;
22
+ className?: string;
23
+ }
24
+ export interface FvMasterTab {
25
+ label: string;
26
+ key: string;
27
+ count?: number;
28
+ colSpan?: number;
29
+ }
30
+ export interface FvMasterColumn {
31
+ header: string;
32
+ key: string;
33
+ render?: (row: any) => React.ReactNode;
34
+ width?: string;
35
+ sortable?: boolean;
36
+ sortKey?: string;
37
+ }
38
+ export interface FvMasterTableActions {
39
+ onView?: (row: any) => void;
40
+ onUpdate?: (row: any) => void;
41
+ onDelete?: (row: any) => void;
42
+ className?: string;
43
+ }
44
+ export interface FvMasterPaginationConfig {
45
+ totalItems: number;
46
+ pageSize: number;
47
+ currentPage: number;
48
+ onPageChange: (page: number) => void;
49
+ onPageSizeChange?: (size: number) => void;
50
+ pageSizeOptions?: number[];
51
+ }
52
+ export interface FvMasterConfig {
53
+ header?: FvMasterHeaderConfig;
54
+ cards?: {
55
+ items: FvMasterCardConfig[];
56
+ maxColsPerRow?: number;
57
+ };
58
+ tabs?: {
59
+ items: FvMasterTab[];
60
+ activeKey: string;
61
+ onTabChange: (key: string) => void;
62
+ };
63
+ filters?: {
64
+ onExport?: (format: 'excel' | 'csv') => void;
65
+ className?: string;
66
+ };
67
+ table?: {
68
+ columns: FvMasterColumn[];
69
+ data: any[];
70
+ actions?: FvMasterTableActions;
71
+ pagination?: FvMasterPaginationConfig;
72
+ };
73
+ className?: string;
74
+ }
75
+ export interface FvMasterScreenProps {
76
+ config: FvMasterConfig;
77
+ }
78
+ export declare const FvMasterScreen: React.FC<FvMasterScreenProps>;
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.FvMasterScreen = void 0;
27
+ const react_1 = __importStar(require("react"));
28
+ // ─── Styles ──────────────────────────────────────────────────────────────────
29
+ const containerStyle = {
30
+ padding: '24px',
31
+ background: '#f8fafc',
32
+ minHeight: '100%',
33
+ fontFamily: 'Inter, system-ui, sans-serif',
34
+ };
35
+ const headerRowStyle = {
36
+ display: 'flex',
37
+ justifyContent: 'space-between',
38
+ alignItems: 'flex-start',
39
+ marginBottom: '24px',
40
+ };
41
+ const titleStyle = {
42
+ fontSize: '28px',
43
+ fontWeight: 700,
44
+ color: '#0f172a',
45
+ margin: 0,
46
+ };
47
+ const subtitleStyle = {
48
+ fontSize: '14px',
49
+ color: '#64748b',
50
+ marginTop: '4px',
51
+ };
52
+ const addBtnStyle = {
53
+ background: '#005bb5',
54
+ color: '#fff',
55
+ border: 'none',
56
+ padding: '10px 20px',
57
+ borderRadius: '8px',
58
+ fontWeight: 600,
59
+ cursor: 'pointer',
60
+ display: 'flex',
61
+ alignItems: 'center',
62
+ gap: '8px',
63
+ boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
64
+ position: 'relative',
65
+ };
66
+ const cardGridStyle = (cols) => ({
67
+ display: 'grid',
68
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
69
+ gap: '20px',
70
+ marginBottom: '32px',
71
+ });
72
+ const cardStyle = {
73
+ background: '#fff',
74
+ padding: '20px',
75
+ borderRadius: '12px',
76
+ border: '1px solid #e2e8f0',
77
+ boxShadow: '0 1px 3px rgba(0,0,0,0.02)',
78
+ display: 'flex',
79
+ alignItems: 'center', // Align horizontal
80
+ gap: '16px',
81
+ };
82
+ const toolbarRowStyle = {
83
+ display: 'flex',
84
+ justifyContent: 'space-between',
85
+ alignItems: 'center',
86
+ marginBottom: '20px',
87
+ gap: '16px',
88
+ };
89
+ const tabBarStyle = {
90
+ display: 'grid',
91
+ gridTemplateColumns: 'repeat(12, 1fr)', // Use 12-column grid for dynamic sizing
92
+ gap: '12px',
93
+ flex: 1,
94
+ };
95
+ const pillTabStyle = (isActive, colSpan) => ({
96
+ padding: '8px 20px',
97
+ borderRadius: '9999px', // Pill shape
98
+ background: isActive ? '#005bb5' : '#fff',
99
+ color: isActive ? '#fff' : '#64748b',
100
+ fontWeight: 600,
101
+ fontSize: '14px',
102
+ cursor: 'pointer',
103
+ display: 'flex',
104
+ alignItems: 'center',
105
+ justifyContent: 'center',
106
+ gap: '8px',
107
+ boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
108
+ transition: 'all 0.2s',
109
+ border: isActive ? '1px solid #005bb5' : '1px solid #e2e8f0', // Simplified
110
+ gridColumn: colSpan ? `span ${colSpan}` : 'span 2', // Default span if not specified
111
+ whiteSpace: 'nowrap',
112
+ overflow: 'hidden',
113
+ textOverflow: 'ellipsis',
114
+ });
115
+ const tableContainerStyle = {
116
+ background: '#fff',
117
+ borderRadius: '12px',
118
+ border: '1px solid #e2e8f0',
119
+ overflow: 'hidden',
120
+ boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
121
+ };
122
+ const tableStyle = {
123
+ width: '100%',
124
+ borderCollapse: 'collapse',
125
+ fontSize: '14px',
126
+ };
127
+ const thStyle = {
128
+ background: '#f8fafc',
129
+ padding: '14px 16px',
130
+ textAlign: 'left',
131
+ fontWeight: 700,
132
+ color: '#475569',
133
+ borderBottom: '1px solid #e2e8f0',
134
+ };
135
+ const tdStyle = {
136
+ padding: '16px',
137
+ borderBottom: '1px solid #e2e8f0',
138
+ color: '#1e293b',
139
+ };
140
+ const actionIconStyle = {
141
+ cursor: 'pointer',
142
+ // padding: '6px',
143
+ borderRadius: '6px',
144
+ fontSize: '18px',
145
+ border: 'none',
146
+ background: 'none',
147
+ transition: 'background 0.2s',
148
+ display: 'inline-flex',
149
+ alignItems: 'center',
150
+ justifyContent: 'center',
151
+ };
152
+ const dropdownMenuStyle = {
153
+ position: 'absolute',
154
+ top: '100%',
155
+ right: 0,
156
+ background: '#fff',
157
+ borderRadius: '8px',
158
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
159
+ marginTop: '8px',
160
+ minWidth: '160px',
161
+ zIndex: 100,
162
+ border: '1px solid #e2e8f0',
163
+ padding: '4px',
164
+ };
165
+ const dropdownItemStyle = {
166
+ width: '100%',
167
+ padding: '10px 16px',
168
+ background: 'none',
169
+ border: 'none',
170
+ textAlign: 'left',
171
+ cursor: 'pointer',
172
+ fontSize: '14px',
173
+ borderRadius: '6px',
174
+ color: '#1e293b',
175
+ fontWeight: 500,
176
+ display: 'flex',
177
+ alignItems: 'center',
178
+ gap: '8px',
179
+ };
180
+ // ─── Component ───────────────────────────────────────────────────────────────
181
+ const FvMasterScreen = ({ config }) => {
182
+ var _a, _b, _c;
183
+ const [showAddDropdown, setShowAddDropdown] = (0, react_1.useState)(false);
184
+ const [sortConfig, setSortConfig] = (0, react_1.useState)(null);
185
+ const handleSort = (key) => {
186
+ let direction = 'desc'; // Default to descending as requested
187
+ if (sortConfig && sortConfig.key === key && sortConfig.direction === 'desc') {
188
+ direction = 'asc';
189
+ }
190
+ setSortConfig({ key, direction });
191
+ };
192
+ const getSortedData = () => {
193
+ var _a;
194
+ const data = [...(((_a = config.table) === null || _a === void 0 ? void 0 : _a.data) || [])];
195
+ if (!sortConfig)
196
+ return data;
197
+ return data.sort((a, b) => {
198
+ const aValue = a[sortConfig.key];
199
+ const bValue = b[sortConfig.key];
200
+ if (aValue === bValue)
201
+ return 0;
202
+ const isAsc = sortConfig.direction === 'asc';
203
+ // Handle numeric sorting
204
+ if (typeof aValue === 'number' && typeof bValue === 'number') {
205
+ return isAsc ? aValue - bValue : bValue - aValue;
206
+ }
207
+ // Handle string/other sorting
208
+ const aStr = String(aValue || '').toLowerCase();
209
+ const bStr = String(bValue || '').toLowerCase();
210
+ if (aStr < bStr)
211
+ return isAsc ? -1 : 1;
212
+ if (aStr > bStr)
213
+ return isAsc ? 1 : -1;
214
+ return 0;
215
+ });
216
+ };
217
+ const sortedData = getSortedData();
218
+ const renderPagination = () => {
219
+ var _a;
220
+ const pag = (_a = config.table) === null || _a === void 0 ? void 0 : _a.pagination;
221
+ if (!pag)
222
+ return null;
223
+ const totalPages = Math.ceil(pag.totalItems / pag.pageSize);
224
+ return (react_1.default.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', borderTop: '1px solid #e2e8f0' }, className: "fv-master-pagination" },
225
+ react_1.default.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '14px', color: '#64748b' } },
226
+ "Results Per Page",
227
+ react_1.default.createElement("select", { value: pag.pageSize, onChange: (e) => { var _a; return (_a = pag.onPageSizeChange) === null || _a === void 0 ? void 0 : _a.call(pag, Number(e.target.value)); }, style: { padding: '6px 12px', borderRadius: '6px', border: '1px solid #e2e8f0', background: '#fff' } }, (pag.pageSizeOptions || [10, 20, 50, 100]).map(opt => (react_1.default.createElement("option", { key: opt, value: opt }, opt))))),
228
+ react_1.default.createElement("div", { style: { display: 'flex', gap: '6px' } },
229
+ react_1.default.createElement("button", { disabled: pag.currentPage === 1, onClick: () => pag.onPageChange(pag.currentPage - 1), style: { padding: '6px 14px', borderRadius: '8px', border: '1px solid #e2e8f0', background: '#fff', cursor: pag.currentPage === 1 ? 'not-allowed' : 'pointer', fontWeight: 600 } }, "\u2039"),
230
+ [...Array(totalPages)].map((_, i) => (react_1.default.createElement("button", { key: i + 1, onClick: () => pag.onPageChange(i + 1), style: {
231
+ padding: '6px 14px',
232
+ borderRadius: '8px',
233
+ border: '1px solid #e2e8f0',
234
+ background: pag.currentPage === i + 1 ? '#005bb5' : '#fff',
235
+ color: pag.currentPage === i + 1 ? '#fff' : '#1e293b',
236
+ fontWeight: 700,
237
+ cursor: 'pointer'
238
+ } }, i + 1))),
239
+ react_1.default.createElement("button", { disabled: pag.currentPage === totalPages, onClick: () => pag.onPageChange(pag.currentPage + 1), style: { padding: '6px 14px', borderRadius: '8px', border: '1px solid #e2e8f0', background: '#fff', cursor: pag.currentPage === totalPages ? 'not-allowed' : 'pointer', fontWeight: 600 } }, "\u203A"))));
240
+ };
241
+ return (react_1.default.createElement("div", { style: containerStyle, className: `fv-master-screen ${config.className || ''}`.trim() },
242
+ config.header && (react_1.default.createElement("div", { style: headerRowStyle, className: `fv-master-header ${config.header.className || ''}`.trim() },
243
+ react_1.default.createElement("div", null,
244
+ config.header.title && react_1.default.createElement("h1", { style: titleStyle }, config.header.title),
245
+ config.header.subtitle && react_1.default.createElement("p", { style: subtitleStyle }, config.header.subtitle)),
246
+ react_1.default.createElement("div", { style: { position: 'relative' } }, config.header.addOptions ? (react_1.default.createElement(react_1.default.Fragment, null,
247
+ react_1.default.createElement("button", { onClick: () => setShowAddDropdown(!showAddDropdown), style: addBtnStyle, className: "fv-master-add-dropdown-btn" },
248
+ react_1.default.createElement("span", null, ((_a = config.header.addAction) === null || _a === void 0 ? void 0 : _a.icon) || '+'),
249
+ ((_b = config.header.addAction) === null || _b === void 0 ? void 0 : _b.label) || 'Add',
250
+ react_1.default.createElement("span", { style: { fontSize: '12px', marginLeft: '4px' } }, "\u25BC")),
251
+ showAddDropdown && (react_1.default.createElement("div", { style: dropdownMenuStyle, className: "fv-master-add-dropdown-menu" }, config.header.addOptions.map((opt, i) => (react_1.default.createElement("button", { key: i, onClick: () => { opt.onClick(); setShowAddDropdown(false); }, style: dropdownItemStyle, onMouseOver: (e) => (e.currentTarget.style.backgroundColor = '#f1f5f9'), onMouseOut: (e) => (e.currentTarget.style.backgroundColor = 'transparent') },
252
+ opt.icon,
253
+ " ",
254
+ opt.label))))))) : (config.header.addAction && (react_1.default.createElement("button", { onClick: config.header.addAction.onClick, style: addBtnStyle, className: `fv-master-add-btn ${config.header.addAction.className || ''}`.trim() },
255
+ config.header.addAction.icon || react_1.default.createElement("span", null, "+"),
256
+ config.header.addAction.label)))))),
257
+ config.cards && config.cards.items.length > 0 && (react_1.default.createElement("div", { style: cardGridStyle(config.cards.maxColsPerRow || 4), className: "fv-master-cards" }, config.cards.items.map((card, idx) => (react_1.default.createElement("div", { key: idx, style: {
258
+ ...cardStyle,
259
+ gridColumn: card.colSpan ? `span ${card.colSpan}` : undefined
260
+ }, className: `fv-master-card ${card.className || ''}`.trim() },
261
+ react_1.default.createElement("div", { style: { background: card.color ? `${card.color}15` : '#f1f5f9', padding: '12px', borderRadius: '12px', color: card.color || '#64748b', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
262
+ react_1.default.createElement("span", { style: { fontSize: '20px' } }, card.icon || '🛒')),
263
+ react_1.default.createElement("div", { style: { display: 'flex', flexDirection: 'column' } },
264
+ react_1.default.createElement("div", { style: { fontSize: '24px', fontWeight: 800, color: '#1e293b', lineHeight: 1.2 } }, card.value),
265
+ react_1.default.createElement("div", { style: { fontSize: '13px', color: '#64748b', fontWeight: 600 } }, card.label))))))),
266
+ react_1.default.createElement("div", { style: toolbarRowStyle, className: "fv-master-toolbar" },
267
+ config.tabs && (react_1.default.createElement("div", { style: tabBarStyle, className: "fv-master-tabs" }, config.tabs.items.map(tab => {
268
+ var _a;
269
+ const isActive = ((_a = config.tabs) === null || _a === void 0 ? void 0 : _a.activeKey) === tab.key;
270
+ return (react_1.default.createElement("button", { key: tab.key, onClick: () => { var _a; return (_a = config.tabs) === null || _a === void 0 ? void 0 : _a.onTabChange(tab.key); }, style: pillTabStyle(isActive, tab.colSpan) },
271
+ tab.label,
272
+ tab.count !== undefined && (react_1.default.createElement("span", { style: {
273
+ fontSize: '11px',
274
+ background: isActive ? 'rgba(255,255,255,0.2)' : '#e2e8f0',
275
+ color: isActive ? '#fff' : '#64748b',
276
+ padding: '1px 8px',
277
+ borderRadius: '10px'
278
+ } }, tab.count))));
279
+ }))),
280
+ ((_c = config.filters) === null || _c === void 0 ? void 0 : _c.onExport) && (react_1.default.createElement("div", { style: { display: 'flex', gap: '8px' } },
281
+ react_1.default.createElement("button", { onClick: () => { var _a, _b; return (_b = (_a = config.filters) === null || _a === void 0 ? void 0 : _a.onExport) === null || _b === void 0 ? void 0 : _b.call(_a, 'excel'); }, style: { ...addBtnStyle, background: '#fff', color: '#1e293b', border: '1px solid #e2e8f0', padding: '8px 16px' }, className: "fv-master-export-btn" },
282
+ react_1.default.createElement("span", { style: { fontSize: '16px' } }, "\uD83D\uDCE5"),
283
+ " Export")))),
284
+ config.table && (react_1.default.createElement("div", { style: tableContainerStyle, className: "fv-master-table-container" },
285
+ react_1.default.createElement("table", { style: tableStyle },
286
+ react_1.default.createElement("thead", null,
287
+ react_1.default.createElement("tr", null,
288
+ config.table.columns.map(col => (react_1.default.createElement("th", { key: col.key, style: {
289
+ ...thStyle,
290
+ width: col.width,
291
+ cursor: col.sortable !== false ? 'pointer' : 'default',
292
+ userSelect: 'none'
293
+ }, onClick: () => col.sortable !== false && handleSort(col.sortKey || col.key) },
294
+ react_1.default.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '4px' } },
295
+ col.header,
296
+ col.sortable !== false && (react_1.default.createElement("span", { style: {
297
+ fontSize: '12px',
298
+ opacity: (sortConfig === null || sortConfig === void 0 ? void 0 : sortConfig.key) === (col.sortKey || col.key) ? 1 : 0.3,
299
+ color: (sortConfig === null || sortConfig === void 0 ? void 0 : sortConfig.key) === (col.sortKey || col.key) ? '#005bb5' : 'inherit'
300
+ } }, (sortConfig === null || sortConfig === void 0 ? void 0 : sortConfig.key) === (col.sortKey || col.key)
301
+ ? (sortConfig.direction === 'asc' ? '↑' : '↓')
302
+ : '↓')))))),
303
+ config.table.actions && react_1.default.createElement("th", { style: { ...thStyle, textAlign: 'center' } }, "Actions"))),
304
+ react_1.default.createElement("tbody", null, sortedData.length > 0 ? (sortedData.map((row, rIdx) => (react_1.default.createElement("tr", { key: rIdx, style: { transition: 'background 0.2s' }, onMouseOver: (e) => (e.currentTarget.style.backgroundColor = '#f8fafc'), onMouseOut: (e) => (e.currentTarget.style.backgroundColor = 'transparent') },
305
+ config.table.columns.map(col => (react_1.default.createElement("td", { key: col.key, style: tdStyle }, col.render ? col.render(row) : row[col.key]))),
306
+ config.table.actions && (react_1.default.createElement("td", { style: { ...tdStyle, textAlign: 'right' } },
307
+ react_1.default.createElement("div", { style: { display: 'flex', justifyContent: 'center', gap: '12px' } },
308
+ config.table.actions.onView && (react_1.default.createElement("button", { onClick: () => config.table.actions.onView(row), style: { ...actionIconStyle, color: '#64748b' }, title: "View" }, "\uD83D\uDC41")),
309
+ config.table.actions.onUpdate && (react_1.default.createElement("button", { onClick: () => config.table.actions.onUpdate(row), style: { ...actionIconStyle, color: '#005bb5' }, title: "Edit" }, "\u270E")),
310
+ config.table.actions.onDelete && (react_1.default.createElement("button", { onClick: () => config.table.actions.onDelete(row), style: { ...actionIconStyle, color: '#ef4444' }, title: "Delete" }, "\uD83D\uDDD1"))))))))) : (react_1.default.createElement("tr", null,
311
+ react_1.default.createElement("td", { colSpan: (config.table.columns.length || 0) + (config.table.actions ? 1 : 0), style: { ...tdStyle, textAlign: 'center', padding: '64px', color: '#94a3b8' } },
312
+ react_1.default.createElement("div", { style: { fontSize: '48px', marginBottom: '16px', opacity: 0.5 } }, "\uD83D\uDCE6"),
313
+ react_1.default.createElement("div", { style: { fontSize: '16px', fontWeight: 600 } }, "No records found"),
314
+ react_1.default.createElement("div", { style: { fontSize: '14px', marginTop: '4px' } }, "Try adjusting your filters or search.")))))),
315
+ renderPagination()))));
316
+ };
317
+ exports.FvMasterScreen = FvMasterScreen;
@@ -27,27 +27,20 @@ exports.FvNumberField = void 0;
27
27
  const react_1 = __importStar(require("react"));
28
28
  const validation_engine_1 = require("@fovestta2/validation-engine");
29
29
  const FvNumberField = ({ label = '', placeholder = '', value, schema, disabled = false, readonly = false, min, max, step = 1, className = '', inputClassName = '', labelClassName = '', errorClassName = '', onChange, onBlur, onFocus, }) => {
30
- const [errorMessage, setErrorMessage] = (0, react_1.useState)(null);
31
- (0, react_1.useEffect)(() => {
32
- validateValue(value);
33
- }, [value, schema]);
34
- const validateValue = (val) => {
35
- if (!schema)
36
- return;
37
- const result = validation_engine_1.Validator.validate(val, schema);
38
- if (!result.isValid && result.errorKey) {
39
- setErrorMessage(result.errorKey);
40
- }
41
- else {
42
- setErrorMessage(null);
43
- }
44
- };
30
+ const [isTouched, setIsTouched] = (0, react_1.useState)(false);
31
+ const errorMessage = (0, react_1.useMemo)(() => {
32
+ if (!isTouched || !schema)
33
+ return null;
34
+ const result = validation_engine_1.Validator.validate(value, schema);
35
+ return result.isValid ? null : (result.message || result.errorKey);
36
+ }, [value, schema, isTouched]);
37
+ // Sync logic removed in favor of useMemo
45
38
  const handleInput = (e) => {
46
39
  const val = e.target.value;
47
40
  onChange(val === '' ? '' : Number(val));
48
41
  };
49
42
  const handleBlur = (e) => {
50
- validateValue(value);
43
+ setIsTouched(true);
51
44
  if (onBlur)
52
45
  onBlur();
53
46
  };
@@ -72,6 +65,9 @@ const FvNumberField = ({ label = '', placeholder = '', value, schema, disabled =
72
65
  ERR_NUMERIC_INVALID: 'Please enter a valid number',
73
66
  ERR_RANGE_INVALID: 'Number is out of range',
74
67
  ERR_DUPLICATE: 'This value already exists',
68
+ ERR_MIN_VALUE: 'Value is too small',
69
+ ERR_MAX_VALUE: 'Value is too large',
70
+ ERR_CUSTOM_INVALID: 'Invalid value',
75
71
  };
76
72
  return errorMessages[errorMessage] || errorMessage;
77
73
  };