@autobusal/account-transactions 1.3.0 → 1.3.1
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/Browse/Admin.tsx +81 -3
- package/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/styles.ts +45 -0
- package/utilities.ts +40 -1
package/Browse/Admin.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { Meta, Table, Row } from '@autobusal/common';
|
|
|
4
4
|
import { usePage, useBreadcrumbs } from '@autobusal/hooks';
|
|
5
5
|
import { getParams } from '@autobusal/utilities';
|
|
6
6
|
import Information from '../Information';
|
|
7
|
+
import { Filters, Filter, Since } from '../styles';
|
|
8
|
+
import { getStatuses, getTypes, fromApiDate, toApiDate } from '../utilities';
|
|
7
9
|
import { useGetTransactions } from '../services';
|
|
8
10
|
|
|
9
11
|
interface Props {
|
|
@@ -12,13 +14,44 @@ interface Props {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const Admin = ({ url, t }: Props): JSX.Element => {
|
|
15
|
-
const [ searchParams ] = useSearchParams();
|
|
17
|
+
const [ searchParams, setSearchParams ] = useSearchParams();
|
|
16
18
|
|
|
17
19
|
usePage('transactions');
|
|
18
20
|
|
|
19
21
|
useBreadcrumbs(t('transactions.browse.title', { ns: 'common' }));
|
|
20
22
|
|
|
21
|
-
const { data, isLoading, isFetching } = useGetTransactions(
|
|
23
|
+
const { data, isLoading, isFetching } = useGetTransactions(
|
|
24
|
+
getParams(searchParams, ['page', 'sort', 'order', 'q', 'id', 'type', 'status', 'from', 'to'])
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Claude - 2026-08-29 (audit A7-41, low)
|
|
29
|
+
*
|
|
30
|
+
* TYPE, STATUS AND A DATE RANGE. The ledger had one search box and a user
|
|
31
|
+
* filter, so "what did we take in fees last month" meant reading every row
|
|
32
|
+
* of the period - and `?type=` was already reaching the API and being
|
|
33
|
+
* ignored there, which is the worse half: the list came back unfiltered
|
|
34
|
+
* and looked like an answer.
|
|
35
|
+
*
|
|
36
|
+
* Written into the query string rather than held in state, so a filtered
|
|
37
|
+
* view is a URL that can be bookmarked or sent to whoever asked the
|
|
38
|
+
* question - the same way the search box and the user filter already work.
|
|
39
|
+
* Any change drops `page`: page 4 of the old filter is rarely page 4 of
|
|
40
|
+
* the new one and is usually past the end of it.
|
|
41
|
+
*/
|
|
42
|
+
const onFilter = (name: string, value: string): void => {
|
|
43
|
+
const next = new URLSearchParams(searchParams);
|
|
44
|
+
|
|
45
|
+
if (value) {
|
|
46
|
+
next.set(name, value);
|
|
47
|
+
} else {
|
|
48
|
+
next.delete(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
next.delete('page');
|
|
52
|
+
|
|
53
|
+
setSearchParams(next);
|
|
54
|
+
};
|
|
22
55
|
|
|
23
56
|
const rows = data?.data.map(item => (
|
|
24
57
|
<Row
|
|
@@ -87,8 +120,53 @@ const Admin = ({ url, t }: Props): JSX.Element => {
|
|
|
87
120
|
fetching={ isFetching }
|
|
88
121
|
t={ t }
|
|
89
122
|
actions={ [
|
|
90
|
-
|
|
123
|
+
// `extra` is opt-in on this list, like every other action - Top
|
|
124
|
+
// renders the slot only when it is named (audit A7-41)
|
|
125
|
+
'extra', 'search', 'update'
|
|
91
126
|
] }
|
|
127
|
+
extra={
|
|
128
|
+
<Filters>
|
|
129
|
+
<Filter
|
|
130
|
+
value={ searchParams.get('type') ?? '' }
|
|
131
|
+
aria-label={ t('transactions.browse.type', { ns: 'common' }) }
|
|
132
|
+
onChange={ event => onFilter('type', event.target.value) }
|
|
133
|
+
>
|
|
134
|
+
<option value="">{ t('transactions.browse.all_types', { ns: 'common' }) }</option>
|
|
135
|
+
|
|
136
|
+
{ getTypes(t).map(item => (
|
|
137
|
+
<option key={ item.id } value={ String(item.id) }>{ item.name }</option>
|
|
138
|
+
)) }
|
|
139
|
+
</Filter>
|
|
140
|
+
|
|
141
|
+
<Filter
|
|
142
|
+
value={ searchParams.get('status') ?? '' }
|
|
143
|
+
aria-label={ t('transactions.browse.status', { ns: 'common' }) }
|
|
144
|
+
onChange={ event => onFilter('status', event.target.value) }
|
|
145
|
+
>
|
|
146
|
+
<option value="">{ t('transactions.browse.all_statuses', { ns: 'common' }) }</option>
|
|
147
|
+
|
|
148
|
+
{ getStatuses(t).map(item => (
|
|
149
|
+
<option key={ item.id } value={ String(item.id) }>{ item.name }</option>
|
|
150
|
+
)) }
|
|
151
|
+
</Filter>
|
|
152
|
+
|
|
153
|
+
<Since
|
|
154
|
+
type="date"
|
|
155
|
+
value={ fromApiDate(searchParams.get('from')) }
|
|
156
|
+
aria-label={ t('transactions.browse.from_date', { ns: 'common' }) }
|
|
157
|
+
title={ t('transactions.browse.from_date', { ns: 'common' }) }
|
|
158
|
+
onChange={ event => onFilter('from', toApiDate(event.target.value)) }
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
<Since
|
|
162
|
+
type="date"
|
|
163
|
+
value={ fromApiDate(searchParams.get('to')) }
|
|
164
|
+
aria-label={ t('transactions.browse.to_date', { ns: 'common' }) }
|
|
165
|
+
title={ t('transactions.browse.to_date', { ns: 'common' }) }
|
|
166
|
+
onChange={ event => onFilter('to', toApiDate(event.target.value)) }
|
|
167
|
+
/>
|
|
168
|
+
</Filters>
|
|
169
|
+
}
|
|
92
170
|
/>
|
|
93
171
|
</Meta>
|
|
94
172
|
);
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.1 - 2026-08-29
|
|
4
|
+
|
|
5
|
+
- The ledger gains TYPE, STATUS and a from/to DATE RANGE, written into the URL so a filtered view can be bookmarked or sent on (audit A7-41). `?type=` was already being sent by this screen and ignored by the API, which is worse than absent - the list came back unfiltered and looked like an answer.
|
|
6
|
+
|
|
3
7
|
## 1.3.0 (2026-08-29)
|
|
4
8
|
|
|
5
9
|
- Editors offer Save & Close alongside Save & Stay, so correcting a record no longer bounces you back to its list every time (opt-in per page, editing only).
|
package/package.json
CHANGED
package/styles.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The ledger's filter strip.
|
|
5
|
+
*
|
|
6
|
+
* Claude - 2026-08-29 (audit A7-41, low). The transactions list had a
|
|
7
|
+
* free-text box and a user filter and nothing else, so answering "what did
|
|
8
|
+
* we take in fees last month" meant reading 275 rows - and `?type=` was
|
|
9
|
+
* already being sent and silently ignored by the API, which is worse than
|
|
10
|
+
* absent: the list came back unfiltered and looked like an answer.
|
|
11
|
+
*
|
|
12
|
+
* Same pill-and-select treatment the review queue's filter row uses, so a
|
|
13
|
+
* filter row looks like a filter row wherever it is met in the admin. Wraps
|
|
14
|
+
* on a narrow screen rather than pushing the search box off the end.
|
|
15
|
+
*/
|
|
16
|
+
export const Filters = styled.div`
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-wrap: wrap;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
align-items: center;
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export const Filter = styled.select`
|
|
24
|
+
padding: 4px 12px;
|
|
25
|
+
border: none;
|
|
26
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
27
|
+
background: ${ props => props.theme.background.neutral };
|
|
28
|
+
color: ${ props => props.theme.font.normal };
|
|
29
|
+
font-size: ${ props => props.theme.size.s };
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A date bound. Native `date`, deliberately: it is one field, every browser
|
|
35
|
+
* and every phone already knows how to present it, and the alternative on a
|
|
36
|
+
* filter strip is a calendar popover competing with the table underneath.
|
|
37
|
+
*/
|
|
38
|
+
export const Since = styled.input`
|
|
39
|
+
padding: 3px 10px;
|
|
40
|
+
border: none;
|
|
41
|
+
border-radius: ${ props => props.theme.borderRadius };
|
|
42
|
+
background: ${ props => props.theme.background.neutral };
|
|
43
|
+
color: ${ props => props.theme.font.normal };
|
|
44
|
+
font-size: ${ props => props.theme.size.s };
|
|
45
|
+
`;
|
package/utilities.ts
CHANGED
|
@@ -17,4 +17,43 @@ export const getStatuses = (t: TFunction<'common'>): DropdownData[] => ([
|
|
|
17
17
|
name: t('data.transaction_status.credit', { ns: 'common' }),
|
|
18
18
|
id: 'credit'
|
|
19
19
|
}
|
|
20
|
-
]);
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Every kind of movement the ledger records, for the type filter.
|
|
24
|
+
*
|
|
25
|
+
* Claude - 2026-08-29 (audit A7-41, low). The same list obtapi validates
|
|
26
|
+
* against (Transactions\AccountController::TYPES) - written out rather than
|
|
27
|
+
* fetched, because it is a fixed vocabulary and an endpoint serving ten
|
|
28
|
+
* constants would be one more thing to keep in step. Ordered by how often
|
|
29
|
+
* somebody reconciling a period reaches for them, not alphabetically.
|
|
30
|
+
*/
|
|
31
|
+
export const TRANSACTION_TYPES = [
|
|
32
|
+
'purchase', 'refund', 'fee', 'deposit', 'withdraw',
|
|
33
|
+
'payout', 'payout_reversal', 'subscription', 'external_sale', 'external_refund'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const getTypes = (t: TFunction<'common'>): DropdownData[] => (
|
|
37
|
+
TRANSACTION_TYPES.map(id => ({
|
|
38
|
+
name: t(`data.transaction_types.${ id }`, { ns: 'common' }),
|
|
39
|
+
id
|
|
40
|
+
}))
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `d/m/Y` - what this API filters by - from the `Y-m-d` a native date input
|
|
45
|
+
* gives, and back again. Two formats because the wire format is the one the
|
|
46
|
+
* rest of the API already speaks and the input's is fixed by the platform.
|
|
47
|
+
* An empty value stays empty rather than becoming a date.
|
|
48
|
+
*/
|
|
49
|
+
export const toApiDate = (value: string): string => {
|
|
50
|
+
const parts = value.split('-');
|
|
51
|
+
|
|
52
|
+
return parts.length === 3 ? `${ parts[2] }/${ parts[1] }/${ parts[0] }` : '';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const fromApiDate = (value: string | null): string => {
|
|
56
|
+
const parts = (value ?? '').split('/');
|
|
57
|
+
|
|
58
|
+
return parts.length === 3 ? `${ parts[2] }-${ parts[1] }-${ parts[0] }` : '';
|
|
59
|
+
};
|