@suflon/rnmd-reporting 0.0.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/App.tsx +69 -0
- package/README.md +97 -0
- package/app.json +4 -0
- package/babel.config.js +18 -0
- package/global.css +143 -0
- package/index.js +9 -0
- package/metro.config.js +209 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +106 -0
- package/patches/@react-navigation+stack+7.6.16.patch +11 -0
- package/patches/@suflon+native-ui+0.0.18.patch +26020 -0
- package/patches/react-native+0.83.1.patch +52 -0
- package/react-native.config.js +27 -0
- package/scripts/fix-suflon-native-ui.js +25 -0
- package/scripts/link-react-native-pnpm.js +42 -0
- package/src/config/index.ts +10 -0
- package/src/context/ConnectionI18nContext.tsx +61 -0
- package/src/modules/Reporting/component/README.md +3 -0
- package/src/modules/Reporting/component/ReportChart.tsx +882 -0
- package/src/modules/Reporting/component/ReportFilterModal.tsx +403 -0
- package/src/modules/Reporting/component/ReportingDetail.tsx +481 -0
- package/src/modules/Reporting/index.tsx +239 -0
- package/src/modules/Reporting/utils.tsx +14 -0
- package/src/navigation/index.tsx +31 -0
- package/src/screens/ConnectionListScreen.tsx +471 -0
- package/src/screens/DevToolsCorner.tsx +810 -0
- package/src/screens/NewConnectionModal.tsx +282 -0
- package/src/services/ApiService.ts +22 -0
- package/src/services/ConnectionService.ts +81 -0
- package/src/services/api.ts +83 -0
- package/src/stores/connection.store.ts +3 -0
- package/src/stores/language.store.ts +54 -0
- package/src/theme/colors.ts +56 -0
- package/src/types/connection.ts +69 -0
- package/src/utils/AsyncStorageUtils.ts +56 -0
- package/src/utils/connectionStrings.ts +158 -0
- package/src/utils/errorMessage.ts +11 -0
- package/tailwind.config.js +196 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
|
2
|
+
import { View, Text, ScrollView, Alert } from 'react-native';
|
|
3
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
4
|
+
import { useConnectionStore, Header, SearchFilter, Button, Loader, BottomModal, ModalTitle, Dropdown, ServerErrorWrapper, Toast, PermissionActions } from '@suflon/native-ui';
|
|
5
|
+
import type { ConnectionFilterParams, ConnectionStatus, Direction } from '@/types/connection';
|
|
6
|
+
import { useConnectionI18n } from '@/context/ConnectionI18nContext';
|
|
7
|
+
|
|
8
|
+
const ConnectionListScreen = ({
|
|
9
|
+
onOpenModal,
|
|
10
|
+
onBackPress,
|
|
11
|
+
title,
|
|
12
|
+
isModalVisible,
|
|
13
|
+
}: {
|
|
14
|
+
onOpenModal?: () => void;
|
|
15
|
+
onBackPress?: () => void;
|
|
16
|
+
title?: string;
|
|
17
|
+
isModalVisible?: boolean;
|
|
18
|
+
}) => {
|
|
19
|
+
const {
|
|
20
|
+
connections,
|
|
21
|
+
isFetching,
|
|
22
|
+
error,
|
|
23
|
+
errorStatus,
|
|
24
|
+
isActioning,
|
|
25
|
+
activeFilters,
|
|
26
|
+
_hasHydrated,
|
|
27
|
+
fetchConnections,
|
|
28
|
+
acceptConnection,
|
|
29
|
+
rejectConnection,
|
|
30
|
+
cancelConnection,
|
|
31
|
+
disconnectConnection,
|
|
32
|
+
clearError,
|
|
33
|
+
} = useConnectionStore();
|
|
34
|
+
|
|
35
|
+
const [filterModalVisible, setFilterModalVisible] = useState(false);
|
|
36
|
+
const [draftStatus, setDraftStatus] = useState('');
|
|
37
|
+
const [draftDirection, setDraftDirection] = useState('');
|
|
38
|
+
const [toast, setToast] = useState({ visible: false, message: '' });
|
|
39
|
+
const prevErrorRef = useRef<string | null>(null);
|
|
40
|
+
const initialFetchDone = useRef(false);
|
|
41
|
+
|
|
42
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
43
|
+
|
|
44
|
+
const filteredConnections = useMemo(() => {
|
|
45
|
+
const conns = connections || [];
|
|
46
|
+
if (!searchQuery.trim()) return conns;
|
|
47
|
+
const query = searchQuery.toLowerCase().trim();
|
|
48
|
+
return conns.filter((item) => {
|
|
49
|
+
const reqName = item.requesting_company?.display_name?.toLowerCase() ?? '';
|
|
50
|
+
const destName = item.requested_company?.display_name?.toLowerCase() ?? '';
|
|
51
|
+
const reqNum = item.requesting_company?.company_num ?? '';
|
|
52
|
+
const destNum = item.requested_company?.company_num ?? '';
|
|
53
|
+
const msg = item.request_message?.toLowerCase() ?? '';
|
|
54
|
+
return (
|
|
55
|
+
reqName.includes(query) ||
|
|
56
|
+
destName.includes(query) ||
|
|
57
|
+
reqNum.includes(query) ||
|
|
58
|
+
destNum.includes(query) ||
|
|
59
|
+
msg.includes(query)
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
}, [connections, searchQuery]);
|
|
63
|
+
|
|
64
|
+
const { t } = useConnectionI18n();
|
|
65
|
+
|
|
66
|
+
const statusOptions = useMemo(
|
|
67
|
+
() => [
|
|
68
|
+
{ label: t('allStatuses'), value: '' },
|
|
69
|
+
{ label: 'PENDING', value: 'PENDING' },
|
|
70
|
+
{ label: 'ACCEPTED', value: 'ACCEPTED' },
|
|
71
|
+
{ label: 'REJECTED', value: 'REJECTED' },
|
|
72
|
+
{ label: 'CANCELLED', value: 'CANCELLED' },
|
|
73
|
+
{ label: 'EXPIRED', value: 'EXPIRED' },
|
|
74
|
+
{ label: 'DISCONNECTED', value: 'DISCONNECTED' },
|
|
75
|
+
],
|
|
76
|
+
[t],
|
|
77
|
+
);
|
|
78
|
+
const directionOptions = useMemo(
|
|
79
|
+
() => [
|
|
80
|
+
{ label: t('allDirections'), value: '' },
|
|
81
|
+
{ label: t('incoming'), value: 'INCOMING' },
|
|
82
|
+
{ label: t('outgoing'), value: 'OUTGOING' },
|
|
83
|
+
],
|
|
84
|
+
[t],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 🐛 DEBUG: render-time log — fires on EVERY render frame (not just after paint)
|
|
88
|
+
console.log('[ConnectionList] RENDER ▶', {
|
|
89
|
+
_hasHydrated,
|
|
90
|
+
isFetching,
|
|
91
|
+
connectionsLength: connections.length,
|
|
92
|
+
showLoader: isFetching && _hasHydrated && connections.length === 0,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
console.log('[ConnectionList] Hydration check — _hasHydrated:', _hasHydrated, '| initialFetchDone:', initialFetchDone.current);
|
|
97
|
+
if (!_hasHydrated) return;
|
|
98
|
+
if (initialFetchDone.current) return;
|
|
99
|
+
initialFetchDone.current = true;
|
|
100
|
+
console.log('[ConnectionList] Triggering fetchConnections()');
|
|
101
|
+
fetchConnections();
|
|
102
|
+
}, [_hasHydrated, fetchConnections]);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (error && error !== prevErrorRef.current) {
|
|
106
|
+
prevErrorRef.current = error;
|
|
107
|
+
if (!isModalVisible) {
|
|
108
|
+
setToast({ visible: true, message: error });
|
|
109
|
+
}
|
|
110
|
+
clearError();
|
|
111
|
+
}
|
|
112
|
+
}, [error, clearError, isModalVisible]);
|
|
113
|
+
|
|
114
|
+
const handleToastClose = () => {
|
|
115
|
+
setToast((t) => ({ ...t, visible: false }));
|
|
116
|
+
prevErrorRef.current = null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const openFilter = () => {
|
|
120
|
+
setDraftStatus(activeFilters.connection_status ?? '');
|
|
121
|
+
setDraftDirection(activeFilters.direction ?? '');
|
|
122
|
+
setFilterModalVisible(true);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const applyFilter = () => {
|
|
126
|
+
const filters: ConnectionFilterParams = {};
|
|
127
|
+
if (draftStatus) filters.connection_status = draftStatus as ConnectionStatus;
|
|
128
|
+
if (draftDirection) filters.direction = draftDirection as Direction;
|
|
129
|
+
fetchConnections(1, filters);
|
|
130
|
+
setFilterModalVisible(false);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const clearFilter = () => {
|
|
134
|
+
setDraftStatus('');
|
|
135
|
+
setDraftDirection('');
|
|
136
|
+
fetchConnections(1, {});
|
|
137
|
+
setFilterModalVisible(false);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const hasActiveFilter = !!(activeFilters.connection_status || activeFilters.direction);
|
|
141
|
+
|
|
142
|
+
// Header: actionIconName2 = left (outline), actionIconName = right (solid). Order: [filter, refresh].
|
|
143
|
+
const primaryIconName = 'refresh-cw';
|
|
144
|
+
const secondaryIconName = 'sliders';
|
|
145
|
+
const handlePrimaryAction = () => fetchConnections(1, undefined, true);
|
|
146
|
+
const handleSecondaryAction = openFilter;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
|
|
150
|
+
<View className="flex-1 bg-background-light dark:bg-background-darkBg">
|
|
151
|
+
<Header
|
|
152
|
+
title={title ?? t('title')}
|
|
153
|
+
showBackButton={true}
|
|
154
|
+
onBackPress={onBackPress}
|
|
155
|
+
actionIconName={primaryIconName}
|
|
156
|
+
actionIconType="Feather"
|
|
157
|
+
onActionPress={handlePrimaryAction}
|
|
158
|
+
actionIconName2={secondaryIconName}
|
|
159
|
+
actionIconType2="Feather"
|
|
160
|
+
onSecondActionPress={handleSecondaryAction}
|
|
161
|
+
/>
|
|
162
|
+
<PermissionActions
|
|
163
|
+
app="connection"
|
|
164
|
+
module="company_connection"
|
|
165
|
+
action="read"
|
|
166
|
+
hideWhenReadOnly={true}
|
|
167
|
+
>
|
|
168
|
+
<View className="px-4 py-2 bg-background-light dark:bg-background-darkBg ">
|
|
169
|
+
{hasActiveFilter && (
|
|
170
|
+
<View className="flex-row items-center mb-2">
|
|
171
|
+
<Text className="text-xs text-text-light-secondary dark:text-text-dark-secondary mr-1">{t('filter')}</Text>
|
|
172
|
+
<Text className="text-xs font-medium text-text-light-violet dark:text-text-light-violet">
|
|
173
|
+
{[activeFilters.connection_status, activeFilters.direction].filter(Boolean).join(' • ') || '—'}
|
|
174
|
+
</Text>
|
|
175
|
+
</View>
|
|
176
|
+
)}
|
|
177
|
+
<View className="flex-row items-center gap-2">
|
|
178
|
+
<View className="flex-1">
|
|
179
|
+
<SearchFilter
|
|
180
|
+
placeholder={t('searchPlaceholder')}
|
|
181
|
+
value={searchQuery}
|
|
182
|
+
onChangeText={setSearchQuery}
|
|
183
|
+
/>
|
|
184
|
+
</View>
|
|
185
|
+
<PermissionActions
|
|
186
|
+
app="connection"
|
|
187
|
+
module="company_connection"
|
|
188
|
+
action="create"
|
|
189
|
+
hideWhenReadOnly={true}
|
|
190
|
+
>
|
|
191
|
+
<Button
|
|
192
|
+
title={t('create')}
|
|
193
|
+
type="primary"
|
|
194
|
+
size="md"
|
|
195
|
+
onPress={onOpenModal}
|
|
196
|
+
icon={{ name: 'plus', type: 'Feather' }}
|
|
197
|
+
iconPosition="left"
|
|
198
|
+
useSafeArea={false}
|
|
199
|
+
/>
|
|
200
|
+
</PermissionActions>
|
|
201
|
+
</View>
|
|
202
|
+
</View>
|
|
203
|
+
|
|
204
|
+
<ServerErrorWrapper
|
|
205
|
+
errorStatus={errorStatus}
|
|
206
|
+
onRetry={() => fetchConnections()}
|
|
207
|
+
className="flex-1"
|
|
208
|
+
>
|
|
209
|
+
<ScrollView className="p-4 flex-1">
|
|
210
|
+
{/* 🐛 DEBUG — remove before release */
|
|
211
|
+
/* isFetching={String(isFetching)} | _hasHydrated={String(_hasHydrated)} | connections={connections.length} */}
|
|
212
|
+
{isFetching && _hasHydrated && connections.length === 0 && (
|
|
213
|
+
<Loader message={t('loading')} size="large" className="mt-10" />
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{filteredConnections.map((item) => (
|
|
217
|
+
<View
|
|
218
|
+
key={item.id}
|
|
219
|
+
className="bg-background-light dark:bg-background-darkSecondaryBg rounded-2xl border border-background-lightGray dark:border-background-darkGray shadow-sm overflow-hidden mb-4"
|
|
220
|
+
>
|
|
221
|
+
<View className="p-4">
|
|
222
|
+
<View className="flex-row justify-between items-start gap-3 mb-3">
|
|
223
|
+
<View className="flex-1 min-w-0">
|
|
224
|
+
<Text className="font-bold text-lg text-text-light-primary dark:text-text-dark-primary" numberOfLines={1}>
|
|
225
|
+
{item.direction === 'OUTGOING' ? item.requested_company.display_name : item.requesting_company.display_name}
|
|
226
|
+
</Text>
|
|
227
|
+
<View className="flex-row items-center mt-1">
|
|
228
|
+
<Text className="text-xs text-text-light-secondary dark:text-text-dark-secondary">{t('from')} </Text>
|
|
229
|
+
<Text className="text-xs font-medium text-text-light-primary dark:text-text-dark-secondary">
|
|
230
|
+
{item.direction === 'OUTGOING' ? item.requesting_company.display_name : item.requested_company.display_name}
|
|
231
|
+
</Text>
|
|
232
|
+
</View>
|
|
233
|
+
</View>
|
|
234
|
+
<View
|
|
235
|
+
className={`px-2 py-1 rounded-md border mt-1 flex-shrink-0 ${item.connection_status === 'PENDING'
|
|
236
|
+
? 'bg-yellow-100 dark:bg-yellow-900 border-yellow-400 dark:border-yellow-900'
|
|
237
|
+
: item.connection_status === 'ACCEPTED'
|
|
238
|
+
? 'bg-green-100 dark:bg-background-darkGreen border-green-400 dark:border-background-darkGreen'
|
|
239
|
+
: item.connection_status === 'REJECTED'
|
|
240
|
+
? 'bg-red-100 dark:bg-red-900 border-red-400 dark:border-red-900'
|
|
241
|
+
: item.connection_status === 'CANCELLED' || item.connection_status === 'EXPIRED'
|
|
242
|
+
? 'bg-red-100 dark:bg-red-900 border-red-400 dark:border-red-900'
|
|
243
|
+
: item.connection_status === 'DISCONNECTED'
|
|
244
|
+
? 'bg-background-secondaryBg dark:bg-background-darkSecondaryBg border-background-lightGray dark:border-background-darkGray'
|
|
245
|
+
: 'bg-background-secondaryBg dark:bg-background-darkSecondaryBg border-background-lightGray dark:border-background-darkGray'
|
|
246
|
+
}`}
|
|
247
|
+
>
|
|
248
|
+
<Text
|
|
249
|
+
className={`text-[10px] font-bold uppercase tracking-wider ${item.connection_status === 'PENDING'
|
|
250
|
+
? 'text-yellow-800 dark:text-yellow-100'
|
|
251
|
+
: item.connection_status === 'ACCEPTED'
|
|
252
|
+
? 'text-green-700 dark:text-green-100'
|
|
253
|
+
: item.connection_status === 'REJECTED'
|
|
254
|
+
? 'text-red-700 dark:text-red-100'
|
|
255
|
+
: item.connection_status === 'CANCELLED' || item.connection_status === 'EXPIRED'
|
|
256
|
+
? 'text-red-700 dark:text-red-100'
|
|
257
|
+
: 'text-text-light-secondary dark:text-text-dark-secondary'
|
|
258
|
+
}`}
|
|
259
|
+
>
|
|
260
|
+
{item.connection_status}
|
|
261
|
+
</Text>
|
|
262
|
+
</View>
|
|
263
|
+
</View>
|
|
264
|
+
|
|
265
|
+
<View className="flex-row py-3 border-y border-background-lightGray dark:border-background-darkGray">
|
|
266
|
+
<View className="flex-1">
|
|
267
|
+
<Text className="text-[10px] uppercase text-text-light-subHeading dark:text-text-dark-subHeading font-bold mb-0.5">
|
|
268
|
+
{t('requestedAt')}
|
|
269
|
+
</Text>
|
|
270
|
+
<Text className="text-sm font-medium text-text-light-primary dark:text-text-dark-primary">
|
|
271
|
+
{new Date(item.requested_at).toLocaleDateString()}
|
|
272
|
+
</Text>
|
|
273
|
+
</View>
|
|
274
|
+
<View className="flex-1">
|
|
275
|
+
<Text className="text-[10px] uppercase text-text-light-subHeading dark:text-text-dark-subHeading font-bold mb-0.5">
|
|
276
|
+
{t('direction')}
|
|
277
|
+
</Text>
|
|
278
|
+
<View className="flex-row items-center gap-1.5">
|
|
279
|
+
<Text className={item.direction === 'OUTGOING' ? 'text-blue-400' : 'text-green-400'}>
|
|
280
|
+
{item.direction === 'OUTGOING' ? '↗' : '↙'}
|
|
281
|
+
</Text>
|
|
282
|
+
<Text
|
|
283
|
+
className={`text-sm font-semibold ${item.direction === 'OUTGOING'
|
|
284
|
+
? 'text-blue-400 dark:text-blue-100'
|
|
285
|
+
: 'text-green-400 dark:text-green-100'
|
|
286
|
+
}`}
|
|
287
|
+
>
|
|
288
|
+
{item.direction === 'OUTGOING' ? t('outgoing') : t('incoming')}
|
|
289
|
+
</Text>
|
|
290
|
+
</View>
|
|
291
|
+
</View>
|
|
292
|
+
</View>
|
|
293
|
+
|
|
294
|
+
<View className="mt-4 flex-row items-center justify-between">
|
|
295
|
+
<View className="flex-row -space-x-2">
|
|
296
|
+
<View className="w-8 h-8 rounded-full bg-background-lightBlue dark:bg-background-lightBlack border-2 border-background-light dark:border-background-darkSecondaryBg items-center justify-center">
|
|
297
|
+
<Text className="text-[10px] font-bold text-text-light-gray dark:text-text-dark-primary">
|
|
298
|
+
{item.requesting_company.display_name.substring(0, 2).toUpperCase()}
|
|
299
|
+
</Text>
|
|
300
|
+
</View>
|
|
301
|
+
<View className="w-8 h-8 rounded-full bg-background-lightPink dark:bg-background-violet border-2 border-background-light dark:border-background-darkSecondaryBg items-center justify-center">
|
|
302
|
+
<Text className="text-[10px] font-bold text-text-light-violet dark:text-text-light-white">
|
|
303
|
+
{item.requested_company.display_name.substring(0, 2).toUpperCase()}
|
|
304
|
+
</Text>
|
|
305
|
+
</View>
|
|
306
|
+
</View>
|
|
307
|
+
|
|
308
|
+
<View className="flex-row items-center gap-2">
|
|
309
|
+
<PermissionActions
|
|
310
|
+
app="connection"
|
|
311
|
+
module="company_connection"
|
|
312
|
+
action="update"
|
|
313
|
+
hideWhenReadOnly={true}
|
|
314
|
+
>
|
|
315
|
+
{item.connection_status === 'PENDING' && (
|
|
316
|
+
<>
|
|
317
|
+
{item.direction === 'INCOMING' ? (
|
|
318
|
+
<View className="flex-row gap-2">
|
|
319
|
+
<Button
|
|
320
|
+
title={t('reject')}
|
|
321
|
+
type="outline"
|
|
322
|
+
size="sm"
|
|
323
|
+
onPress={() => {
|
|
324
|
+
Alert.alert(
|
|
325
|
+
t('confirmTitle'),
|
|
326
|
+
t('rejectConfirm'),
|
|
327
|
+
[
|
|
328
|
+
{ text: t('cancel'), style: 'cancel' },
|
|
329
|
+
{ text: t('yes'), style: 'destructive', onPress: () => rejectConnection(item.id) }
|
|
330
|
+
]
|
|
331
|
+
);
|
|
332
|
+
}}
|
|
333
|
+
disabled={isActioning}
|
|
334
|
+
/>
|
|
335
|
+
<Button
|
|
336
|
+
title={t('accept')}
|
|
337
|
+
type="primary"
|
|
338
|
+
size="sm"
|
|
339
|
+
onPress={() => {
|
|
340
|
+
Alert.alert(
|
|
341
|
+
t('confirmTitle'),
|
|
342
|
+
t('acceptConfirm'),
|
|
343
|
+
[
|
|
344
|
+
{ text: t('cancel'), style: 'cancel' },
|
|
345
|
+
{ text: t('yes'), onPress: () => acceptConnection(item.id) }
|
|
346
|
+
]
|
|
347
|
+
);
|
|
348
|
+
}}
|
|
349
|
+
disabled={isActioning}
|
|
350
|
+
/>
|
|
351
|
+
</View>
|
|
352
|
+
) : (
|
|
353
|
+
<Button
|
|
354
|
+
title={t('cancel')}
|
|
355
|
+
type="outline"
|
|
356
|
+
size="sm"
|
|
357
|
+
onPress={() => {
|
|
358
|
+
Alert.alert(
|
|
359
|
+
t('confirmTitle'),
|
|
360
|
+
t('cancelConfirm'),
|
|
361
|
+
[
|
|
362
|
+
{ text: t('cancel'), style: 'cancel' },
|
|
363
|
+
{ text: t('yes'), style: 'destructive', onPress: () => cancelConnection(item.id) }
|
|
364
|
+
]
|
|
365
|
+
);
|
|
366
|
+
}}
|
|
367
|
+
disabled={isActioning}
|
|
368
|
+
/>
|
|
369
|
+
)}
|
|
370
|
+
</>
|
|
371
|
+
)}
|
|
372
|
+
|
|
373
|
+
{item.connection_status === 'ACCEPTED' && (
|
|
374
|
+
<Button
|
|
375
|
+
title={t('disconnect')}
|
|
376
|
+
type="outline"
|
|
377
|
+
size="sm"
|
|
378
|
+
onPress={() => {
|
|
379
|
+
Alert.alert(
|
|
380
|
+
t('confirmTitle'),
|
|
381
|
+
t('disconnectConfirm'),
|
|
382
|
+
[
|
|
383
|
+
{ text: t('cancel'), style: 'cancel' },
|
|
384
|
+
{ text: t('yes'), style: 'destructive', onPress: () => disconnectConnection(item.id) }
|
|
385
|
+
]
|
|
386
|
+
);
|
|
387
|
+
}}
|
|
388
|
+
disabled={isActioning}
|
|
389
|
+
/>
|
|
390
|
+
)}
|
|
391
|
+
</PermissionActions>
|
|
392
|
+
</View>
|
|
393
|
+
</View>
|
|
394
|
+
</View>
|
|
395
|
+
</View>
|
|
396
|
+
))}
|
|
397
|
+
|
|
398
|
+
<View className="items-center justify-center py-6">
|
|
399
|
+
<Text className="text-xs text-slate-400 dark:text-slate-500">
|
|
400
|
+
{filteredConnections.length > 0
|
|
401
|
+
? t('showingCount', { count: filteredConnections.length })
|
|
402
|
+
: t('noConnections')}
|
|
403
|
+
</Text>
|
|
404
|
+
</View>
|
|
405
|
+
</ScrollView>
|
|
406
|
+
</ServerErrorWrapper>
|
|
407
|
+
|
|
408
|
+
<BottomModal
|
|
409
|
+
isVisible={filterModalVisible}
|
|
410
|
+
onClose={() => setFilterModalVisible(false)}
|
|
411
|
+
heightMode="fixed"
|
|
412
|
+
fixedHeightPercent={55}
|
|
413
|
+
className="bg-background-light dark:bg-background-darkSecondaryBg"
|
|
414
|
+
>
|
|
415
|
+
<ModalTitle
|
|
416
|
+
heading={t('filterConnections')}
|
|
417
|
+
showCloseIcon
|
|
418
|
+
onClose={() => setFilterModalVisible(false)}
|
|
419
|
+
/>
|
|
420
|
+
<View className="px-4">
|
|
421
|
+
<Dropdown
|
|
422
|
+
label={t('status')}
|
|
423
|
+
placeholder={t('allStatuses')}
|
|
424
|
+
items={statusOptions}
|
|
425
|
+
value={draftStatus}
|
|
426
|
+
onValueChange={(v) => setDraftStatus(v ?? '')}
|
|
427
|
+
/>
|
|
428
|
+
<View className="mt-4">
|
|
429
|
+
<Dropdown
|
|
430
|
+
label={t('direction')}
|
|
431
|
+
placeholder={t('allDirections')}
|
|
432
|
+
items={directionOptions}
|
|
433
|
+
value={draftDirection}
|
|
434
|
+
onValueChange={(v) => setDraftDirection(v ?? '')}
|
|
435
|
+
/>
|
|
436
|
+
</View>
|
|
437
|
+
<View className="flex-row mt-6 gap-3">
|
|
438
|
+
<Button
|
|
439
|
+
title={t('clear')}
|
|
440
|
+
type="outline"
|
|
441
|
+
size="md"
|
|
442
|
+
onPress={clearFilter}
|
|
443
|
+
className="flex-1"
|
|
444
|
+
useSafeArea={false}
|
|
445
|
+
/>
|
|
446
|
+
<Button
|
|
447
|
+
title={t('apply')}
|
|
448
|
+
type="primary"
|
|
449
|
+
size="md"
|
|
450
|
+
onPress={applyFilter}
|
|
451
|
+
className="flex-1"
|
|
452
|
+
useSafeArea={false}
|
|
453
|
+
/>
|
|
454
|
+
</View>
|
|
455
|
+
</View>
|
|
456
|
+
</BottomModal>
|
|
457
|
+
|
|
458
|
+
<Toast
|
|
459
|
+
message={toast.message}
|
|
460
|
+
visible={toast.visible}
|
|
461
|
+
type="error"
|
|
462
|
+
position="bottom"
|
|
463
|
+
onClose={handleToastClose}
|
|
464
|
+
/>
|
|
465
|
+
</PermissionActions>
|
|
466
|
+
</View>
|
|
467
|
+
</SafeAreaView>
|
|
468
|
+
);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export default ConnectionListScreen;
|