diff --git a/src/components/Icon/index.test.tsx b/src/components/Icon/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b3745f1be2a082967242f15055a77e786321e2e4 --- /dev/null +++ b/src/components/Icon/index.test.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import Icon from '.'; +import renderer from 'react-test-renderer'; + +it('renders correctly', () => { + const instance = renderer.create( + + ); + + expect(instance).toBeTruthy(); +}); diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..32ad03b6a77645f710ce5e1188b979b90cb5588f --- /dev/null +++ b/src/components/Icon/index.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import styled from 'styled-components'; + +interface StyledIconProps { + height?: string; + width?: string; + cursor?: string; + origin?: string; + transform?: string; + opacity?: number; +} + +const StyledIcon = styled.img` + cursor: ${(props: StyledIconProps) => props.cursor || 'auto'}; + height: ${(props: StyledIconProps) => props.height || 'auto'}; + width: ${(props: StyledIconProps) => props.width || 'auto'}; + origin: ${(props: StyledIconProps) => props.origin || '50% 50%'}; + transform: ${(props: StyledIconProps) => props.transform || 'none'}; + opacity: ${(props: StyledIconProps) => props.opacity || 1}; +`; + +interface IconProps extends StyledIconProps { + id?: string; + src: string; + onClick?: () => void; +} + +const Icon = (props: IconProps) => { + return ( + + ); +} + +export default Icon; \ No newline at end of file diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 4efc288cd91637293b22308b282165e4f9a345bc..888a002d14904f24094716a1bff3024d2f7c2589 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -6,6 +6,7 @@ import Box from 'components/Box'; import Gap from 'components/Gap'; import Cloud from 'components/Cloud'; import Button from 'components/Button'; +import Icon from 'components/Icon'; type ValueType = string | number; @@ -24,24 +25,6 @@ const Click = styled.div` cursor: pointer; `; -interface IconProps { - height?: string; - width?: string; - cursor?: string; - origin?: string; - transform?: string; - opacity?: number; -} - -const Icon = styled.img` - cursor: ${(props: IconProps) => props.cursor || 'auto'}; - height: ${(props: IconProps) => props.height || 'auto'}; - width: ${(props: IconProps) => props.width || 'auto'}; - origin: ${(props: IconProps) => props.origin || '50% 50%'}; - transform: ${(props: IconProps) => props.transform || 'none'}; - opacity: ${(props: IconProps) => props.opacity || 1}; -`; - interface SearchBarProps { theme: ThemeProps; } diff --git a/src/components/index.ts b/src/components/index.ts index 5c333a235fcbc34f09bf2187abcae9ca092f1c26..a9777f446131861e4159b9418e6c48ddeebce05e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,6 +10,7 @@ import Image from './Image'; import Content from './Content'; import Checkbox from './Checkbox'; import Loading from './Loading'; +import Icon from './Icon'; export { StyledBox, @@ -25,4 +26,5 @@ export { Image, Content, Loading, + Icon, }; diff --git a/src/scenes/Home/components/ActivityLog/index.test.tsx b/src/scenes/Home/components/ActivityLog/index.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..21edc14bedea9472241bdc25dd7977b2db7cd482 --- /dev/null +++ b/src/scenes/Home/components/ActivityLog/index.test.tsx @@ -0,0 +1,296 @@ +import React from 'react'; +import axios from 'axios'; +import renderer, { act } from 'react-test-renderer'; +import ActivityLog from '.'; +import { useMainService } from 'services'; +import { AppContext } from 'contexts'; +import { mount } from 'enzyme'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; +const testProps = { + services: { + main: useMainService('dummyToken'), + }, +}; + +it('renders correctly', () => { + const instance = renderer.create( + + ); + + expect(instance).toBeTruthy(); +}); + +describe('load logs and generate log messages correctly', () => { + it('Monitoring case', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Create", + model_name: "Monitoring Case", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ], + investigation_case: { + case_subject: { + name: "Test" + } + } + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Investigation case', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Create", + model_name: "Investigation Case", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ], + case_subject: { + name: "Test" + } + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Case subject', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Create", + model_name: "Case Subject", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ], + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Create account', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + username: "Test", + is_admin: false, + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Create", + model_name: "Account", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ] + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Create account, but account deleted', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Create", + model_name: "Account", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ] + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Edit account', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + username: "Test", + is_admin: false, + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Edit", + model_name: "Account", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ] + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Edit account, but account deleted', () => { + mockedAxios.request.mockResolvedValue({ + status: 404, + data: { + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Edit", + model_name: "Account", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ] + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); + + it('Delete account', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + username: "Test", + is_admin: false, + previous: null, + next: null, + count: 1, + results: [ + { + action_type: "Delete", + model_name: "Account", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ] + } + }); + + const instance = mount( + + + + ); + + expect(mockedAxios.request).toBeCalled(); + }); +}); + +it('fetch new logs and change page number when press next or previous button', () => { + mockedAxios.request.mockResolvedValue({ + status: 200, + data: { + username: "Test", + is_admin: false, + previous: true, + next: true, + count: 1, + results: [ + { + action_type: "Delete", + model_name: "Account", + object_id: "1234", + recorded_at: "2020-05-31T23:03:35.854615+07:00", + }, + ] + } + }); + + const instance = mount( + + + + ); + expect(mockedAxios.request).toBeCalled(); + + const prevButton = instance.find('#prev-button'); + const nextButton = instance.find('#next-button'); + + let pageNumber = instance.find('Text').findWhere(elem => elem.prop('id') === 'page-number'); + expect(pageNumber.text()).toBe('1'); + nextButton.at(0).simulate('click'); + expect(mockedAxios.request).toBeCalled(); + + prevButton.at(0).simulate('click'); + pageNumber = instance.find('Text').findWhere(elem => elem.prop('id') === 'page-number'); + expect(pageNumber.text()).toBe('1'); + expect(mockedAxios.request).toBeCalled(); +}); \ No newline at end of file diff --git a/src/scenes/Home/components/ActivityLog/index.tsx b/src/scenes/Home/components/ActivityLog/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..95970a079b950b4ce443c6aabd269f696d9752e0 --- /dev/null +++ b/src/scenes/Home/components/ActivityLog/index.tsx @@ -0,0 +1,187 @@ +import React, { useContext, useEffect, useState } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { Box, Text, Content, Gap, Icon } from 'components'; +import { AppContext } from 'contexts'; + +interface Log { + timestamp: Date; + message: string; +} + +const DEFAULT_THEME = { + colors: { + totallyWhite: 'white', + mediumGray: 'gray', + }, +}; + +export default function ActivityLog() { + const { colors } = useContext(ThemeContext) || DEFAULT_THEME; + const { services } = useContext(AppContext); + const [logList, setLogList] = useState([]); + const [page, setPage] = useState(1); + const [next, setNext] = useState(false); + const [prev, setPrev] = useState(false); + const [totalLog, setTotalLog] = useState(0); + const verticalLineHeight = ((logList.length - 1) * 54); + + const VerticalLine = styled.div` + border-left: 2px solid ${colors.mediumGray}; + height: ${verticalLineHeight}px; + position: absolute; + left: 36px; + top: 24px; + `; + const Circle = styled.div` + left: 12px; + width: 24px; + height: 24px; + border-radius: 12px; + background: ${colors.mediumGray}; + margin-right: 20px; + `; + + const generateLogMessage = async (log: any) => { + let message = ""; + + switch (log.model_name) { + case "Account": { + const accountResponse = await services.main.getAccount(log.object_id); + switch (log.action_type) { + case "Create": { + if (accountResponse.status === 200) { + let username = accountResponse.data.username; + let accountType = accountResponse.data.is_admin ? "admin" : "kader"; + message = "Menambahkan akun " + accountType + " dengan username " + username; + } else { + message = "Menambahkan akun yang sudah dihapus"; + } + break; + } + case "Edit": { + if (accountResponse.status === 200) { + let username = accountResponse.data.username; + let accountType = accountResponse.data.is_admin ? "admin" : "kader"; + message = "Memperbarui akun " + accountType + " dengan username " + username; + } else { + message = "Memperbarui akun yang sudah dihapus"; + } + break; + } + case "Delete": { + message = "Menghapus akun" + break; + } + } + break; + } + case "Case Subject": { + const caseSubjectResponse = await services.main.getCaseSubject(log.object_id); + message = "Menambahkan subjek kasus baru atas nama " + caseSubjectResponse.data.name; + break; + } + case "Investigation Case": { + const investigationCaseResponse = await services.main.getInvestigationCase(log.object_id); + message = "Menambahkan kasus positif atas nama " + investigationCaseResponse.data.case_subject.name; + break; + } + case "Monitoring Case": { + const monitoringCaseResponse = await services.main.getMonitoringCase(log.object_id); + message = "Menambahkan objek pemantauan baru atas nama " + monitoringCaseResponse.data.investigation_case.case_subject.name; + break; + } + } + return message; + } + + const fetchLog = async (page: number) => { + const logResponse = await services.main.getLog(page); + + let logs: Log[] = []; + for (let log of logResponse.data.results) { + let logObject: Log = { + timestamp: new Date(log.recorded_at), + message: await generateLogMessage(log), + }; + logs.push(logObject); + } + setLogList(logs); + setNext(logResponse.data.next ? true : false); + setPrev(logResponse.data.previous ? true : false); + setTotalLog(logResponse.data.count); + } + + useEffect(() => { + fetchLog(page); + }, [page]); + + return ( + + + { logList.length == 0 ? + + Belum ada aktivitas yang tercatat + + : + <> + } + + { + logList?.map((log: Log, index: number) => { + return ( + + + + {log.timestamp.toLocaleString('id-ID', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + })} + + {log.message} + + + + ) + }) + } + + + {((page-1)*10 + logList.length).toString() + ' dari ' + totalLog} + + + setPage(page - 1) : () => {}} + src="/assets/icons/right-paging.svg" + origin="50% 50%" + height="16px" + opacity={prev ? 1 : 0.5} + transform="rotateZ(180deg)" + /> + + {`${page}`} + + setPage(page + 1) : () => {}} + src="/assets/icons/right-paging.svg" + height="16px" + opacity={next ? 1 : 0.5} + /> + + + + ); +} diff --git a/src/scenes/Home/index.tsx b/src/scenes/Home/index.tsx index 225a05a33e6738ce051b121a47bff86aef9d0759..4da2c52072513bece2060b460d96ae436c0e10ee 100644 --- a/src/scenes/Home/index.tsx +++ b/src/scenes/Home/index.tsx @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import { ThemeContext } from 'styled-components'; import { Box, Text, Content, Gap } from 'components'; +import ActivityLog from './components/ActivityLog'; const DEFAULT_THEME = { colors: { @@ -40,6 +41,8 @@ export default function Home() { Coming soon + + ); } diff --git a/src/services/hooks/useMainService/index.tsx b/src/services/hooks/useMainService/index.tsx index 9fa6bf6b67ac9536d94451386ca4e9983fccb907..7fe17d89358ce6c06e09f2e462f0edbe07d11199 100644 --- a/src/services/hooks/useMainService/index.tsx +++ b/src/services/hooks/useMainService/index.tsx @@ -11,6 +11,7 @@ const END_POINTS = { INVESTIGATION_CASES: createEndpoint(['cases/investigation-cases']), MONITORING_CASES: createEndpoint(['cases/monitoring-cases']), CASE_SUBJECTS: createEndpoint(['cases/case-subjects']), + LOGS: createEndpoint(['logs']), LOGIN: '/auth/token/', }; @@ -100,6 +101,26 @@ export default function useMainService(token: string) { return fetchWithAuthentication(endpoint, Method.POST, data); } + async function getAccount(id: string) { + const endpoint = END_POINTS.ACCOUNTS([id]); + return fetchWithAuthentication(endpoint, Method.GET); + } + + async function getCaseSubject(id: string) { + const endpoint = END_POINTS.CASE_SUBJECTS([id]); + return fetchWithAuthentication(endpoint, Method.GET); + } + + async function getInvestigationCase(id: string) { + const endpoint = END_POINTS.INVESTIGATION_CASES([id]); + return fetchWithAuthentication(endpoint, Method.GET); + } + + async function getMonitoringCase(id: string) { + const endpoint = END_POINTS.MONITORING_CASES([id]); + return fetchWithAuthentication(endpoint, Method.GET); + } + async function listInvestigationCases( page: number, includePositive: boolean = false ) { @@ -208,6 +229,22 @@ export default function useMainService(token: string) { return fetchWithAuthentication(endpoint, Method.PUT, body); } + async function getLog(page: number) { + if (page === 1) { + const endpoint = END_POINTS.LOGS([ + null, + ]) + return fetchWithAuthentication(endpoint, Method.GET); + } + const endpointWithPaging = END_POINTS.LOGS([ + `?page=${page}`, + ]) + return fetchWithAuthentication( + endpointWithPaging.slice(0, -1), + Method.GET, + ); + } + return { // Authentication login, @@ -217,11 +254,15 @@ export default function useMainService(token: string) { searchAccount, editAccount, addAccount, + getAccount, + deleteAccount, // InvestigationCase listInvestigationCases, searchInvestigationCases, filterInvestigationCases, - deleteAccount, + getInvestigationCase, + getCaseSubject, + getMonitoringCase, // Input Positive Cases createCaseSubject, createInvestigationCase, @@ -230,5 +271,7 @@ export default function useMainService(token: string) { searchNotCheckedMonitoringCase, editMonitoringCase, editInvestigationCase, + // Log + getLog, }; }