@autobusal/admin-api-consumers 1.0.0 → 1.1.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 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
@@ -4,6 +4,21 @@ All notable changes to `@autobusal/admin-api-consumers` are documented here.
4
4
  This project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.0] - 2026-07-27
8
+
9
+ ### Added
10
+
11
+ - Package 3 of the external-routes plan: **receivables reports + settlements**.
12
+ `Receivables.tsx` is a per-partner dashboard for a selectable period
13
+ (orders/tickets/gross amount/commission/net/settled/outstanding, the
14
+ last two accumulated all-time regardless of the period filter).
15
+ `ReceivablesManage.tsx` is the per-partner drill-down (ticket-level order
16
+ rows, doubling as the CSV/dispute document via a plain browser-download
17
+ link to the backend export endpoint - no frontend CSV generation, same
18
+ convention as `@autobusal/account-reports`), plus a settlements section
19
+ to record/list/delete actual payments received. Backed by obtapi's
20
+ `App\Http\Controllers\ApiConsumers\ReportsController`.
21
+
7
22
  ## [1.0.0] - 2026-07-27
8
23
 
9
24
  ### Added
@@ -0,0 +1,89 @@
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 />
54
+ </tr>
55
+ </thead>
56
+
57
+ <tbody>
58
+ { data?.map(row => (
59
+ <tr key={ row.agent_id }>
60
+ <td>{ row.company }</td>
61
+ <td>{ row.orders }</td>
62
+ <td>{ row.tickets }</td>
63
+ <td>{ row.amount.toFixed(2) }</td>
64
+ <td>{ row.commission.toFixed(2) }</td>
65
+ <td>{ row.net.toFixed(2) }</td>
66
+ <td>{ row.settled.toFixed(2) }</td>
67
+ <td>{ row.outstanding.toFixed(2) }</td>
68
+ <td>
69
+ <Link to={ `${ url }/receivables/${ row.agent_id }?from=${ from }&to=${ to }` }>
70
+ { t('admin_api_consumers.receivables.view', { ns: 'common' }) }
71
+ </Link>
72
+ </td>
73
+ </tr>
74
+ )) }
75
+
76
+ { data?.length === 0 && (
77
+ <tr>
78
+ <td colSpan={ 9 }>{ t('admin_api_consumers.receivables.empty', { ns: 'common' }) }</td>
79
+ </tr>
80
+ ) }
81
+ </tbody>
82
+ </Table>
83
+ </div>
84
+ ) }
85
+ </Meta>
86
+ );
87
+ };
88
+
89
+ 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
@@ -1,7 +1,11 @@
1
1
  import Browse from './Browse';
2
2
  import Manage from './Manage';
3
+ import Receivables from './Receivables';
4
+ import ReceivablesManage from './ReceivablesManage';
3
5
 
4
6
  export {
5
7
  Browse,
6
- Manage
8
+ Manage,
9
+ Receivables,
10
+ ReceivablesManage
7
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/admin-api-consumers",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
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
@@ -17,3 +17,41 @@ export interface ApiConsumerData {
17
17
  }
18
18
  }
19
19
  }
20
+
21
+ // Edited: Ferjolt Ozuni - Date: 2026-07-27
22
+ // Package 3 - API partner receivables reports + settlements
23
+ export interface ReceivableData {
24
+ agent_id: number
25
+ company: string
26
+ email: string
27
+ orders: number
28
+ tickets: number
29
+ amount: number
30
+ commission: number
31
+ net: number
32
+ settled: number
33
+ outstanding: number
34
+ }
35
+
36
+ export interface ReceivableOrderData {
37
+ id: number
38
+ hash: string
39
+ tickets: number
40
+ date_travel: string
41
+ amount: number
42
+ comission_agent: number
43
+ net: number
44
+ created_at: string
45
+ route?: { id: number, name: string }
46
+ passengers?: { id: number, first_name: string, last_name: string, bus_seat?: number }[]
47
+ }
48
+
49
+ export interface SettlementData {
50
+ id: number
51
+ api_user_id: number
52
+ period: string
53
+ amount_received: string
54
+ received_at: string
55
+ note?: string
56
+ created_at: string
57
+ }