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,
};
}