@visns-studio/visns-components 5.7.6 → 5.7.8
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 +2 -2
- package/src/components/crm/TableFilter.jsx +57 -17
- package/src/components/crm/generic/ActionButtons.jsx +22 -0
- package/src/components/crm/generic/GenericIndex.jsx +2 -0
- package/src/components/crm/generic/GenericQuote.jsx +278 -152
- package/src/components/crm/generic/styles/ActionButtons.module.scss +65 -0
- package/src/components/crm/generic/styles/GenericQuote.module.scss +425 -130
- package/src/components/crm/generic/utils/formatters.js +43 -0
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
import '../../styles/global.css';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useParams } from 'react-router-dom';
|
|
5
5
|
import moment from 'moment';
|
|
6
6
|
import get from 'lodash/get';
|
|
7
7
|
|
|
8
|
-
import Breadcrumb from '../../crm/Breadcrumb';
|
|
9
8
|
import CustomFetch from '../../crm/Fetch';
|
|
10
|
-
import
|
|
11
|
-
import
|
|
9
|
+
import ActionButtons from './ActionButtons';
|
|
10
|
+
import { formatCurrency } from './utils/formatters';
|
|
12
11
|
|
|
13
12
|
import styles from './styles/GenericQuote.module.scss';
|
|
14
13
|
|
|
15
|
-
function GenericQuote({
|
|
16
|
-
extraUrlParam,
|
|
17
|
-
layout = 'full',
|
|
18
|
-
setting,
|
|
19
|
-
setActiveNav = null,
|
|
20
|
-
urlParam,
|
|
21
|
-
userProfile,
|
|
22
|
-
}) {
|
|
14
|
+
function GenericQuote({ logo, setting, urlParam }) {
|
|
23
15
|
const routeParams = useParams();
|
|
24
16
|
|
|
25
|
-
const {
|
|
17
|
+
const { actions: actionItems, page, tables } = setting;
|
|
26
18
|
|
|
27
19
|
const [data, setData] = useState({});
|
|
28
|
-
const [
|
|
20
|
+
const [actions, setActions] = useState(actionItems || []);
|
|
29
21
|
|
|
30
22
|
useEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
if (actionItems && actionItems.length > 0) {
|
|
24
|
+
setActions(actionItems);
|
|
25
|
+
}
|
|
26
|
+
}, [actionItems]);
|
|
33
27
|
|
|
34
28
|
useEffect(() => {
|
|
35
29
|
const fetchData = async () => {
|
|
@@ -50,177 +44,309 @@ function GenericQuote({
|
|
|
50
44
|
fetchData();
|
|
51
45
|
}, [routeParams['*']]);
|
|
52
46
|
|
|
47
|
+
const handleActionClick = (actionId) => {
|
|
48
|
+
// Handle different actions based on actionId
|
|
49
|
+
switch (actionId) {
|
|
50
|
+
case 'addItem':
|
|
51
|
+
// Handle add item action
|
|
52
|
+
console.log('Add item clicked');
|
|
53
|
+
break;
|
|
54
|
+
case 'editQuote':
|
|
55
|
+
// Handle edit quote action
|
|
56
|
+
console.log('Edit quote clicked');
|
|
57
|
+
break;
|
|
58
|
+
case 'viewTicket':
|
|
59
|
+
// Handle view ticket action
|
|
60
|
+
console.log('View ticket clicked');
|
|
61
|
+
break;
|
|
62
|
+
case 'saveAsPdf':
|
|
63
|
+
// Handle save as PDF action
|
|
64
|
+
console.log('Save as PDF clicked');
|
|
65
|
+
break;
|
|
66
|
+
case 'saveToTicket':
|
|
67
|
+
// Handle save to ticket action
|
|
68
|
+
console.log('Save to ticket clicked');
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
console.log(`Action ${actionId} clicked`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
53
75
|
return (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
<div className={styles.quoteWrapper}>
|
|
77
|
+
{/* Top section with quote number and action buttons */}
|
|
78
|
+
<div className={styles.quoteTopSection}>
|
|
79
|
+
<div className={styles.quoteIdentifier}>
|
|
80
|
+
<h1 className={styles.quoteTitle}>
|
|
81
|
+
Quote
|
|
82
|
+
<span className={styles.quoteNumber}>
|
|
83
|
+
#{data.id || ''}
|
|
84
|
+
</span>
|
|
85
|
+
</h1>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Action buttons */}
|
|
89
|
+
<div className={styles.actionButtonsContainer}>
|
|
90
|
+
<ActionButtons
|
|
91
|
+
actions={actions}
|
|
92
|
+
onActionClick={handleActionClick}
|
|
93
|
+
/>
|
|
60
94
|
</div>
|
|
61
95
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
|
|
97
|
+
{/* Quote status banner */}
|
|
98
|
+
<div className={styles.quotestatus}>
|
|
99
|
+
<div className={styles.statusLeft}>
|
|
100
|
+
<span
|
|
101
|
+
className={
|
|
102
|
+
styles[
|
|
103
|
+
'q__' +
|
|
104
|
+
(data.status
|
|
105
|
+
? data.status.name.toLowerCase()
|
|
106
|
+
: '')
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
>
|
|
110
|
+
{data.status ? data.status.name : ''}
|
|
111
|
+
{data.task && data.task.ticket_id && (
|
|
112
|
+
<span className={styles.ticketId}>
|
|
113
|
+
- #{data.task.ticket_id}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className={styles.statusRight}>
|
|
119
|
+
{data.label && (
|
|
120
|
+
<span className={styles.quoteReference}>
|
|
121
|
+
{data.label}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{/* Quote content */}
|
|
128
|
+
<div className={styles.quoteContent}>
|
|
129
|
+
{/* Quote header with logo and date */}
|
|
130
|
+
<div className={styles.quoteHeaderSection}>
|
|
131
|
+
<div className={styles.quoteLogoContainer}>
|
|
132
|
+
<img
|
|
133
|
+
src={logo}
|
|
134
|
+
alt="Company Logo"
|
|
135
|
+
className={styles.quoteLogo}
|
|
136
|
+
/>
|
|
66
137
|
</div>
|
|
67
|
-
<div className={styles.
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
: '')
|
|
77
|
-
]
|
|
78
|
-
}
|
|
79
|
-
>
|
|
80
|
-
{data.status ? data.status.name : ''} - #
|
|
81
|
-
{data.task ? data.task.ticket_id : ''}
|
|
82
|
-
</span>
|
|
83
|
-
</div>
|
|
138
|
+
<div className={styles.quoteTitleContainer}>
|
|
139
|
+
<h2 className={styles.quoteBigNumber}>
|
|
140
|
+
Quote #{data.id || ''}
|
|
141
|
+
</h2>
|
|
142
|
+
<div className={styles.quoteDate}>
|
|
143
|
+
<strong>Date:</strong>{' '}
|
|
144
|
+
{data.created_at
|
|
145
|
+
? moment(data.created_at).format('DD-MM-YYYY')
|
|
146
|
+
: ''}
|
|
84
147
|
</div>
|
|
85
|
-
|
|
86
|
-
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Quote parties */}
|
|
152
|
+
<div className={styles.quoteParties}>
|
|
153
|
+
<div className={styles.quoteParty}>
|
|
154
|
+
<h3 className={styles.partyTitle}>Quote To:</h3>
|
|
155
|
+
<div className={styles.partyDetails}>
|
|
156
|
+
{data.customer ? data.customer.name : ''}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div className={styles.quoteParty}>
|
|
160
|
+
<h3 className={styles.partyTitle}>Quote From:</h3>
|
|
161
|
+
<div className={styles.partyDetails}>
|
|
162
|
+
Visns Studio
|
|
163
|
+
<br />
|
|
164
|
+
660C Newcastle Street
|
|
165
|
+
<br />
|
|
166
|
+
Leederville, WA, 6007
|
|
167
|
+
<br />
|
|
168
|
+
08 6383 6600
|
|
169
|
+
<br />
|
|
170
|
+
team@visns.studio
|
|
171
|
+
<br />
|
|
172
|
+
ABN: 41 994 317 718
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Quote items tables */}
|
|
178
|
+
<div className={styles.quoteItems}>
|
|
179
|
+
{tables.length > 0 &&
|
|
180
|
+
tables.map((table) => {
|
|
181
|
+
// Only show tables that have entries
|
|
182
|
+
if (
|
|
183
|
+
!data[table.id] ||
|
|
184
|
+
data[table.id].length === 0
|
|
185
|
+
) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Calculate subtotal for this table
|
|
190
|
+
const subtotal = table.total
|
|
191
|
+
? parseFloat(table.total)
|
|
192
|
+
: data[table.id].reduce((sum, row) => {
|
|
193
|
+
// Make sure we're parsing numbers correctly
|
|
194
|
+
let total;
|
|
195
|
+
if (typeof row.total === 'number') {
|
|
196
|
+
total = row.total;
|
|
197
|
+
} else if (row.total) {
|
|
198
|
+
total = parseFloat(
|
|
199
|
+
String(row.total).replace(
|
|
200
|
+
/[^0-9.-]+/g,
|
|
201
|
+
''
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
} else if (row.rate && row.qty) {
|
|
205
|
+
// If total is not available, calculate from rate and qty
|
|
206
|
+
const rate = parseFloat(
|
|
207
|
+
String(row.rate).replace(
|
|
208
|
+
/[^0-9.-]+/g,
|
|
209
|
+
''
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
const qty = parseFloat(
|
|
213
|
+
String(row.qty).replace(
|
|
214
|
+
/[^0-9.-]+/g,
|
|
215
|
+
''
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
total = rate * qty;
|
|
219
|
+
} else {
|
|
220
|
+
total = 0;
|
|
221
|
+
}
|
|
222
|
+
return sum + (isNaN(total) ? 0 : total);
|
|
223
|
+
}, 0);
|
|
224
|
+
|
|
225
|
+
// We're using the imported formatCurrency function
|
|
226
|
+
|
|
227
|
+
return (
|
|
87
228
|
<div
|
|
88
|
-
|
|
229
|
+
key={`table-section-${table.id}`}
|
|
230
|
+
className={styles.quoteTableSection}
|
|
89
231
|
>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
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) => (
|
|
232
|
+
<h2 className={styles.tableTitle}>
|
|
233
|
+
{table.label}
|
|
234
|
+
</h2>
|
|
156
235
|
<table
|
|
157
|
-
key={`table-${table.id}`}
|
|
158
|
-
className={
|
|
236
|
+
key={`table-${table.id}`}
|
|
237
|
+
className={styles.quoteTable}
|
|
159
238
|
>
|
|
160
239
|
<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
240
|
<tr>
|
|
173
|
-
<th></th>
|
|
174
241
|
{table.columns.map((column) => (
|
|
175
242
|
<th
|
|
176
243
|
key={`${table.id}-column-${column.id}`}
|
|
177
244
|
style={
|
|
178
245
|
column.style || {}
|
|
179
246
|
}
|
|
247
|
+
className={
|
|
248
|
+
styles.tableHeader
|
|
249
|
+
}
|
|
180
250
|
>
|
|
181
|
-
{column.label
|
|
251
|
+
{column.label ===
|
|
252
|
+
'Total'
|
|
253
|
+
? 'Subtotal'
|
|
254
|
+
: column.label}
|
|
182
255
|
</th>
|
|
183
256
|
))}
|
|
184
257
|
</tr>
|
|
185
258
|
</thead>
|
|
186
259
|
<tbody>
|
|
187
|
-
{data[table.id]
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
260
|
+
{data[table.id].map(
|
|
261
|
+
(row, index) => (
|
|
262
|
+
<tr
|
|
263
|
+
key={`row-${index}`}
|
|
264
|
+
className={
|
|
265
|
+
styles.tableRow
|
|
266
|
+
}
|
|
267
|
+
>
|
|
268
|
+
{table.columns.map(
|
|
269
|
+
(column) => {
|
|
270
|
+
const cellValue =
|
|
271
|
+
get(
|
|
272
|
+
row,
|
|
273
|
+
column.id,
|
|
274
|
+
''
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return (
|
|
196
278
|
<td
|
|
197
279
|
key={`row-${index}-column-${column.id}`}
|
|
198
280
|
style={
|
|
199
281
|
column.style ||
|
|
200
282
|
{}
|
|
201
283
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
get(
|
|
205
|
-
row,
|
|
206
|
-
column.id,
|
|
207
|
-
''
|
|
208
|
-
) // Safely access nested properties
|
|
284
|
+
className={
|
|
285
|
+
styles.tableCell
|
|
209
286
|
}
|
|
287
|
+
>
|
|
288
|
+
{column.id ===
|
|
289
|
+
'subtotal' ||
|
|
290
|
+
column.label ===
|
|
291
|
+
'Subtotal'
|
|
292
|
+
? formatCurrency(
|
|
293
|
+
parseFloat(
|
|
294
|
+
get(
|
|
295
|
+
row,
|
|
296
|
+
'rate',
|
|
297
|
+
0
|
|
298
|
+
)
|
|
299
|
+
) *
|
|
300
|
+
parseFloat(
|
|
301
|
+
get(
|
|
302
|
+
row,
|
|
303
|
+
'qty',
|
|
304
|
+
1
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
: column.type ===
|
|
309
|
+
'currency'
|
|
310
|
+
? formatCurrency(
|
|
311
|
+
cellValue
|
|
312
|
+
)
|
|
313
|
+
: cellValue}
|
|
210
314
|
</td>
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
)}
|
|
318
|
+
</tr>
|
|
319
|
+
)
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Total row */}
|
|
323
|
+
<tr className={styles.subtotalRow}>
|
|
324
|
+
<td
|
|
325
|
+
colSpan={
|
|
326
|
+
table.columns.length - 1
|
|
327
|
+
}
|
|
328
|
+
className={
|
|
329
|
+
styles.subtotalLabel
|
|
330
|
+
}
|
|
331
|
+
>
|
|
332
|
+
Total
|
|
333
|
+
</td>
|
|
334
|
+
<td
|
|
335
|
+
className={
|
|
336
|
+
styles.subtotalValue
|
|
337
|
+
}
|
|
338
|
+
>
|
|
339
|
+
{formatCurrency(subtotal)}
|
|
340
|
+
</td>
|
|
341
|
+
</tr>
|
|
216
342
|
</tbody>
|
|
217
343
|
</table>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
})}
|
|
221
347
|
</div>
|
|
222
348
|
</div>
|
|
223
|
-
|
|
349
|
+
</div>
|
|
224
350
|
);
|
|
225
351
|
}
|
|
226
352
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.actionButtons {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
padding: 0;
|
|
6
|
+
background: transparent;
|
|
7
|
+
width: 100%;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
justify-content: flex-end;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.actionButton {
|
|
13
|
+
appearance: none;
|
|
14
|
+
border: 1px solid var(--primary-color);
|
|
15
|
+
border-radius: var(--br, 4px);
|
|
16
|
+
background: white;
|
|
17
|
+
color: var(--primary-color);
|
|
18
|
+
padding: 0.4rem 0.9rem;
|
|
19
|
+
font-size: 0.85rem;
|
|
20
|
+
font-weight: 500;
|
|
21
|
+
transition: all 0.2s ease;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
min-height: 32px;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
position: relative;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
30
|
+
|
|
31
|
+
&:hover {
|
|
32
|
+
background: var(--primary-color);
|
|
33
|
+
color: white;
|
|
34
|
+
transform: translateY(-1px);
|
|
35
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:active {
|
|
39
|
+
transform: translateY(0);
|
|
40
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&:first-child {
|
|
44
|
+
margin-left: 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@media (max-width: 768px) {
|
|
49
|
+
.actionButtons {
|
|
50
|
+
flex-wrap: nowrap;
|
|
51
|
+
overflow-x: auto;
|
|
52
|
+
-webkit-overflow-scrolling: touch;
|
|
53
|
+
padding-bottom: 5px;
|
|
54
|
+
justify-content: flex-start;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.actionButton {
|
|
59
|
+
padding: 0.3rem 0.7rem;
|
|
60
|
+
font-size: 0.75rem;
|
|
61
|
+
min-height: 30px;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
flex: 0 0 auto;
|
|
64
|
+
}
|
|
65
|
+
}
|