@autobusal/admin-api-consumers 1.0.0 → 1.2.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/Browse.tsx +8 -3
- package/CHANGELOG.md +28 -0
- package/Manage.tsx +10 -0
- package/Receivables.tsx +96 -0
- package/ReceivablesManage.tsx +207 -0
- package/index.ts +5 -1
- package/package.json +1 -1
- package/services.ts +99 -1
- package/styles.ts +42 -0
- package/types.ts +49 -0
package/Browse.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TFunction } from 'i18next';
|
|
2
|
-
import { useSearchParams } from 'react-router-dom';
|
|
3
|
-
import { Meta, Table, Row } from '@autobusal/common';
|
|
2
|
+
import { Link, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { Meta, Table, Row, Button } from '@autobusal/common';
|
|
4
4
|
import { usePage, useBreadcrumbs } from '@autobusal/hooks';
|
|
5
5
|
import { DisplayStatus, getParams, success } from '@autobusal/utilities';
|
|
6
6
|
import { useGetApiConsumers, useDeleteApiConsumer } from './services';
|
|
@@ -80,8 +80,13 @@ const Browse = ({ url, t }: Props): JSX.Element => {
|
|
|
80
80
|
fetching={ isFetching }
|
|
81
81
|
t={ t }
|
|
82
82
|
actions={ [
|
|
83
|
-
'create', 'search', 'view', 'delete'
|
|
83
|
+
'create', 'search', 'extra', 'view', 'delete'
|
|
84
84
|
] }
|
|
85
|
+
extra={
|
|
86
|
+
<Link to={ `${ url }/receivables` }>
|
|
87
|
+
<Button type="button" size="medium" subtype="secondary" noMargin text={ t('admin_api_consumers.receivables.title', { ns: 'common' }) } />
|
|
88
|
+
</Link>
|
|
89
|
+
}
|
|
85
90
|
handlers={ {
|
|
86
91
|
delete: onDelete
|
|
87
92
|
} }
|
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Credit limit on the partner form, running balance on receivables.** A
|
|
8
|
+
Mode 1 partner now sells against a running account (obtapi Payments\
|
|
9
|
+
External\Charge): every sale debits the net they owe, cancellations
|
|
10
|
+
credit it back, and recorded settlements raise it. `credit_limit` is the
|
|
11
|
+
floor - 0 means prepaid (deposit before selling), a positive number means
|
|
12
|
+
postpaid down to -that-much. The receivables table shows the live
|
|
13
|
+
`balance` and `headroom` alongside the statement figures, deliberately
|
|
14
|
+
from two books: when they disagree, something needs investigating.
|
|
15
|
+
|
|
3
16
|
All notable changes to `@autobusal/admin-api-consumers` are documented here.
|
|
4
17
|
This project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
18
|
and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
19
|
|
|
20
|
+
## [1.1.0] - 2026-07-27
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Package 3 of the external-routes plan: **receivables reports + settlements**.
|
|
25
|
+
`Receivables.tsx` is a per-partner dashboard for a selectable period
|
|
26
|
+
(orders/tickets/gross amount/commission/net/settled/outstanding, the
|
|
27
|
+
last two accumulated all-time regardless of the period filter).
|
|
28
|
+
`ReceivablesManage.tsx` is the per-partner drill-down (ticket-level order
|
|
29
|
+
rows, doubling as the CSV/dispute document via a plain browser-download
|
|
30
|
+
link to the backend export endpoint - no frontend CSV generation, same
|
|
31
|
+
convention as `@autobusal/account-reports`), plus a settlements section
|
|
32
|
+
to record/list/delete actual payments received. Backed by obtapi's
|
|
33
|
+
`App\Http\Controllers\ApiConsumers\ReportsController`.
|
|
34
|
+
|
|
7
35
|
## [1.0.0] - 2026-07-27
|
|
8
36
|
|
|
9
37
|
### Added
|
package/Manage.tsx
CHANGED
|
@@ -131,6 +131,16 @@ const Manage = ({ url, t }: Props): JSX.Element => {
|
|
|
131
131
|
type: 'select',
|
|
132
132
|
value: data?.user.comissions?.unique ?? 0,
|
|
133
133
|
values: getAllowed(t)
|
|
134
|
+
}, {
|
|
135
|
+
// Claude - 2026-08-22: the floor of the partner's running
|
|
136
|
+
// account. 0 = prepaid (they deposit before they can sell);
|
|
137
|
+
// a positive number = postpaid down to -that-much. "Unlimited"
|
|
138
|
+
// is a big number typed on purpose, never a switch.
|
|
139
|
+
label: t('admin_api_consumers.manage.credit_limit', { ns: 'common' }),
|
|
140
|
+
name: 'credit_limit',
|
|
141
|
+
type: 'text',
|
|
142
|
+
value: data?.credit_limit ?? 0,
|
|
143
|
+
rules: 'required|numeric'
|
|
134
144
|
}, {
|
|
135
145
|
label: t('admin_api_consumers.manage.status', { ns: 'common' }),
|
|
136
146
|
name: 'status',
|
package/Receivables.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { Meta, Box } from '@autobusal/common';
|
|
5
|
+
import { usePage, useBreadcrumbs } from '@autobusal/hooks';
|
|
6
|
+
import { Table, Filters } from './styles';
|
|
7
|
+
import { useGetReceivables } from './services';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
url: string
|
|
11
|
+
t: TFunction<'common'>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Receivables = ({ url, t }: Props): JSX.Element => {
|
|
15
|
+
usePage('api_consumers');
|
|
16
|
+
|
|
17
|
+
useBreadcrumbs(t('admin_api_consumers.receivables.title', { ns: 'common' }), [{ name: t('admin_api_consumers.browse.title', { ns: 'common' }), url }]);
|
|
18
|
+
|
|
19
|
+
const [ from, setFrom ] = useState<string>('');
|
|
20
|
+
const [ to, setTo ] = useState<string>('');
|
|
21
|
+
|
|
22
|
+
const { data, isLoading } = useGetReceivables({ from: from || undefined, to: to || undefined });
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Meta title={ t('admin_api_consumers.receivables.title', { ns: 'common' }) }>
|
|
26
|
+
<h1>{ t('admin_api_consumers.receivables.title', { ns: 'common' }) }</h1>
|
|
27
|
+
|
|
28
|
+
<Filters>
|
|
29
|
+
<div>
|
|
30
|
+
<label>{ t('admin_api_consumers.receivables.from', { ns: 'common' }) }</label>
|
|
31
|
+
<input type="date" value={ from } onChange={ (e) => setFrom(e.target.value) } />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div>
|
|
35
|
+
<label>{ t('admin_api_consumers.receivables.to', { ns: 'common' }) }</label>
|
|
36
|
+
<input type="date" value={ to } onChange={ (e) => setTo(e.target.value) } />
|
|
37
|
+
</div>
|
|
38
|
+
</Filters>
|
|
39
|
+
|
|
40
|
+
{ isLoading ? <Box /> : (
|
|
41
|
+
<div className="box">
|
|
42
|
+
<Table>
|
|
43
|
+
<thead>
|
|
44
|
+
<tr>
|
|
45
|
+
<th>{ t('admin_api_consumers.receivables.company', { ns: 'common' }) }</th>
|
|
46
|
+
<th>{ t('admin_api_consumers.receivables.orders', { ns: 'common' }) }</th>
|
|
47
|
+
<th>{ t('admin_api_consumers.receivables.tickets', { ns: 'common' }) }</th>
|
|
48
|
+
<th>{ t('admin_api_consumers.receivables.amount', { ns: 'common' }) }</th>
|
|
49
|
+
<th>{ t('admin_api_consumers.receivables.commission', { ns: 'common' }) }</th>
|
|
50
|
+
<th>{ t('admin_api_consumers.receivables.net', { ns: 'common' }) }</th>
|
|
51
|
+
<th>{ t('admin_api_consumers.receivables.settled', { ns: 'common' }) }</th>
|
|
52
|
+
<th>{ t('admin_api_consumers.receivables.outstanding', { ns: 'common' }) }</th>
|
|
53
|
+
<th>{ t('admin_api_consumers.receivables.balance', { ns: 'common' }) }</th>
|
|
54
|
+
<th>{ t('admin_api_consumers.receivables.headroom', { ns: 'common' }) }</th>
|
|
55
|
+
<th />
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
|
|
59
|
+
<tbody>
|
|
60
|
+
{ data?.map(row => (
|
|
61
|
+
<tr key={ row.agent_id }>
|
|
62
|
+
<td>{ row.company }</td>
|
|
63
|
+
<td>{ row.orders }</td>
|
|
64
|
+
<td>{ row.tickets }</td>
|
|
65
|
+
<td>{ row.amount.toFixed(2) }</td>
|
|
66
|
+
<td>{ row.commission.toFixed(2) }</td>
|
|
67
|
+
<td>{ row.net.toFixed(2) }</td>
|
|
68
|
+
<td>{ row.settled.toFixed(2) }</td>
|
|
69
|
+
<td>{ row.outstanding.toFixed(2) }</td>
|
|
70
|
+
{ /* the running account: negative is what they owe right
|
|
71
|
+
now; headroom is what they can still sell before
|
|
72
|
+
their credit floor refuses them */ }
|
|
73
|
+
<td>{ row.balance.toFixed(2) }</td>
|
|
74
|
+
<td>{ row.headroom.toFixed(2) }</td>
|
|
75
|
+
<td>
|
|
76
|
+
<Link to={ `${ url }/receivables/${ row.agent_id }?from=${ from }&to=${ to }` }>
|
|
77
|
+
{ t('admin_api_consumers.receivables.view', { ns: 'common' }) }
|
|
78
|
+
</Link>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
)) }
|
|
82
|
+
|
|
83
|
+
{ data?.length === 0 && (
|
|
84
|
+
<tr>
|
|
85
|
+
<td colSpan={ 9 }>{ t('admin_api_consumers.receivables.empty', { ns: 'common' }) }</td>
|
|
86
|
+
</tr>
|
|
87
|
+
) }
|
|
88
|
+
</tbody>
|
|
89
|
+
</Table>
|
|
90
|
+
</div>
|
|
91
|
+
) }
|
|
92
|
+
</Meta>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default Receivables;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { TFunction } from 'i18next';
|
|
3
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
4
|
+
import { Meta, BackWithTitle, Button, Box } from '@autobusal/common';
|
|
5
|
+
import { usePage, useBreadcrumbs } from '@autobusal/hooks';
|
|
6
|
+
import { success } from '@autobusal/utilities';
|
|
7
|
+
import { Table, SectionTitle, Filters } from './styles';
|
|
8
|
+
import { useGetReceivableDrilldown, downloadReceivablesExport, useGetSettlements, useStoreSettlement, useDeleteSettlement } from './services';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
url: string
|
|
12
|
+
t: TFunction<'common'>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ReceivablesManage = ({ url, t }: Props): JSX.Element => {
|
|
16
|
+
const params = useParams();
|
|
17
|
+
const [ searchParams ] = useSearchParams();
|
|
18
|
+
|
|
19
|
+
const agentId = Number(params.agent_id);
|
|
20
|
+
const from = searchParams.get('from') || undefined;
|
|
21
|
+
const to = searchParams.get('to') || undefined;
|
|
22
|
+
|
|
23
|
+
usePage('api_consumers');
|
|
24
|
+
|
|
25
|
+
useBreadcrumbs(t('admin_api_consumers.receivables.drilldown_title', { ns: 'common' }), [
|
|
26
|
+
{ name: t('admin_api_consumers.browse.title', { ns: 'common' }), url },
|
|
27
|
+
{ name: t('admin_api_consumers.receivables.title', { ns: 'common' }), url: `${ url }/receivables` }
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const { data, isLoading } = useGetReceivableDrilldown({ agent_id: agentId, from, to });
|
|
31
|
+
const { data: settlements, isLoading: isLoadingSettlements, refetch: refetchSettlements } = useGetSettlements(agentId);
|
|
32
|
+
|
|
33
|
+
const { mutate: StoreSettlement, isPending } = useStoreSettlement();
|
|
34
|
+
const { mutate: DeleteSettlement } = useDeleteSettlement();
|
|
35
|
+
|
|
36
|
+
const [ isExporting, setIsExporting ] = useState<boolean>(false);
|
|
37
|
+
|
|
38
|
+
const onExport = (): void => {
|
|
39
|
+
setIsExporting(true);
|
|
40
|
+
|
|
41
|
+
downloadReceivablesExport({ agent_id: agentId, from, to })
|
|
42
|
+
.finally(() => setIsExporting(false));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const [ period, setPeriod ] = useState<string>('');
|
|
46
|
+
const [ amountReceived, setAmountReceived ] = useState<string>('');
|
|
47
|
+
const [ receivedAt, setReceivedAt ] = useState<string>('');
|
|
48
|
+
const [ note, setNote ] = useState<string>('');
|
|
49
|
+
|
|
50
|
+
const onRecordSettlement = (): void => {
|
|
51
|
+
StoreSettlement({
|
|
52
|
+
agent_id: agentId,
|
|
53
|
+
period: period ? `${ period }-01` : '',
|
|
54
|
+
amount_received: amountReceived,
|
|
55
|
+
received_at: receivedAt,
|
|
56
|
+
note
|
|
57
|
+
}, {
|
|
58
|
+
onSuccess: () => {
|
|
59
|
+
success(t('admin_api_consumers.receivables.settlement_saved', { ns: 'common' }));
|
|
60
|
+
|
|
61
|
+
setPeriod('');
|
|
62
|
+
setAmountReceived('');
|
|
63
|
+
setReceivedAt('');
|
|
64
|
+
setNote('');
|
|
65
|
+
|
|
66
|
+
refetchSettlements();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onDeleteSettlement = (id: number): void => {
|
|
72
|
+
if (!window.confirm(t('table.confirm', { ns: 'common' }))) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
DeleteSettlement(id, {
|
|
77
|
+
onSuccess: () => refetchSettlements()
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Meta title={ t('admin_api_consumers.receivables.drilldown_title', { ns: 'common' }) }>
|
|
83
|
+
<BackWithTitle title={ t('admin_api_consumers.receivables.drilldown_title', { ns: 'common' }) } to={ `${ url }/receivables` } t={ t } />
|
|
84
|
+
|
|
85
|
+
<Filters>
|
|
86
|
+
<Button
|
|
87
|
+
type="button"
|
|
88
|
+
size="medium"
|
|
89
|
+
subtype="secondary"
|
|
90
|
+
loading={ isExporting }
|
|
91
|
+
text={ t('admin_api_consumers.receivables.export_csv', { ns: 'common' }) }
|
|
92
|
+
onClick={ onExport }
|
|
93
|
+
/>
|
|
94
|
+
</Filters>
|
|
95
|
+
|
|
96
|
+
{ isLoading ? <Box /> : (
|
|
97
|
+
<div className="box">
|
|
98
|
+
<Table>
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
<th>{ t('admin_api_consumers.receivables.order', { ns: 'common' }) }</th>
|
|
102
|
+
<th>{ t('admin_api_consumers.receivables.sale_date', { ns: 'common' }) }</th>
|
|
103
|
+
<th>{ t('admin_api_consumers.receivables.route', { ns: 'common' }) }</th>
|
|
104
|
+
<th>{ t('admin_api_consumers.receivables.travel_date', { ns: 'common' }) }</th>
|
|
105
|
+
<th>{ t('admin_api_consumers.receivables.amount', { ns: 'common' }) }</th>
|
|
106
|
+
<th>{ t('admin_api_consumers.receivables.commission', { ns: 'common' }) }</th>
|
|
107
|
+
<th>{ t('admin_api_consumers.receivables.net', { ns: 'common' }) }</th>
|
|
108
|
+
</tr>
|
|
109
|
+
</thead>
|
|
110
|
+
|
|
111
|
+
<tbody>
|
|
112
|
+
{ data?.data.map(order => (
|
|
113
|
+
<tr key={ order.id }>
|
|
114
|
+
<td>{ order.hash }</td>
|
|
115
|
+
<td>{ order.created_at }</td>
|
|
116
|
+
<td>{ order.route?.name ?? '-' }</td>
|
|
117
|
+
<td>{ order.date_travel ?? '-' }</td>
|
|
118
|
+
<td>{ order.amount }</td>
|
|
119
|
+
<td>{ order.comission_agent }</td>
|
|
120
|
+
<td>{ order.net }</td>
|
|
121
|
+
</tr>
|
|
122
|
+
)) }
|
|
123
|
+
|
|
124
|
+
{ data?.data.length === 0 && (
|
|
125
|
+
<tr>
|
|
126
|
+
<td colSpan={ 7 }>{ t('admin_api_consumers.receivables.empty', { ns: 'common' }) }</td>
|
|
127
|
+
</tr>
|
|
128
|
+
) }
|
|
129
|
+
</tbody>
|
|
130
|
+
</Table>
|
|
131
|
+
</div>
|
|
132
|
+
) }
|
|
133
|
+
|
|
134
|
+
<SectionTitle>{ t('admin_api_consumers.receivables.settlements', { ns: 'common' }) }</SectionTitle>
|
|
135
|
+
|
|
136
|
+
{ isLoadingSettlements ? <Box /> : (
|
|
137
|
+
<div className="box">
|
|
138
|
+
<Table>
|
|
139
|
+
<thead>
|
|
140
|
+
<tr>
|
|
141
|
+
<th>{ t('admin_api_consumers.receivables.period', { ns: 'common' }) }</th>
|
|
142
|
+
<th>{ t('admin_api_consumers.receivables.amount_received', { ns: 'common' }) }</th>
|
|
143
|
+
<th>{ t('admin_api_consumers.receivables.received_at', { ns: 'common' }) }</th>
|
|
144
|
+
<th>{ t('admin_api_consumers.receivables.note', { ns: 'common' }) }</th>
|
|
145
|
+
<th />
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
|
|
149
|
+
<tbody>
|
|
150
|
+
{ settlements?.map(item => (
|
|
151
|
+
<tr key={ item.id }>
|
|
152
|
+
<td>{ item.period.slice(0, 7) }</td>
|
|
153
|
+
<td>{ item.amount_received }</td>
|
|
154
|
+
<td>{ item.received_at.slice(0, 10) }</td>
|
|
155
|
+
<td>{ item.note ?? '-' }</td>
|
|
156
|
+
<td>
|
|
157
|
+
<button type="button" onClick={ () => onDeleteSettlement(item.id) }>{ t('table.actions.delete', { ns: 'common' }) }</button>
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
)) }
|
|
161
|
+
|
|
162
|
+
{ settlements?.length === 0 && (
|
|
163
|
+
<tr>
|
|
164
|
+
<td colSpan={ 5 }>{ t('admin_api_consumers.receivables.no_settlements', { ns: 'common' }) }</td>
|
|
165
|
+
</tr>
|
|
166
|
+
) }
|
|
167
|
+
</tbody>
|
|
168
|
+
</Table>
|
|
169
|
+
</div>
|
|
170
|
+
) }
|
|
171
|
+
|
|
172
|
+
<SectionTitle>{ t('admin_api_consumers.receivables.record_settlement', { ns: 'common' }) }</SectionTitle>
|
|
173
|
+
|
|
174
|
+
<Filters>
|
|
175
|
+
<div>
|
|
176
|
+
<label>{ t('admin_api_consumers.receivables.period', { ns: 'common' }) }</label>
|
|
177
|
+
<input type="month" value={ period } onChange={ (e) => setPeriod(e.target.value) } />
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div>
|
|
181
|
+
<label>{ t('admin_api_consumers.receivables.amount_received', { ns: 'common' }) }</label>
|
|
182
|
+
<input type="number" step="0.01" value={ amountReceived } onChange={ (e) => setAmountReceived(e.target.value) } />
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div>
|
|
186
|
+
<label>{ t('admin_api_consumers.receivables.received_at', { ns: 'common' }) }</label>
|
|
187
|
+
<input type="date" value={ receivedAt } onChange={ (e) => setReceivedAt(e.target.value) } />
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div>
|
|
191
|
+
<label>{ t('admin_api_consumers.receivables.note', { ns: 'common' }) }</label>
|
|
192
|
+
<input type="text" value={ note } onChange={ (e) => setNote(e.target.value) } />
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<Button
|
|
196
|
+
type="button"
|
|
197
|
+
size="medium"
|
|
198
|
+
loading={ isPending }
|
|
199
|
+
text={ t('admin_api_consumers.receivables.save_settlement', { ns: 'common' }) }
|
|
200
|
+
onClick={ onRecordSettlement }
|
|
201
|
+
/>
|
|
202
|
+
</Filters>
|
|
203
|
+
</Meta>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export default ReceivablesManage;
|
package/index.ts
CHANGED
package/package.json
CHANGED
package/services.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { apiClient } from '@autobusal/providers';
|
|
|
3
3
|
import { PaginationData } from '@autobusal/providers/types/pagination';
|
|
4
4
|
import { TableParamsData } from '@autobusal/providers/types/other';
|
|
5
5
|
import { DropdownData } from '@autobusal/providers/types/other';
|
|
6
|
-
import { ApiConsumerData } from './types';
|
|
6
|
+
import { ApiConsumerData, ReceivableData, ReceivableOrderData, SettlementData } from './types';
|
|
7
7
|
|
|
8
8
|
export const useGetApiConsumers = (params: TableParamsData): UseQueryResult<PaginationData<ApiConsumerData>> => (
|
|
9
9
|
useQuery({
|
|
@@ -144,3 +144,101 @@ export const useRegenerateApiConsumerToken = (): UseMutationResult<{ api_token:
|
|
|
144
144
|
)
|
|
145
145
|
})
|
|
146
146
|
);
|
|
147
|
+
|
|
148
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
149
|
+
// Package 3 - API partner receivables reports + settlements
|
|
150
|
+
|
|
151
|
+
export const useGetReceivables = (params: { from?: string, to?: string }): UseQueryResult<ReceivableData[]> => (
|
|
152
|
+
useQuery({
|
|
153
|
+
queryKey: ['admin-api-consumers-receivables', params],
|
|
154
|
+
queryFn: async () => (
|
|
155
|
+
await apiClient
|
|
156
|
+
.get('/api/api_consumers/admin/reports/receivables', { params })
|
|
157
|
+
.then(response => (
|
|
158
|
+
response.data
|
|
159
|
+
))
|
|
160
|
+
)
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
export const useGetReceivableDrilldown = (params: { agent_id: number, from?: string, to?: string, page?: number }): UseQueryResult<PaginationData<ReceivableOrderData>> => (
|
|
165
|
+
useQuery({
|
|
166
|
+
queryKey: ['admin-api-consumers-drilldown', params],
|
|
167
|
+
queryFn: async () => (
|
|
168
|
+
await apiClient
|
|
169
|
+
.get('/api/api_consumers/admin/reports/drilldown', { params })
|
|
170
|
+
.then(response => (
|
|
171
|
+
response.data
|
|
172
|
+
))
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
178
|
+
// A plain cross-origin `<a href>` straight to obtapi (the
|
|
179
|
+
// @autobusal/account-reports' Downloads.tsx convention this was first
|
|
180
|
+
// modeled on) does NOT authenticate here: a top-level browser navigation
|
|
181
|
+
// never carries the bearer-mode Authorization header (it lives in
|
|
182
|
+
// localStorage, JS-only), and confirmed live there's no separate session
|
|
183
|
+
// cookie backing it either - the export 401'd/500'd depending on whether
|
|
184
|
+
// the browser even sent a Referer. Downloading through `apiClient`
|
|
185
|
+
// instead reuses the SAME authenticated request path as every other call
|
|
186
|
+
// in the app (bearer header or BFF cookie, whichever the brand uses) and
|
|
187
|
+
// works regardless of auth mode.
|
|
188
|
+
export const downloadReceivablesExport = async (params: { agent_id: number, from?: string, to?: string }): Promise<void> => {
|
|
189
|
+
const response = await apiClient.get('/api/api_consumers/admin/reports/export', {
|
|
190
|
+
params,
|
|
191
|
+
responseType: 'blob'
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'text/csv' }));
|
|
195
|
+
const link = document.createElement('a');
|
|
196
|
+
|
|
197
|
+
link.href = url;
|
|
198
|
+
link.download = `receivables-${ params.agent_id }.csv`;
|
|
199
|
+
|
|
200
|
+
document.body.appendChild(link);
|
|
201
|
+
link.click();
|
|
202
|
+
document.body.removeChild(link);
|
|
203
|
+
|
|
204
|
+
window.URL.revokeObjectURL(url);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const useGetSettlements = (agentId: number): UseQueryResult<SettlementData[]> => (
|
|
208
|
+
useQuery({
|
|
209
|
+
queryKey: ['admin-api-consumers-settlements', { agentId }],
|
|
210
|
+
queryFn: async () => (
|
|
211
|
+
await apiClient
|
|
212
|
+
.get('/api/api_consumers/admin/reports/settlements', { params: { agent_id: agentId } })
|
|
213
|
+
.then(response => (
|
|
214
|
+
response.data
|
|
215
|
+
))
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
export const useStoreSettlement = (): UseMutationResult<void, Error, Record<string, any>, unknown> => (
|
|
221
|
+
useMutation({
|
|
222
|
+
mutationKey: ['admin-api-consumers-settlements-store'],
|
|
223
|
+
mutationFn: async (data: Record<string, any>) => (
|
|
224
|
+
await apiClient
|
|
225
|
+
.post('/api/api_consumers/admin/reports/settlements', data)
|
|
226
|
+
.then(response => (
|
|
227
|
+
response.data
|
|
228
|
+
))
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
export const useDeleteSettlement = (): UseMutationResult<void, Error, number, unknown> => (
|
|
234
|
+
useMutation({
|
|
235
|
+
mutationKey: ['admin-api-consumers-settlements-delete'],
|
|
236
|
+
mutationFn: async (id: number) => (
|
|
237
|
+
await apiClient
|
|
238
|
+
.delete('/api/api_consumers/admin/reports/settlements', { params: { id } })
|
|
239
|
+
.then(response => (
|
|
240
|
+
response.data
|
|
241
|
+
))
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
);
|
package/styles.ts
CHANGED
|
@@ -29,3 +29,45 @@ export const TokenActions = styled.div`
|
|
|
29
29
|
display: flex;
|
|
30
30
|
gap: 10px;
|
|
31
31
|
`;
|
|
32
|
+
|
|
33
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
34
|
+
// Package 3 - receivables/settlements tables + filters, matching
|
|
35
|
+
// @autobusal/admin-external's plain-table approach (this data isn't
|
|
36
|
+
// paginated the same way as the generic <Table>/<Row> components assume)
|
|
37
|
+
export const Table = styled.table`
|
|
38
|
+
width: 100%;
|
|
39
|
+
border-collapse: collapse;
|
|
40
|
+
|
|
41
|
+
th, td {
|
|
42
|
+
text-align: left;
|
|
43
|
+
padding: 10px 12px;
|
|
44
|
+
border-bottom: 1px solid ${ props => props.theme.primary.neutral };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
th {
|
|
48
|
+
font-size: ${ props => props.theme.size.sm };
|
|
49
|
+
color: ${ props => props.theme.font.faded };
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
export const Actions = styled.div`
|
|
54
|
+
display: flex;
|
|
55
|
+
gap: 10px;
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
export const SectionTitle = styled.h2`
|
|
59
|
+
margin: 25px 0 10px;
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export const Filters = styled.div`
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 15px;
|
|
65
|
+
align-items: flex-end;
|
|
66
|
+
flex-wrap: wrap;
|
|
67
|
+
margin-bottom: 20px;
|
|
68
|
+
|
|
69
|
+
input {
|
|
70
|
+
padding: 8px 10px;
|
|
71
|
+
border-radius: 6px;
|
|
72
|
+
}
|
|
73
|
+
`;
|
package/types.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface ApiConsumerData {
|
|
|
6
6
|
operators: (number | string)[]
|
|
7
7
|
external_providers: (number | string)[]
|
|
8
8
|
is_api_consumer: boolean
|
|
9
|
+
// Claude - 2026-08-22: the running account and its floor - see
|
|
10
|
+
// ReceivableData below for what they mean
|
|
11
|
+
funds?: number
|
|
12
|
+
credit_limit?: number
|
|
9
13
|
user: {
|
|
10
14
|
id: number
|
|
11
15
|
email: string
|
|
@@ -17,3 +21,48 @@ export interface ApiConsumerData {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
}
|
|
24
|
+
|
|
25
|
+
// Edited: Ferjolt Ozuni - Date: 2026-07-27
|
|
26
|
+
// Package 3 - API partner receivables reports + settlements
|
|
27
|
+
export interface ReceivableData {
|
|
28
|
+
agent_id: number
|
|
29
|
+
company: string
|
|
30
|
+
email: string
|
|
31
|
+
orders: number
|
|
32
|
+
tickets: number
|
|
33
|
+
amount: number
|
|
34
|
+
commission: number
|
|
35
|
+
net: number
|
|
36
|
+
settled: number
|
|
37
|
+
outstanding: number
|
|
38
|
+
// Claude - 2026-08-22: the LIVE running account (debited per sale,
|
|
39
|
+
// credited by settlements) and its floor - alongside the statement
|
|
40
|
+
// figures, which describe the same debt from the frozen columns. When
|
|
41
|
+
// balance and -outstanding disagree, something needs investigating.
|
|
42
|
+
balance: number
|
|
43
|
+
credit_limit: number
|
|
44
|
+
headroom: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ReceivableOrderData {
|
|
48
|
+
id: number
|
|
49
|
+
hash: string
|
|
50
|
+
tickets: number
|
|
51
|
+
date_travel: string
|
|
52
|
+
amount: number
|
|
53
|
+
comission_agent: number
|
|
54
|
+
net: number
|
|
55
|
+
created_at: string
|
|
56
|
+
route?: { id: number, name: string }
|
|
57
|
+
passengers?: { id: number, first_name: string, last_name: string, bus_seat?: number }[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SettlementData {
|
|
61
|
+
id: number
|
|
62
|
+
api_user_id: number
|
|
63
|
+
period: string
|
|
64
|
+
amount_received: string
|
|
65
|
+
received_at: string
|
|
66
|
+
note?: string
|
|
67
|
+
created_at: string
|
|
68
|
+
}
|