@faststore/core 4.4.0 → 4.5.0-dev.0
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/.turbo/turbo-generate.log +4 -4
- package/.turbo/turbo-test.log +63 -50
- package/@generated/cached-operations.json +2 -0
- package/@generated/gql.ts +12 -0
- package/@generated/graphql.ts +73 -0
- package/@generated/persisted-documents.json +2 -0
- package/@generated/schema.graphql +59 -0
- package/CHANGELOG.md +10 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +5 -1
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +16 -6
- package/src/components/account/Layout/Layout.tsx +14 -5
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotes.tsx +170 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountListQuotesTable.tsx +194 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountListQuotesTable/MyAccountQuoteStatusBadge.tsx +15 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/MyAccountQuotesFilterSlider.tsx +162 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesFilterSlider/section.module.scss +49 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesSelectedTags/MyAccountQuotesSelectedTags.tsx +125 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/MyAccountQuotesStatusSelector.tsx +107 -0
- package/src/components/account/quotes/MyAccountListQuotes/MyAccountQuotesStatusSelector/styles.module.scss +144 -0
- package/src/components/account/quotes/MyAccountListQuotes/index.ts +2 -0
- package/src/components/account/quotes/MyAccountListQuotes/quoteFilters.ts +89 -0
- package/src/components/account/quotes/MyAccountListQuotes/styles.module.scss +174 -0
- package/src/customizations/src/myAccount/extensions/quotes/after.tsx +5 -0
- package/src/customizations/src/myAccount/extensions/quotes/before.tsx +5 -0
- package/src/pages/pvt/account/quotes/index.tsx +264 -0
- package/src/sdk/account/getMyAccountRoutes.ts +7 -0
- package/src/utils/quoteStatus.ts +27 -0
- package/test/components/account/Layout.test.tsx +62 -0
- package/test/components/account/OrganizationDrawerBody.test.tsx +52 -0
- package/test/components/quotes/MyAccountListQuotes.test.tsx +232 -0
- package/test/components/quotes/MyAccountListQuotesTable.test.tsx +240 -0
- package/test/components/quotes/MyAccountQuoteStatusBadge.test.tsx +28 -0
- package/test/components/quotes/MyAccountQuotesFilterSlider.test.tsx +152 -0
- package/test/components/quotes/MyAccountQuotesSelectedTags.test.tsx +122 -0
- package/test/components/quotes/MyAccountQuotesStatusSelector.test.tsx +85 -0
- package/test/components/quotes/quoteFilters.test.ts +185 -0
- package/test/sdk/account/getMyAccountRoutes.test.ts +90 -0
- package/test/server/index.test.ts +2 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Button } from '@faststore/ui'
|
|
2
|
+
import { useSession } from 'src/sdk/session'
|
|
3
|
+
import { quoteStatusMap, type QuoteStatusKey } from 'src/utils/quoteStatus'
|
|
4
|
+
import { formatFilterDate } from '../quoteFilters'
|
|
5
|
+
|
|
6
|
+
type MyAccountQuotesSelectedTagsProps = Readonly<{
|
|
7
|
+
filters: {
|
|
8
|
+
status?: string[]
|
|
9
|
+
createdAtFrom?: string
|
|
10
|
+
createdAtTo?: string
|
|
11
|
+
expiresAtFrom?: string
|
|
12
|
+
expiresAtTo?: string
|
|
13
|
+
}
|
|
14
|
+
onClearAll: () => void
|
|
15
|
+
onRemoveFilter: (
|
|
16
|
+
key:
|
|
17
|
+
| 'status'
|
|
18
|
+
| 'createdAtFrom'
|
|
19
|
+
| 'createdAtTo'
|
|
20
|
+
| 'expiresAtFrom'
|
|
21
|
+
| 'expiresAtTo',
|
|
22
|
+
value: string
|
|
23
|
+
) => void
|
|
24
|
+
}>
|
|
25
|
+
|
|
26
|
+
function formatDateRangeLabel(
|
|
27
|
+
from: string | undefined,
|
|
28
|
+
to: string | undefined,
|
|
29
|
+
locale: string
|
|
30
|
+
) {
|
|
31
|
+
if (from && to) {
|
|
32
|
+
return `${formatFilterDate(from, locale)} to ${formatFilterDate(to, locale)}`
|
|
33
|
+
}
|
|
34
|
+
if (from) {
|
|
35
|
+
return `from ${formatFilterDate(from, locale)}`
|
|
36
|
+
}
|
|
37
|
+
return `to ${formatFilterDate(to!, locale)}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Tags({
|
|
41
|
+
filters,
|
|
42
|
+
onRemoveFilter,
|
|
43
|
+
}: Pick<MyAccountQuotesSelectedTagsProps, 'filters' | 'onRemoveFilter'>) {
|
|
44
|
+
const { locale } = useSession()
|
|
45
|
+
const { createdAtFrom, createdAtTo, expiresAtFrom, expiresAtTo, status } =
|
|
46
|
+
filters
|
|
47
|
+
|
|
48
|
+
const createdTag = (createdAtFrom || createdAtTo) && (
|
|
49
|
+
<div key="created-date-range" data-fs-list-orders-selected-tag>
|
|
50
|
+
<span data-fs-list-orders-selected-tag-label>
|
|
51
|
+
{`Created: ${formatDateRangeLabel(createdAtFrom, createdAtTo, locale)}`}
|
|
52
|
+
</span>
|
|
53
|
+
<button
|
|
54
|
+
data-fs-list-orders-selected-tag-clear
|
|
55
|
+
onClick={() => onRemoveFilter('createdAtFrom', createdAtFrom ?? '')}
|
|
56
|
+
>
|
|
57
|
+
×
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const expiresTag = (expiresAtFrom || expiresAtTo) && (
|
|
63
|
+
<div key="expires-date-range" data-fs-list-orders-selected-tag>
|
|
64
|
+
<span data-fs-list-orders-selected-tag-label>
|
|
65
|
+
{`Expires: ${formatDateRangeLabel(expiresAtFrom, expiresAtTo, locale)}`}
|
|
66
|
+
</span>
|
|
67
|
+
<button
|
|
68
|
+
data-fs-list-orders-selected-tag-clear
|
|
69
|
+
onClick={() => onRemoveFilter('expiresAtFrom', expiresAtFrom ?? '')}
|
|
70
|
+
>
|
|
71
|
+
×
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const statusTags = (status || []).map((value) => (
|
|
77
|
+
<div key={`status-${value}`} data-fs-list-orders-selected-tag>
|
|
78
|
+
<span>{quoteStatusMap[value as QuoteStatusKey]?.label ?? value}</span>
|
|
79
|
+
<button
|
|
80
|
+
data-fs-list-orders-selected-tag-clear
|
|
81
|
+
onClick={() => onRemoveFilter('status', value)}
|
|
82
|
+
>
|
|
83
|
+
×
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<>
|
|
90
|
+
{createdTag}
|
|
91
|
+
{expiresTag}
|
|
92
|
+
{statusTags}
|
|
93
|
+
</>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function MyAccountQuotesSelectedTags({
|
|
98
|
+
filters,
|
|
99
|
+
onClearAll,
|
|
100
|
+
onRemoveFilter,
|
|
101
|
+
}: MyAccountQuotesSelectedTagsProps) {
|
|
102
|
+
const hasFilters = Object.entries(filters).some(([, values]) =>
|
|
103
|
+
Array.isArray(values) ? values.length > 0 : Boolean(values)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
{hasFilters && (
|
|
109
|
+
<div data-fs-list-orders-selected-tags>
|
|
110
|
+
<Tags filters={filters} onRemoveFilter={onRemoveFilter} />
|
|
111
|
+
<Button
|
|
112
|
+
variant="tertiary"
|
|
113
|
+
size="small"
|
|
114
|
+
data-fs-list-orders-selected-tags-clear-all-button
|
|
115
|
+
onClick={onClearAll}
|
|
116
|
+
>
|
|
117
|
+
Clear All
|
|
118
|
+
</Button>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default MyAccountQuotesSelectedTags
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { quoteStatusMap } from 'src/utils/quoteStatus'
|
|
4
|
+
import styles from './styles.module.scss'
|
|
5
|
+
|
|
6
|
+
type MyAccountQuotesStatusSelectorProps = Readonly<{
|
|
7
|
+
value: string[]
|
|
8
|
+
onChange: (selected: string[]) => void
|
|
9
|
+
}>
|
|
10
|
+
|
|
11
|
+
const statusEntries = Object.entries(quoteStatusMap).map(([key, entry]) => ({
|
|
12
|
+
key,
|
|
13
|
+
label: entry.label,
|
|
14
|
+
variant: entry.variant,
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
export default function MyAccountQuotesStatusSelector({
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
}: MyAccountQuotesStatusSelectorProps) {
|
|
21
|
+
const [open, setOpen] = useState(false)
|
|
22
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
23
|
+
const labelId = useId()
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
function handleClickOutside(e: MouseEvent) {
|
|
27
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
28
|
+
setOpen(false)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
32
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
const toggle = (key: string) => {
|
|
36
|
+
const next = value.includes(key)
|
|
37
|
+
? value.filter((v) => v !== key)
|
|
38
|
+
: [...value, key]
|
|
39
|
+
onChange(next)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const remove = (key: string, e: React.MouseEvent) => {
|
|
43
|
+
e.stopPropagation()
|
|
44
|
+
onChange(value.filter((v) => v !== key))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={styles.wrapper} ref={ref} data-fs-quotes-status-selector>
|
|
49
|
+
<span id={labelId} className={styles.label}>
|
|
50
|
+
Status
|
|
51
|
+
</span>
|
|
52
|
+
<div data-fs-quotes-status-input-wrapper>
|
|
53
|
+
<div className={styles.input} data-open={open || undefined}>
|
|
54
|
+
{value.map((key) => {
|
|
55
|
+
const entry = statusEntries.find((e) => e.key === key)
|
|
56
|
+
return (
|
|
57
|
+
<span
|
|
58
|
+
key={key}
|
|
59
|
+
className={styles.chip}
|
|
60
|
+
data-variant={entry?.variant ?? 'neutral'}
|
|
61
|
+
>
|
|
62
|
+
{entry?.label ?? key}
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className={styles.chipRemove}
|
|
66
|
+
onClick={(e) => remove(key, e)}
|
|
67
|
+
aria-label={`Remove ${entry?.label ?? key}`}
|
|
68
|
+
>
|
|
69
|
+
×
|
|
70
|
+
</button>
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
})}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className={styles.toggle}
|
|
77
|
+
aria-labelledby={labelId}
|
|
78
|
+
aria-expanded={open}
|
|
79
|
+
aria-controls="status-listbox"
|
|
80
|
+
onClick={() => setOpen((o) => !o)}
|
|
81
|
+
>
|
|
82
|
+
<span aria-hidden="true">▾</span>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
{open && (
|
|
86
|
+
<div id="status-listbox" className={styles.dropdown} tabIndex={-1}>
|
|
87
|
+
{statusEntries.map(({ key, label }) => {
|
|
88
|
+
const id = `status-opt-${key}`
|
|
89
|
+
const checked = value.includes(key)
|
|
90
|
+
return (
|
|
91
|
+
<div key={key} className={styles.option}>
|
|
92
|
+
<input
|
|
93
|
+
type="checkbox"
|
|
94
|
+
id={id}
|
|
95
|
+
checked={checked}
|
|
96
|
+
onChange={() => toggle(key)}
|
|
97
|
+
/>
|
|
98
|
+
<label htmlFor={id}>{label}</label>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
@use "@faststore/ui/src/styles/base/utilities";
|
|
2
|
+
|
|
3
|
+
.wrapper {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--fs-spacing-1);
|
|
7
|
+
min-width: 220px;
|
|
8
|
+
|
|
9
|
+
[data-fs-quotes-status-input-wrapper] {
|
|
10
|
+
position: relative;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.label {
|
|
15
|
+
font-size: var(--fs-text-size-2);
|
|
16
|
+
font-weight: var(--fs-text-weight-medium);
|
|
17
|
+
color: var(--fs-color-text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.input {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
gap: var(--fs-spacing-1);
|
|
24
|
+
align-items: center;
|
|
25
|
+
min-height: 2.5rem;
|
|
26
|
+
padding: var(--fs-spacing-1) var(--fs-spacing-2);
|
|
27
|
+
background-color: var(--fs-color-neutral-0);
|
|
28
|
+
border: 1px solid var(--fs-border-color);
|
|
29
|
+
border-radius: var(--fs-border-radius);
|
|
30
|
+
|
|
31
|
+
&[data-open] {
|
|
32
|
+
border-color: var(--fs-color-focus-ring);
|
|
33
|
+
box-shadow: 0 0 0 2px var(--fs-color-focus-ring-outline);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.chip {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
gap: var(--fs-spacing-0);
|
|
40
|
+
align-items: center;
|
|
41
|
+
padding: var(--fs-spacing-0) var(--fs-spacing-2);
|
|
42
|
+
font-size: var(--fs-text-size-1);
|
|
43
|
+
color: var(--fs-color-text);
|
|
44
|
+
background-color: var(--fs-color-neutral-2);
|
|
45
|
+
border-radius: var(--fs-border-radius-pill);
|
|
46
|
+
|
|
47
|
+
&[data-variant="success"] {
|
|
48
|
+
background-color: #e9fce3;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&[data-variant="warning"] {
|
|
52
|
+
background-color: #ffedcd;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&[data-variant="info"] {
|
|
56
|
+
background-color: #d0f0e8;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&[data-variant="danger"] {
|
|
60
|
+
background-color: #fce3e3;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&[data-variant="neutral"] {
|
|
64
|
+
background-color: #f5f5f5;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.toggle {
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
padding: 0 var(--fs-spacing-1);
|
|
74
|
+
margin-left: auto;
|
|
75
|
+
color: var(--fs-color-text);
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
background: none;
|
|
78
|
+
border: none;
|
|
79
|
+
outline: none;
|
|
80
|
+
|
|
81
|
+
&:focus-visible {
|
|
82
|
+
outline: 2px solid var(--fs-color-focus-ring);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.chipRemove {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
padding: 0;
|
|
91
|
+
margin-left: var(--fs-spacing-0);
|
|
92
|
+
font-size: var(--fs-text-size-2);
|
|
93
|
+
line-height: 1;
|
|
94
|
+
color: var(--fs-color-text);
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
background: none;
|
|
97
|
+
border: none;
|
|
98
|
+
|
|
99
|
+
&:hover {
|
|
100
|
+
opacity: 0.6;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dropdown {
|
|
105
|
+
position: absolute;
|
|
106
|
+
top: 100%;
|
|
107
|
+
right: 0;
|
|
108
|
+
left: 0;
|
|
109
|
+
z-index: 100;
|
|
110
|
+
padding: var(--fs-spacing-1) 0;
|
|
111
|
+
margin: 0;
|
|
112
|
+
list-style: none;
|
|
113
|
+
background-color: var(--fs-color-neutral-0);
|
|
114
|
+
border: 1px solid var(--fs-border-color-light);
|
|
115
|
+
border-top: none;
|
|
116
|
+
border-radius: 0 0 var(--fs-border-radius) var(--fs-border-radius);
|
|
117
|
+
box-shadow: 0 4px 8px rgb(0 0 0 / 8%);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.option {
|
|
121
|
+
display: flex;
|
|
122
|
+
gap: var(--fs-spacing-2);
|
|
123
|
+
align-items: center;
|
|
124
|
+
padding: var(--fs-spacing-2) var(--fs-spacing-3);
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
|
|
127
|
+
&:hover {
|
|
128
|
+
background-color: var(--fs-color-neutral-1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
input[type="checkbox"] {
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
width: 1.125rem;
|
|
134
|
+
height: 1.125rem;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
accent-color: var(--fs-color-primary-bkg);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
label {
|
|
140
|
+
font-size: var(--fs-text-size-body);
|
|
141
|
+
color: var(--fs-color-text);
|
|
142
|
+
cursor: pointer;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { quoteStatusMap } from 'src/utils/quoteStatus'
|
|
2
|
+
|
|
3
|
+
export type QuoteFilters = {
|
|
4
|
+
page: number
|
|
5
|
+
status: string[]
|
|
6
|
+
createdAtFrom: string
|
|
7
|
+
createdAtTo: string
|
|
8
|
+
expiresAtFrom: string
|
|
9
|
+
expiresAtTo: string
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type QuoteSelectedFacet = { key: string; value: string }
|
|
14
|
+
|
|
15
|
+
export function getSelectedFacets(filters: QuoteFilters): QuoteSelectedFacet[] {
|
|
16
|
+
const facets: QuoteSelectedFacet[] = []
|
|
17
|
+
if (filters.status.length > 0) {
|
|
18
|
+
filters.status.forEach((s) => facets.push({ key: 'status', value: s }))
|
|
19
|
+
}
|
|
20
|
+
return facets
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getAllFacets(filters: QuoteFilters) {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
__typename: 'StoreFacetBoolean' as const,
|
|
27
|
+
key: 'status',
|
|
28
|
+
label: 'Status',
|
|
29
|
+
values: Object.entries(quoteStatusMap).map(([key, { label }]) => ({
|
|
30
|
+
label,
|
|
31
|
+
quantity: 0,
|
|
32
|
+
selected: false,
|
|
33
|
+
value: key,
|
|
34
|
+
})),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
__typename: 'StoreFacetRange' as const,
|
|
38
|
+
key: 'createdAt',
|
|
39
|
+
label: 'Created Date',
|
|
40
|
+
from: filters.createdAtFrom,
|
|
41
|
+
to: filters.createdAtTo,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
__typename: 'StoreFacetRange' as const,
|
|
45
|
+
key: 'expiresAt',
|
|
46
|
+
label: 'Expiry Date',
|
|
47
|
+
from: filters.expiresAtFrom,
|
|
48
|
+
to: filters.expiresAtTo,
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hasActiveFilters(filters: QuoteFilters): boolean {
|
|
54
|
+
return (
|
|
55
|
+
filters.status.length > 0 ||
|
|
56
|
+
Boolean(filters.createdAtFrom) ||
|
|
57
|
+
Boolean(filters.createdAtTo) ||
|
|
58
|
+
Boolean(filters.expiresAtFrom) ||
|
|
59
|
+
Boolean(filters.expiresAtTo) ||
|
|
60
|
+
Boolean(filters.label)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function countActiveFilters(filters: QuoteFilters): number {
|
|
65
|
+
let count = 0
|
|
66
|
+
if (filters.status.length > 0) count++
|
|
67
|
+
if (filters.createdAtFrom || filters.createdAtTo) count++
|
|
68
|
+
if (filters.expiresAtFrom || filters.expiresAtTo) count++
|
|
69
|
+
return count
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatFilterDate(date: string, locale: string): string {
|
|
73
|
+
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return date
|
|
74
|
+
const [year, month, day] = date.split('-').map(Number)
|
|
75
|
+
if (!year || !month || !day) return date
|
|
76
|
+
const parsed = new Date(year, month - 1, day)
|
|
77
|
+
if (
|
|
78
|
+
parsed.getFullYear() !== year ||
|
|
79
|
+
parsed.getMonth() !== month - 1 ||
|
|
80
|
+
parsed.getDate() !== day
|
|
81
|
+
) {
|
|
82
|
+
return date
|
|
83
|
+
}
|
|
84
|
+
return parsed.toLocaleDateString(locale, {
|
|
85
|
+
year: 'numeric',
|
|
86
|
+
month: '2-digit',
|
|
87
|
+
day: '2-digit',
|
|
88
|
+
})
|
|
89
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
@use "sass:meta";
|
|
2
|
+
@use "@faststore/ui/src/styles/base/utilities";
|
|
3
|
+
|
|
4
|
+
.page {
|
|
5
|
+
@include meta.load-css("../../components/StatusBadge/styles.scss");
|
|
6
|
+
@include meta.load-css("~@faststore/ui/src/components/atoms/Button/styles.scss");
|
|
7
|
+
@include meta.load-css("~@faststore/ui/src/components/molecules/SearchInputField/styles.scss");
|
|
8
|
+
@include meta.load-css("~@faststore/ui/src/components/organisms/EmptyState/styles.scss");
|
|
9
|
+
|
|
10
|
+
// --------------------------------------------------------
|
|
11
|
+
// Design Tokens
|
|
12
|
+
// --------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
--fs-page-height: 100%;
|
|
15
|
+
--fs-quotes-list-border-color: var(--fs-border-color-light);
|
|
16
|
+
--fs-quotes-list-row-hover-bg: var(--fs-color-neutral-1);
|
|
17
|
+
--fs-quotes-list-id-size: var(--fs-text-size-base);
|
|
18
|
+
--fs-quotes-list-label-size: var(--fs-text-size-2);
|
|
19
|
+
--fs-quotes-list-label-color: var(--fs-color-neutral-6);
|
|
20
|
+
--fs-quotes-list-date-label-size: var(--fs-text-size-tiny);
|
|
21
|
+
--fs-quotes-list-date-label-color: var(--fs-color-neutral-6);
|
|
22
|
+
--fs-quotes-list-date-value-size: var(--fs-text-size-2);
|
|
23
|
+
--fs-quotes-list-relative-expiry-color: var(--fs-color-neutral-5);
|
|
24
|
+
--fs-quotes-list-total-size: var(--fs-text-size-2);
|
|
25
|
+
--fs-quotes-list-total-color: var(--fs-color-neutral-6);
|
|
26
|
+
--fs-quotes-list-filter-margin-bottom: var(--fs-spacing-3);
|
|
27
|
+
|
|
28
|
+
height: var(--fs-page-height);
|
|
29
|
+
|
|
30
|
+
// --------------------------------------------------------
|
|
31
|
+
// Controls bar (filter button + pagination)
|
|
32
|
+
// --------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
[data-fs-list-quotes-controls] {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
margin-bottom: var(--fs-quotes-list-filter-margin-bottom);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-fs-list-quotes-search-filters] {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex: 1;
|
|
44
|
+
gap: var(--fs-spacing-3);
|
|
45
|
+
align-items: center;
|
|
46
|
+
max-width: 55%;
|
|
47
|
+
|
|
48
|
+
[data-fs-search-input-field-wrapper] {
|
|
49
|
+
flex: 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-fs-filter-count] {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
width: 1.25rem;
|
|
58
|
+
height: 1.25rem;
|
|
59
|
+
margin-left: var(--fs-spacing-1);
|
|
60
|
+
font-size: var(--fs-text-size-tiny);
|
|
61
|
+
font-weight: var(--fs-text-weight-bold);
|
|
62
|
+
line-height: 1;
|
|
63
|
+
color: var(--fs-color-primary-text);
|
|
64
|
+
background-color: var(--fs-color-primary-bkg);
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --------------------------------------------------------
|
|
69
|
+
// Quotes list
|
|
70
|
+
// --------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
[data-fs-quotes-list] {
|
|
73
|
+
width: 100%;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
[data-fs-quotes-list-row] {
|
|
77
|
+
display: flex;
|
|
78
|
+
gap: var(--fs-spacing-4);
|
|
79
|
+
align-items: flex-start;
|
|
80
|
+
padding: var(--fs-spacing-4) 0;
|
|
81
|
+
border-bottom: 1px solid var(--fs-quotes-list-border-color);
|
|
82
|
+
transition: background-color 0.2s ease;
|
|
83
|
+
|
|
84
|
+
&:last-child {
|
|
85
|
+
border-bottom: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&:hover {
|
|
89
|
+
background-color: var(--fs-quotes-list-row-hover-bg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Left column: ID + label
|
|
94
|
+
[data-fs-quotes-list-col-main] {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex: 1 1 35%;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
gap: var(--fs-spacing-1);
|
|
99
|
+
min-width: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-fs-quotes-list-id] {
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
font-size: var(--fs-quotes-list-id-size);
|
|
105
|
+
font-weight: var(--fs-text-weight-bold);
|
|
106
|
+
text-overflow: ellipsis;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[data-fs-quotes-list-label] {
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
font-size: var(--fs-quotes-list-label-size);
|
|
113
|
+
color: var(--fs-quotes-list-label-color);
|
|
114
|
+
text-overflow: ellipsis;
|
|
115
|
+
white-space: nowrap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Middle column: created by + dates
|
|
119
|
+
[data-fs-quotes-list-col-dates] {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex: 1 1 40%;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
gap: var(--fs-spacing-2);
|
|
124
|
+
min-width: 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[data-fs-quotes-list-date-group] {
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
gap: var(--fs-spacing-0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[data-fs-quotes-list-date-label] {
|
|
134
|
+
font-size: var(--fs-quotes-list-date-label-size);
|
|
135
|
+
font-weight: var(--fs-text-weight-medium);
|
|
136
|
+
color: var(--fs-quotes-list-date-label-color);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
[data-fs-quotes-list-date-value] {
|
|
140
|
+
font-size: var(--fs-quotes-list-date-value-size);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[data-fs-quotes-list-relative-expiry] {
|
|
144
|
+
color: var(--fs-quotes-list-relative-expiry-color);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Right column: badge + total
|
|
148
|
+
[data-fs-quotes-list-col-status] {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex: 0 0 auto;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: var(--fs-spacing-2);
|
|
153
|
+
align-items: flex-end;
|
|
154
|
+
min-width: 120px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[data-fs-quotes-list-total] {
|
|
158
|
+
font-size: var(--fs-quotes-list-total-size);
|
|
159
|
+
color: var(--fs-quotes-list-total-color);
|
|
160
|
+
text-align: right;
|
|
161
|
+
|
|
162
|
+
strong {
|
|
163
|
+
font-weight: var(--fs-text-weight-bold);
|
|
164
|
+
color: var(--fs-color-text);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --------------------------------------------------------
|
|
169
|
+
// Pagination + selected tags (reuse from orders)
|
|
170
|
+
// --------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
@include meta.load-css("../../orders/ListOrders/ListOrdersTable/styles.scss");
|
|
173
|
+
@include meta.load-css("../../orders/ListOrders/SelectedTags/styles.scss");
|
|
174
|
+
}
|