@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.
@@ -1,35 +1,29 @@
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 } 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 TableFilter from '../TableFilter';
11
- import MultiSelect from '../../crm/MultiSelect';
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 { filters, page, tables } = setting;
17
+ const { actions: actionItems, page, tables } = setting;
26
18
 
27
19
  const [data, setData] = useState({});
28
- const [subnav, setSubnav] = useState(filters || []);
20
+ const [actions, setActions] = useState(actionItems || []);
29
21
 
30
22
  useEffect(() => {
31
- setSubnav(filters);
32
- }, [filters]);
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
- <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>
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
- <div className={styles.grid}>
63
- <div className={styles.grid__row}>
64
- <div className={styles.grid__subnav}>
65
- <TableFilter filters={subnav} setFilters={setSubnav} />
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.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>
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
- <div className={styles.quotecontainer}>
86
- <div className={styles.qitem}>
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
- className={`${styles.qitem__half} ${styles.quoteimg}`}
229
+ key={`table-section-${table.id}`}
230
+ className={styles.quoteTableSection}
89
231
  >
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) => (
232
+ <h2 className={styles.tableTitle}>
233
+ {table.label}
234
+ </h2>
156
235
  <table
157
- key={`table-${table.id}`} // Adding a key to identify each table in the list
158
- className={`${styles['content-table']} ${styles['quote-table']}`}
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
- data[table.id].map(
189
- (row, index) => (
190
- <tr
191
- key={`row-${index}`}
192
- >
193
- <td></td>
194
- {table.columns.map(
195
- (column) => (
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
- </tr>
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
- </div>
220
- </div>
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
+ }