@visns-studio/visns-components 5.14.9 → 5.14.11

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.
@@ -0,0 +1,307 @@
1
+ /* Grouped Report Base Styles */
2
+ .grouped-report {
3
+ --group-header-bg-primary: #2c5282;
4
+ --group-header-bg-secondary: #4a5568;
5
+ --group-header-bg-success: #38a169;
6
+ --group-header-bg-warning: #d69e2e;
7
+ --group-header-bg-danger: #e53e3e;
8
+ --group-header-color: white;
9
+ --empty-row-bg-light: #f8f9fa;
10
+ --empty-row-bg-transparent: transparent;
11
+ --border-color: #dee2e6;
12
+ --border-color-light: #e2e8f0;
13
+ --text-muted: #6c757d;
14
+ }
15
+
16
+ .grouped-report * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ /* Report Header */
21
+ .grouped-report-header {
22
+ display: flex;
23
+ justify-content: space-between;
24
+ align-items: center;
25
+ margin-bottom: 1.5rem;
26
+ padding: 1rem;
27
+ background-color: #f8f9fa;
28
+ border-radius: 0.375rem;
29
+ border: 1px solid var(--border-color);
30
+ }
31
+
32
+ .grouped-report-summary {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 1rem;
36
+ }
37
+
38
+ .summary-text {
39
+ font-weight: 500;
40
+ color: var(--text-muted);
41
+ }
42
+
43
+ .grouped-report-actions {
44
+ display: flex;
45
+ gap: 0.5rem;
46
+ }
47
+
48
+ .btn-export {
49
+ background-color: #0d6efd;
50
+ color: white;
51
+ border: none;
52
+ padding: 0.5rem 1rem;
53
+ border-radius: 0.375rem;
54
+ cursor: pointer;
55
+ font-weight: 500;
56
+ transition: background-color 0.15s ease-in-out;
57
+ }
58
+
59
+ .btn-export:hover {
60
+ background-color: #0b5ed7;
61
+ }
62
+
63
+ /* Section-based Grouping */
64
+ .section-grouped-report {
65
+ width: 100%;
66
+ }
67
+
68
+ .grouped-report--sections .group-section {
69
+ margin-bottom: 2rem;
70
+ page-break-inside: avoid;
71
+ border: 1px solid var(--border-color);
72
+ border-radius: 0.5rem;
73
+ overflow: hidden;
74
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
75
+ }
76
+
77
+ /* Group Headers */
78
+ .group-header {
79
+ padding: 0.75rem 1rem;
80
+ font-weight: 600;
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ color: var(--group-header-color);
85
+ }
86
+
87
+ .group-header-content {
88
+ display: flex;
89
+ justify-content: space-between;
90
+ align-items: center;
91
+ width: 100%;
92
+ }
93
+
94
+ .group-title {
95
+ font-size: 1rem;
96
+ margin: 0;
97
+ }
98
+
99
+ .group-stats {
100
+ font-size: 0.875rem;
101
+ font-weight: 400;
102
+ opacity: 0.9;
103
+ }
104
+
105
+ /* Group Header Styles */
106
+ .group-section--primary .group-header {
107
+ background-color: var(--group-header-bg-primary);
108
+ }
109
+
110
+ .group-section--secondary .group-header {
111
+ background-color: var(--group-header-bg-secondary);
112
+ }
113
+
114
+ .group-section--success .group-header {
115
+ background-color: var(--group-header-bg-success);
116
+ }
117
+
118
+ .group-section--warning .group-header {
119
+ background-color: var(--group-header-bg-warning);
120
+ }
121
+
122
+ .group-section--danger .group-header {
123
+ background-color: var(--group-header-bg-danger);
124
+ }
125
+
126
+ /* Group Content */
127
+ .group-content {
128
+ background-color: white;
129
+ }
130
+
131
+ .group-table,
132
+ .grouped-table {
133
+ width: 100%;
134
+ border-collapse: collapse;
135
+ margin: 0;
136
+ }
137
+
138
+ .group-table thead th,
139
+ .grouped-table thead th {
140
+ background-color: #f8f9fa;
141
+ color: #495057;
142
+ font-weight: 600;
143
+ padding: 0.75rem;
144
+ text-align: left;
145
+ border-bottom: 2px solid var(--border-color);
146
+ border-right: 1px solid var(--border-color-light);
147
+ font-size: 0.875rem;
148
+ text-transform: uppercase;
149
+ letter-spacing: 0.05em;
150
+ }
151
+
152
+ .group-table thead th:last-child,
153
+ .grouped-table thead th:last-child {
154
+ border-right: none;
155
+ }
156
+
157
+ .group-table thead th.required-column,
158
+ .grouped-table thead th.required-column {
159
+ background-color: #e3f2fd;
160
+ font-weight: 700;
161
+ }
162
+
163
+ .group-table tbody td,
164
+ .grouped-table tbody td {
165
+ padding: 0.75rem;
166
+ border-bottom: 1px solid var(--border-color-light);
167
+ border-right: 1px solid var(--border-color-light);
168
+ vertical-align: top;
169
+ font-size: 0.875rem;
170
+ }
171
+
172
+ .group-table tbody td:last-child,
173
+ .grouped-table tbody td:last-child {
174
+ border-right: none;
175
+ }
176
+
177
+ .group-table tbody td.required-column,
178
+ .grouped-table tbody td.required-column {
179
+ font-weight: 500;
180
+ }
181
+
182
+ /* Data Rows */
183
+ .data-row {
184
+ background-color: white;
185
+ transition: background-color 0.15s ease-in-out;
186
+ }
187
+
188
+ .data-row:hover {
189
+ background-color: #f8f9fa;
190
+ }
191
+
192
+ .data-row:nth-child(even) {
193
+ background-color: #fbfcfd;
194
+ }
195
+
196
+ .data-row:nth-child(even):hover {
197
+ background-color: #f1f3f4;
198
+ }
199
+
200
+ /* Empty Rows */
201
+ .empty-row td {
202
+ height: 2.5rem;
203
+ min-height: 2.5rem;
204
+ border-bottom: 1px solid var(--border-color-light);
205
+ }
206
+
207
+ .empty-row--light {
208
+ background-color: var(--empty-row-bg-light);
209
+ }
210
+
211
+ .empty-row--transparent {
212
+ background-color: var(--empty-row-bg-transparent);
213
+ }
214
+
215
+ .empty-row--striped:nth-child(even) {
216
+ background-color: #f8f9fa;
217
+ }
218
+
219
+ /* Empty State */
220
+ .grouped-report-empty {
221
+ text-align: center;
222
+ padding: 3rem;
223
+ color: var(--text-muted);
224
+ background-color: #f8f9fa;
225
+ border: 1px solid var(--border-color);
226
+ border-radius: 0.5rem;
227
+ }
228
+
229
+ .grouped-report-error {
230
+ text-align: center;
231
+ padding: 2rem;
232
+ color: #dc3545;
233
+ background-color: #f8d7da;
234
+ border: 1px solid #f5c6cb;
235
+ border-radius: 0.5rem;
236
+ }
237
+
238
+ /* Responsive Design */
239
+ @media (max-width: 768px) {
240
+ .grouped-report-header {
241
+ flex-direction: column;
242
+ gap: 1rem;
243
+ align-items: stretch;
244
+ }
245
+
246
+ .group-table,
247
+ .grouped-table {
248
+ font-size: 0.75rem;
249
+ }
250
+
251
+ .group-table thead th,
252
+ .group-table tbody td,
253
+ .grouped-table thead th,
254
+ .grouped-table tbody td {
255
+ padding: 0.5rem;
256
+ }
257
+
258
+ .group-header {
259
+ padding: 0.5rem;
260
+ }
261
+
262
+ .group-title {
263
+ font-size: 0.875rem;
264
+ }
265
+
266
+ .group-stats {
267
+ font-size: 0.75rem;
268
+ }
269
+ }
270
+
271
+ /* Print Styles */
272
+ @media print {
273
+ .grouped-report-actions {
274
+ display: none;
275
+ }
276
+
277
+ .group-section {
278
+ page-break-inside: avoid;
279
+ margin-bottom: 1rem;
280
+ border: 1px solid #000;
281
+ }
282
+
283
+ .group-header {
284
+ background-color: #000 !important;
285
+ color: white !important;
286
+ -webkit-print-color-adjust: exact;
287
+ print-color-adjust: exact;
288
+ }
289
+
290
+ .group-table,
291
+ .grouped-table {
292
+ border-collapse: collapse;
293
+ }
294
+
295
+ .group-table thead th,
296
+ .group-table tbody td,
297
+ .grouped-table thead th,
298
+ .grouped-table tbody td {
299
+ border: 1px solid #000;
300
+ }
301
+
302
+ .empty-row--light {
303
+ background-color: #f0f0f0 !important;
304
+ -webkit-print-color-adjust: exact;
305
+ print-color-adjust: exact;
306
+ }
307
+ }
@@ -2,6 +2,132 @@ import parse from 'html-react-parser';
2
2
  import moment from 'moment';
3
3
  import numeral from 'numeral';
4
4
 
5
+ /**
6
+ * Smart boolean/binary value interpreter based on column name and context
7
+ * @param {string|number} value - The value (0/1, true/false, etc.)
8
+ * @param {Object} column - Column configuration with id/name
9
+ * @returns {string} Human-friendly representation
10
+ */
11
+ const interpretBooleanValue = (value, column) => {
12
+ const columnName = (column.id || column.name || '').toLowerCase();
13
+ const isTrue = value === 1 || value === true || value === 'true' || value === 'yes' || value === 'Yes';
14
+ const isFalse = value === 0 || value === false || value === 'false' || value === 'no' || value === 'No';
15
+
16
+ if (!isTrue && !isFalse) return value; // Return original if not clearly boolean
17
+
18
+ // Contextual interpretations based on column name patterns
19
+ const interpretations = {
20
+ // Status/State patterns
21
+ 'active': { true: 'Active', false: 'Inactive' },
22
+ 'enabled': { true: 'Enabled', false: 'Disabled' },
23
+ 'status': { true: 'Active', false: 'Inactive' },
24
+ 'visible': { true: 'Visible', false: 'Hidden' },
25
+ 'published': { true: 'Published', false: 'Draft' },
26
+ 'public': { true: 'Public', false: 'Private' },
27
+ 'live': { true: 'Live', false: 'Offline' },
28
+
29
+ // Completion/Progress patterns
30
+ 'completed': { true: 'Completed', false: 'Pending' },
31
+ 'finished': { true: 'Finished', false: 'In Progress' },
32
+ 'done': { true: 'Done', false: 'To Do' },
33
+ 'complete': { true: 'Complete', false: 'Incomplete' },
34
+ 'resolved': { true: 'Resolved', false: 'Open' },
35
+ 'closed': { true: 'Closed', false: 'Open' },
36
+
37
+ // Approval/Verification patterns
38
+ 'approved': { true: 'Approved', false: 'Pending' },
39
+ 'verified': { true: 'Verified', false: 'Unverified' },
40
+ 'confirmed': { true: 'Confirmed', false: 'Unconfirmed' },
41
+ 'accepted': { true: 'Accepted', false: 'Rejected' },
42
+ 'valid': { true: 'Valid', false: 'Invalid' },
43
+
44
+ // Access/Permission patterns
45
+ 'allowed': { true: 'Allowed', false: 'Denied' },
46
+ 'permitted': { true: 'Permitted', false: 'Restricted' },
47
+ 'authorized': { true: 'Authorized', false: 'Unauthorized' },
48
+ 'locked': { true: 'Locked', false: 'Unlocked' },
49
+
50
+ // Notification/Communication patterns
51
+ 'sent': { true: 'Sent', false: 'Not Sent' },
52
+ 'delivered': { true: 'Delivered', false: 'Pending' },
53
+ 'read': { true: 'Read', false: 'Unread' },
54
+ 'notified': { true: 'Notified', false: 'Not Notified' },
55
+
56
+ // Business/Financial patterns
57
+ 'paid': { true: 'Paid', false: 'Unpaid' },
58
+ 'invoiced': { true: 'Invoiced', false: 'Not Invoiced' },
59
+ 'billed': { true: 'Billed', false: 'Unbilled' },
60
+ 'taxable': { true: 'Taxable', false: 'Tax Free' },
61
+
62
+ // Quality/Condition patterns
63
+ 'available': { true: 'Available', false: 'Unavailable' },
64
+ 'ready': { true: 'Ready', false: 'Not Ready' },
65
+ 'urgent': { true: 'Urgent', false: 'Normal' },
66
+ 'priority': { true: 'High Priority', false: 'Normal Priority' },
67
+ 'critical': { true: 'Critical', false: 'Normal' },
68
+
69
+ // Agreement/Contract patterns
70
+ 'signed': { true: 'Signed', false: 'Unsigned' },
71
+ 'executed': { true: 'Executed', false: 'Draft' },
72
+ 'expired': { true: 'Expired', false: 'Current' },
73
+
74
+ // Project/Work patterns
75
+ 'assigned': { true: 'Assigned', false: 'Unassigned' },
76
+ 'allocated': { true: 'Allocated', false: 'Available' },
77
+ 'booked': { true: 'Booked', false: 'Available' },
78
+ 'scheduled': { true: 'Scheduled', false: 'Unscheduled' },
79
+ 'stage': { true: 'Completed', false: 'Pending' },
80
+ 'final': { true: 'Completed', false: 'Incomplete' },
81
+ 'finished': { true: 'Finished', false: 'In Progress' },
82
+ 'done': { true: 'Done', false: 'To Do' },
83
+
84
+ // System/Technical patterns
85
+ 'online': { true: 'Online', false: 'Offline' },
86
+ 'connected': { true: 'Connected', false: 'Disconnected' },
87
+ 'synced': { true: 'Synced', false: 'Not Synced' },
88
+ 'backup': { true: 'Backed Up', false: 'Not Backed Up' },
89
+ };
90
+
91
+ // Try to find a match based on column name
92
+ for (const [pattern, values] of Object.entries(interpretations)) {
93
+ if (columnName.includes(pattern)) {
94
+ return isTrue ? values.true : values.false;
95
+ }
96
+ }
97
+
98
+ // Special handling for specific common patterns
99
+ if (columnName.includes('is_') || columnName.startsWith('has_') || columnName.endsWith('_flag')) {
100
+ // Extract the meaningful part
101
+ const meaningfulPart = columnName
102
+ .replace(/^(is_|has_)/, '')
103
+ .replace(/_flag$/, '')
104
+ .replace(/_/g, ' ')
105
+ .replace(/\b\w/g, l => l.toUpperCase()); // Title case
106
+
107
+ return isTrue ? meaningfulPart : `Not ${meaningfulPart}`;
108
+ }
109
+
110
+ // Default fallback - still better than raw 0/1
111
+ return isTrue ? 'Yes' : 'No';
112
+ };
113
+
114
+ /**
115
+ * Auto-detect appropriate date format based on value content
116
+ * @param {*} value - The date value to analyze
117
+ * @param {string} fallbackFormat - Default format if detection fails
118
+ * @returns {string} Appropriate moment.js format string
119
+ */
120
+ export const autoDetectDateFormat = (value, fallbackFormat = 'DD-MM-YYYY') => {
121
+ if (!value || !moment(value).isValid()) {
122
+ return fallbackFormat;
123
+ }
124
+
125
+ const momentValue = moment(value);
126
+ const hasTime = momentValue.format('HH:mm:ss') !== '00:00:00';
127
+
128
+ return hasTime ? 'DD-MM-YYYY HH:mm' : 'DD-MM-YYYY';
129
+ };
130
+
5
131
  /**
6
132
  * Format quarter columns with status, contacts, and reasons in tooltips
7
133
  * @param {*} value - The date value
@@ -29,7 +155,7 @@ const formatQuarterWithStatus = (value, column, row) => {
29
155
  // If status is 'sent' and we have a date
30
156
  if (status === 'sent' && value) {
31
157
  const formattedDate = moment(value).isValid()
32
- ? moment(value).format('DD/MM/YYYY')
158
+ ? moment(value).format('DD-MM-YYYY')
33
159
  : value;
34
160
 
35
161
  const contactCount = Array.isArray(contacts) ? contacts.length : 0;
@@ -71,7 +197,7 @@ const formatQuarterWithStatus = (value, column, row) => {
71
197
  // If we have a date but no clear status (legacy data)
72
198
  if (value) {
73
199
  const formattedDate = moment(value).isValid()
74
- ? moment(value).format('DD/MM/YYYY')
200
+ ? moment(value).format('DD-MM-YYYY')
75
201
  : value;
76
202
 
77
203
  const contactCount = Array.isArray(contacts) ? contacts.length : 0;
@@ -125,11 +251,11 @@ export const formatCellContent = (value, column, row = null) => {
125
251
  switch (valueType) {
126
252
  case 'date':
127
253
  return moment(value).isValid()
128
- ? moment(value).format(column.format || 'DD/MM/YYYY')
254
+ ? moment(value).format(column.format || autoDetectDateFormat(value, 'DD-MM-YYYY'))
129
255
  : value;
130
256
  case 'datetime':
131
257
  return moment(value).isValid()
132
- ? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
258
+ ? moment(value).format(column.format || autoDetectDateFormat(value, 'DD-MM-YYYY HH:mm'))
133
259
  : value;
134
260
  case 'currency':
135
261
  return numeral(value).format(column.format || '$0,0.00');
@@ -139,25 +265,7 @@ export const formatCellContent = (value, column, row = null) => {
139
265
  ? numeral(value).format(column.format || '0,0')
140
266
  : value;
141
267
  case 'boolean':
142
- if (
143
- value === 1 ||
144
- value === true ||
145
- value === 'true' ||
146
- value === 'yes' ||
147
- value === 'Yes'
148
- ) {
149
- return 'Yes';
150
- }
151
- if (
152
- value === 0 ||
153
- value === false ||
154
- value === 'false' ||
155
- value === 'no' ||
156
- value === 'No'
157
- ) {
158
- return 'No';
159
- }
160
- return value;
268
+ return interpretBooleanValue(value, column);
161
269
  case 'year':
162
270
  // Special handling for year values
163
271
  return value.toString();
@@ -174,6 +282,27 @@ export const formatCellContent = (value, column, row = null) => {
174
282
  // Parse rich text content
175
283
  return parse(String(value));
176
284
  default:
285
+ // Check if this might be a boolean value based on its content
286
+ if ((value === 0 || value === 1) && column.id) {
287
+ // Only apply smart boolean interpretation if the column name suggests it's boolean
288
+ const columnName = (column.id || column.name || '').toLowerCase();
289
+ const booleanIndicators = [
290
+ 'is_', 'has_', 'can_', 'should_', 'will_', 'did_',
291
+ 'active', 'enabled', 'completed', 'confirmed', 'approved', 'verified',
292
+ 'sent', 'delivered', 'paid', 'signed', 'expired', 'urgent', 'critical',
293
+ 'public', 'visible', 'available', 'ready', 'locked', 'online',
294
+ '_flag', '_status', '_state'
295
+ ];
296
+
297
+ const seemsBooleanish = booleanIndicators.some(indicator =>
298
+ columnName.includes(indicator)
299
+ );
300
+
301
+ if (seemsBooleanish) {
302
+ return interpretBooleanValue(value, column);
303
+ }
304
+ }
305
+
177
306
  // For text values, check if it contains HTML tags
178
307
  if (
179
308
  typeof value === 'string' &&
@@ -0,0 +1,118 @@
1
+ export const getNestedValue = (obj, path) => {
2
+ if (typeof path === 'string') {
3
+ path = path.split('.');
4
+ }
5
+ return path.reduce((current, key) => current?.[key], obj) || '';
6
+ };
7
+
8
+ export const formatCellValue = (value, column) => {
9
+ if (value === null || value === undefined || value === '') {
10
+ return '';
11
+ }
12
+
13
+ switch (column.type) {
14
+ case 'date':
15
+ return new Date(value).toLocaleDateString();
16
+ case 'datetime':
17
+ return new Date(value).toLocaleString();
18
+ case 'decimal':
19
+ return parseFloat(value).toFixed(2);
20
+ case 'integer':
21
+ return parseInt(value).toLocaleString();
22
+ case 'currency':
23
+ return new Intl.NumberFormat('en-AU', {
24
+ style: 'currency',
25
+ currency: 'AUD'
26
+ }).format(value);
27
+ default:
28
+ return value.toString();
29
+ }
30
+ };
31
+
32
+ export const sortGroups = (groups, sortDirection = 'asc', customOrder = null) => {
33
+ if (customOrder) {
34
+ return groups.sort((a, b) => {
35
+ const aIndex = customOrder.indexOf(a.groupName);
36
+ const bIndex = customOrder.indexOf(b.groupName);
37
+
38
+ // If both found in custom order, sort by position
39
+ if (aIndex !== -1 && bIndex !== -1) {
40
+ return aIndex - bIndex;
41
+ }
42
+
43
+ // If only one found, prioritize it
44
+ if (aIndex !== -1) return -1;
45
+ if (bIndex !== -1) return 1;
46
+
47
+ // If neither found, sort alphabetically
48
+ return sortDirection === 'asc'
49
+ ? a.groupName.localeCompare(b.groupName)
50
+ : b.groupName.localeCompare(a.groupName);
51
+ });
52
+ }
53
+
54
+ return groups.sort((a, b) => {
55
+ return sortDirection === 'asc'
56
+ ? a.groupName.localeCompare(b.groupName)
57
+ : b.groupName.localeCompare(a.groupName);
58
+ });
59
+ };
60
+
61
+ export const processGroupData = (data, groupingConfig) => {
62
+ if (!data || !Array.isArray(data) || !groupingConfig?.enabled) {
63
+ return {
64
+ grouped: false,
65
+ data: data || []
66
+ };
67
+ }
68
+
69
+ const { groupByField, rowsPerGroup = 10, showEmptyRows = true } = groupingConfig;
70
+
71
+ // Group data by the specified field
72
+ const grouped = data.reduce((acc, row) => {
73
+ const groupValue = getNestedValue(row, groupByField) || 'Unassigned';
74
+ if (!acc[groupValue]) {
75
+ acc[groupValue] = [];
76
+ }
77
+ acc[groupValue].push(row);
78
+ return acc;
79
+ }, {});
80
+
81
+ // Process each group
82
+ const processedGroups = Object.keys(grouped).map(groupName => {
83
+ const rows = grouped[groupName];
84
+ const emptyRows = [];
85
+
86
+ if (showEmptyRows && rows.length < rowsPerGroup) {
87
+ const emptyRowsCount = rowsPerGroup - rows.length;
88
+ for (let i = 0; i < emptyRowsCount; i++) {
89
+ emptyRows.push({ __empty_row__: true, id: `empty_${groupName}_${i}` });
90
+ }
91
+ }
92
+
93
+ return {
94
+ groupName,
95
+ groupDisplayName: groupName,
96
+ rows,
97
+ emptyRows,
98
+ totalRows: rows.length,
99
+ maxRows: rowsPerGroup,
100
+ hasData: rows.length > 0
101
+ };
102
+ });
103
+
104
+ // Sort groups
105
+ const sortedGroups = sortGroups(
106
+ processedGroups,
107
+ groupingConfig.sortDirection,
108
+ groupingConfig.customGroupOrder
109
+ );
110
+
111
+ return {
112
+ grouped: true,
113
+ groupingConfig,
114
+ groups: sortedGroups,
115
+ totalGroups: sortedGroups.length,
116
+ totalRecords: data.length
117
+ };
118
+ };