@visns-studio/visns-components 5.7.7 → 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 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.8",
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.id)}
13
+ title={action.label}
14
+ >
15
+ {action.label}
16
+ </button>
17
+ ))}
18
+ </div>
19
+ );
20
+ }
21
+
22
+ export default ActionButtons;
@@ -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
+ }
@@ -4,7 +4,7 @@
4
4
  align-items: flex-start;
5
5
  flex-wrap: nowrap;
6
6
  height: auto;
7
- gap: 1rem;
7
+ gap: 0.5rem;
8
8
  }
9
9
 
10
10
  .grid__row {
@@ -13,7 +13,7 @@
13
13
  align-items: flex-start;
14
14
  flex-wrap: nowrap;
15
15
  height: auto;
16
- gap: 1rem;
16
+ gap: 0.5rem;
17
17
  }
18
18
 
19
19
  .grid__three {
@@ -51,8 +51,8 @@
51
51
  overflow: visible;
52
52
  position: relative;
53
53
  padding: 2px;
54
- margin: 8px 0;
55
- box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
54
+ margin: 4px 0;
55
+ box-shadow: 0 5px 20px rgba(var(--primary-rgb), 0.05);
56
56
  }
57
57
 
58
58
  .grid__dashfull {
@@ -76,55 +76,7 @@
76
76
  gap: 0.5rem;
77
77
  }
78
78
 
79
- .grid__subnav {
80
- flex: 1;
81
- background: white;
82
- border-radius: var(--br);
83
- box-sizing: border-box;
84
- padding: 5px;
85
- margin: 8px 0;
86
- height: auto;
87
- box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
88
- }
89
-
90
- .grid__subnav > ul {
91
- width: 100%;
92
- list-style: none;
93
- padding: 0;
94
- margin: 0;
95
- display: flex;
96
- flex-wrap: wrap;
97
- }
98
-
99
- .grid__subnav > ul li {
100
- width: 100%;
101
- margin-bottom: 3px;
102
- }
103
-
104
- .grid__subnav > ul li a {
105
- width: 100%;
106
- padding: 0.35rem 1rem;
107
- box-sizing: border-box;
108
- display: block;
109
- text-decoration: none;
110
- background: rgba(var(--primary-rgb), 0.05);
111
- color: var(--primary-color);
112
- border-radius: 5px;
113
- transition: background 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
114
- outline: none;
115
- cursor: pointer;
116
- }
117
-
118
- .grid__subnav > ul li:hover a {
119
- background: var(--primary-color);
120
- color: var(--background-color);
121
- }
122
-
123
- .grid__subnav > ul li .subactive {
124
- font-weight: 700;
125
- background: var(--primary-color);
126
- color: var(--background-color);
127
- }
79
+ /* Removed grid__subnav styles as we're using ActionButtons component instead */
128
80
 
129
81
  .grid__minheight {
130
82
  min-height: 300px;
@@ -135,15 +87,17 @@
135
87
  }
136
88
 
137
89
  .grid__subcontent {
138
- flex: 0 1 80%;
90
+ flex: 1;
139
91
  background: var(--tertiary-color);
140
92
  border-radius: var(--br);
141
- border: 1px solid rgba(var(--primary-rgb), 0.05);
93
+ border: 1px solid rgba(var(--primary-rgb), 0.1);
142
94
  box-sizing: border-box;
143
- padding: 2px;
144
- margin: 8px 0;
95
+ padding: 0;
96
+ margin: 4px 0;
145
97
  height: auto;
146
98
  position: relative;
99
+ box-shadow: 0 3px 10px rgba(var(--primary-rgb), 0.05);
100
+ overflow: hidden;
147
101
  }
148
102
 
149
103
  .grid__subcontent h2 {
@@ -262,6 +216,55 @@
262
216
  }
263
217
  }
264
218
 
219
+ .quoteWrapper {
220
+ width: 100%;
221
+ max-width: 1200px;
222
+ margin: 0 auto;
223
+ padding: 0;
224
+ box-sizing: border-box;
225
+ background-color: white;
226
+ border: 1px solid #e0e0e0;
227
+ }
228
+
229
+ .quoteTopSection {
230
+ display: flex;
231
+ justify-content: space-between;
232
+ align-items: center;
233
+ width: 100%;
234
+ padding: 25px;
235
+ box-sizing: border-box;
236
+ margin-bottom: 0;
237
+ border-bottom: 1px solid rgba(var(--primary-rgb), 0.1);
238
+ }
239
+
240
+ .quoteIdentifier {
241
+ flex: 1;
242
+ }
243
+
244
+ .quoteTitle {
245
+ font-size: 1.3rem;
246
+ font-weight: 600;
247
+ color: var(--primary-color);
248
+ margin: 0;
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 8px;
252
+ }
253
+
254
+ .quoteNumber {
255
+ font-weight: 700;
256
+ color: var(--primary-color);
257
+ margin-left: 5px;
258
+ }
259
+
260
+ .actionButtonsContainer {
261
+ flex: 0 0 auto;
262
+ margin: 0;
263
+ padding: 0;
264
+ box-sizing: border-box;
265
+ min-width: 50%;
266
+ }
267
+
265
268
  .titleInfo {
266
269
  position: absolute;
267
270
  right: 2em;
@@ -276,12 +279,14 @@
276
279
  width: 100%;
277
280
  max-width: 1450px;
278
281
  margin: 0 auto;
279
- margin-bottom: 20px;
282
+ margin-bottom: 15px;
280
283
  box-sizing: border-box;
281
284
  display: flex;
282
285
  flex-wrap: wrap;
283
- border: 1px solid rgba(var(--primary-rgb), 0.65);
284
- border-radius: 3px;
286
+ border: 1px solid rgba(var(--primary-rgb), 0.2);
287
+ border-radius: var(--br);
288
+ background-color: white;
289
+ box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.05);
285
290
  }
286
291
 
287
292
  .qitem {
@@ -289,26 +294,32 @@
289
294
  display: flex;
290
295
  flex-wrap: wrap;
291
296
  box-sizing: border-box;
292
- padding: 30px;
293
- border-bottom: 1px solid rgba(var(--primary-rgb), 0.65);
297
+ padding: 15px 20px;
298
+ border-bottom: 1px solid rgba(var(--primary-rgb), 0.15);
294
299
  color: var(--paragraph-color);
295
300
 
301
+ &:last-child {
302
+ border-bottom: none;
303
+ }
304
+
296
305
  &__half {
297
306
  width: 50%;
298
307
  box-sizing: border-box;
299
308
 
300
309
  .qtitle {
301
310
  display: block;
302
- font-size: 2rem;
311
+ font-size: 1.75rem;
303
312
  text-align: right;
304
313
  color: var(--primary-color);
314
+ font-weight: 600;
305
315
  }
306
316
 
307
317
  .qno,
308
318
  .qdate {
309
319
  display: block;
310
- font-size: 1rem;
320
+ font-size: 0.9rem;
311
321
  text-align: right;
322
+ line-height: 1.4;
312
323
  }
313
324
 
314
325
  .qdate {
@@ -317,6 +328,13 @@
317
328
 
318
329
  .qvisns {
319
330
  text-align: right;
331
+ line-height: 1.4;
332
+ font-size: 0.9rem;
333
+ }
334
+
335
+ .qdetails {
336
+ line-height: 1.4;
337
+ font-size: 0.9rem;
320
338
  }
321
339
  }
322
340
 
@@ -348,113 +366,390 @@
348
366
  }
349
367
  }
350
368
 
351
- .quoteimg {
352
- img {
353
- width: 100%;
354
- max-width: 250px;
355
- height: auto;
356
- display: block;
369
+ .quoteLogoContainer {
370
+ flex: 0 0 30%;
371
+ padding-right: 20px;
372
+ }
373
+
374
+ .quoteLogo {
375
+ max-width: 180px;
376
+ height: auto;
377
+ display: block;
378
+ }
379
+
380
+ .quoteTitleContainer {
381
+ flex: 0 0 70%;
382
+ text-align: right;
383
+ }
384
+
385
+ .quoteBigNumber {
386
+ font-size: 1.6rem;
387
+ color: var(--primary-color);
388
+ margin: 0 0 5px 0;
389
+ font-weight: 600;
390
+ }
391
+
392
+ .quoteDate {
393
+ font-size: 0.9rem;
394
+ color: var(--paragraph-color);
395
+ }
396
+
397
+ .quoteParties {
398
+ display: flex;
399
+ justify-content: space-between;
400
+ margin: 25px 0;
401
+ border-top: 1px solid rgba(var(--primary-rgb), 0.1);
402
+ border-bottom: 1px solid rgba(var(--primary-rgb), 0.1);
403
+ padding: 20px 0;
404
+ background-color: rgba(var(--primary-rgb), 0.02);
405
+ border-radius: var(--br, 4px);
406
+ }
407
+
408
+ .quoteParty {
409
+ flex: 0 0 48%;
410
+ padding: 0 15px;
411
+
412
+ &:first-child {
413
+ text-align: left;
357
414
  }
415
+
416
+ &:last-child {
417
+ text-align: right;
418
+ }
419
+ }
420
+
421
+ .partyTitle {
422
+ font-size: 0.95rem;
423
+ font-weight: 600;
424
+ color: var(--primary-color);
425
+ margin: 0 0 8px 0;
426
+ padding-bottom: 5px;
427
+ border-bottom: 1px dashed rgba(var(--primary-rgb), 0.1);
428
+ text-align: inherit; /* Inherit text alignment from parent */
429
+ }
430
+
431
+ .partyDetails {
432
+ font-size: 0.9rem;
433
+ line-height: 1.5;
434
+ color: var(--paragraph-color);
435
+ text-align: inherit; /* Inherit text alignment from parent */
358
436
  }
359
437
 
360
438
  .quotestatus {
361
439
  width: 100%;
362
- padding: 30px;
363
- text-align: center;
364
- font-size: 1.65em;
440
+ padding: 10px 25px;
441
+ display: flex;
442
+ justify-content: space-between;
443
+ align-items: center;
444
+ font-size: 1em;
365
445
  box-sizing: border-box;
446
+ background-color: var(--primary-color, #1a3d66);
447
+ color: var(--tertiary-color, white);
448
+ border-radius: 0;
449
+ margin-bottom: 0;
450
+ box-shadow: none;
451
+
452
+ .statusLeft,
453
+ .statusRight {
454
+ display: flex;
455
+ align-items: center;
456
+ }
457
+
458
+ .statusRight {
459
+ font-weight: 500;
460
+ }
366
461
 
367
462
  .q__accepted {
368
- color: #84c333;
463
+ color: var(--tertiary-color, white);
464
+ font-weight: 600;
465
+ background-color: #84c333;
466
+ padding: 4px 12px;
467
+ border-radius: 20px;
468
+ display: inline-block;
469
+ font-size: 0.9em;
369
470
  }
370
471
 
371
472
  .q__pending {
372
- color: #d4c141;
473
+ color: var(--tertiary-color, white);
474
+ font-weight: 600;
475
+ background-color: #d4c141;
476
+ padding: 4px 12px;
477
+ border-radius: 20px;
478
+ display: inline-block;
479
+ font-size: 0.9em;
373
480
  }
374
481
 
375
482
  .q__declined {
376
- color: #c33333;
483
+ color: var(--tertiary-color, white);
484
+ font-weight: 600;
485
+ background-color: #c33333;
486
+ padding: 4px 12px;
487
+ border-radius: 20px;
488
+ display: inline-block;
489
+ font-size: 0.9em;
490
+ }
491
+
492
+ .ticketId {
493
+ font-size: 0.85em;
494
+ opacity: 0.9;
495
+ margin-left: 5px;
496
+ }
497
+
498
+ .quoteReference {
499
+ font-size: 0.9em;
500
+ opacity: 0.9;
501
+ font-weight: 500;
377
502
  }
378
503
  }
379
504
 
380
- .splitheader {
381
- background: var(--primary-color) !important;
382
- border-bottom: 1px solid #dfe3eb;
505
+ .quoteContent {
506
+ background-color: white;
507
+ border-radius: 0;
508
+ box-shadow: none;
509
+ padding: 25px;
510
+ margin-bottom: 0;
383
511
  }
384
512
 
385
- .splittitle {
386
- font-size: 0.975em;
387
- text-transform: uppercase;
388
- font-weight: 700;
389
- color: #3dd87c;
390
- padding: 0;
513
+ .quoteHeaderSection {
514
+ display: flex;
515
+ justify-content: space-between;
516
+ align-items: center;
517
+ margin-bottom: 20px;
518
+ }
519
+
520
+ .quoteItems {
521
+ margin-top: 30px;
522
+ }
523
+
524
+ .quoteTableSection {
525
+ margin-bottom: 25px;
526
+ }
527
+
528
+ .tableTitle {
529
+ font-size: 1rem;
530
+ font-weight: 600;
531
+ color: var(--tertiary-color, white);
391
532
  margin: 0;
533
+ padding: 10px 15px;
534
+ background-color: var(--primary-color, #1a3d66);
535
+ border-radius: 0;
536
+ border: none;
392
537
  }
393
538
 
394
- .content-table {
539
+ .quoteTable {
395
540
  width: 100%;
541
+ table-layout: fixed; /* Ensures consistent column widths across tables */
396
542
  border-collapse: collapse;
397
- margin: 0 auto;
398
- font-size: 1em;
399
- border-radius: 5px 5px 0 0;
543
+ font-size: 0.9rem;
544
+ border-radius: 0;
400
545
  overflow: hidden;
401
546
  border: none;
402
- border-spacing: 0;
403
- border-collapse: collapse;
404
547
  box-sizing: border-box;
548
+ box-shadow: none;
549
+ margin-bottom: 25px;
550
+ }
551
+
552
+ .tableHeader {
553
+ background-color: #4fbfa5;
554
+ color: white;
555
+ text-align: left;
556
+ font-weight: 600;
557
+ font-size: 0.85rem;
558
+ padding: 10px 15px;
559
+ border-bottom: none;
405
560
 
406
- thead {
407
- display: table-row-group;
561
+ &:first-child {
562
+ border-top-left-radius: 0;
408
563
  }
409
564
 
410
- thead tr {
411
- background: var(--table-color);
412
- color: var(--primary-color);
413
- text-align: left;
414
- font-weight: 700;
415
- text-transform: capitalize;
416
- font-size: 1em;
565
+ &:last-child {
566
+ border-top-right-radius: 0;
417
567
  }
418
568
 
419
- .filterRow {
420
- th {
421
- padding: 5px;
422
- }
569
+ &[style*='text-align: right'] {
570
+ text-align: right;
423
571
  }
424
572
 
425
- th,
426
- td {
427
- font-size: 0.95em;
428
- padding: 8px 15px;
429
- color: var(--paragraph-color);
430
- border-right: 1px solid var(--table-border);
431
- line-height: 1;
432
- border-spacing: 0;
433
- border-collapse: collapse;
573
+ &[style*='text-align: center'] {
574
+ text-align: center;
575
+ }
576
+ }
434
577
 
435
- &:last-of-type {
436
- border-right: none;
437
- }
578
+ .tableRow {
579
+ border-bottom: 1px solid rgba(var(--primary-rgb), 0.05);
580
+ transition: background-color 0.2s ease;
581
+ background-color: white;
582
+
583
+ &:hover {
584
+ background-color: rgba(var(--primary-rgb), 0.02);
585
+ }
586
+
587
+ &:last-child {
588
+ border-bottom: none;
589
+ }
590
+
591
+ &:nth-child(odd) {
592
+ background-color: #f9f9f9;
593
+ }
594
+ }
595
+
596
+ .tableCell {
597
+ padding: 12px 15px;
598
+ color: var(--paragraph-color);
599
+ font-size: 0.85rem;
600
+ line-height: 1.4;
601
+ vertical-align: middle;
602
+
603
+ &[style*='text-align: right'] {
604
+ font-weight: 600;
605
+ }
606
+
607
+ &[style*='text-align: center'] {
608
+ text-align: center !important;
609
+ }
610
+ }
611
+
612
+ .subtotalRow {
613
+ background-color: rgba(var(--primary-rgb), 0.05);
614
+ border-top: 1px solid rgba(var(--primary-rgb), 0.1);
438
615
 
616
+ &:hover {
617
+ background-color: rgba(var(--primary-rgb), 0.05);
618
+ }
619
+ }
620
+
621
+ .subtotalLabel {
622
+ padding: 12px 15px;
623
+ text-align: right;
624
+ font-weight: 600;
625
+ color: var(--primary-color);
626
+ font-size: 0.9rem;
627
+ }
628
+
629
+ .subtotalValue {
630
+ padding: 12px 15px;
631
+ text-align: right;
632
+ font-weight: 700;
633
+ color: var(--primary-color);
634
+ font-size: 0.9rem;
635
+ white-space: nowrap; /* Prevents wrapping of the value */
636
+ }
637
+
638
+ /* Responsive styles */
639
+ @media (max-width: 768px) {
640
+ .quoteWrapper {
641
+ padding: 0 10px;
642
+ }
643
+
644
+ .quoteTopSection {
645
+ flex-direction: column;
646
+ align-items: flex-start;
647
+ gap: 15px;
648
+ padding: 10px 0;
649
+ }
650
+
651
+ .quoteIdentifier {
652
+ width: 100%;
653
+ margin-bottom: 0;
654
+ }
655
+
656
+ .actionButtonsContainer {
657
+ width: 100%;
658
+ min-width: 100%;
659
+ }
660
+
661
+ .quotestatus {
662
+ flex-direction: column;
663
+ align-items: flex-start;
664
+ gap: 8px;
665
+ font-size: 0.9em;
666
+ padding: 10px 15px;
667
+ }
668
+
669
+ .statusLeft,
670
+ .statusRight {
671
+ width: 100%;
672
+ }
673
+
674
+ .statusRight {
675
+ margin-top: 5px;
676
+ }
677
+
678
+ .quoteContent {
679
+ padding: 15px;
680
+ }
681
+
682
+ .quoteHeaderSection {
683
+ flex-direction: column;
684
+ }
685
+
686
+ .quoteLogoContainer {
687
+ flex: 0 0 100%;
688
+ padding-right: 0;
689
+ margin-bottom: 15px;
690
+ text-align: center;
691
+ }
692
+
693
+ .quoteLogo {
694
+ max-width: 150px;
695
+ margin: 0 auto;
696
+ }
697
+
698
+ .quoteTitleContainer {
699
+ flex: 0 0 100%;
700
+ text-align: center;
701
+ }
702
+
703
+ .quoteBigNumber {
704
+ font-size: 1.4rem;
705
+ }
706
+
707
+ .quoteParties {
708
+ flex-direction: column;
709
+ gap: 20px;
710
+ padding: 15px 0;
711
+ }
712
+
713
+ .quoteParty {
714
+ flex: 0 0 100%;
715
+ padding: 0 10px;
716
+
717
+ &:first-child,
439
718
  &:last-child {
440
- border-right: none;
719
+ text-align: left;
441
720
  }
442
721
  }
443
722
 
444
- td {
445
- .tdaction {
446
- background: #f0f0f0;
447
- display: block;
448
- line-height: 1;
449
- padding: 0.05rem;
450
- border-radius: 5px;
451
- margin: 0 0.25rem;
452
- }
723
+ .partyTitle {
724
+ font-size: 0.9rem;
725
+ }
726
+
727
+ .partyDetails {
728
+ font-size: 0.85rem;
729
+ }
730
+
731
+ .tableTitle {
732
+ font-size: 0.9rem;
733
+ padding: 8px 12px;
734
+ }
735
+
736
+ .quoteTable {
737
+ font-size: 0.8rem;
738
+ }
739
+
740
+ .tableHeader {
741
+ padding: 8px 10px;
742
+ font-size: 0.8rem;
743
+ }
744
+
745
+ .tableCell {
746
+ padding: 8px 10px;
747
+ font-size: 0.8rem;
453
748
  }
454
749
 
455
- tbody tr {
456
- border-bottom: 1px solid var(--table-border);
457
- cursor: pointer;
458
- background: var(--tertiary-color);
750
+ .subtotalLabel,
751
+ .subtotalValue {
752
+ padding: 8px 10px;
753
+ font-size: 0.85rem;
459
754
  }
460
755
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Format a number with thousand separators (commas)
3
+ * @param {number|string} value - The value to format
4
+ * @param {number} [decimals=2] - Number of decimal places
5
+ * @returns {string} - Formatted number string
6
+ */
7
+ export const formatNumber = (value, decimals = 2) => {
8
+ if (!value && value !== 0) return '';
9
+
10
+ // Parse the value to make sure it's a number
11
+ const numValue = parseFloat(String(value).replace(/[^0-9.-]+/g, ''));
12
+
13
+ if (isNaN(numValue)) return value;
14
+
15
+ // Format with specified decimal places and thousand separators
16
+ return numValue.toLocaleString('en-US', {
17
+ minimumFractionDigits: decimals,
18
+ maximumFractionDigits: decimals,
19
+ });
20
+ };
21
+
22
+ /**
23
+ * Format a number as currency with dollar sign and thousand separators
24
+ * @param {number|string} value - The value to format
25
+ * @returns {string} - Formatted currency string
26
+ */
27
+ export const formatCurrency = (value) => {
28
+ if (!value && value !== 0) return '';
29
+
30
+ // Parse the value to make sure it's a number
31
+ const numValue = parseFloat(String(value).replace(/[^0-9.-]+/g, ''));
32
+
33
+ if (isNaN(numValue)) return value;
34
+
35
+ // Format with 2 decimal places, thousand separators, and dollar sign
36
+ return (
37
+ '$' +
38
+ numValue.toLocaleString('en-US', {
39
+ minimumFractionDigits: 2,
40
+ maximumFractionDigits: 2,
41
+ })
42
+ );
43
+ };