@campxdev/shared 3.1.7 → 3.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@campxdev/shared",
3
- "version": "3.1.7",
3
+ "version": "3.1.9",
4
4
  "main": "./exports.ts",
5
5
  "scripts": {
6
6
  "start": "react-scripts start",
@@ -1,24 +1,40 @@
1
+ import RefreshIcon from '@mui/icons-material/Refresh'
1
2
  import {
2
3
  Timeline,
4
+ TimelineConnector,
3
5
  TimelineContent,
4
6
  TimelineItem,
5
7
  TimelineSeparator,
6
8
  } from '@mui/lab'
7
- import { Box, SxProps, Typography } from '@mui/material'
9
+ import {
10
+ Box,
11
+ CircularProgress,
12
+ IconButton,
13
+ SxProps,
14
+ Typography,
15
+ } from '@mui/material'
8
16
  import moment from 'moment'
9
- import { useCallback, useRef } from 'react'
17
+ import { useEffect, useRef } from 'react'
10
18
  import { useInfiniteQuery } from 'react-query'
11
19
  import { NoDataIllustration } from '..'
12
20
  import axios from '../../config/axios'
13
21
  import { useErrorModal } from '../ErrorModalWrapper/ErrorModalWrapper'
14
22
  import Spinner from '../Spinner'
15
23
  import Table from '../Tables/BasicTable/Table'
16
- import {
17
- StyledAvatar,
18
- StyledCircleIcon,
19
- StyledSpinnerBox,
20
- StyledTimeLineDot,
21
- } from './Styles'
24
+ import { StyledAvatar, StyledCircleIcon, StyledTimeLineDot } from './Styles'
25
+
26
+ interface ActivityLogItem {
27
+ timestamp: string
28
+ message: string
29
+ userName: string
30
+ action: string
31
+ typeId: number
32
+ type: string
33
+ tenantId: string
34
+ institutionId: number
35
+ userId: string
36
+ cursor: string
37
+ }
22
38
 
23
39
  interface Props {
24
40
  endPoint: string
@@ -26,6 +42,7 @@ interface Props {
26
42
  enableTitle?: boolean
27
43
  params: any
28
44
  sx?: SxProps
45
+ reloadKey?: string | number
29
46
  }
30
47
 
31
48
  export default function ActivityLog({
@@ -34,31 +51,64 @@ export default function ActivityLog({
34
51
  enableTitle,
35
52
  params,
36
53
  sx,
54
+ reloadKey,
37
55
  }: Props) {
38
56
  const errorModal = useErrorModal()
39
- const fetchActivities = async ({ pageParam = 0 }) => {
57
+ const timestamp = useRef(Date.now())
58
+ const queryKey = [
59
+ 'activities',
60
+ endPoint,
61
+ JSON.stringify(params),
62
+ reloadKey ?? timestamp.current,
63
+ ]
64
+
65
+ const fetchActivities = async ({ pageParam = null }) => {
40
66
  try {
41
- const response = await axios.get(endPoint, { params: { ...params } })
42
- return response?.data
67
+ const queryParams = { ...params }
68
+ if (pageParam) {
69
+ queryParams.cursor = pageParam
70
+ }
71
+ const response = await axios.get(endPoint, { params: queryParams })
72
+ return response?.data || []
43
73
  } catch (error) {
44
74
  // eslint-disable-next-line no-console
45
- console.log(error)
75
+ console.error('Error fetching activities:', error)
46
76
  errorModal({ error: error })
77
+ return []
47
78
  }
48
79
  }
49
80
 
50
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
51
- useInfiniteQuery('activities', fetchActivities, {
52
- getNextPageParam: (lastPage) => {
53
- return lastPage?.length ? lastPage[lastPage?.length - 1].id : null
54
- },
55
- })
81
+ const {
82
+ data,
83
+ fetchNextPage,
84
+ hasNextPage,
85
+ isFetchingNextPage,
86
+ isLoading,
87
+ refetch,
88
+ } = useInfiniteQuery(queryKey, fetchActivities, {
89
+ getNextPageParam: (lastPage: ActivityLogItem[]) => {
90
+ return lastPage?.length ? lastPage[lastPage.length - 1].cursor : null
91
+ },
92
+ refetchOnWindowFocus: false,
93
+ cacheTime: 0,
94
+ })
56
95
 
57
- const activitesData = data ? data?.pages?.flatMap((page) => page) : []
96
+ const activitiesData = data?.pages?.flatMap((page) => page) || []
58
97
 
59
- if (isLoading) return <Spinner />
98
+ if (isLoading) {
99
+ return (
100
+ <Box
101
+ display="flex"
102
+ justifyContent="center"
103
+ alignItems="center"
104
+ minHeight="200px"
105
+ >
106
+ <Spinner />
107
+ </Box>
108
+ )
109
+ }
60
110
 
61
- if (data?.pages?.length === 0 || !activitesData?.length) {
111
+ if (!activitiesData?.length) {
62
112
  return (
63
113
  <NoDataIllustration
64
114
  imageSrc=""
@@ -67,7 +117,7 @@ export default function ActivityLog({
67
117
  variant="h6"
68
118
  style={{ textAlign: 'center', marginTop: '20px' }}
69
119
  >
70
- {'No data found'}
120
+ No activity logs found
71
121
  </Typography>
72
122
  }
73
123
  />
@@ -77,37 +127,40 @@ export default function ActivityLog({
77
127
  return (
78
128
  <Box>
79
129
  {enableTitle && (
80
- <Typography
81
- variant="h1"
82
- sx={{ fontSize: '18px', fontWeight: 700, margin: '20px' }}
83
- >
84
- Activity Log
85
- </Typography>
86
- )}
87
-
88
- {!tableView ? (
89
130
  <Box
90
131
  sx={{
91
- overflowY: 'scroll',
92
- '&::-webkit-scrollbar': {
93
- display: 'none',
94
- },
95
- height: '380px',
132
+ display: 'flex',
133
+ justifyContent: 'space-between',
134
+ alignItems: 'center',
135
+ margin: '20px',
96
136
  }}
97
137
  >
98
- <TimeLineComponent
99
- activitesData={activitesData}
100
- fetchNextPage={fetchNextPage}
101
- isFetchingNextPage={isFetchingNextPage}
102
- hasNextPage={hasNextPage}
103
- sx={sx}
104
- />
138
+ <Typography variant="h1" sx={{ fontSize: '18px', fontWeight: 700 }}>
139
+ Activity Logs
140
+ </Typography>
141
+ <IconButton
142
+ size="small"
143
+ onClick={() => refetch()}
144
+ sx={{ '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.04)' } }}
145
+ >
146
+ <RefreshIcon fontSize="small" />
147
+ </IconButton>
105
148
  </Box>
149
+ )}
150
+
151
+ {!tableView ? (
152
+ <TimeLineComponent
153
+ activitiesData={activitiesData}
154
+ fetchNextPage={fetchNextPage}
155
+ hasNextPage={hasNextPage}
156
+ isFetchingNextPage={isFetchingNextPage}
157
+ sx={sx}
158
+ />
106
159
  ) : (
107
160
  <Box sx={{ margin: '0 25px' }}>
108
161
  <Table
109
- columns={columns}
110
- dataSource={activitesData}
162
+ columns={tableColumns}
163
+ dataSource={activitiesData}
111
164
  loading={isLoading}
112
165
  />
113
166
  </Box>
@@ -116,12 +169,13 @@ export default function ActivityLog({
116
169
  )
117
170
  }
118
171
 
119
- const columns = [
172
+ // Table columns for table view
173
+ const tableColumns = [
120
174
  {
121
175
  title: 'Date',
122
176
  dataIndex: 'timestamp',
123
177
  key: 'timestamp',
124
- render: (timestamp) =>
178
+ render: (timestamp: string) =>
125
179
  moment.utc(timestamp)?.local().format('DD MMM YYYY - hh:mm A'),
126
180
  },
127
181
  {
@@ -138,131 +192,165 @@ const columns = [
138
192
  title: 'Message',
139
193
  dataIndex: 'message',
140
194
  key: 'message',
141
- render: (message) => (
195
+ render: (message: string) => (
142
196
  <Typography sx={{ fontSize: '16px' }} variant="subtitle1">
143
- {message?.split("'")?.map((text: string, index: number) =>
144
- index % 2 === 0 ? (
145
- <span key={index}>{text}</span>
146
- ) : (
147
- <Typography key={index} sx={{ display: 'inline', fontWeight: 900 }}>
148
- {text}
149
- </Typography>
150
- ),
151
- )}
197
+ {renderMessageWithBold(message)}
152
198
  </Typography>
153
199
  ),
154
200
  },
155
201
  ]
156
202
 
157
- export const TimeLineComponent = ({
158
- activitesData,
159
- isFetchingNextPage,
160
- hasNextPage,
203
+ // Helper: Render message with bold text (text within single quotes)
204
+ const renderMessageWithBold = (message: string) => {
205
+ return message?.split("'")?.map((text: string, index: number) =>
206
+ index % 2 === 0 ? (
207
+ <span key={index}>{text}</span>
208
+ ) : (
209
+ <Typography key={index} component="span" sx={{ fontWeight: 900 }}>
210
+ {text}
211
+ </Typography>
212
+ ),
213
+ )
214
+ }
215
+
216
+ // Timeline Component with Infinite Scroll
217
+ interface TimeLineComponentProps {
218
+ activitiesData: ActivityLogItem[]
219
+ fetchNextPage: () => void
220
+ hasNextPage: boolean | undefined
221
+ isFetchingNextPage: boolean
222
+ sx?: SxProps
223
+ }
224
+
225
+ const TimeLineComponent = ({
226
+ activitiesData,
161
227
  fetchNextPage,
228
+ hasNextPage,
229
+ isFetchingNextPage,
162
230
  sx,
163
- }) => {
164
- const lastItemRef = useIntersectionObserver<HTMLDivElement>(() => {
165
- if (!isFetchingNextPage && hasNextPage) fetchNextPage()
166
- })
231
+ }: TimeLineComponentProps) => {
232
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
233
+ const loadMoreTriggerRef = useRef<HTMLDivElement>(null)
234
+
235
+ // Intersection observer for infinite scroll
236
+ useEffect(() => {
237
+ const trigger = loadMoreTriggerRef.current
238
+ const container = scrollContainerRef.current
239
+
240
+ if (!trigger || !container) return
241
+
242
+ const observer = new IntersectionObserver(
243
+ (entries) => {
244
+ const [entry] = entries
245
+ if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
246
+ fetchNextPage()
247
+ }
248
+ },
249
+ {
250
+ root: container,
251
+ rootMargin: '100px',
252
+ threshold: 0.1,
253
+ },
254
+ )
255
+
256
+ observer.observe(trigger)
257
+
258
+ return () => {
259
+ observer.disconnect()
260
+ }
261
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage])
167
262
 
168
263
  return (
169
- <Timeline sx={{ padding: 0, margin: 0 }}>
170
- {activitesData?.map((item, index, items) => (
171
- <Box
172
- key={index}
173
- ref={items.length - 1 === index ? lastItemRef : null}
174
- sx={{ maxWidth: '550px', ...sx }}
175
- >
176
- <TimelineItem
177
- sx={{
178
- margin: 2,
179
- padding: 0,
180
- '&:before': { display: 'none' },
181
- }}
264
+ <Box
265
+ ref={scrollContainerRef}
266
+ sx={{
267
+ overflowY: 'auto',
268
+ maxHeight: '600px',
269
+ '&::-webkit-scrollbar': {
270
+ display: 'none',
271
+ },
272
+ }}
273
+ >
274
+ <Timeline sx={{ padding: 0, margin: 0 }}>
275
+ {activitiesData.map((item, index) => (
276
+ <Box
277
+ key={`${item.cursor}-${index}`}
278
+ ref={
279
+ activitiesData.length - 1 === index ? loadMoreTriggerRef : null
280
+ }
281
+ sx={{ maxWidth: '550px', ...sx }}
182
282
  >
183
- <TimelineSeparator>
184
- <StyledTimeLineDot>
185
- <StyledCircleIcon />
186
- </StyledTimeLineDot>
187
- {index < activitesData?.length - 1 && (
188
- <Box
189
- sx={{
190
- width: '1px',
191
- height: '115%',
192
- bgcolor: '#12121233',
193
- position: 'absolute',
194
- top: '25px',
195
- }}
196
- />
197
- )}
198
- </TimelineSeparator>
199
- <TimelineContent sx={{ padding: '6px 8px' }}>
200
- <Box>
201
- <Typography variant="subtitle2" sx={{ fontSize: '14px' }}>
202
- {moment
203
- .utc(item?.timestamp)
204
- .local()
205
- .format('DD MMM YYYY - hh:mm A')}
206
- </Typography>
207
- <Typography sx={{ fontSize: '16px' }} variant="subtitle1">
208
- {item?.message
209
- ?.split("'")
210
- ?.map((text: string, index: number) =>
211
- index % 2 === 0 ? (
212
- <span key={index}>{text}</span>
213
- ) : (
214
- <Typography
215
- key={index}
216
- sx={{ display: 'inline', fontWeight: 900 }}
217
- >
218
- {text}
219
- </Typography>
220
- ),
221
- )}
222
- </Typography>
223
- <Typography
224
- style={{
225
- display: 'flex',
226
- alignItems: 'center',
227
- marginTop: '8px',
228
- }}
229
- >
230
- <StyledAvatar>
231
- {item?.userName?.charAt(0)?.toUpperCase()}
232
- </StyledAvatar>
233
- <Typography sx={{ fontSize: '13px', fontWeight: 900 }}>
234
- {item?.userName}
283
+ <TimelineItem
284
+ sx={{
285
+ margin: 2,
286
+ padding: 0,
287
+ '&:before': { display: 'none' },
288
+ }}
289
+ >
290
+ <TimelineSeparator>
291
+ <StyledTimeLineDot>
292
+ <StyledCircleIcon />
293
+ </StyledTimeLineDot>
294
+ {index < activitiesData.length - 1 && (
295
+ <Box
296
+ sx={{
297
+ width: '1px',
298
+ height: '115%',
299
+ bgcolor: '#12121233',
300
+ position: 'absolute',
301
+ top: '25px',
302
+ }}
303
+ />
304
+ )}
305
+ </TimelineSeparator>
306
+ <TimelineContent sx={{ padding: '6px 8px' }}>
307
+ <Box>
308
+ <Typography variant="subtitle2" sx={{ fontSize: '14px' }}>
309
+ {moment
310
+ .utc(item.timestamp)
311
+ .local()
312
+ .format('DD MMM YYYY - hh:mm A')}
235
313
  </Typography>
236
- </Typography>
237
- </Box>
238
- </TimelineContent>
239
- </TimelineItem>
240
- {isFetchingNextPage && index === items?.length - 1 && hasNextPage && (
241
- <StyledSpinnerBox>
242
- <Spinner />
243
- </StyledSpinnerBox>
244
- )}
245
- </Box>
246
- ))}
247
- </Timeline>
248
- )
249
- }
250
-
251
- function useIntersectionObserver<T extends HTMLDivElement>(
252
- callback: () => void,
253
- ) {
254
- const observer = useRef<IntersectionObserver | null>(null)
314
+ <Typography sx={{ fontSize: '16px' }} variant="subtitle1">
315
+ {item.message?.split('\n')?.map((line: string, lineIdx: number) => (
316
+ <Box key={lineIdx} component="div">
317
+ {renderMessageWithBold(line)}
318
+ </Box>
319
+ ))}
320
+ </Typography>
321
+ <Typography
322
+ style={{
323
+ display: 'flex',
324
+ alignItems: 'center',
325
+ marginTop: '8px',
326
+ }}
327
+ >
328
+ <StyledAvatar>
329
+ {item.userName?.charAt(0)?.toUpperCase()}
330
+ </StyledAvatar>
331
+ <Typography sx={{ fontSize: '13px', fontWeight: 900 }}>
332
+ {item.userName}
333
+ </Typography>
334
+ </Typography>
335
+ </Box>
336
+ </TimelineContent>
337
+ </TimelineItem>
338
+ </Box>
339
+ ))}
340
+ </Timeline>
255
341
 
256
- const handleObserver = useCallback(
257
- (node: T) => {
258
- if (observer?.current) observer?.current?.disconnect()
259
- observer.current = new IntersectionObserver((entries) => {
260
- if (entries[0]?.isIntersecting) callback()
261
- })
262
- if (node) observer?.current?.observe(node)
263
- },
264
- [callback],
342
+ {/* Loading indicator */}
343
+ {isFetchingNextPage && (
344
+ <Box
345
+ sx={{
346
+ display: 'flex',
347
+ justifyContent: 'center',
348
+ padding: 2,
349
+ }}
350
+ >
351
+ <CircularProgress size={20} />
352
+ </Box>
353
+ )}
354
+ </Box>
265
355
  )
266
-
267
- return handleObserver
268
356
  }
@@ -25,6 +25,7 @@ const workspaceApiMapping: Record<string, string> = {
25
25
  'hostel-admin-workspace': '/hostel-admin-api',
26
26
  'transport-coordinator-workspace': '/transport-coordinator-api',
27
27
  'employee-workspace': '/employee-api',
28
+ 'librarian-workspace': '/librarian-api',
28
29
  }
29
30
 
30
31
  export const formatParams = (params) => {