@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 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(getParams(searchParams, ['page', 'sort', 'order', 'q', 'id']));
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
- 'search', 'update'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/account-transactions",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
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
+ };