@visns-studio/visns-components 5.7.7 → 5.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -54,7 +54,7 @@
54
54
  "reactjs-popup": "^2.0.6",
55
55
  "style-loader": "^4.0.0",
56
56
  "swapy": "^1.0.5",
57
- "sweetalert2": "^11.19.1",
57
+ "sweetalert2": "^11.20.0",
58
58
  "truncate": "^3.0.0",
59
59
  "uuid": "^11.1.0",
60
60
  "validator": "^13.15.0",
@@ -82,7 +82,7 @@
82
82
  "react-dom": "^17.0.0 || ^18.0.0"
83
83
  },
84
84
  "name": "@visns-studio/visns-components",
85
- "version": "5.7.7",
85
+ "version": "5.7.9",
86
86
  "description": "Various packages to assist in the development of our Custom Applications.",
87
87
  "main": "src/index.js",
88
88
  "files": [
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import styles from './styles/ActionButtons.module.scss';
3
+
4
+ function ActionButtons({ actions, onActionClick }) {
5
+ return (
6
+ <div className={styles.actionButtons}>
7
+ {actions &&
8
+ actions.map((action) => (
9
+ <button
10
+ key={`action-${action.id}`}
11
+ className={styles.actionButton}
12
+ onClick={() => onActionClick(action)}
13
+ title={action.label}
14
+ >
15
+ {action.label}
16
+ </button>
17
+ ))}
18
+ </div>
19
+ );
20
+ }
21
+
22
+ export default ActionButtons;
@@ -1,35 +1,50 @@
1
1
  import '../../styles/global.css';
2
2
 
3
3
  import React, { useState, useEffect } from 'react';
4
- import { Link, useParams } from 'react-router-dom';
4
+ import { useParams, useNavigate } from 'react-router-dom';
5
+ import Popup from 'reactjs-popup';
5
6
  import moment from 'moment';
6
7
  import get from 'lodash/get';
7
8
 
8
- import Breadcrumb from '../../crm/Breadcrumb';
9
9
  import CustomFetch from '../../crm/Fetch';
10
- import TableFilter from '../TableFilter';
11
- import MultiSelect from '../../crm/MultiSelect';
10
+ import ActionButtons from './ActionButtons';
11
+ import Form from '../../crm/Form';
12
+ import { formatCurrency } from './utils/formatters';
12
13
 
13
14
  import styles from './styles/GenericQuote.module.scss';
14
15
 
15
- function GenericQuote({
16
- extraUrlParam,
17
- layout = 'full',
18
- setting,
19
- setActiveNav = null,
20
- urlParam,
21
- userProfile,
22
- }) {
16
+ function GenericQuote({ logo, setting, urlParam }) {
23
17
  const routeParams = useParams();
18
+ const navigate = useNavigate();
24
19
 
25
- const { filters, page, tables } = setting;
20
+ const { actions: actionItems, page, tables, form } = setting;
26
21
 
27
22
  const [data, setData] = useState({});
28
- const [subnav, setSubnav] = useState(filters || []);
23
+ const [actions, setActions] = useState(actionItems || []);
24
+
25
+ // Modal state variables
26
+ const [modalShow, setModalShow] = useState(false);
27
+ const [formType, setFormType] = useState('');
28
+ const [formId, setFormId] = useState(0);
29
+ const [formData, setFormData] = useState({});
30
+
31
+ // Modal functions
32
+ const modalOpen = (type, id) => {
33
+ setModalShow(true);
34
+ setFormType(type);
35
+ setFormId(id);
36
+ setFormData(form);
37
+ };
38
+
39
+ const modalClose = () => {
40
+ setModalShow(false);
41
+ };
29
42
 
30
43
  useEffect(() => {
31
- setSubnav(filters);
32
- }, [filters]);
44
+ if (actionItems && actionItems.length > 0) {
45
+ setActions(actionItems);
46
+ }
47
+ }, [actionItems]);
33
48
 
34
49
  useEffect(() => {
35
50
  const fetchData = async () => {
@@ -50,177 +65,414 @@ function GenericQuote({
50
65
  fetchData();
51
66
  }, [routeParams['*']]);
52
67
 
68
+ const handleActionClick = (action) => {
69
+ // Handle different actions based on action properties
70
+ console.log(`Action ${action.id} clicked:`, action);
71
+
72
+ // Check if action has a type property
73
+ if (action.type === 'link' && action.url) {
74
+ // Handle link type actions
75
+ let url = action.url;
76
+
77
+ // If urlParam is specified, append the corresponding data value
78
+ if (action.urlParam && data[action.urlParam]) {
79
+ url = `${url}${data[action.urlParam]}`;
80
+ }
81
+
82
+ // Navigate to the URL using React Router
83
+ navigate(url);
84
+ return;
85
+ }
86
+
87
+ // Handle editModal type
88
+ if (action.type === 'editModal') {
89
+ // Open the edit modal with the current quote data
90
+ modalOpen('update', data.id);
91
+ return;
92
+ }
93
+
94
+ // Handle windowOpen type - opens URL in a new browser window/tab
95
+ if (action.type === 'windowOpen' && action.url) {
96
+ // Handle window open actions
97
+ let url = action.url;
98
+
99
+ // If urlParam is specified, append the corresponding data value
100
+ if (action.urlParam && data[action.urlParam]) {
101
+ url = `${url}${data[action.urlParam]}`;
102
+ }
103
+
104
+ // Open the URL in a new window/tab
105
+ window.open(url, '_blank');
106
+ return;
107
+ }
108
+
109
+ // Handle other actions based on their id if no specific type is defined
110
+ switch (action.id) {
111
+ case 'addItem':
112
+ // Handle add item action
113
+ console.log('Add item clicked');
114
+ break;
115
+ case 'editQuote':
116
+ // If no type is specified but id is editQuote, default to opening the edit modal
117
+ if (!action.type) {
118
+ modalOpen('update', data.id);
119
+ }
120
+ break;
121
+ case 'saveAsPdf':
122
+ // If no type is specified but id is saveAsPdf, default to opening PDF in new window
123
+ if (!action.type) {
124
+ console.log('Save as PDF clicked');
125
+ // You could implement a default PDF generation here if needed
126
+ }
127
+ break;
128
+ case 'saveToTicket':
129
+ // Handle save to ticket action
130
+ console.log('Save to ticket clicked');
131
+ break;
132
+ default:
133
+ console.log(
134
+ `Action ${action.id} clicked with no specific handler`
135
+ );
136
+ }
137
+ };
138
+
53
139
  return (
54
- <>
55
- <div className={styles.grid}>
56
- <div className={styles.grid__row}>
57
- <div className={`${styles.grid__full} ${styles.crmtitle}`}>
58
- <Breadcrumb data={data} page={page} />
59
- </div>
140
+ <div className={styles.quoteWrapper}>
141
+ {/* Top section with quote number and action buttons */}
142
+ <div className={styles.quoteTopSection}>
143
+ <div className={styles.quoteIdentifier}>
144
+ <h1 className={styles.quoteTitle}>
145
+ Quote
146
+ <span className={styles.quoteNumber}>
147
+ #{data.id || ''}
148
+ </span>
149
+ </h1>
150
+ </div>
151
+
152
+ {/* Action buttons */}
153
+ <div className={styles.actionButtonsContainer}>
154
+ <ActionButtons
155
+ actions={actions}
156
+ onActionClick={handleActionClick}
157
+ />
158
+ </div>
159
+ </div>
160
+
161
+ {/* Quote status banner */}
162
+ <div className={styles.quotestatus}>
163
+ <div className={styles.statusLeft}>
164
+ <span
165
+ className={
166
+ styles[
167
+ 'q__' +
168
+ (data.status
169
+ ? data.status.name.toLowerCase()
170
+ : '')
171
+ ]
172
+ }
173
+ >
174
+ {data.status ? data.status.name : ''}
175
+ {data.task && data.task.ticket_id && (
176
+ <span className={styles.ticketId}>
177
+ - #{data.task.ticket_id}
178
+ </span>
179
+ )}
180
+ </span>
181
+ </div>
182
+ <div className={styles.statusRight}>
183
+ {data.label && (
184
+ <span className={styles.quoteReference}>
185
+ {data.label}
186
+ </span>
187
+ )}
60
188
  </div>
61
189
  </div>
62
- <div className={styles.grid}>
63
- <div className={styles.grid__row}>
64
- <div className={styles.grid__subnav}>
65
- <TableFilter filters={subnav} setFilters={setSubnav} />
190
+
191
+ {/* Quote content */}
192
+ <div className={styles.quoteContent}>
193
+ {/* Quote header with logo and date */}
194
+ <div className={styles.quoteHeaderSection}>
195
+ <div className={styles.quoteLogoContainer}>
196
+ <img
197
+ src={logo}
198
+ alt="Company Logo"
199
+ className={styles.quoteLogo}
200
+ />
66
201
  </div>
67
- <div className={styles.grid__subcontent}>
68
- <div className={styles.quotestatus}>
69
- <div>
70
- <span
71
- className={
72
- styles[
73
- 'q__' +
74
- (data.status
75
- ? data.status.name.toLowerCase()
76
- : '')
77
- ]
78
- }
79
- >
80
- {data.status ? data.status.name : ''} - #
81
- {data.task ? data.task.ticket_id : ''}
82
- </span>
83
- </div>
202
+ <div className={styles.quoteTitleContainer}>
203
+ <h2 className={styles.quoteBigNumber}>
204
+ Quote #{data.id || ''}
205
+ </h2>
206
+ <div className={styles.quoteDate}>
207
+ <strong>Date:</strong>{' '}
208
+ {data.created_at
209
+ ? moment(data.created_at).format('DD-MM-YYYY')
210
+ : ''}
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ {/* Quote parties */}
216
+ <div className={styles.quoteParties}>
217
+ <div className={styles.quoteParty}>
218
+ <h3 className={styles.partyTitle}>Quote To:</h3>
219
+ <div className={styles.partyDetails}>
220
+ {data.customer ? data.customer.name : ''}
221
+ </div>
222
+ </div>
223
+ <div className={styles.quoteParty}>
224
+ <h3 className={styles.partyTitle}>Quote From:</h3>
225
+ <div className={styles.partyDetails}>
226
+ Visns Studio
227
+ <br />
228
+ 660C Newcastle Street
229
+ <br />
230
+ Leederville, WA, 6007
231
+ <br />
232
+ 08 6383 6600
233
+ <br />
234
+ team@visns.studio
235
+ <br />
236
+ ABN: 41 994 317 718
84
237
  </div>
85
- <div className={styles.quotecontainer}>
86
- <div className={styles.qitem}>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Quote items tables */}
242
+ <div className={styles.quoteItems}>
243
+ {tables.length > 0 &&
244
+ tables.map((table) => {
245
+ // Only show tables that have entries
246
+ if (
247
+ !data[table.id] ||
248
+ data[table.id].length === 0
249
+ ) {
250
+ return null;
251
+ }
252
+
253
+ // Calculate subtotal for this table
254
+ const subtotal = table.total
255
+ ? parseFloat(table.total)
256
+ : data[table.id].reduce((sum, row) => {
257
+ // Make sure we're parsing numbers correctly
258
+ let total;
259
+ if (typeof row.total === 'number') {
260
+ total = row.total;
261
+ } else if (row.total) {
262
+ total = parseFloat(
263
+ String(row.total).replace(
264
+ /[^0-9.-]+/g,
265
+ ''
266
+ )
267
+ );
268
+ } else if (row.rate && row.qty) {
269
+ // If total is not available, calculate from rate and qty
270
+ const rate = parseFloat(
271
+ String(row.rate).replace(
272
+ /[^0-9.-]+/g,
273
+ ''
274
+ )
275
+ );
276
+ const qty = parseFloat(
277
+ String(row.qty).replace(
278
+ /[^0-9.-]+/g,
279
+ ''
280
+ )
281
+ );
282
+ total = rate * qty;
283
+ } else {
284
+ total = 0;
285
+ }
286
+ return sum + (isNaN(total) ? 0 : total);
287
+ }, 0);
288
+
289
+ // We're using the imported formatCurrency function
290
+
291
+ return (
87
292
  <div
88
- className={`${styles.qitem__half} ${styles.quoteimg}`}
293
+ key={`table-section-${table.id}`}
294
+ className={styles.quoteTableSection}
89
295
  >
90
- <span>
91
- <img
92
- // src={logo}
93
- alt="Visns Studio Quote"
94
- />
95
- </span>
96
- </div>
97
- <div className={styles.qitem__half}>
98
- <div className={styles.qtitle}>
99
- Quote #{data.id || ''}
100
- </div>
101
- </div>
102
- </div>
103
- <div className={styles.qitem}>
104
- <div className={styles.qitem__half}>
105
- <div className={styles.qdate}>
106
- <strong>Date:</strong>{' '}
107
- {data.created_at
108
- ? moment(data.created_at).format(
109
- 'DD-MM-YYYY'
110
- )
111
- : ''}
112
- </div>
113
- </div>
114
- <div className={styles.qitem__half}>
115
- <div className={styles.qno}>
116
- <strong>{data.label}</strong>
117
- </div>
118
- </div>
119
- </div>
120
- <div className={styles.qitem}>
121
- <div className={styles.qitem__half}>
122
- <div className={styles.qdetails}>
123
- <p style={{ margin: 0 }}>
124
- <strong>Quote To:</strong>
125
- <br />
126
- {data.customer
127
- ? data.customer.name
128
- : ''}
129
- </p>
130
- </div>
131
- </div>
132
- <div className={styles.qitem__half}>
133
- <div className={styles.qvisns}>
134
- <p style={{ margin: 0 }}>
135
- <strong>Quote From:</strong>
136
- <br />
137
- Visns Studio
138
- <br />
139
- 660C Newcastle Street
140
- <br />
141
- Leederville, WA, 6007
142
- <br />
143
- 08 6383 6600
144
- <br />
145
- team@visns.studio
146
- <br />
147
- ABN: 41 994 317 718
148
- </p>
149
- </div>
150
- </div>
151
- </div>
152
- </div>
153
- <div className={styles.qitem}>
154
- {tables.length > 0 &&
155
- tables.map((table) => (
296
+ <h2 className={styles.tableTitle}>
297
+ {table.label}
298
+ </h2>
156
299
  <table
157
- key={`table-${table.id}`} // Adding a key to identify each table in the list
158
- className={`${styles['content-table']} ${styles['quote-table']}`}
300
+ key={`table-${table.id}`}
301
+ className={styles.quoteTable}
159
302
  >
160
303
  <thead>
161
- <tr className={styles.splitheader}>
162
- <th colSpan="8">
163
- <h1
164
- className={
165
- styles.splittitle
166
- }
167
- >
168
- {table.label}
169
- </h1>
170
- </th>
171
- </tr>
172
304
  <tr>
173
- <th></th>
174
305
  {table.columns.map((column) => (
175
306
  <th
176
307
  key={`${table.id}-column-${column.id}`}
177
308
  style={
178
309
  column.style || {}
179
310
  }
311
+ className={
312
+ styles.tableHeader
313
+ }
180
314
  >
181
- {column.label}
315
+ {column.label ===
316
+ 'Total'
317
+ ? 'Subtotal'
318
+ : column.label}
182
319
  </th>
183
320
  ))}
184
321
  </tr>
185
322
  </thead>
186
323
  <tbody>
187
- {data[table.id] &&
188
- data[table.id].map(
189
- (row, index) => (
190
- <tr
191
- key={`row-${index}`}
192
- >
193
- <td></td>
194
- {table.columns.map(
195
- (column) => (
324
+ {data[table.id].map(
325
+ (row, index) => (
326
+ <tr
327
+ key={`row-${index}`}
328
+ className={
329
+ styles.tableRow
330
+ }
331
+ >
332
+ {table.columns.map(
333
+ (column) => {
334
+ const cellValue =
335
+ get(
336
+ row,
337
+ column.id,
338
+ ''
339
+ );
340
+
341
+ return (
196
342
  <td
197
343
  key={`row-${index}-column-${column.id}`}
198
344
  style={
199
345
  column.style ||
200
346
  {}
201
347
  }
202
- >
203
- {
204
- get(
205
- row,
206
- column.id,
207
- ''
208
- ) // Safely access nested properties
348
+ className={
349
+ styles.tableCell
209
350
  }
351
+ >
352
+ {column.id ===
353
+ 'subtotal' ||
354
+ column.label ===
355
+ 'Subtotal'
356
+ ? formatCurrency(
357
+ parseFloat(
358
+ get(
359
+ row,
360
+ 'rate',
361
+ 0
362
+ )
363
+ ) *
364
+ parseFloat(
365
+ get(
366
+ row,
367
+ 'qty',
368
+ 1
369
+ )
370
+ )
371
+ )
372
+ : column.type ===
373
+ 'currency'
374
+ ? formatCurrency(
375
+ cellValue
376
+ )
377
+ : cellValue}
210
378
  </td>
211
- )
212
- )}
213
- </tr>
214
- )
215
- )}
379
+ );
380
+ }
381
+ )}
382
+ </tr>
383
+ )
384
+ )}
385
+
386
+ {/* Total row */}
387
+ <tr className={styles.subtotalRow}>
388
+ <td
389
+ colSpan={
390
+ table.columns.length - 1
391
+ }
392
+ className={
393
+ styles.subtotalLabel
394
+ }
395
+ >
396
+ Total
397
+ </td>
398
+ <td
399
+ className={
400
+ styles.subtotalValue
401
+ }
402
+ >
403
+ {formatCurrency(subtotal)}
404
+ </td>
405
+ </tr>
216
406
  </tbody>
217
407
  </table>
218
- ))}
408
+ </div>
409
+ );
410
+ })}
411
+ </div>
412
+
413
+ {/* Terms and Condition Section */}
414
+ {data.terms_and_condition && (
415
+ <div className={styles.termsSection}>
416
+ <h3 className={styles.termsTitle}>
417
+ Terms and Conditions
418
+ </h3>
419
+ <div className={styles.termsContent}>
420
+ {data.terms_and_condition}
219
421
  </div>
220
422
  </div>
221
- </div>
423
+ )}
222
424
  </div>
223
- </>
425
+
426
+ {/* Form Modal */}
427
+ <Popup
428
+ open={modalShow}
429
+ onClose={modalClose}
430
+ closeOnDocumentClick={false}
431
+ contentStyle={{ width: '80vw', maxWidth: '1200px' }}
432
+ >
433
+ <div className="modalwrap top--modal modalWide">
434
+ <div className="modal">
435
+ <Form
436
+ closeModal={modalClose}
437
+ columnId={formId}
438
+ fetchTable={() => {
439
+ // Reload the quote data after form submission
440
+ const fetchData = async () => {
441
+ if (
442
+ routeParams[urlParam] &&
443
+ routeParams[urlParam] > 0
444
+ ) {
445
+ try {
446
+ const res = await CustomFetch(
447
+ `${page.fetchUrl}/${routeParams[urlParam]}`,
448
+ 'GET',
449
+ {}
450
+ );
451
+ setData(res.data);
452
+ } catch (error) {
453
+ console.error(
454
+ 'Error fetching data:',
455
+ error
456
+ );
457
+ }
458
+ }
459
+ };
460
+ fetchData();
461
+ }}
462
+ formSettings={formData}
463
+ formType={formType}
464
+ style={form.style || {}}
465
+ updateForm={setFormData}
466
+ paramValue={
467
+ routeParams[urlParam]
468
+ ? routeParams[urlParam]
469
+ : 0
470
+ }
471
+ />
472
+ </div>
473
+ </div>
474
+ </Popup>
475
+ </div>
224
476
  );
225
477
  }
226
478