Browse Source

first commit

fauzanooor 3 years ago
commit
86eb88891c
100 changed files with 7025 additions and 0 deletions
  1. 40 0
      actions/PT.js
  2. 48 0
      actions/auth.js
  3. 27 0
      actions/banding.js
  4. 23 0
      actions/cabutSanksi.js
  5. 12 0
      actions/docPerbaikan.js
  6. 23 0
      actions/keberatan.js
  7. 24 0
      actions/log.js
  8. 31 0
      actions/pelanggaran.js
  9. 75 0
      actions/pelaporan.js
  10. 11 0
      actions/pemeriksaan.js
  11. 22 0
      actions/penjadwalan.js
  12. 38 0
      actions/sanksi.js
  13. 19 0
      actions/user.js
  14. 48 0
      components/Banding/Riwayat.js
  15. 67 0
      components/Banding/TableSanksi.js
  16. 76 0
      components/Charts/Flot.js
  17. 43 0
      components/Charts/Morris.js
  18. 63 0
      components/Charts/chart-flot.scss
  19. 111 0
      components/Common/CardTool.js
  20. 21 0
      components/Common/Loader.js
  21. 12 0
      components/Common/Message.js
  22. 44 0
      components/Common/Now.js
  23. 11 0
      components/Common/PageLoader.js
  24. 37 0
      components/Common/Scrollable.js
  25. 77 0
      components/Common/Sparklines.js
  26. 36 0
      components/Common/Swal.js
  27. 81 0
      components/Common/ToggleFullscreen.js
  28. 47 0
      components/Common/TooltipWrapper.js
  29. 145 0
      components/Common/Translate.js
  30. 28 0
      components/Common/constants.js
  31. 49 0
      components/DocPerbaikan/Riwayat.js
  32. 52 0
      components/Extras/calendar.events.js
  33. 271 0
      components/Extras/calendar.view.js
  34. 111 0
      components/Forms/Validator.js
  35. 50 0
      components/Keberatan/Riwayat.js
  36. 67 0
      components/Keberatan/TableSanksi.js
  37. 34 0
      components/Layout/Base.js
  38. 54 0
      components/Layout/BaseHorizontal.js
  39. 11 0
      components/Layout/BasePage.js
  40. 25 0
      components/Layout/ContentWrapper.js
  41. 16 0
      components/Layout/Footer.js
  42. 21 0
      components/Layout/Head.js
  43. 224 0
      components/Layout/Header.js
  44. 259 0
      components/Layout/HeaderHorizontal.js
  45. 51 0
      components/Layout/HeaderSearch.js
  46. 67 0
      components/Layout/Menu.js
  47. 54 0
      components/Layout/MenuPT.js
  48. 398 0
      components/Layout/Offsidebar.js
  49. 34 0
      components/Layout/SettingsProvider.js
  50. 325 0
      components/Layout/Sidebar.js
  51. 57 0
      components/Layout/SidebarUserBlock.js
  52. 26 0
      components/Layout/ThemesProvider.js
  53. 83 0
      components/Main/CaseProgress.js
  54. 131 0
      components/Main/DetailLaporan.js
  55. 31 0
      components/Main/DetailPT.js
  56. 108 0
      components/Main/DetailSanksi.js
  57. 18 0
      components/Main/Header.js
  58. 136 0
      components/Main/Login.js
  59. 37 0
      components/Main/PermohonanPT.js
  60. 81 0
      components/Main/PublicPage.js
  61. 39 0
      components/Main/RiwayatEvaluasi.js
  62. 73 0
      components/Main/TableLaporan.js
  63. 67 0
      components/Main/TableSanksi.js
  64. 72 0
      components/Main/Timeline.js
  65. 56 0
      components/Maps/VectorMap.js
  66. 45 0
      components/Maps/vector-map.scss
  67. 48 0
      components/PT/CabutSanksi/Riwayat.js
  68. 55 0
      components/PT/CabutSanksi/TableSanksiJawaban.js
  69. 48 0
      components/PT/DocPerbaikan/Riwayat.js
  70. 44 0
      components/PT/JawabanBanding/DetailJawaban.js
  71. 55 0
      components/PT/JawabanBanding/TableSanksiJawaban.js
  72. 50 0
      components/PT/JawabanKeberatan/DetailJawaban.js
  73. 157 0
      components/PT/JawabanKeberatan/ModalPermohonan.js
  74. 47 0
      components/PT/JawabanKeberatan/Riwayat.js
  75. 55 0
      components/PT/JawabanKeberatan/TableSanksiJawaban.js
  76. 5 0
      components/PT/JawabanPencabutanSanksi/DetailJawaban.js
  77. 55 0
      components/PT/JawabanPencabutanSanksi/TableSanksiJawaban.js
  78. 160 0
      components/PT/Keberatan/ModalPermohonan.js
  79. 47 0
      components/PT/Keberatan/Riwayat.js
  80. 62 0
      components/PT/Riwayat.js
  81. 51 0
      components/PT/TableSanksi.js
  82. 55 0
      components/PT/TableSanksiJawaban.js
  83. 70 0
      components/PT/Timeline.js
  84. 192 0
      components/Pelaporan/InputData.js
  85. 167 0
      components/Pemeriksaan/InputEvaluasi.js
  86. 39 0
      components/Pemeriksaan/TableRiwayat.js
  87. 51 0
      components/PencabutanSanksi/Riwayat.js
  88. 67 0
      components/PencabutanSanksi/TableSanksi.js
  89. 124 0
      components/Penjadwalan/DetailLaporan.js
  90. 86 0
      components/Public/DetailLaporan.js
  91. 0 0
      components/Public/Timeline.js
  92. 147 0
      components/Sanksi/Ringkasan.js
  93. 67 0
      components/Sanksi/TableLaporan.js
  94. 88 0
      components/Sanksi/TablePenetapanSanksi.js
  95. 122 0
      components/Sanksi/UploadSurat.js
  96. 63 0
      components/Tables/Datatable.js
  97. 41 0
      config/axios.js
  98. 68 0
      config/request.js
  99. 56 0
      json/dataUser.js
  100. 10 0
      keys-note.txt

+ 40 - 0
actions/PT.js

@@ -0,0 +1,40 @@
+import { get } from "@/config/request";
+
+export const getPT = async (params) => {
+	try {
+		let url = "/perguruan-tinggi";
+		if (params) {
+			url += "?";
+			if (params.id) {
+				url += `id=${params.id}`;
+			} else if (params.search || params.pembina) {
+				const parseURL = [];
+				if (params.search) parseURL.push(`search=${params.search}`);
+				if (params.pembina) parseURL.push(`pembina=${params.pembina}`);
+				url += parseURL.join('&')
+			}
+		}
+		const response = await get(url);
+		return response.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const getPembina = async (params) => {
+	try {
+		let url = "/perguruan-tinggi/lembaga";
+		if (params) {
+			url += "?";
+			if (params.search) {
+				url += `search=${params.search}`;
+			}
+		}
+		const response = await get(url);
+		return response.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 48 - 0
actions/auth.js

@@ -0,0 +1,48 @@
+import { get } from "../config/request";
+import axiosAPI from "../config/axios";
+
+export const login = async (username, password) => {
+	try {
+		const data = {
+			username,
+			password,
+		};
+
+		const response = await axiosAPI.post("/login", data, {
+			headers: {
+				"Content-Type": "application/json",
+			},
+		});
+
+		return response.data;
+	} catch (error) {
+		if (error.response) return error.response.data;
+	}
+};
+
+export const refreshToken = async () => {
+	try {
+		const response = await axiosAPI.get("/token");
+		return response.data;
+	} catch (error) {
+		if (error.response) return error.response.data;
+	}
+};
+
+export const getUser = async () => {
+	try {
+		const response = await get("/user");
+		return response.data;
+	} catch (error) {
+		if (error.response) return error.response.data;
+	}
+};
+
+export const logout = async () => {
+	try {
+		const response = await axiosAPI.delete("/logout");
+		return response.data;
+	} catch (error) {
+		if (error.response) return error.response.data;
+	}
+};

+ 27 - 0
actions/banding.js

@@ -0,0 +1,27 @@
+import { post } from "../config/request";
+
+export const addBanding = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`/banding/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		// addLog({ status: "SUCCESS", action: "CREATE", from: { id: result.added._id, data: "banding" }, description: "membuat permohonan banding" });
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		// addLog({ status: "FAIL", action: "ADD", from: { data: "banding" }, description: error.message || "membuat permohonan banding" });
+		return false;
+	}
+};
+
+export const addJawabanBanding = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`/banding/jawaban/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		// addLog({ status: "SUCCESS", action: "CREATE", from: { id: result.added._id, data: "banding" }, description: "membuat permohonan banding" });
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		// addLog({ status: "FAIL", action: "ADD", from: { data: "banding" }, description: error.message || "membuat permohonan banding" });
+		return false;
+	}
+};

+ 23 - 0
actions/cabutSanksi.js

@@ -0,0 +1,23 @@
+import { post } from "../config/request";
+
+export const addCabutSanksi = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`http://localhost:5000/cabut-sanksi/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const addJawabanCabutSanksi = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`/cabut-sanksi/jawaban/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 12 - 0
actions/docPerbaikan.js

@@ -0,0 +1,12 @@
+import { post } from "../config/request";
+
+export const addDocPerbaikan = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`/doc-perbaikan/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 23 - 0
actions/keberatan.js

@@ -0,0 +1,23 @@
+import { post } from "../config/request";
+
+export const addKeberatan = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`/keberatan/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const addJawabanKeberatan = async ({ noSanksi, ptId }, data) => {
+	try {
+		const res = await post(`/keberatan/jawaban/add?noSanksi=${noSanksi}&ptId=${ptId}`, data);
+		console.log(res);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 24 - 0
actions/log.js

@@ -0,0 +1,24 @@
+import { get } from "../config/request";
+
+export const getLog = async ({ ptId, isPT }) => {
+	try {
+		let url = `/log?ptId=${ptId}`;
+		if (isPT) url += `&isPT=true`;
+		const res = await get(url);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const getLogPublic = async ({ ptId, laporanId }) => {
+	try {
+		let url = `/log/public?ptId=${ptId}&laporan=${laporanId}`;
+		const res = await get(url);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 31 - 0
actions/pelanggaran.js

@@ -0,0 +1,31 @@
+export const getPelanggaranId = async (id) => {
+	try {
+		const data = { id };
+		const myHeaders = new Headers();
+		myHeaders.append("Content-Type", "application/json");
+
+		const raw = JSON.stringify(data);
+
+		const requestOptions = {
+			method: "POST",
+			body: raw,
+			headers: myHeaders,
+		};
+
+		const res = await fetch("http://localhost:5000/pelanggaran", requestOptions);
+		return await res.json();
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const getPelanggaran = async () => {
+	try {
+		const res = await fetch("http://localhost:5000/pelanggaran");
+		return await res.json();
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 75 - 0
actions/pelaporan.js

@@ -0,0 +1,75 @@
+import { get, post, del, put } from "../config/request";
+import axiosAPI from "../config/axios";
+
+export const getPelaporan = async (query = {}) => {
+	try {
+		let url = "/pelaporan";
+		if (query != null) {
+			const { number, ptId, penjadwalan, pemeriksaan, active } = query;
+			url += "?";
+			const parseURL = [];
+			if (number) parseURL.push(`number=${number}`);
+			if (ptId) parseURL.push(`ptId=${ptId}`);
+			if (penjadwalan) parseURL.push(`penjadwalan=true`);
+			if (pemeriksaan) parseURL.push(`pemeriksaan=true`);
+			if (active) parseURL.push(`active=${active}`);
+			url += parseURL.join("&");
+		}
+
+		const res = await get(url);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const getPelaporanPublic = async ({ number, noHp }) => {
+	try {
+		const res = await get(`/pelaporan/public?number=${number}&noHp=${noHp}`);
+		return res.data;
+	} catch (error) {
+		console.log("error", error.response.data);
+		return false;
+	}
+};
+
+export const createPelaporan = async (data) => {
+	try {
+		const res = await axiosAPI.post("/pelaporan/create", data);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const addStatus = async ({ number, ptId }, data) => {
+	try {
+		const res = await post(`/pelaporan/status/add?number=${number}&ptId=${ptId}`, data);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const removeLaporan = async ({ number, ptId }) => {
+	try {
+		const res = await del(`/pelaporan/remove?number=${number}&ptId=${ptId}`);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const activeLaporan = async ({ number, ptId }) => {
+	try {
+		const res = await put(`/pelaporan/active?number=${number}&ptId=${ptId}`);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 11 - 0
actions/pemeriksaan.js

@@ -0,0 +1,11 @@
+import { post } from "../config/request";
+
+export const insertPemeriksaan = async ({ number, ptId }, data) => {
+	try {
+		const res = await post(`/pelaporan/pemeriksaan/create?number=${number}&ptId=${ptId}`, data);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 22 - 0
actions/penjadwalan.js

@@ -0,0 +1,22 @@
+import { post } from "../config/request";
+
+export const updateJadwal = async ({ number, ptId }, data) => {
+	try {
+		// const myHeaders = new Headers();
+		// myHeaders.append("Content-Type", "application/json");
+
+		// const raw = JSON.stringify(data);
+
+		// const requestOptions = {
+		// 	method: "POST",
+		// 	body: raw,
+		// 	headers: myHeaders,
+		// };
+
+		const res = await post(`/pelaporan/jadwal/add?number=${number}&ptId=${ptId}`, data);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 38 - 0
actions/sanksi.js

@@ -0,0 +1,38 @@
+import { post, get } from "../config/request";
+
+export const createSanksi = async ({ number, ptId }, data) => {
+	try {
+		const res = await post(`/sanksi/create?number=${number}&ptId=${ptId}`, data);
+		console.log(res);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};
+
+export const getSanksi = async (query = {}) => {
+	try {
+		let url = "http://localhost:5000/sanksi";
+		if (query != null) {
+			const { ptId, noSanksi, keberatan, jawaban, banding, active, cabutSanksi, docPerbaikan } = query;
+			url += "?";
+			const parseURL = [];
+			if (noSanksi) parseURL.push(`noSanksi=${noSanksi}`);
+			if (ptId) parseURL.push(`ptId=${ptId}`);
+			if (keberatan) parseURL.push(`keberatan=true`);
+			if (banding) parseURL.push(`banding=true`);
+			if (cabutSanksi) parseURL.push(`cabutSanksi=true`);
+			if (docPerbaikan) parseURL.push(`docPerbaikan=true`);
+			if (jawaban) parseURL.push(`jawaban=true`);
+			parseURL.push(`active=${active || "true"}`);
+			url += parseURL.join("&");
+		}
+
+		const res = await get(url);
+		return res.data;
+	} catch (error) {
+		console.log("error", error);
+		return false;
+	}
+};

+ 19 - 0
actions/user.js

@@ -0,0 +1,19 @@
+import axiosAPI from "../config/axios";
+
+export const getPublicUser = async () => {
+	try {
+		const response = await axiosAPI.get("/user/public");
+		return response.data;
+	} catch (error) {
+		if (error.response) return error.response.data;
+	}
+};
+
+export const createPublicUser = async (data) => {
+	try {
+		const response = await axiosAPI.post("/user/create", data);
+		return response.data;
+	} catch (error) {
+		if (error.response) return error.response.data;
+	}
+};

+ 48 - 0
components/Banding/Riwayat.js

@@ -0,0 +1,48 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Status</th>
+								<th>Dokumen Jawaban</th>
+							</tr>
+						</thead>
+						<tbody>
+							{data ? (
+								<tr>
+									<td>{moment(data.createAt).format("DD MMMM YYYY")}</td>
+									<td>{data.status}</td>
+									<td>
+										{data.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 67 - 0
components/Banding/TableSanksi.js

@@ -0,0 +1,67 @@
+import Datatable from "@/components/Tables/Datatable";
+import { Button } from "reactstrap";
+import Link from "next/link";
+import moment from "moment";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				{listData && (
+					<Datatable options={{ responsive: true }}>
+						<table className="table w-100">
+							<thead>
+								<tr>
+									<th>Nomor Sanksi</th>
+									<th>Keterangan Sanksi</th>
+									<th>Created</th>
+									<th>Status</th>
+									<th></th>
+								</tr>
+							</thead>
+							<tbody>
+								{listData.length
+									? listData.map((data) => {
+											return (
+												<tr key={data._id}>
+													<td>{data.sanksi.no_sanksi}</td>
+													<td>
+														<div className="media align-items-center">
+															<div className="media-body d-flex">
+																<div>
+																	<h4 className="m-0">Universitas Satyagama</h4>
+																	<p>{data.sanksi.description}</p>
+																</div>
+															</div>
+														</div>
+													</td>
+													<td>{moment(data.sanksi.createdAt).fromNow()}</td>
+													<td>{data.sanksi.banding.jawaban ? <div className="badge badge-info">Sudah Dijawab</div> : <div className="badge badge-danger">Belum Dijawab</div>}</td>
+													<td>
+														<div className="ml-auto">
+															<Link
+																href={{
+																	pathname: to,
+																	query: { noSanksi: data.sanksi.no_sanksi, ptId: data.pt_id },
+																}}
+															>
+																<Button color="primary" size="sm">
+																	{linkName}
+																</Button>
+															</Link>
+														</div>
+													</td>
+												</tr>
+											);
+									  })
+									: ""}
+							</tbody>
+						</table>
+					</Datatable>
+				)}
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 76 - 0
components/Charts/Flot.js

@@ -0,0 +1,76 @@
+import $ from 'jquery';
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import deepEqual from 'deep-equal';
+
+import './chart-flot.scss';
+
+/**
+ * Wrapper component for jquery-flot plugin
+ */
+class FlotChart extends Component {
+    static propTypes = {
+        /** data to display */
+        data: PropTypes.array.isRequired,
+        /** flot options object */
+        options: PropTypes.object.isRequired,
+        /** height of the container element */
+        height: PropTypes.string,
+        /** width of the container element */
+        width: PropTypes.string
+    }
+
+    static defaultProps = {
+        height: null,
+        width: '100%'
+    }
+
+    componentDidMount() {
+
+        // Flot Charts
+        require('flot/jquery.flot.js');
+        require('flot/jquery.flot.categories.js');
+        require('flot/jquery.flot.pie.js');
+        require('flot/jquery.flot.resize.js');
+        require('flot/jquery.flot.time.js');
+        require('jquery.flot.spline/jquery.flot.spline.js');
+        require('jquery.flot.tooltip/js/jquery.flot.tooltip.min.js');
+
+        setTimeout(() => {
+            this.drawChart();
+        }, 100);
+    }
+
+    componentDidUpdate(prevProps) {
+        if (!deepEqual(prevProps.data, this.props.data) || !deepEqual(prevProps.options, this.props.options)) {
+            this.drawChart();
+        }
+    }
+
+    componentWillUnmount() {
+        $(this.flotElement).data('plot').shutdown();
+    }
+
+    drawChart(nextProps) {
+        const data = (nextProps && nextProps.data) || this.props.data;
+        const options = (nextProps && nextProps.options) || this.props.options;
+        $.plot(this.flotElement, data, options);
+    }
+
+    setRef = node => {
+        this.flotElement = node;
+    }
+
+    render() {
+        const style = {
+            height: this.props.height,
+            width: this.props.width
+        };
+
+        return (
+            <div ref={this.setRef} style={style} {...this.props}/>
+        );
+    }
+}
+
+export default FlotChart;

+ 43 - 0
components/Charts/Morris.js

@@ -0,0 +1,43 @@
+/* global Morris */
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * Wrapper for morris chart plugin
+ */
+class MorrisChart extends Component {
+    static propTypes = {
+        /** id of the container element */
+        id: PropTypes.string.isRequired,
+        /** data to display */
+        data: PropTypes.array.isRequired,
+        /** morris option object */
+        options: PropTypes.object.isRequired,
+        /** chart type */
+        type: PropTypes.oneOf(['Line', 'Area', 'Donut', 'Bar']).isRequired
+    };
+
+    componentDidMount() {
+        // Morris.js
+        require('morris.js.so/morris.js');
+        require('morris.js.so/morris.css');
+
+        window.requestAnimationFrame(() => this.drawChart());
+    }
+
+    drawChart() {
+        const element = { element: this.props.id };
+        const data = { data: this.props.data };
+        this.chart = new Morris[this.props.type]({
+            ...element,
+            ...data,
+            ...this.props.options
+        });
+    }
+
+    render() {
+        return <div id={this.props.id} />;
+    }
+}
+
+export default MorrisChart;

+ 63 - 0
components/Charts/chart-flot.scss

@@ -0,0 +1,63 @@
+/* ========================================================================
+     Component: chart-flot
+ ======================================================================== */
+
+
+.flot-chart {
+    display: block;
+    width: 100%;
+    height: 250px;
+    .legend {
+        >table tr td {
+            padding: 3px;
+        }
+        >table tr td:first-child {
+            padding-left: 3px;
+        }
+        >table tr td:last-child {
+            padding-right: 3px;
+        }
+        >table tr+tr td {
+            padding-top: 0;
+        }
+
+        >div:first-child {
+            border-color: rgba(0, 0, 0, .1) !important;
+        }
+
+        .legendColorBox>div,
+        .legendColorBox>div>div {
+            border-radius: 400px;
+        }
+    }
+}
+
+.flot-chart-content {
+    width: 100%;
+    height: 100%;
+}
+
+// Labels for PIE CHARTS
+.flot-pie-label {
+    padding: 3px 5px;
+    font-size: 10px;
+    text-align: center;
+    color: #fff;
+}
+
+.flot-base {
+    max-width: 100% !important;
+}
+
+// Tooltip style
+// --------------------------------------
+#flotTip {
+    position: relative;
+    padding: 5px;
+    font-size: 12px !important;
+    border-radius: 2px !important;
+    border-color: transparent !important;
+    background-color: rgba(0, 0, 0, .75) !important;
+    color: #f1f1f1;
+    z-index: 5;
+}

+ 111 - 0
components/Common/CardTool.js

@@ -0,0 +1,111 @@
+// Card Tools
+// -----------------------------------
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+const checkRequiredProps = (props, propName, componentName) => {
+  if (!props.dismiss && !props.refresh) {
+    return new Error(`One of 'dismiss' or 'refresh' is required by '${componentName}' component.`)
+  }
+}
+
+/**
+ * Add action icons to card components to allow
+ * refresh data or remove a card element
+ */
+class CardTool extends Component {
+
+    static propTypes = {
+        /** show the refreshe icon */
+        refresh: checkRequiredProps,
+        /** show the remove icon */
+        dismiss: checkRequiredProps,
+        /** triggers before card is removed */
+        onRemove: PropTypes.func,
+        /** triggers after card was removed */
+        onRemoved: PropTypes.func,
+        /** triggers when user click on refresh button */
+        onRefresh: PropTypes.func,
+        /** name if the icon class to use as spinner */
+        spinner: PropTypes.string
+    }
+
+    static defaultProps = {
+        refresh: false,
+        dismiss: false,
+        onRemove: () => {},
+        onRemoved: () => {},
+        onRefresh: () => {},
+        spinner: 'standard'
+    }
+
+    /**
+     * Helper function to find the closest
+     * ascending .card element
+     */
+    getCardParent(item) {
+        var el = item.parentElement;
+        while (el && !el.classList.contains('card'))
+            el = el.parentElement
+        return el
+    }
+
+    handleDismiss = e => {
+        // find the first parent card
+        const card = this.getCardParent(this.element);
+
+        const destroyCard = () => {
+            // remove card
+            card.parentNode.removeChild(card);
+            // An event to catch when the card has been removed from DOM
+            this.props.onRemoved();
+        }
+
+        const animate = function(item, cb) {
+            if ('onanimationend' in window) { // animation supported
+                item.addEventListener('animationend', cb.bind(this))
+                item.className += ' animated bounceOut'; // requires animate.css
+            } else cb.call(this) // no animation, just remove
+        }
+
+        const confirmRemove = function() {
+            animate(card, function() {
+                destroyCard();
+            })
+        }
+
+        // Trigger the event and finally remove the element
+        this.props.onRemove(card, confirmRemove);
+
+    }
+
+    handleRefresh = e => {
+        const WHIRL_CLASS = 'whirl';
+        const card = this.getCardParent(this.element);
+
+        const showSpinner = function(card, spinner) {
+            card.classList.add(WHIRL_CLASS);
+            spinner.forEach(function(s) { card.classList.add(s) })
+        }
+
+        // method to clear the spinner when done
+        const done = () => { card.classList.remove(WHIRL_CLASS); }
+        // start showing the spinner
+        showSpinner(card, this.props.spinner.split(' '));
+        // event to remove spinner when refres is done
+        this.props.onRefresh(card, done);
+    }
+
+    setRef = node => this.element = node;
+
+    render() {
+        return (
+            <div ref={this.setRef} className="card-tool float-right">
+                { this.props.refresh && <em onClick={this.handleRefresh} className="fas fa-sync"></em> }
+                { this.props.dismiss && <em onClick={this.handleDismiss} className="fa fa-times"></em> }
+            </div>
+        )
+    }
+}
+
+export default CardTool;

+ 21 - 0
components/Common/Loader.js

@@ -0,0 +1,21 @@
+import React from "react";
+import { Spinner } from "react-bootstrap";
+
+const Loader = () => {
+	return (
+		<Spinner
+			animation="border"
+			role="status"
+			style={{
+				width: "100px",
+				height: "100px",
+				margin: "auto",
+				display: "block",
+			}}
+		>
+			<span className="sr-only">Loading...</span>
+		</Spinner>
+	);
+};
+
+export default Loader;

+ 12 - 0
components/Common/Message.js

@@ -0,0 +1,12 @@
+import React from "react";
+import { Alert } from "react-bootstrap";
+
+const Message = ({ variant, children }) => {
+	return <Alert variant={variant}>{children}</Alert>;
+};
+
+Message.defaultProps = {
+	variant: "info",
+};
+
+export default Message;

+ 44 - 0
components/Common/Now.js

@@ -0,0 +1,44 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import * as moment from 'moment';
+
+/**
+ * Updates every second the content of the element
+ * with the current time formmated
+ */
+export default class Now extends Component {
+
+    static propTypes = {
+        /** string to format current date */
+        format: PropTypes.string.isRequired
+    }
+
+    state = {
+        currentTime: null,
+        format: ''
+    }
+
+    componentDidMount() {
+        this.updateTime();
+        this.interval = setInterval(this.updateTime, 1000);
+    }
+
+    componentWillUnmount() {
+        if(this.interval)
+            clearInterval(this.interval);
+    }
+
+    updateTime = () => {
+        this.setState({
+            currentTime: moment(new Date()).format(this.props.format)
+        })
+    }
+
+    render() {
+        return (
+            <div {...this.props} style={{display: 'inline-block'}}>
+                {this.state.currentTime}
+            </div>
+        )
+    }
+}

+ 11 - 0
components/Common/PageLoader.js

@@ -0,0 +1,11 @@
+import React from 'react';
+
+// See more loading icons here:
+// https://fontawesome.com/how-to-use/on-the-web/styling/animating-icons
+const PageLoader = () => (
+    <div className="page-loader">
+        <em className="fas fa-circle-notch fa-spin fa-2x text-muted"></em>
+    </div>
+)
+
+export default PageLoader;

+ 37 - 0
components/Common/Scrollable.js

@@ -0,0 +1,37 @@
+// SLIMSCROLL
+// -----------------------------------
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+// Perfect Scrollbar
+import PerfectScrollbar from "react-perfect-scrollbar";
+
+// ensure rails are shown over the rest
+const fixRailsZIndex = ".ps__rail-y, ps__rail-x {z-index: 999999; }";
+
+const Scrollable = (props) => {
+	const scrollStyle = {
+		position: "relative",
+	};
+	if (props.height !== null) {
+		scrollStyle.maxHeight = props.height;
+	}
+	return (
+		<>
+			<style>{fixRailsZIndex}</style>
+			<PerfectScrollbar {...props} style={scrollStyle}>
+				{props.children}
+			</PerfectScrollbar>
+		</>
+	);
+};
+
+Scrollable.propTypes = {
+	/** height of the element */
+	height: PropTypes.string,
+};
+
+Scrollable.defaultProps = {
+	height: "250px",
+};
+
+export default Scrollable;

+ 77 - 0
components/Common/Sparklines.js

@@ -0,0 +1,77 @@
+// SPARKLINE
+// -----------------------------------
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+
+const RESIZE_EVENT = 'resize.sparkline';
+
+/**
+ * Wrapper for for jquery-sparkline plugin
+ */
+export default class Sparkline extends Component {
+
+    static propTypes = {
+        /** sparkline options object */
+        options: PropTypes.object.isRequired,
+        /** tag to use, defaults to div */
+        tag: PropTypes.string,
+        /** values to display, allows array or csv string */
+        values: PropTypes.oneOfType([
+            PropTypes.string.isRequired,
+            PropTypes.array.isRequired
+        ])
+    }
+
+    static defaultProps = {
+        options: {},
+        tag: 'div'
+    }
+
+    state = {
+        values: this.props.values,
+        options: this.props.options
+    }
+
+    normalizeParams() {
+        let { options, values } = this.state;
+
+        options.disableHiddenCheck = true; // allow draw when initially is not visible
+        options.type = options.type || 'bar'; // default chart is bar
+        values = Array.isArray(values) ? values : values.split(','); // support array of csv strings
+
+        this.setState({ options, values });
+    }
+
+    componentDidMount() {
+        this.normalizeParams();
+        // Sparklines
+        require('jquery-sparkline/jquery.sparkline.min.js');
+
+        // init sparkline
+        $(this.element).sparkline(this.state.values, this.state.options);
+
+        // allow responsive
+        if (this.state.options.resize) {
+            $(window).on(RESIZE_EVENT, () => {
+                $(this.element).sparkline(this.state.values, this.state.options);
+            });
+        }
+    }
+
+    componentWillUnmount() {
+        $(window).off(RESIZE_EVENT);
+        $(this.element).sparkline('destroy');
+    }
+
+    setRef = node => {
+        this.element = node;
+    }
+
+    render() {
+        const {tag:Tag} = this.props;
+        return (
+            <Tag ref={this.setRef} {...this.props}></Tag>
+        )
+    }
+}

+ 36 - 0
components/Common/Swal.js

@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+// Sweet Alert
+import swal from 'sweetalert';
+
+/**
+ * Wrapper component for sweetalert plugin
+ */
+const Swal = props => {
+
+    const handleClick = e => {
+        e.preventDefault();
+        // pass swal reference so is possible to chain popups
+        swal(props.options).then(p => props.callback(p, swal));
+    }
+
+    const { callback, ...rest } = props;
+    return (
+        <div {...rest} onClick={handleClick}>
+            {props.children}
+        </div>
+    )
+}
+
+Swal.propType = {
+    /** swal options object */
+    options: PropTypes.object.isRequired,
+    /** callback function for swal response */
+    callback: PropTypes.func
+}
+
+Swal.defaultProps = {
+    callback: () => {}
+}
+
+export default Swal;

+ 81 - 0
components/Common/ToggleFullscreen.js

@@ -0,0 +1,81 @@
+// FULLSCREEN
+// -----------------------------------
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import screenfull from 'screenfull';
+
+const FULLSCREEN_ON_ICON = 'fa fa-expand';
+const FULLSCREEN_OFF_ICON = 'fa fa-compress';
+
+/**
+ * Wrapper for screenfull plugin
+ * Wraps child element and toggles
+ * fullscreen mode on click
+ */
+export default class ToggleFullscreen extends Component {
+    static propTypes = {
+        /** tag to use, defaults to A */
+        tag: PropTypes.string
+    };
+
+    static defaultProps = {
+        tag: 'a'
+    };
+
+    state = {
+        iconClass: FULLSCREEN_ON_ICON
+    };
+
+    componentDidMount() {
+        this.fsToggler = this.element;
+
+        // Not supported under IE
+        const ua = window.navigator.userAgent;
+        if (ua.indexOf('MSIE ') > 0 || !!ua.match(/Trident.*rv:11\./)) {
+            this.fsToggler.style.display = 'none';
+            return; // and abort
+        }
+
+        this.fsToggler.addEventListener('click', this.handleClisk);
+
+        if (screenfull.raw && screenfull.raw.fullscreenchange)
+            document.addEventListener(screenfull.raw.fullscreenchange, this.toggleFSIcon);
+    }
+
+    handleClisk = e => {
+        e.preventDefault();
+
+        if (screenfull.enabled) {
+            screenfull.toggle();
+
+            // Switch icon indicator
+            this.toggleFSIcon();
+        } else {
+            console.log('Fullscreen not enabled');
+        }
+    };
+
+    toggleFSIcon = () => {
+        this.setState({
+            iconClass: screenfull.isFullscreen ? FULLSCREEN_OFF_ICON : FULLSCREEN_ON_ICON
+        });
+    };
+
+    componentWillUnmount() {
+        this.fsToggler.removeEventListener('click', this.handleClisk);
+        document.removeEventListener(screenfull.raw.fullscreenchange, this.toggleFSIcon);
+    }
+
+    setRef = node => {
+        this.element = node;
+    };
+
+    render() {
+        const { tag: Tag } = this.props;
+        return (
+            <Tag ref={this.setRef} {...this.props}>
+                <em className={this.state.iconClass} />
+            </Tag>
+        );
+    }
+}

+ 47 - 0
components/Common/TooltipWrapper.js

@@ -0,0 +1,47 @@
+import React, { Component } from 'react';
+import { Tooltip } from 'reactstrap';
+
+// track id generation
+let idCounter = 0;
+// return unique id number
+const UUID = () => idCounter++;
+// reset to sync client/server rendering
+export const resetUUID = () => idCounter = 0;
+
+/**
+ * Wrap an element and assign automatically an ID,
+ * creates a handler to show/hide tooltips without
+ * the hassle of creating new states and class methods.
+ * Support only one child and simple text content.
+ */
+
+class TooltipWrapper extends Component {
+    // static propTypes { content: PropTypes.string }
+    state = {
+        _id: 'id4tooltip_' + UUID(),
+        tooltipOpen: false
+    };
+    toggle = e => {
+        this.setState({ tooltipOpen: !this.state.tooltipOpen });
+    };
+    render() {
+        return [
+            <Tooltip
+                {...this.props}
+                isOpen={this.state.tooltipOpen}
+                toggle={this.toggle}
+                target={this.state._id}
+                placement={this.props.placement}
+                key="1"
+            >
+                {this.props.content}
+            </Tooltip>,
+            React.cloneElement(React.Children.only(this.props.children), {
+                id: this.state._id,
+                key: '2'
+            })
+        ];
+    }
+}
+
+export default TooltipWrapper;

+ 145 - 0
components/Common/Translate.js

@@ -0,0 +1,145 @@
+import React, { useContext, useState } from 'react';
+import PropTypes from 'prop-types';
+
+const DEFAULT_LANGUAGE = 'en';
+const LANGUAGES_PATH = 'static/locales';
+const LANGUAGES_FILE = 'translations.json';
+const VARIABLE_REGEX = /\{([^}]+)\}/g;
+
+export const store = {
+    /* dictionaries */
+};
+
+/**
+ * Set a dictionary for a specific language
+ * @param  {String} lang language
+ * @param  {Object} data dictionary
+ * @return {Object}      dictionary added
+ */
+export const setDict = (lang, data) => (store[lang] = data);
+
+/**
+ * Get a dictionary for specific language
+ * @param  {String} lang language
+ * @return {Object}      dictionary
+ */
+export const getDict = lang => store[lang];
+
+/**
+ * Fetch a dictionary for specific language defined in JSON format
+ * @param  {Strong} lang language
+ * @return {Object}      dictionary loaded
+ */
+export const fetchStore = async lang => {
+    if (!store[lang]) {
+        const res = await fetch(`/${LANGUAGES_PATH}/${lang}/${LANGUAGES_FILE}`);
+        store[lang] = await res.json();
+    }
+    return store[lang];
+};
+
+/**
+ * Interpolates values given in 'params'
+ * @param  {String} str    text
+ * @param  {Object} params object to interpolate
+ * @return {String}
+ */
+const compile = (str = '', params) => {
+    const matches = str.match(VARIABLE_REGEX);
+    if (matches) {
+        matches
+            .map(v => v.replace(/\{|\}/g, ''))
+            .forEach(v => (str = str.replace('{' + v + '}', params[v])));
+    }
+    return str;
+};
+
+/**
+ * Reads an object value using dot notation for nested keys
+ * @param  {Object} obj  object to parse
+ * @param  {String} skey key to search
+ * @return {String|null}
+ */
+export const accessKey = (obj, skey = '') => skey.split('.').reduce((a, b) => a && a[b], obj);
+
+/**
+ * Returns the translated text for given key and interpolates values in 'params'
+ * @param  {String} key    the key to search for
+ * @param  {String} lang   language
+ * @param  {Object} params object with params to interpolate
+ * @return {[type]}        [description]
+ */
+export const translateKey = (key, lang, params = {}) => compile(accessKey(getDict(lang), key));
+
+// =====================
+// REACT INTERFACE
+// =====================
+
+/**
+ * Context used to handle translation in the component tree
+ */
+const TranslateContext = React.createContext({
+    language: DEFAULT_LANGUAGE,
+    changeLanguage: () => {}
+});
+
+/**
+ * Component provider to pass down context to child components
+ * Must be used to wrap application
+ */
+export class Provider extends React.Component {
+    changeLanguage = language => {
+        fetchStore(language).then(() => {
+            this.setState(state => ({
+                language
+            }));
+        });
+    };
+
+    state = {
+        language: DEFAULT_LANGUAGE,
+        changeLanguage: this.changeLanguage
+    };
+
+    constructor(props) {
+        super(props);
+        if (props.store) {
+            Object.keys(props.store).forEach(l => setDict(l, props.store[l]));
+        }
+    }
+
+    render() {
+        return (
+            <TranslateContext.Provider value={this.state}>
+                {this.props.children}
+            </TranslateContext.Provider>
+        );
+    }
+}
+
+/**
+ * HOC to provide 'changeLanguage' and 't' method to WrappedComponent
+ */
+export function withTranslation(WrappedComponent) {
+    return function TranslatedComponent(props) {
+        const { language, changeLanguage } = useContext(TranslateContext);
+        const t = (k, l, p) => translateKey(k, l || language, p);
+        return <WrappedComponent changeLanguage={changeLanguage} t={t} {...props} />;
+    };
+}
+
+/**
+ * Component to translate a given key
+ * If key is missing, render children without changes
+ */
+export const Trans = props => {
+    const { language } = useContext(TranslateContext);
+    const { i18nKey, lang, params, children } = props;
+    return [translateKey(i18nKey, lang || language, params) || children];
+};
+
+Trans.proptypes = {
+    i18nKey: PropTypes.string.isRequired,
+    lang: PropTypes.string,
+    params: PropTypes.object
+};

+ 28 - 0
components/Common/constants.js

@@ -0,0 +1,28 @@
+// GLOBAL CONSTANTS
+// -----------------------------------
+
+export const APP_COLORS = {
+    'primary':                '#5d9cec',
+    'success':                '#27c24c',
+    'info':                   '#23b7e5',
+    'warning':                '#ff902b',
+    'danger':                 '#f05050',
+    'inverse':                '#131e26',
+    'green':                  '#37bc9b',
+    'pink':                   '#f532e5',
+    'purple':                 '#7266ba',
+    'dark':                   '#3a3f51',
+    'yellow':                 '#fad732',
+    'gray-darker':            '#232735',
+    'gray-dark':              '#3a3f51',
+    'gray':                   '#dde6e9',
+    'gray-light':             '#e4eaec',
+    'gray-lighter':           '#edf1f2'
+};
+
+export const APP_MEDIAQUERY = {
+    'desktopLG':             1200,
+    'desktop':                992,
+    'tablet':                 768,
+    'mobile':                 480
+};

+ 49 - 0
components/DocPerbaikan/Riwayat.js

@@ -0,0 +1,49 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	console.log(data);
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Keterangan</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{data.length
+								? data.map((value) => (
+										<tr>
+											<td>{moment(value.createAt).format("DD MMMM YYYY")}</td>
+											<td>{value.description}</td>
+											<td>
+												{value.files.map((e) => (
+													<>
+														<em className="fa-lg far fa-file-code"></em>
+														<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+															{e.name}
+														</a>
+													</>
+												))}
+											</td>
+										</tr>
+								  ))
+								: ""}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 52 - 0
components/Extras/calendar.events.js

@@ -0,0 +1,52 @@
+// Date for the calendar events (dummy data)
+var date = new Date();
+var d = date.getDate(),
+    m = date.getMonth(),
+    y = date.getFullYear();
+
+export default [
+    {
+        title: 'Jadwal Pemeriksaan - BI:88389',
+        start: new Date(y, m, 1),
+        backgroundColor: '#f56954', //red
+        borderColor: '#f56954' //red
+    },
+    {
+        title: 'Jadwal Pemeriksaan - BI:77589',
+        start: new Date(y, m, d - 5),
+        end: new Date(y, m, d - 2),
+        backgroundColor: '#f39c12', //yellow
+        borderColor: '#f39c12' //yellow
+    },
+    {
+        title: 'Jadwal Pemeriksaan - BI:36458',
+        start: new Date(y, m, d, 10, 30),
+        allDay: false,
+        backgroundColor: '#0073b7', //Blue
+        borderColor: '#0073b7' //Blue
+    },
+    {
+        title: 'Jadwal Pemeriksaan - BI:36KLP',
+        start: new Date(y, m, d, 12, 0),
+        end: new Date(y, m, d, 14, 0),
+        allDay: false,
+        backgroundColor: '#00c0ef', //Info (aqua)
+        borderColor: '#00c0ef' //Info (aqua)
+    },
+    {
+        title: 'Jadwal Pemeriksaan - BI:36589',
+        start: new Date(y, m, d + 1, 19, 0),
+        end: new Date(y, m, d + 1, 22, 30),
+        allDay: false,
+        backgroundColor: '#00a65a', //Success (green)
+        borderColor: '#00a65a' //Success (green)
+    },
+    {
+        title: 'Jadwal Pemeriksaan - BI:56989',
+        start: new Date(y, m, 28),
+        end: new Date(y, m, 29),
+        url: '//google.com/',
+        backgroundColor: '#3c8dbc', //Primary (light-blue)
+        borderColor: '#3c8dbc' //Primary (light-blue)
+    }
+];

+ 271 - 0
components/Extras/calendar.view.js

@@ -0,0 +1,271 @@
+import React, { Component } from "react";
+import ContentWrapper from "@/components/Layout/ContentWrapper";
+import { Card, CardBody, CardHeader, CardTitle } from "reactstrap";
+import { getPelaporan, addStatus, removeLaporan, activeLaporan } from "@/actions/pelaporan";
+import { updateJadwal } from "@/actions/penjadwalan";
+import DetailLaporan from "@/components/Main/DetailLaporan";
+import Link from "next/link";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import interactionPlugin, { Draggable } from "@fullcalendar/interaction";
+import listPlugin from "@fullcalendar/list";
+import bootstrapPlugin from "@fullcalendar/bootstrap";
+import events from "./calendar.events";
+import Select from "react-select";
+import moment from "moment";
+
+const status = [
+	{ value: "Ditindaklanjuti Dikti Ristek", label: "Ditindaklanjuti Dikti Ristek", className: "State-ACT" },
+	{ value: "Delegasi ke LLDIKTI", label: "Delegasi ke LLDIKTI", className: "State-ACT" },
+	{ value: "Ditutup", label: "Ditutup", className: "State-ACT" },
+];
+class Calendar extends Component {
+	calendarEvents = events;
+
+	calendarPlugins = [interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin, bootstrapPlugin];
+
+	calendarHeader = {
+		left: "prev,next today",
+		center: "title",
+		right: "dayGridMonth,timeGridWeek,timeGridDay",
+	};
+
+	constructor(props) {
+		super(props);
+		this.state = {
+			selectedEvent: null,
+			evRemoveOnDrop: true,
+			evNewName: "",
+			externalEvents: [],
+			dataLaporan: [],
+			dataEvent: [],
+			laporan: {},
+			selectedOption: null,
+		};
+	}
+
+	static getInitialProps = ({ query }) => ({ query });
+
+	async componentDidMount() {
+		/* initialize the external events */
+		new Draggable(this.refs.externalEventsList, {
+			itemSelector: ".fce-event",
+			eventData: function (eventEl) {
+				return {
+					title: eventEl.innerText.trim(),
+				};
+			},
+		});
+
+		const dataLaporan = await getPelaporan({ penjadwalan: true, active: true });
+		const laporan = await getPelaporan({ number: this.props.query.number, ptId: this.props.query.ptId });
+
+		this.setState({ dataLaporan });
+		this.getDataEvent();
+		await this.defaultStatus(laporan.data[0]);
+		// const cek = this.state.dataLaporan.data.filter((e) => e._number === this.props.query.number && e.pt_id == this.props.query.ptId)[0];
+		this.setState({ laporan });
+		let color = "#" + Math.floor(Math.random() * 16777215).toString(16);
+		if (laporan.data[0].penjadwalan) {
+			color = laporan.data[0].penjadwalan.background_color;
+		}
+		this.setState({ externalEvents: [{ id: this.props.query.number, color, name: `Jadwal Pemeriksaan - No.Laporan : ${this.props.query.number} - ${laporan.data[0].pt.nama}`, allDay: true }] });
+	}
+
+	getDataEvent = () => {
+		const dataEvent = this.state.dataLaporan.data
+			.filter((e) => e.penjadwalan)
+			.map((e) => ({
+				id: e._id,
+				title: e.penjadwalan.title,
+				start: new Date(e.penjadwalan.from_date),
+				end: new Date(e.penjadwalan.to_date),
+				backgroundColor: e.penjadwalan.background_color, //red
+				borderColor: e.penjadwalan.background_color,
+			}));
+		this.setState({ dataEvent });
+	};
+
+	eventClick = (info) => {
+		const data = {
+			title: info.event.title,
+			start: moment(info.event.start).format("DD MMMM YYYY"),
+			end: moment(info.event.end - 1).format("DD MMMM YYYY"),
+		};
+		this.setState({ selectedEvent: data });
+	};
+
+	addEvent(event) {
+		this.calendarEvents.push(event);
+	}
+
+	handleEventReceive = (info) => {
+		var styles = getComputedStyle(info.draggedEl);
+		info.event.setProp("backgroundColor", styles.backgroundColor);
+		info.event.setProp("borderColor", styles.borderColor);
+		this.handleEventCalendar(info);
+	};
+
+	handleEventCalendar = async ({ event }) => {
+		const number = this.props.query.number;
+		const ptId = this.props.query.ptId;
+		const data = {
+			title: event.title,
+			from_date: event.start,
+			to_date: event.end || event.start,
+			background_color: event.backgroundColor,
+		};
+
+		const update = await updateJadwal({ number, ptId }, data);
+	};
+
+	defaultStatus = async (data) => {
+		const { ptId, number } = this.props.query;
+		if (!data.status) {
+			await addStatus({ number, ptId }, { status: status[0].value });
+			return this.setState({ selectedOption: status[0] });
+		}
+		return this.setState({ selectedOption: status.filter((e) => e.value === data.status)[0] });
+	};
+
+	handleChangeSelect = async (selectedOption) => {
+		const { ptId, number } = this.props.query;
+		this.setState({ selectedOption });
+		await addStatus({ number, ptId }, { status: selectedOption.value });
+		if (selectedOption.value === "Ditutup") {
+			await removeLaporan({ number, ptId });
+		} else if (!this.state.laporan.data[0].active) {
+			await activeLaporan({ number, ptId });
+		}
+	};
+
+	render() {
+		const { externalEvents, laporan, selectedOption, selectedEvent } = this.state;
+		return (
+			<ContentWrapper>
+				<div className="content-heading">
+					<div>Jadwal Pemeriksaan</div>
+					<div className="ml-auto">
+						<Link href="/app/penjadwalan">
+							<button className="btn btn-sm btn-secondary text-sm">&lt; back</button>
+						</Link>
+					</div>
+				</div>
+				<div className="calendar-app">
+					<div class="row">
+						<div class="col">
+							<Card className="card-default">
+								<CardBody>{laporan.data && <DetailLaporan noStatus query={this.props.query} data={laporan.data[0]} handleChangeSelect={this.handleChangeSelect} />}</CardBody>
+							</Card>
+						</div>
+					</div>
+					<div className="row">
+						<div className="col-xl-4 col-lg-5">
+							<div className="row">
+								<div className="col-lg-12 col-md-6 col-12">
+									<Card className="card-default">
+										<CardHeader>
+											<CardTitle tag="h4">Status Pelaporan</CardTitle>
+										</CardHeader>
+										<CardBody>
+											<Select value={selectedOption} onChange={this.handleChangeSelect} options={status} required />
+										</CardBody>
+									</Card>
+									{selectedOption?.value === "Ditutup" ? (
+										""
+									) : (
+										<>
+											<Card className="card-default" title="">
+												<CardHeader>
+													<CardTitle tag="h4">Daftar Pemeriksaan</CardTitle>
+												</CardHeader>
+												<CardBody>
+													<div className="external-events" ref="externalEventsList">
+														{externalEvents.map((ev) => (
+															<div
+																className="fce-event"
+																style={{
+																	backgroundColor: ev.color,
+																}}
+																key={ev.name + ev.color}
+																data-id={ev.id}
+															>
+																{ev.name}
+															</div>
+														))}
+													</div>
+												</CardBody>
+											</Card>
+											{laporan.data && laporan.data[0].penjadwalan && (
+												<Card className="card-default">
+													<CardHeader>
+														<CardTitle tag="h4">Jadwal Pemeriksaan</CardTitle>
+													</CardHeader>
+													<CardBody>
+														<table className="table">
+															<tbody>
+																<tr>
+																	<td>Judul</td>
+																	<td>{laporan.data[0].penjadwalan.title}</td>
+																</tr>
+																<tr>
+																	<td>Waktu</td>
+																	<td>{`${moment(laporan.data[0].penjadwalan.from_date).format("DD MMMM YYYY")} ${
+																		moment(laporan.data[0].penjadwalan.from_date).format("DD MMMM YYYY") === moment(laporan.data[0].penjadwalan.to_date).format("DD MMMM YYYY")
+																			? ""
+																			: `- ${moment(laporan.data[0].penjadwalan.to_date).add(-1, "d").format("DD MMMM YYYY")}`
+																	}`}</td>
+																</tr>
+															</tbody>
+														</table>
+													</CardBody>
+												</Card>
+											)}
+										</>
+									)}
+									<div className="mb-3">
+										{selectedEvent && (
+											<div>
+												<p>Selected:</p>
+												<div className="box-placeholder">{JSON.stringify(selectedEvent)}</div>
+											</div>
+										)}
+										{!selectedEvent && (
+											<div>
+												<p>Click calendar to show information</p>
+											</div>
+										)}
+									</div>
+								</div>
+							</div>
+						</div>
+						<div className="col-xl-8 col-lg-7">
+							<Card className="card-default">
+								<CardBody>
+									{/* START calendar */}
+									<FullCalendar
+										defaultView={this.dayGridMonth}
+										plugins={this.calendarPlugins}
+										events={this.state.dataEvent}
+										themeSystem={"bootstrap"}
+										header={this.calendarHeader}
+										editable={true}
+										droppable={true}
+										deepChangeDetection={true}
+										eventClick={this.eventClick}
+										eventReceive={this.handleEventReceive}
+										eventDrop={this.handleEventCalendar}
+										eventResize={this.handleEventCalendar}
+									></FullCalendar>
+								</CardBody>
+							</Card>
+						</div>
+					</div>
+				</div>
+			</ContentWrapper>
+		);
+	}
+}
+
+export default Calendar;

+ 111 - 0
components/Forms/Validator.js

@@ -0,0 +1,111 @@
+// https://github.com/chriso/validator.js
+import validator from 'validator';
+
+/**
+ * Helper methods to validate form inputs
+ * using controlled components
+ */
+const FormValidator = {
+    /**
+     * Validate input element
+     * @param element Dome element of the input
+     * Uses the following attributes
+     *     data-validate: array in json format with validation methods
+     *     data-param: used to provide arguments for certain methods.
+     */
+    validate(element) {
+
+        const isCheckbox = element.type === 'checkbox';
+        const value = isCheckbox ? element.checked : element.value;
+        const name = element.name;
+
+        if (!name) throw new Error('Input name must not be empty.');
+
+        // use getAttribute to support IE10+
+        const param = element.getAttribute('data-param');
+        const validations = JSON.parse(element.getAttribute('data-validate'));
+
+        let result = []
+        if(validations && validations.length) {
+            /*  Result of each validation must be true if the input is invalid
+                and false if valid. */
+            validations.forEach(m => {
+                switch (m) {
+                    case 'required':
+                        result[m] = isCheckbox ? value === false : validator.isEmpty(value)
+                        break;
+                    case 'email':
+                        result[m] = !validator.isEmail(value)
+                        break;
+                    case 'number':
+                        result[m] = !validator.isNumeric(value)
+                        break;
+                    case 'integer':
+                        result[m] = !validator.isInt(value)
+                        break;
+                    case 'alphanum':
+                        result[m] = !validator.isAlphanumeric(value)
+                        break;
+                    case 'url':
+                        result[m] = !validator.isURL(value)
+                        break;
+                    case 'equalto':
+                        // here we expect a valid ID as param
+                        const value2 = document.getElementById(param).value;
+                        result[m] = !validator.equals(value, value2)
+                        break;
+                    case 'minlen':
+                        result[m] = !validator.isLength(value, { min: param })
+                        break;
+                    case 'maxlen':
+                        result[m] = !validator.isLength(value, { max: param })
+                        break;
+                    case 'len':
+                        const [min, max] = JSON.parse(param)
+                        result[m] = !validator.isLength(value, { min, max })
+                        break;
+                    case 'min':
+                        result[m] = !validator.isInt(value, { min: validator.toInt(param) })
+                        break;
+                    case 'max':
+                        result[m] = !validator.isInt(value, { max: validator.toInt(param) })
+                        break;
+                    case 'list':
+                        const list = JSON.parse(param)
+                        result[m] = !validator.isIn(value, list)
+                        break;
+                    default:
+                        throw new Error('Unrecognized validator.');
+                }
+
+            })
+        }
+
+        return result;
+    },
+
+    /**
+     * Bulk validation of input elements.
+     * Used with form elements collection.
+     * @param  {Array} inputs Array for DOM element
+     * @return {Object}       Contains array of error and a flag to
+     *                        indicate if there was a validation error
+     */
+    bulkValidate(inputs) {
+        let errors = {},
+            hasError = false;
+
+        inputs.forEach(input => {
+            let result = this.validate(input)
+            errors = { ...errors, [input.name]: result }
+            if (!hasError) hasError = Object.keys(result).some(val => result[val])
+        })
+
+        return {
+            errors,
+            hasError
+        }
+    }
+}
+
+export default FormValidator;

+ 50 - 0
components/Keberatan/Riwayat.js

@@ -0,0 +1,50 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Status</th>
+								<th>Keterangan Jawaban</th>
+								<th>Dokumen Jawaban</th>
+							</tr>
+						</thead>
+						<tbody>
+							{data ? (
+								<tr>
+									<td>{moment(data.createAt).format("DD MMMM YYYY")}</td>
+									<td>{data.status}</td>
+									<td>{data.description}</td>
+									<td>
+										{data.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 67 - 0
components/Keberatan/TableSanksi.js

@@ -0,0 +1,67 @@
+import Datatable from "@/components/Tables/Datatable";
+import { Button } from "reactstrap";
+import Link from "next/link";
+import moment from "moment";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				{listData && (
+					<Datatable options={{ responsive: true }}>
+						<table className="table w-100">
+							<thead>
+								<tr>
+									<th>Nomor Sanksi</th>
+									<th>Keterangan Sanksi</th>
+									<th>Created</th>
+									<th>Status</th>
+									<th></th>
+								</tr>
+							</thead>
+							<tbody>
+								{listData.length
+									? listData.map((data) => {
+											return (
+												<tr key={data._id}>
+													<td>{data.sanksi.no_sanksi}</td>
+													<td>
+														<div className="media align-items-center">
+															<div className="media-body d-flex">
+																<div>
+																	<h4 className="m-0">Universitas Satyagama</h4>
+																	<p>{data.sanksi.description}</p>
+																</div>
+															</div>
+														</div>
+													</td>
+													<td>{moment(data.sanksi.createdAt).fromNow()}</td>
+													<td>{data.sanksi.keberatan.jawaban ? <div className="badge badge-info">Sudah Dijawab</div> : <div className="badge badge-danger">Belum Dijawab</div>}</td>
+													<td>
+														<div className="ml-auto">
+															<Link
+																href={{
+																	pathname: to,
+																	query: { noSanksi: data.sanksi.no_sanksi, ptId: data.pt_id },
+																}}
+															>
+																<Button color="primary" size="sm">
+																	{linkName}
+																</Button>
+															</Link>
+														</div>
+													</td>
+												</tr>
+											);
+									  })
+									: ""}
+							</tbody>
+						</table>
+					</Datatable>
+				)}
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 34 - 0
components/Layout/Base.js

@@ -0,0 +1,34 @@
+import React from 'react';
+
+import Head from './Head'
+import Header from './Header'
+import Sidebar from './Sidebar'
+import Offsidebar from './Offsidebar'
+import Footer from './Footer'
+import SettingsProvider from './SettingsProvider'
+import ThemesProvider from './ThemesProvider'
+
+const Base = props => (
+    <ThemesProvider>
+        <SettingsProvider>
+            <div className="wrapper">
+
+                <Head />
+
+                <Header />
+
+                <Sidebar />
+
+                <Offsidebar />
+
+                <section className="section-container">
+                    { props.children }
+                </section>
+
+                <Footer />
+            </div>
+        </SettingsProvider>
+    </ThemesProvider>
+)
+
+export default Base;

+ 54 - 0
components/Layout/BaseHorizontal.js

@@ -0,0 +1,54 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as actions from '../../store/actions/actions';
+
+import Head from './Head';
+import HeaderHorizontal from './HeaderHorizontal';
+import Offsidebar from './Offsidebar';
+import Footer from './Footer';
+import SettingsProvider from './SettingsProvider';
+import ThemesProvider from './ThemesProvider';
+
+class BaseHorizontal extends Component {
+    /* Toggle Horizontal layout for demo purposes.
+        Set the 'horizontal' flag using redux in the settingsReducer
+        and remove bwloe methods so it gets rendered on the server
+    */
+    componentWillMount = () => this.props.actions.changeSetting('horizontal', true);
+    componentWillUnmount = () => this.props.actions.changeSetting('horizontal', false);
+
+    render() {
+        return (
+            <ThemesProvider>
+                <SettingsProvider>
+                    <div className="wrapper">
+                        <Head />
+
+                        <HeaderHorizontal />
+
+                        <Offsidebar />
+
+                        <section className="section-container">{this.props.children}</section>
+
+                        <Footer />
+                    </div>
+                </SettingsProvider>
+            </ThemesProvider>
+        );
+    }
+}
+
+BaseHorizontal.propTypes = {
+    actions: PropTypes.object,
+    settings: PropTypes.object
+};
+
+const mapStateToProps = state => ({ settings: state.settings });
+const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(actions, dispatch) });
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(BaseHorizontal);

+ 11 - 0
components/Layout/BasePage.js

@@ -0,0 +1,11 @@
+import React from 'react';
+import Head from './Head';
+
+const BasePage = props => (
+    <>
+        <Head />
+        <div className="wrapper">{props.children}</div>
+    </>
+);
+
+export default BasePage;

+ 25 - 0
components/Layout/ContentWrapper.js

@@ -0,0 +1,25 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * Wrapper element for template content
+ */
+const ContentWrapper = props =>(
+    <div className="content-wrapper">
+        {props.unwrap ?
+            (<div className="unwrap">{props.children}</div>)
+            :
+            (props.children)
+        }
+    </div>
+)
+
+ContentWrapper.propTypes = {
+    /** add element with 'unwrap' class to expand content area */
+    unwrap: PropTypes.bool
+}
+ContentWrapper.defaultProps = {
+    unwrap: false
+}
+
+export default ContentWrapper;

+ 16 - 0
components/Layout/Footer.js

@@ -0,0 +1,16 @@
+import React, { Component } from 'react';
+
+class Footer extends Component {
+
+    render() {
+        const year = new Date().getFullYear()
+        return (
+            <footer className="footer-container">
+                <span>&copy; {year}</span>
+            </footer>
+        );
+    }
+
+}
+
+export default Footer;

+ 21 - 0
components/Layout/Head.js

@@ -0,0 +1,21 @@
+import React from "react";
+import NextHead from "next/head";
+import PropTypes from "prop-types";
+
+const defaultDescription = "";
+
+const Head = (props) => (
+	<NextHead>
+		<meta charSet="UTF-8" />
+		<title>PTB-Ristekdikti</title>
+		<meta name="description" content={props.description || defaultDescription} />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+		<link rel="icon" href="/static/img/logo-single.png" />
+	</NextHead>
+);
+
+Head.propTypes = {
+	description: PropTypes.string,
+};
+
+export default Head;

+ 224 - 0
components/Layout/Header.js

@@ -0,0 +1,224 @@
+import React, { Component } from "react";
+import Router from "next/router";
+import PropTypes from "prop-types";
+import Link from "next/link";
+import { UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem, ListGroup, ListGroupItem } from "reactstrap";
+import { logout } from "@/actions/auth";
+
+import { connect } from "react-redux";
+import { bindActionCreators } from "redux";
+import * as actions from "../../store/actions/actions";
+
+import ToggleFullscreen from "../Common/ToggleFullscreen";
+import HeaderSearch from "./HeaderSearch";
+
+class Header extends Component {
+	state = {
+		navSearchOpen: false,
+	};
+
+	toggleNavSearch = (e) => {
+		e.preventDefault();
+		this.setState({
+			navSearchOpen: !this.state.navSearchOpen,
+		});
+	};
+
+	closeNavSearch = (e) => {
+		e.preventDefault();
+		this.setState({
+			navSearchOpen: false,
+		});
+	};
+
+	toggleUserblock = (e) => {
+		e.preventDefault();
+		this.props.actions.toggleSetting("showUserBlock");
+	};
+
+	toggleOffsidebar = (e) => {
+		e.preventDefault();
+		this.props.actions.toggleSetting("offsidebarOpen");
+	};
+
+	toggleCollapsed = (e) => {
+		e.preventDefault();
+		this.props.actions.toggleSetting("isCollapsed");
+		this.resize();
+	};
+
+	toggleAside = (e) => {
+		e.preventDefault();
+		this.props.actions.toggleSetting("asideToggled");
+	};
+
+	handleLogout = async (e) => {
+		e.preventDefault();
+		const cek = await logout();
+		if (cek.success) {
+			Router.push({ pathname: "/app" });
+		}
+	};
+
+	resize() {
+		// all IE friendly dispatchEvent
+		var evt = document.createEvent("UIEvents");
+		evt.initUIEvent("resize", true, false, window, 0);
+		window.dispatchEvent(evt);
+		// modern dispatchEvent way
+		// window.dispatchEvent(new Event('resize'));
+	}
+
+	render() {
+		return (
+			<header className="topnavbar-wrapper">
+				{/* START Top Navbar */}
+				<nav className="navbar topnavbar">
+					{/* START navbar header */}
+					<div className="navbar-header">
+						<a className="navbar-brand" href="#/">
+							<div className="brand-logo">
+								<img className="img-fluid" src="/static/img/logo-inner.png" alt="App Logo" />
+							</div>
+							<div className="brand-logo-collapsed">
+								<img className="img-fluid" src="/static/img/logo-single.png" alt="App Logo" />
+							</div>
+						</a>
+					</div>
+					{/* END navbar header */}
+
+					{/* START Left navbar */}
+					<ul className="navbar-nav mr-auto flex-row">
+						<li className="nav-item">
+							{/* Button used to collapse the left sidebar. Only visible on tablet and desktops */}
+							<a href="" className="nav-link d-none d-md-block d-lg-block d-xl-block" onClick={this.toggleCollapsed}>
+								<em className="fas fa-bars" />
+							</a>
+							{/* Button to show/hide the sidebar on mobile. Visible on mobile only. */}
+							<a href="" className="nav-link sidebar-toggle d-md-none" onClick={this.toggleAside}>
+								<em className="fas fa-bars" />
+							</a>
+						</li>
+						{/* START User avatar toggle */}
+						{/* <li className="nav-item d-none d-md-block">
+							<a className="nav-link" onClick={this.toggleUserblock}>
+								<em className="icon-user" />
+							</a>
+						</li> */}
+						{/* END User avatar toggle */}
+						{/* START lock screen */}
+						{/* <li className="nav-item d-none d-md-block">
+							<Link href="/pages/lock" as="/lock">
+								<a title="Lock screen" className="nav-link">
+									<em className="icon-lock" />
+								</a>
+							</Link>
+						</li> */}
+						{/* END lock screen */}
+					</ul>
+					{/* END Left navbar */}
+					{/* START Right Navbar */}
+					<ul className="navbar-nav flex-row">
+						{/* Search icon */}
+						{/* <li className="nav-item">
+							<a className="nav-link" href="" onClick={this.toggleNavSearch}>
+								<em className="icon-magnifier" />
+							</a>
+						</li> */}
+						{/* Fullscreen (only desktops) */}
+						<li className="nav-item d-none d-md-block">
+							<ToggleFullscreen className="nav-link" />
+						</li>
+						{/* START Alert menu */}
+						<UncontrolledDropdown nav inNavbar className="dropdown-list">
+							<DropdownToggle nav className="dropdown-toggle-nocaret">
+								<em className="icon-bell" />
+								<span className="badge badge-danger">11</span>
+							</DropdownToggle>
+							{/* START Dropdown menu */}
+							<DropdownMenu right className="dropdown-menu-right animated flipInX">
+								<DropdownItem>
+									{/* START list group */}
+									<ListGroup>
+										<ListGroupItem action tag="a" href="" onClick={(e) => e.preventDefault()}>
+											<div className="media">
+												<div className="align-self-start mr-2">
+													<em className="fab fa-twitter fa-2x text-info" />
+												</div>
+												<div className="media-body">
+													<p className="m-0">New followers</p>
+													<p className="m-0 text-muted text-sm">1 new follower</p>
+												</div>
+											</div>
+										</ListGroupItem>
+										<ListGroupItem action tag="a" href="" onClick={(e) => e.preventDefault()}>
+											<div className="media">
+												<div className="align-self-start mr-2">
+													<em className="fa fa-envelope fa-2x text-warning" />
+												</div>
+												<div className="media-body">
+													<p className="m-0">New e-mails</p>
+													<p className="m-0 text-muted text-sm">You have 10 new emails</p>
+												</div>
+											</div>
+										</ListGroupItem>
+										<ListGroupItem action tag="a" href="" onClick={(e) => e.preventDefault()}>
+											<div className="media">
+												<div className="align-self-start mr-2">
+													<em className="fa fa-tasks fa-2x text-success" />
+												</div>
+												<div className="media-body">
+													<p className="m-0">Pending Tasks</p>
+													<p className="m-0 text-muted text-sm">11 pending task</p>
+												</div>
+											</div>
+										</ListGroupItem>
+										<ListGroupItem action tag="a" href="" onClick={(e) => e.preventDefault()}>
+											<span className="d-flex align-items-center">
+												<span className="text-sm">More notifications</span>
+												<span className="badge badge-danger ml-auto">14</span>
+											</span>
+										</ListGroupItem>
+									</ListGroup>
+									{/* END list group */}
+								</DropdownItem>
+							</DropdownMenu>
+							{/* END Dropdown menu */}
+						</UncontrolledDropdown>
+						{/* END Alert menu */}
+						<li className="nav-item">
+							<a className="nav-link" href="" onClick={this.handleLogout}>
+								<em className="icon-logout" />
+							</a>
+						</li>
+						{/* START Offsidebar button */}
+						<li className="nav-item">
+							<a className="nav-link" href="" onClick={this.toggleOffsidebar}>
+								<em className="icon-notebook" />
+							</a>
+						</li>
+						{/* END Offsidebar menu */}
+					</ul>
+					{/* END Right Navbar */}
+
+					{/* START Search form */}
+					<HeaderSearch isOpen={this.state.navSearchOpen} onClose={this.closeNavSearch} />
+					{/* END Search form */}
+				</nav>
+				{/* END Top Navbar */}
+			</header>
+		);
+	}
+}
+
+Header.propTypes = {
+	actions: PropTypes.object,
+	settings: PropTypes.object,
+};
+
+const mapStateToProps = (state) => ({ settings: state.settings });
+const mapDispatchToProps = (dispatch) => ({
+	actions: bindActionCreators(actions, dispatch),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Header);

+ 259 - 0
components/Layout/HeaderHorizontal.js

@@ -0,0 +1,259 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Link from 'next/link';
+import {
+    UncontrolledDropdown,
+    DropdownToggle,
+    DropdownMenu,
+    DropdownItem,
+    ListGroup,
+    ListGroupItem,
+    Nav,
+    Collapse,
+    NavItem,
+    NavLink,
+    NavbarToggler
+} from 'reactstrap';
+
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as actions from '../../store/actions/actions';
+
+import ToggleFullscreen from '../Common/ToggleFullscreen';
+import HeaderSearch from './HeaderSearch';
+
+import Menu from './Menu.js';
+
+class HeaderHorizontal extends Component {
+    state = {
+        navSearchOpen: false,
+        isOpen: false
+    };
+
+    toggleNavSearch = e => {
+        e.preventDefault();
+        this.setState({
+            navSearchOpen: !this.state.navSearchOpen
+        });
+    };
+
+    closeNavSearch = e => {
+        e.preventDefault();
+        this.setState({
+            navSearchOpen: false
+        });
+    };
+
+    toggle = () => {
+        this.setState({
+            isOpen: !this.state.isOpen
+        });
+    };
+
+    toggleOffsidebar = e => {
+        e.preventDefault();
+        this.props.actions.toggleSetting('offsidebarOpen');
+    };
+
+    /** map menu config to string to determine which element to render */
+    itemType = item => {
+        if (item.heading) return 'heading';
+        if (!item.submenu) return 'menu';
+        if (item.submenu) return 'submenu';
+    };
+
+    render() {
+        return (
+            <header className="topnavbar-wrapper">
+                {/* START Top Navbar */}
+                <nav className="navbar topnavbar navbar-expand-lg navbar-light">
+                    {/* START navbar header */}
+                    <div className="navbar-header">
+                        <a className="navbar-brand" href="#/">
+                            <div className="brand-logo">
+                                {/* <img
+                                    className="img-fluid"
+                                    src="/static/img/logo-inner.png"
+                                    alt="App Logo"
+                                /> */}
+                            </div>
+                            <div className="brand-logo-collapsed">
+                                <img
+                                    className="img-fluid"
+                                    src="/static/img/logo-single.png"
+                                    alt="App Logo"
+                                />
+                            </div>
+                        </a>
+                        <NavbarToggler onClick={this.toggle} />
+                    </div>
+                    {/* END navbar header */}
+                    {/* START Nav wrapper */}
+                    <Collapse isOpen={this.state.isOpen} navbar>
+                        <Nav navbar className="mr-auto flex-column flex-lg-row">
+                            {Menu.map((item, i) => {
+                                if (this.itemType(item) === 'menu') {
+                                    return (
+                                        <NavItem key={i}>
+                                            <Link href={item.path}>
+                                                <NavLink>{item.name}</NavLink>
+                                            </Link>
+                                        </NavItem>
+                                    );
+                                }
+                                if (this.itemType(item) === 'submenu') {
+                                    return (
+                                        <UncontrolledDropdown nav inNavbar key={i}>
+                                            <DropdownToggle nav>{item.name}</DropdownToggle>
+                                            <DropdownMenu className="animated fadeIn">
+                                                {item.submenu.map((sitem, si) => {
+                                                    return (
+                                                        <Link href={sitem.path} key={si}>
+                                                            <DropdownItem>
+                                                                {sitem.name}
+                                                            </DropdownItem>
+                                                        </Link>
+                                                    );
+                                                })}
+                                            </DropdownMenu>
+                                        </UncontrolledDropdown>
+                                    );
+                                }
+                            })}
+                            {/* END Left navbar */}
+                        </Nav>
+                        <Nav className="flex-row" navbar>
+                            {/* Search icon */}
+                            <NavItem>
+                                <NavLink onClick={this.toggleNavSearch}>
+                                    <em className="icon-magnifier" />
+                                </NavLink>
+                            </NavItem>
+                            {/* Fullscreen (only desktops) */}
+                            <NavItem className="d-none d-md-block">
+                                <ToggleFullscreen className="nav-link" />
+                            </NavItem>
+                            {/* START Alert menu */}
+                            <UncontrolledDropdown nav inNavbar className="dropdown-list">
+                                <DropdownToggle nav className="dropdown-toggle-nocaret">
+                                    <em className="icon-bell" />
+                                    <span className="badge badge-danger">11</span>
+                                </DropdownToggle>
+                                {/* START Dropdown menu */}
+                                <DropdownMenu
+                                    right
+                                    className="dropdown-menu-right animated flipInX"
+                                >
+                                    <DropdownItem>
+                                        {/* START list group */}
+                                        <ListGroup>
+                                            <ListGroupItem
+                                                action
+                                                tag="a"
+                                                href=""
+                                                onClick={e => e.preventDefault()}
+                                            >
+                                                <div className="media">
+                                                    <div className="align-self-start mr-2">
+                                                        <em className="fab fa-twitter fa-2x text-info" />
+                                                    </div>
+                                                    <div className="media-body">
+                                                        <p className="m-0">New followers</p>
+                                                        <p className="m-0 text-muted text-sm">
+                                                            1 new follower
+                                                        </p>
+                                                    </div>
+                                                </div>
+                                            </ListGroupItem>
+                                            <ListGroupItem
+                                                action
+                                                tag="a"
+                                                href=""
+                                                onClick={e => e.preventDefault()}
+                                            >
+                                                <div className="media">
+                                                    <div className="align-self-start mr-2">
+                                                        <em className="fa fa-envelope fa-2x text-warning" />
+                                                    </div>
+                                                    <div className="media-body">
+                                                        <p className="m-0">New e-mails</p>
+                                                        <p className="m-0 text-muted text-sm">
+                                                            You have 10 new emails
+                                                        </p>
+                                                    </div>
+                                                </div>
+                                            </ListGroupItem>
+                                            <ListGroupItem
+                                                action
+                                                tag="a"
+                                                href=""
+                                                onClick={e => e.preventDefault()}
+                                            >
+                                                <div className="media">
+                                                    <div className="align-self-start mr-2">
+                                                        <em className="fa fa-tasks fa-2x text-success" />
+                                                    </div>
+                                                    <div className="media-body">
+                                                        <p className="m-0">Pending Tasks</p>
+                                                        <p className="m-0 text-muted text-sm">
+                                                            11 pending task
+                                                        </p>
+                                                    </div>
+                                                </div>
+                                            </ListGroupItem>
+                                            <ListGroupItem
+                                                action
+                                                tag="a"
+                                                href=""
+                                                onClick={e => e.preventDefault()}
+                                            >
+                                                <span className="d-flex align-items-center">
+                                                    <span className="text-sm">
+                                                        More notifications
+                                                    </span>
+                                                    <span className="badge badge-danger ml-auto">
+                                                        14
+                                                    </span>
+                                                </span>
+                                            </ListGroupItem>
+                                        </ListGroup>
+                                        {/* END list group */}
+                                    </DropdownItem>
+                                </DropdownMenu>
+                                {/* END Dropdown menu */}
+                            </UncontrolledDropdown>
+                            {/* END Alert menu */}
+                            {/* START Offsidebar button */}
+                            <NavItem>
+                                <NavLink href="" onClick={this.toggleOffsidebar}>
+                                    <em className="icon-notebook" />
+                                </NavLink>
+                            </NavItem>
+                            {/* END Offsidebar menu */}
+                        </Nav>
+                    </Collapse>
+                    {/* END Nav wrapper */}
+                    {/* START Search form */}
+                    <HeaderSearch isOpen={this.state.navSearchOpen} onClose={this.closeNavSearch} />
+                    {/* END Search form */}
+                </nav>
+                {/* END Top Navbar */}
+            </header>
+        );
+    }
+}
+
+HeaderHorizontal.propTypes = {
+    actions: PropTypes.object,
+    settings: PropTypes.object
+};
+
+const mapStateToProps = state => ({ settings: state.settings });
+const mapDispatchToProps = dispatch => ({
+    actions: bindActionCreators(actions, dispatch)
+});
+
+export default connect(
+    mapStateToProps,
+    mapDispatchToProps
+)(HeaderHorizontal);

+ 51 - 0
components/Layout/HeaderSearch.js

@@ -0,0 +1,51 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+class HeaderSearch extends Component {
+    componentDidMount() {
+        document.addEventListener('keydown', this.closeNavSearchKey);
+    }
+
+    componentWillUnmount() {
+        document.removeEventListener('keydown', this.closeNavSearchKey);
+    }
+
+    setInputSearch = isOpen => input => {
+        if (input) input[isOpen ? 'focus' : 'blur']();
+    };
+
+    closeNavSearchKey = e => {
+        if (e.keyCode === 27) this.props.onClose(e);
+    };
+
+    render() {
+        const { isOpen, onClose } = this.props;
+        return (
+            <form
+                className={'navbar-form ' + (isOpen ? 'open' : '')}
+                role="search"
+                action="search.html"
+            >
+                <div className="form-group">
+                    <input
+                        ref={this.setInputSearch(isOpen)}
+                        className="form-control"
+                        type="text"
+                        placeholder="Type and hit enter ..."
+                    />
+                    <div className="fa fa-times navbar-form-close" onClick={onClose} />
+                </div>
+                <button className="d-none" type="submit">
+                    Submit
+                </button>
+            </form>
+        );
+    }
+}
+
+HeaderSearch.propTypes = {
+    isOpen: PropTypes.bool,
+    onClose: PropTypes.func
+};
+
+export default HeaderSearch;

+ 67 - 0
components/Layout/Menu.js

@@ -0,0 +1,67 @@
+const Menu = [
+	{
+		heading: "Main Navigation",
+		translate: "sidebar.heading.HEADER",
+	},
+	{
+		name: "Pemantauan",
+		path: "/app/pemantauan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PEMANTAUAN",
+		label: { value: 3, color: "success" },
+	},
+	{
+		name: "Pelaporan",
+		path: "/app/pelaporan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PELAPORAN",
+	},
+	{
+		name: "Penjadwalan Evaluasi",
+		path: "/app/penjadwalan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PENJADWALAN",
+	},
+	{
+		name: "Pemeriksaan",
+		path: "/app/pemeriksaan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PEMERIKSAAN",
+	},
+	{
+		name: "Sanksi",
+		path: "/app/sanksi",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.SANKSI",
+	},
+	{
+		heading: "Dikti Ristek/LLDIKTI",
+		translate: "sidebar.heading.DIKTI_RISTEK",
+	},
+	{
+		name: "Keberatan",
+		path: "/app/keberatan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.KEBERATAN",
+	},
+	{
+		name: "Banding",
+		path: "/app/banding",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.BANDING",
+	},
+	{
+		name: "Permohonan Pencabutan Sanksi",
+		path: "/app/pencabutan-sanksi",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PENCABUTAN_SANKSI",
+	},
+	{
+		name: "Pemantauan Perbaikan",
+		path: "/app/pemantauan-perbaikan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PEMANTAUAN_PERBAIKAN",
+	},
+];
+
+export default Menu;

+ 54 - 0
components/Layout/MenuPT.js

@@ -0,0 +1,54 @@
+const MenuPT = [
+	{
+		heading: "Main Navigation",
+		translate: "sidebar.heading.HEADER",
+	},
+	{
+		name: "Pemantauan",
+		path: "/app/pt/pemantauan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PT_PEMANTAUAN",
+	},
+	{
+		name: "Pengajuan Keberatan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PENGAJUAN_KEBERATAN",
+		submenu: [
+			{
+				name: "a. Permohonan Keberatan",
+				path: "/app/pt/keberatan",
+			},
+			{
+				name: "b. Jawaban atas permohonan keberatan",
+				path: "/app/pt/jawaban-keberatan",
+			},
+			{
+				name: "c. Jawaban atas permohonan banding",
+				path: "/app/pt/jawaban-banding",
+			},
+		],
+	},
+	{
+		name: "Pencabutan Sanksi",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PENCABUTAN_SANKSI",
+		submenu: [
+			{
+				name: "Permohonan",
+				path: "/app/pt/pencabutan-sanksi",
+			},
+			{
+				name: "Jawaban",
+				path: "/app/pt/jawaban-pencabutan-sanksi",
+			},
+		],
+	},
+	{
+		name: "Dokumen Perbaikan",
+		path: "/app/pt/dokumen-perbaikan",
+		icon: "icon-notebook",
+		translate: "sidebar.nav.PT_DOKUMEN_PERBAIKAN",
+	},
+];
+
+export default MenuPT;

+ 398 - 0
components/Layout/Offsidebar.js

@@ -0,0 +1,398 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+import { bindActionCreators } from "redux";
+import * as actions from "../../store/actions/actions";
+
+import { TabContent, TabPane, Nav, NavItem, NavLink } from "reactstrap";
+
+class Offsidebar extends Component {
+	state = {
+		activeTab: "settings",
+		offsidebarReady: false,
+	};
+
+	componentDidMount() {
+		// When mounted display the offsidebar
+		window.requestAnimationFrame(() => this.setState({ offsidebarReady: true }));
+	}
+
+	toggle = (tab) => {
+		if (this.state.activeTab !== tab) {
+			this.setState({
+				activeTab: tab,
+			});
+		}
+	};
+
+	handleSettingCheckbox = (event) => {
+		this.props.actions.changeSetting(event.target.name, event.target.checked);
+	};
+
+	handleThemeRadio = (event) => {
+		this.props.actions.changeTheme(event.target.value);
+	};
+
+	render() {
+		return (
+			this.state.offsidebarReady && (
+				<aside className="offsidebar">
+					{/* START Off Sidebar (right) */}
+					<nav>
+						<div>
+							{/* Nav tabs */}
+							<Nav tabs justified>
+								<NavItem>
+									<NavLink
+										className={this.state.activeTab === "settings" ? "active" : ""}
+										onClick={() => {
+											this.toggle("settings");
+										}}
+									>
+										<em className="icon-equalizer fa-lg"></em>
+									</NavLink>
+								</NavItem>
+								{/* <NavItem>
+                                <NavLink className={ this.state.activeTab === 'chat' ? 'active':'' }
+                                    onClick={() => { this.toggle('chat'); }}
+                                >
+                                    <em className="icon-user fa-lg"></em>
+                                </NavLink>
+                            </NavItem> */}
+							</Nav>
+							{/* Tab panes */}
+							<TabContent activeTab={this.state.activeTab}>
+								<TabPane tabId="settings">
+									<h3 className="text-center text-thin mt-4">Settings</h3>
+									<div className="p-2">
+										<h4 className="text-muted text-thin">Themes</h4>
+										<div className="row row-flush mb-2">
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-a"} value="theme-a" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-info"></span>
+															<span className="color bg-info-light"></span>
+														</span>
+														<span className="color bg-white"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-b"} value="theme-b" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-green"></span>
+															<span className="color bg-green-light"></span>
+														</span>
+														<span className="color bg-white"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-c"} value="theme-c" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-purple"></span>
+															<span className="color bg-purple-light"></span>
+														</span>
+														<span className="color bg-white"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-d"} value="theme-d" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-danger"></span>
+															<span className="color bg-danger-light"></span>
+														</span>
+														<span className="color bg-white"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-e"} value="theme-e" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-info-dark"></span>
+															<span className="color bg-info"></span>
+														</span>
+														<span className="color bg-gray-dark"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-f"} value="theme-f" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-green-dark"></span>
+															<span className="color bg-green"></span>
+														</span>
+														<span className="color bg-gray-dark"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-g"} value="theme-g" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-purple-dark"></span>
+															<span className="color bg-purple"></span>
+														</span>
+														<span className="color bg-gray-dark"></span>
+													</label>
+												</div>
+											</div>
+											<div className="col-3 mb-3">
+												<div className="setting-color">
+													<label>
+														<input type="radio" name="setting-theme" checked={this.props.theme.name === "theme-h"} value="theme-h" onChange={this.handleThemeRadio} />
+														<span className="icon-check"></span>
+														<span className="split">
+															<span className="color bg-danger-dark"></span>
+															<span className="color bg-danger"></span>
+														</span>
+														<span className="color bg-gray-dark"></span>
+													</label>
+												</div>
+											</div>
+										</div>
+									</div>
+									<div className="p-2">
+										<h4 className="text-muted text-thin">Layout</h4>
+										<div className="clearfix">
+											<p className="float-left">Fixed</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-fixed" type="checkbox" name="isFixed" checked={this.props.settings.isFixed} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+										<div className="clearfix">
+											<p className="float-left">Boxed</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-boxed" type="checkbox" name="isBoxed" checked={this.props.settings.isBoxed} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+									</div>
+									<div className="p-2">
+										<h4 className="text-muted text-thin">Aside</h4>
+										<div className="clearfix">
+											<p className="float-left">Collapsed</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-collapsed" type="checkbox" name="isCollapsed" checked={this.props.settings.isCollapsed} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+										<div className="clearfix">
+											<p className="float-left">Collapsed Text</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-collapsed-text" type="checkbox" name="isCollapsedText" checked={this.props.settings.isCollapsedText} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+										<div className="clearfix">
+											<p className="float-left">Float</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-float" type="checkbox" name="isFloat" checked={this.props.settings.isFloat} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+										<div className="clearfix">
+											<p className="float-left">Hover</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-hover" type="checkbox" name="asideHover" checked={this.props.settings.asideHover} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+										<div className="clearfix">
+											<p className="float-left">Show Scrollbar</p>
+											<div className="float-right">
+												<label className="switch">
+													<input id="chk-scrollbar" type="checkbox" name="asideScrollbar" checked={this.props.settings.asideScrollbar} onChange={this.handleSettingCheckbox} />
+													<span></span>
+												</label>
+											</div>
+										</div>
+									</div>
+								</TabPane>
+								<TabPane tabId="chat">
+									<h3 className="text-center text-thin mt-4">Connections</h3>
+									<div className="list-group">
+										{/* START list title */}
+										<div className="list-group-item border-0">
+											<small className="text-muted">ONLINE</small>
+										</div>
+										{/* END list title */}
+										<div className="list-group-item list-group-item-action border-0">
+											<div className="media">
+												<img className="align-self-center mr-3 rounded-circle thumb48" src="/static/img/user/05.jpg" alt="User avatar" />
+												<div className="media-body text-truncate">
+													<a href="">
+														<strong>Juan Sims</strong>
+													</a>
+													<br />
+													<small className="text-muted">Designeer</small>
+												</div>
+												<div className="ml-auto">
+													<span className="circle bg-success circle-lg"></span>
+												</div>
+											</div>
+										</div>
+										<div className="list-group-item list-group-item-action border-0">
+											<div className="media">
+												<img className="align-self-center mr-3 rounded-circle thumb48" src="/static/img/user/06.jpg" alt="User avatar" />
+												<div className="media-body text-truncate">
+													<a href="">
+														<strong>Maureen Jenkins</strong>
+													</a>
+													<br />
+													<small className="text-muted">Designeer</small>
+												</div>
+												<div className="ml-auto">
+													<span className="circle bg-success circle-lg"></span>
+												</div>
+											</div>
+										</div>
+										<div className="list-group-item list-group-item-action border-0">
+											<div className="media">
+												<img className="align-self-center mr-3 rounded-circle thumb48" src="/static/img/user/07.jpg" alt="User avatar" />
+												<div className="media-body text-truncate">
+													<a href="">
+														<strong>Billie Dunn</strong>
+													</a>
+													<br />
+													<small className="text-muted">Designeer</small>
+												</div>
+												<div className="ml-auto">
+													<span className="circle bg-danger circle-lg"></span>
+												</div>
+											</div>
+										</div>
+										<div className="list-group-item list-group-item-action border-0">
+											<div className="media">
+												<img className="align-self-center mr-3 rounded-circle thumb48" src="/static/img/user/08.jpg" alt="User avatar" />
+												<div className="media-body text-truncate">
+													<a href="">
+														<strong>Tomothy Roberts</strong>
+													</a>
+													<br />
+													<small className="text-muted">Designeer</small>
+												</div>
+												<div className="ml-auto">
+													<span className="circle bg-warning circle-lg"></span>
+												</div>
+											</div>
+										</div>
+										{/* START list title */}
+										<div className="list-group-item border-0">
+											<small className="text-muted">OFFLINE</small>
+										</div>
+										{/* END list title */}
+										<div className="list-group-item list-group-item-action border-0">
+											<div className="media">
+												<img className="align-self-center mr-3 rounded-circle thumb48" src="/static/img/user/09.jpg" alt="User avatar" />
+												<div className="media-body text-truncate">
+													<a href="">
+														<strong>Lawrence Robinson</strong>
+													</a>
+													<br />
+													<small className="text-muted">Designeer</small>
+												</div>
+												<div className="ml-auto">
+													<span className="circle bg-warning circle-lg"></span>
+												</div>
+											</div>
+										</div>
+										<div className="list-group-item list-group-item-action border-0">
+											<div className="media">
+												<img className="align-self-center mr-3 rounded-circle thumb48" src="/static/img/user/10.jpg" alt="User avatar" />
+												<div className="media-body text-truncate">
+													<a href="">
+														<strong>Tyrone Owens</strong>
+													</a>
+													<br />
+													<small className="text-muted">Designeer</small>
+												</div>
+												<div className="ml-auto">
+													<span className="circle bg-warning circle-lg"></span>
+												</div>
+											</div>
+										</div>
+									</div>
+									<div className="px-3 py-4 text-center">
+										{/* Optional link to list more users */}
+										<a className="btn btn-purple btn-sm" href="" title="See more contacts">
+											<strong>Load more..</strong>
+										</a>
+									</div>
+									{/* Extra items */}
+									<div className="px-3 py-2">
+										<p>
+											<small className="text-muted">Tasks completion</small>
+										</p>
+										<div className="progress progress-xs m-0">
+											<div className="progress-bar bg-success" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100" style={{ width: "80%" }}>
+												<span className="sr-only">80% Complete</span>
+											</div>
+										</div>
+									</div>
+									<div className="px-3 py-2">
+										<p>
+											<small className="text-muted">Upload quota</small>
+										</p>
+										<div className="progress progress-xs m-0">
+											<div className="progress-bar bg-warning" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style={{ width: "40%" }}>
+												<span className="sr-only">40% Complete</span>
+											</div>
+										</div>
+									</div>
+								</TabPane>
+							</TabContent>
+						</div>
+					</nav>
+					{/* END Off Sidebar (right) */}
+				</aside>
+			)
+		);
+	}
+}
+
+Offsidebar.propTypes = {
+	actions: PropTypes.object,
+	settings: PropTypes.object,
+	theme: PropTypes.object,
+};
+
+const mapStateToProps = (state) => ({ settings: state.settings, theme: state.theme });
+const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators(actions, dispatch) });
+
+export default connect(mapStateToProps, mapDispatchToProps)(Offsidebar);

+ 34 - 0
components/Layout/SettingsProvider.js

@@ -0,0 +1,34 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+const getClasses = settings => {
+    let c = [];
+
+    if (settings.isFixed) c.push('layout-fixed');
+    if (settings.isBoxed) c.push('layout-boxed');
+    if (settings.isCollapsed) c.push('aside-collapsed');
+    if (settings.isCollapsedText) c.push('aside-collapsed-text');
+    if (settings.isFloat) c.push('aside-float');
+    if (settings.asideHover) c.push('aside-hover');
+    if (settings.offsidebarOpen) c.push('offsidebar-open');
+    if (settings.asideToggled) c.push('aside-toggled');
+    // layout horizontal
+    if (settings.horizontal) c.push('layout-h');
+
+    return c.join(' ');
+};
+
+const SettingsProvider = props => (
+    <div id="__settings_provider" className={getClasses(props.settings)}>
+        {props.children}
+    </div>
+);
+
+SettingsProvider.propTypes = {
+    settings: PropTypes.object
+};
+
+export default connect(
+    state => ({ settings: state.settings })
+)(SettingsProvider);

+ 325 - 0
components/Layout/Sidebar.js

@@ -0,0 +1,325 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { withTranslation, Trans } from "@/components/Common/Translate";
+import Link from "next/link";
+import Router, { withRouter } from "next/router";
+import { Collapse, Badge } from "reactstrap";
+
+import { connect } from "react-redux";
+import { bindActionCreators } from "redux";
+import * as actions from "../../store/actions/actions";
+
+import SidebarUserBlock from "./SidebarUserBlock";
+// import { getUser } from "@/actions/auth";
+
+import Menu from "./Menu.js";
+import MenuPT from "./MenuPT.js";
+// localStorage.getItem("user");
+// import Menu from './MenuPT.js';
+
+// Helper to check for parrent of an given elements
+const parents = (element, selector) => {
+	if (typeof selector !== "string") {
+		return null;
+	}
+
+	const parents = [];
+	let ancestor = element.parentNode;
+
+	while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== 3 /*NODE_TEXT*/) {
+		if (ancestor.matches(selector)) {
+			parents.push(ancestor);
+		}
+
+		ancestor = ancestor.parentNode;
+	}
+	return parents;
+};
+// Helper to get outerHeight of a dom element
+const outerHeight = (elem, includeMargin) => {
+	const style = getComputedStyle(elem);
+	const margins = includeMargin ? parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10) : 0;
+	return elem.offsetHeight + margins;
+};
+
+/**
+    Component to display headings on sidebar
+*/
+const SidebarItemHeader = ({ item }) => (
+	<li className="nav-heading">
+		<span>
+			<Trans i18nKey={item.translate}>{item.heading}</Trans>
+		</span>
+	</li>
+);
+
+/**
+    Normal items for the sidebar
+*/
+const SidebarItem = ({ item, isActive, className, onMouseEnter }) => (
+	<li className={isActive ? "active" : ""} onMouseEnter={onMouseEnter}>
+		<Link href={item.path} as={item.as}>
+			<a title={item.name}>
+				{item.label && (
+					<Badge tag="div" className="float-right" color={item.label.color}>
+						{item.label.value}
+					</Badge>
+				)}
+				{item.icon && <em className={item.icon} />}
+				<span>
+					<Trans i18nKey={item.translate}>{item.name}</Trans>
+				</span>
+			</a>
+		</Link>
+	</li>
+);
+
+/**
+    Build a sub menu with items inside and attach collapse behavior
+*/
+const SidebarSubItem = ({ item, isActive, handler, children, isOpen, onMouseEnter }) => (
+	<li className={isActive ? "active" : ""}>
+		<div className="nav-item" onClick={handler} onMouseEnter={onMouseEnter}>
+			{item.label && (
+				<Badge tag="div" className="float-right" color={item.label.color}>
+					{item.label.value}
+				</Badge>
+			)}
+			{item.icon && <em className={item.icon} />}
+			<span>
+				<Trans i18nKey={item.translate}>{item.name}</Trans>
+			</span>
+		</div>
+		<Collapse isOpen={isOpen}>
+			<ul id={item.path} className="sidebar-nav sidebar-subnav">
+				{children}
+			</ul>
+		</Collapse>
+	</li>
+);
+
+/**
+    Component used to display a header on menu when using collapsed/hover mode
+*/
+const SidebarSubHeader = ({ item }) => <li className="sidebar-subnav-header">{item.name}</li>;
+
+const SidebarBackdrop = ({ closeFloatingNav }) => <div className="sidebar-backdrop" onClick={closeFloatingNav} />;
+
+const FloatingNav = ({ item, target, routeActive, isFixed, closeFloatingNav }) => {
+	let asideContainer = document.querySelector(".aside-container");
+	let asideInner = asideContainer.firstElementChild; /*('.aside-inner')*/
+	let sidebar = asideInner.firstElementChild; /*('.sidebar')*/
+
+	let mar = parseInt(getComputedStyle(asideInner)["padding-top"], 0) + parseInt(getComputedStyle(asideContainer)["padding-top"], 0);
+	let itemTop = target.parentElement.offsetTop + mar - sidebar.scrollTop;
+	let vwHeight = document.body.clientHeight;
+
+	const setPositionStyle = (el) => {
+		if (!el) return;
+		el.style.position = isFixed ? "fixed" : "absolute";
+		el.style.top = itemTop + "px";
+		el.style.bottom = outerHeight(el, true) + itemTop > vwHeight ? 0 : "auto";
+	};
+
+	return (
+		<ul id={item.path} ref={setPositionStyle} className="sidebar-nav sidebar-subnav nav-floating" onMouseLeave={closeFloatingNav}>
+			<SidebarSubHeader item={item} />
+			{item.submenu.map((subitem, i) => (
+				<SidebarItem key={i} item={subitem} isActive={routeActive(subitem.path)} />
+			))}
+		</ul>
+	);
+};
+
+/**
+    The main sidebar component
+*/
+class Sidebar extends Component {
+	menu = [];
+	state = {
+		collapse: {},
+		showSidebarBackdrop: false,
+		currentFloatingItem: null,
+		currentFloatingItemTarget: null,
+		pathname: this.props.router.pathname,
+	};
+
+	async componentDidMount() {
+		// const user = await getUser();
+		const user = this.props.user;
+		this.menu = user.peran[0].peran.id === 2022 ? MenuPT : Menu;
+		// prepare the flags to handle menu collapsed states
+		this.buildCollapseList();
+
+		// Listen for routes changes in order to hide the sidebar on mobile
+		Router.events.on("routeChangeStart", this.handleRouteChange);
+		Router.events.on("routeChangeComplete", this.handleRouteComplete);
+
+		// Attach event listener to automatically close sidebar when click outside
+		document.addEventListener("click", this.closeSidebarOnExternalClicks);
+	}
+
+	handleRouteComplete = (pathname) => {
+		this.setState({
+			pathname,
+		});
+	};
+
+	handleRouteChange = () => {
+		this.closeFloatingNav();
+		this.closeSidebar();
+	};
+
+	componentWillUnmount() {
+		document.removeEventListener("click", this.closeSidebarOnExternalClicks);
+		Router.events.off("routeChangeStart", this.handleRouteChange);
+		Router.events.off("routeChangeComplete", this.handleRouteComplete);
+	}
+
+	closeSidebar = () => {
+		this.props.actions.toggleSetting("asideToggled");
+	};
+
+	closeSidebarOnExternalClicks = (e) => {
+		// don't check if sidebar not visible
+		if (!this.props.settings.asideToggled) return;
+
+		if (
+			!parents(e.target, ".aside-container").length && // if not child of sidebar
+			!parents(e.target, ".topnavbar-wrapper").length && // if not child of header
+			!e.target.matches("#user-block-toggle") && // user block toggle anchor
+			!e.target.parentElement.matches("#user-block-toggle") // user block toggle icon
+		) {
+			this.closeSidebar();
+		}
+	};
+
+	/** prepare initial state of collapse menus.*/
+	buildCollapseList = () => {
+		let collapse = {};
+		this.menu
+			.filter(({ heading }) => !heading)
+			.forEach(({ name, path, submenu }) => {
+				collapse[name] = this.routeActive(submenu ? submenu.map(({ path }) => path) : path);
+			});
+		this.setState({ collapse });
+	};
+
+	routeActive = (paths) => {
+		const currpath = this.state.pathname;
+		paths = Array.isArray(paths) ? paths : [paths];
+		return paths.some((p) => (p === "/" ? currpath === p : currpath.indexOf(p) > -1));
+	};
+
+	toggleItemCollapse = (stateName) => () => {
+		for (let c in this.state.collapse) {
+			if (this.state.collapse[c] === true && c !== stateName)
+				this.setState({
+					collapse: {
+						[c]: false,
+					},
+				});
+		}
+		this.setState({
+			collapse: {
+				[stateName]: !this.state.collapse[stateName],
+			},
+		});
+	};
+
+	getSubRoutes = (item) => item.submenu.map(({ path }) => path);
+
+	/** map menu config to string to determine which element to render */
+	itemType = (item) => {
+		if (item.heading) return "heading";
+		if (!item.submenu) return "menu";
+		if (item.submenu) return "submenu";
+	};
+
+	shouldUseFloatingNav = () => {
+		return this.props.settings.isCollapsed || this.props.settings.isCollapsedText || this.props.settings.asideHover;
+	};
+
+	showFloatingNav = (item) => (e) => {
+		if (this.shouldUseFloatingNav())
+			this.setState({
+				currentFloatingItem: item,
+				currentFloatingItemTarget: e.currentTarget,
+				showSidebarBackdrop: true,
+			});
+	};
+
+	closeFloatingNav = () => {
+		this.setState({
+			currentFloatingItem: null,
+			currentFloatingItemTarget: null,
+			showSidebarBackdrop: false,
+		});
+	};
+
+	render() {
+		return (
+			<>
+				<aside className="aside-container">
+					{/* START Sidebar (left) */}
+					<div className="aside-inner">
+						<nav className={"sidebar " + (this.props.settings.asideScrollbar ? "show-scrollbar" : "")}>
+							{/* START sidebar nav */}
+							<ul className="sidebar-nav">
+								{/* START user info */}
+								<li className="has-user-block">
+									<SidebarUserBlock />
+								</li>
+								{/* END user info */}
+
+								{/* Iterates over all sidebar items */}
+								{this.menu.map((item, i) => {
+									// heading
+									if (this.itemType(item) === "heading") return <SidebarItemHeader item={item} key={i} />;
+									else {
+										if (this.itemType(item) === "menu") return <SidebarItem isActive={this.routeActive(item.path)} item={item} key={i} onMouseEnter={this.closeFloatingNav} />;
+										if (this.itemType(item) === "submenu")
+											return [
+												<SidebarSubItem
+													item={item}
+													isOpen={this.state.collapse[item.name]}
+													handler={this.toggleItemCollapse(item.name)}
+													isActive={this.routeActive(this.getSubRoutes(item))}
+													key={i}
+													onMouseEnter={this.showFloatingNav(item)}
+												>
+													<SidebarSubHeader item={item} key={i} />
+													{item.submenu.map((subitem, i) => (
+														<SidebarItem key={i} item={subitem} isActive={this.routeActive(subitem.path)} />
+													))}
+												</SidebarSubItem>,
+											];
+									}
+									return null; // unrecognized item
+								})}
+							</ul>
+							{/* END sidebar nav */}
+						</nav>
+					</div>
+					{/* END Sidebar (left) */}
+					{this.state.currentFloatingItem && this.state.currentFloatingItem.submenu && (
+						<FloatingNav item={this.state.currentFloatingItem} target={this.state.currentFloatingItemTarget} routeActive={this.routeActive} isFixed={this.props.settings.isFixed} closeFloatingNav={this.closeFloatingNav} />
+					)}
+				</aside>
+				{this.state.showSidebarBackdrop && <SidebarBackdrop closeFloatingNav={this.closeFloatingNav} />}
+			</>
+		);
+	}
+}
+
+Sidebar.propTypes = {
+	actions: PropTypes.object,
+	settings: PropTypes.object,
+};
+
+const mapStateToProps = (state) => ({ settings: state.settings, user: state.user });
+const mapDispatchToProps = (dispatch) => ({
+	actions: bindActionCreators(actions, dispatch),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(withRouter(withTranslation(Sidebar)));

+ 57 - 0
components/Layout/SidebarUserBlock.js

@@ -0,0 +1,57 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { Collapse } from "reactstrap";
+
+import { connect } from "react-redux";
+
+class SidebarUserBlock extends Component {
+	state = {
+		showUserBlock: true,
+		user: {},
+		role: "",
+	};
+
+	async componentDidMount() {
+		// const user = await getUser();
+		const user = this.props.user;
+		this.setState({ user, role: user.peran[0].peran });
+	}
+
+	componentDidUpdate(oldProps) {
+		if (oldProps.showUserBlock !== this.props.showUserBlock) {
+			this.setState({ showUserBlock: this.props.showUserBlock });
+		}
+	}
+
+	render() {
+		const { user, role } = this.state;
+		return (
+			<Collapse id="user-block" isOpen={this.state.showUserBlock}>
+				<div>
+					<div className="item user-block">
+						{/* User picture */}
+						<div className="user-block-picture">
+							<div className="user-block-status">
+								<img className="rounded-circle" src={role.id === 2022 ? "/static/img/univ-avatar.png" : "/static/img/logo-single.png"} alt="Avatar" width="60" height="60" />
+								<div className="circle bg-success circle-lg"></div>
+							</div>
+						</div>
+						{/* Name and Job */}
+						<div className="user-block-info">
+							<span className="user-block-name">{user.nama}</span>
+							<span className="user-block-role">{role.nama}</span>
+						</div>
+					</div>
+				</div>
+			</Collapse>
+		);
+	}
+}
+
+SidebarUserBlock.propTypes = {
+	showUserBlock: PropTypes.bool,
+};
+
+const mapStateToProps = (state) => ({ showUserBlock: state.settings.showUserBlock, user: state.user });
+
+export default connect(mapStateToProps)(SidebarUserBlock);

+ 26 - 0
components/Layout/ThemesProvider.js

@@ -0,0 +1,26 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { connect } from "react-redux";
+
+// Import all theme into main css chunk
+// todo: import dynamically
+import "../../styles/themes/theme-a.scss";
+import "../../styles/themes/theme-b.scss";
+import "../../styles/themes/theme-c.scss";
+import "../../styles/themes/theme-d.scss";
+import "../../styles/themes/theme-e.scss";
+import "../../styles/themes/theme-f.scss";
+import "../../styles/themes/theme-g.scss";
+import "../../styles/themes/theme-h.scss";
+
+const ThemesProvider = (props) => (
+	<div id="__themes_provider" className={props.theme.name}>
+		{props.children}
+	</div>
+);
+
+ThemesProvider.propTypes = {
+	theme: PropTypes.object,
+};
+
+export default connect((state) => ({ theme: state.theme }))(ThemesProvider);

+ 83 - 0
components/Main/CaseProgress.js

@@ -0,0 +1,83 @@
+import { Progress } from "reactstrap";
+import Sparkline from "@/components/Common/Sparklines";
+
+function CaseProgress() {
+	return (
+		<div className="card b">
+			<div className="card-body bb">
+				<p>Overvall progress</p>
+				<div className="d-flex align-items-center mb-2">
+					<div className="w-100">
+						<Progress className="progress-xs m0" color="info" value={20} />
+					</div>
+					<div className="ml-auto">
+						<div className="col wd-xxs text-right">
+							<div className="text-bold text-muted">20%</div>
+						</div>
+					</div>
+				</div>
+			</div>
+			<div className="card-body">
+				<p>Metrics</p>
+				<div className="row text-center">
+					<div className="col-6 col-lg-6 col-xl-6">
+						<Sparkline
+							values={[20, 80]}
+							options={{
+								type: "pie",
+								height: "50",
+								sliceColors: ["#edf1f2", "#23b7e5"],
+							}}
+							className="sparkline"
+						/>
+						<p className="mt-3">Open Case</p>
+					</div>
+					<div className="col-6 col-lg-6 col-xl-6">
+						<Sparkline
+							values={[80, 20]}
+							options={{
+								type: "pie",
+								height: "50",
+								sliceColors: ["#edf1f2", "#27c24c"],
+							}}
+							className="sparkline"
+						/>
+						<p className="mt-3">Close Case</p>
+					</div>
+				</div>
+			</div>
+			<table className="table bb">
+				<tbody>
+					<tr>
+						<td>
+							<strong>Open Case</strong>
+						</td>
+						<td>80</td>
+					</tr>
+					<tr>
+						<td>
+							<strong>Close Case</strong>
+						</td>
+						<td>20</td>
+					</tr>
+					<tr>
+						<td>
+							<strong>Performance</strong>
+						</td>
+						<td>
+							<em className="far fa-smile fa-lg text-warning"></em>
+						</td>
+					</tr>
+					<tr>
+						<td>
+							<strong>Last Case Closed</strong>
+						</td>
+						<td>BI:1107 - 12/01/2016</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	);
+}
+
+export default CaseProgress;

+ 131 - 0
components/Main/DetailLaporan.js

@@ -0,0 +1,131 @@
+import React, { useEffect, useState } from "react";
+import Scrollable from "@/components/Common/Scrollable";
+import moment from "moment";
+import { Col, FormGroup } from "reactstrap";
+import { useSelector } from "react-redux";
+import { getPT } from "@/actions/PT";
+
+function DetailLaporan({ data, noTitle = false, noStatus = false }) {
+	const user = useSelector((state) => state.user);
+
+	return (
+		<>
+			{(!data.user_id.isPrivate || user.peran[0].peran.id === 2020) && (
+				<>
+					{noTitle ? "" : <p className="lead bb">Identitas Pelapor - {data.user_id.isPublic ? "Umum" : "Internal"}</p>}
+					<FormGroup row>
+						<Col md="4">Nama Pelapor:</Col>
+						<Col md="8">
+							<strong>{data.user_id.nama}</strong>
+						</Col>
+					</FormGroup>
+					<FormGroup row>
+						<Col md="4">Nomor yang dapat dihubungi:</Col>
+						<Col md="8">
+							<strong>{data.user_id.no_hp}</strong>
+						</Col>
+					</FormGroup>
+					<FormGroup row>
+						<Col md="4">Email:</Col>
+						<Col md="8">
+							<strong>{data.user_id.email}</strong>
+						</Col>
+					</FormGroup>
+
+					{data.user_id.isPublic && (
+						<>
+							<FormGroup row>
+								<Col md="4">Alamat:</Col>
+								<Col md="8">
+									<strong>{data.user_id.alamat}</strong>
+								</Col>
+							</FormGroup>
+							<FormGroup row>
+								<Col md="4">Foto Identitas:</Col>
+								<Col md="8">
+									<img src={`data:${data.user_id.files[0].type};base64, ${Buffer.from(data.user_id.files[0].data).toString("base64")}`} height={200} alt="Foto Identitas" />
+								</Col>
+							</FormGroup>
+						</>
+					)}
+				</>
+			)}
+			{noTitle ? "" : <p className="lead bb">Detail Laporan</p>}
+			<form className="form-horizontal">
+				<FormGroup row>
+					<Col md="4">Nomor Laporan:</Col>
+					<Col md="8">
+						<strong>{data._number}</strong>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Nama Perguruan Tinggi:</Col>
+					<Col md="8">
+						<strong>{data.pt.nama}</strong>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Jenis Pelanggaran:</Col>
+					<Col md="8">
+						<Scrollable height="125px" className="list-group">
+							<ul>
+								{data.pelanggaran.map((e) => (
+									<li>{e.pelanggaran}</li>
+								))}
+							</ul>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Keterangan Laporan:</Col>
+					<Col md="8">
+						<Scrollable height="100px" className="list-group">
+							<p>{data.description}</p>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Dibuat Pada:</Col>
+					<Col md="8">
+						<strong>{moment(data.createdAt).format("D MMMM YYYY")}</strong>
+					</Col>
+				</FormGroup>
+				{!noStatus && data.status ? (
+					<FormGroup row>
+						<Col md="4">Status:</Col>
+						<Col md="8">
+							<div className="badge badge-info">{data.status}</div>
+						</Col>
+					</FormGroup>
+				) : (
+					""
+				)}
+				<FormGroup row>
+					<Col md="4">File Pendukung:</Col>
+					<Col md="8">
+						<Scrollable height="120px" className="list-group">
+							<table className="table table-bordered bg-transparent">
+								<tbody>
+									{data.files.map((e, index) => (
+										<tr key={`files-${index}`}>
+											<td>
+												<em className="fa-lg far fa-file-code"></em>
+											</td>
+											<td>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</td>
+										</tr>
+									))}
+								</tbody>
+							</table>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+			</form>
+		</>
+	);
+}
+
+export default DetailLaporan;

+ 31 - 0
components/Main/DetailPT.js

@@ -0,0 +1,31 @@
+function DetailPT({ data }) {
+	return (
+		<div className="card card-default">
+			<div className="card-body">
+				<div className="text-center">
+					<h3 className="mt-0">{data.nama}</h3>
+					<p>{data.sk_pendirian}</p>
+					<p>Pembina: {data.pembina.nama}</p>
+					<p>{`${data.alamat.jalan} ${data.alamat.rt ? `rt ${data.alamat.rt}` : ""} ${data.alamat.rw ? `rt ${data.alamat.rw}` : ""}, ${data.alamat.kab_kota.nama}, ${data.propinsi.nama}`}</p>
+				</div>
+				<hr />
+				<ul className="list-unstyled px-4">
+					<li>
+						<em className="fa fa-globe fa-fw mr-3"></em>
+						<a href={`https://${data.website}`}>{data.website}</a>
+					</li>
+					<li>
+						<em className="fa fa-phone fa-fw mr-3"></em>
+						{data.telepon}
+					</li>
+					<li>
+						<em className="fa fa-at fa-fw mr-3"></em>
+						<a href={`mailto:${data.email}`}>{data.email}</a>
+					</li>
+				</ul>
+			</div>
+		</div>
+	);
+}
+
+export default DetailPT;

+ 108 - 0
components/Main/DetailSanksi.js

@@ -0,0 +1,108 @@
+import Scrollable from "@/components/Common/Scrollable";
+import moment from "moment";
+import { Col, FormGroup, Table } from "reactstrap";
+
+function DetailSanksi({ data, noTitle = false }) {
+	return (
+		<>
+			{noTitle ? "" : <p className="lead bb">Detail Sanksi</p>}
+			<form className="form-horizontal">
+				<FormGroup row>
+					<Col md="4">Nomor Sanksi:</Col>
+					<Col md="8">
+						<strong>{data.sanksi.no_sanksi}</strong>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Nama Perguruan Tinggi:</Col>
+					<Col md="8">
+						<strong>{data.pt.nama}</strong>
+					</Col>
+				</FormGroup>
+
+				<FormGroup row>
+					<Col md="4">Keterangan:</Col>
+					<Col md="8">
+						<Scrollable height="100px" className="list-group">
+							<p>{data.sanksi.description}</p>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Dibuat Pada:</Col>
+					<Col md="8">
+						<strong>{moment(data.sanksi.createdAt).format("D MMMM YYYY")}</strong>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Dokumen Sanksi:</Col>
+					<Col md="8">
+						<Scrollable height="120px" className="list-group">
+							<table className="table table-bordered bg-transparent">
+								<tbody>
+									{data.sanksi.files.map((e) => (
+										<tr>
+											<td>
+												<em className="fa-lg far fa-file-code"></em>
+											</td>
+											<td>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</td>
+										</tr>
+									))}
+								</tbody>
+							</table>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md={12}>
+						<div className="card b">
+							<div className="card-body bb">
+								<Table responsive>
+									<thead>
+										<tr>
+											<th>Jenis Pelanggaran</th>
+											<th>Sanksi</th>
+										</tr>
+									</thead>
+									<tbody>
+										{data.sanksi.pelanggaran.map((jp, index) => (
+											<tr key={jp._id}>
+												<td width={50}>
+													<div className="media align-items-center">
+														<div className="media-body d-flex">
+															<div>
+																<p>{jp.pelanggaran}</p>
+																<p>TMT : {jp.tmt_bulan} Bulan</p>
+																<p>Jenis Sanksi Administratif : {jp.label_sanksi}</p>
+															</div>
+														</div>
+													</div>
+												</td>
+												<td width={50}>
+													<div className="media align-items-center">
+														<div className="media-body d-flex">
+															<div>
+																<p>{jp.sanksi}</p>
+																<p>Keterangan : {jp.keterangan_sanksi}</p>
+															</div>
+														</div>
+													</div>
+												</td>
+											</tr>
+										))}
+									</tbody>
+								</Table>
+							</div>
+						</div>
+					</Col>
+				</FormGroup>
+			</form>
+		</>
+	);
+}
+
+export default DetailSanksi;

+ 18 - 0
components/Main/Header.js

@@ -0,0 +1,18 @@
+function Header({ data }) {
+	const styleHeaderText = {
+		color: "brown",
+	};
+
+	return (
+		<div className="bg-cover" style={{ backgroundImage: "url(/static/img/profile-bg.png)" }}>
+			<div className="p-4 text-center" style={styleHeaderText}>
+				<img className="img-thumbnail rounded-circle thumb128" src="/static/img/univ-avatar.png" alt="Avatar" />
+				<h3 className="m-0">{data.nama}</h3>
+				<p>{data.sk_pendirian}</p>
+				<p>{`${data.alamat.jalan} ${data.alamat.rt ? `rt ${data.alamat.rt}` : ""} ${data.alamat.rw ? `rt ${data.alamat.rw}` : ""}, ${data.alamat.kab_kota.nama}, ${data.propinsi.nama}`}</p>
+			</div>
+		</div>
+	);
+}
+
+export default Header;

+ 136 - 0
components/Main/Login.js

@@ -0,0 +1,136 @@
+import React, { Component } from "react";
+import { Input, Card, CardBody, Button } from "reactstrap";
+import Router from "next/router";
+import FormValidator from "@/components/Forms/Validator.js";
+import { connect } from "react-redux";
+import { login, getUser } from "@/actions/auth";
+import axiosAPI from "@/config/axios";
+import { getPT } from "@/actions/PT";
+
+class Login extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			/* Group each form state in an object.
+           Property name MUST match the form name */
+			error: null,
+			formLogin: {
+				username: "",
+				password: "",
+			},
+		};
+	}
+
+	/**
+	 * Validate input using onChange event
+	 * @param  {String} formName The name of the form in the state object
+	 * @return {Function} a function used for the event
+	 */
+	validateOnChange = (event) => {
+		const input = event.target;
+		const form = input.form;
+		const value = input.type === "checkbox" ? input.checked : input.value;
+
+		const result = FormValidator.validate(input);
+
+		this.setState({
+			[form.name]: {
+				...this.state[form.name],
+				[input.name]: value,
+				errors: {
+					...this.state[form.name].errors,
+					[input.name]: result,
+				},
+			},
+		});
+	};
+
+	onSubmit = async (e) => {
+		const form = e.target;
+		const inputs = [...form.elements].filter((i) => ["INPUT", "SELECT"].includes(i.nodeName));
+
+		const { errors, hasError } = FormValidator.bulkValidate(inputs);
+
+		this.setState({
+			[form.name]: {
+				...this.state[form.name],
+				errors,
+			},
+		});
+
+		console.log(hasError ? "Form has errors. Check!" : "Form Submitted!");
+		e.preventDefault();
+		if (!hasError) {
+			const { username, password } = this.state.formLogin;
+			const auth = await login(username, password);
+			if (auth.success) {
+				axiosAPI.defaults.headers.Authorization = `Bearer ${auth.access_token}`;
+				const dataUser = await getUser();
+				this.props.setUser(dataUser.data);
+				if (dataUser.data.peran[0].peran.id === 2022) {
+					const org_id = dataUser.data.peran[0].organisasi.id;
+					const pt = await getPT({ id: org_id });
+					if (pt?.success) {
+						this.props.setPT(pt.data);
+					}
+					Router.push({ pathname: "/app/pt/pemantauan" });
+				} else {
+					Router.push({ pathname: "/app/pemantauan" });
+				}
+			} else {
+				this.setState({ error: auth.message || auth.error });
+			}
+		}
+		// e.preventDefault();
+	};
+
+	/* Simplify error check */
+	hasError = (formName, inputName, method) => {
+		return this.state[formName] && this.state[formName].errors && this.state[formName].errors[inputName] && this.state[formName].errors[inputName][method];
+	};
+
+	render() {
+		return (
+			<Card className="card card-flat">
+				<img className="card-img-top" src="/static/img/logo.png" alt="Logo" />
+				<CardBody className="card-body">
+					{" "}
+					<h5 className="card-title text-center py-2 bg-gray">Aplikasi Pengendalian Kelembagaan Pendidikan Tinggi (Aldila Dikti)</h5>
+					{this.state.error}
+					<form onSubmit={this.onSubmit} method="post" name="formLogin">
+						<div className="form-group">
+							<label className="col-form-label">Username *</label>
+							<Input type="text" name="username" invalid={this.hasError("formLogin", "username", "required")} onChange={this.validateOnChange} data-validate='["required"]' value={this.state.formLogin.username} />
+							{this.hasError("formLogin", "username", "required") && <span className="invalid-feedback">Field is required</span>}
+						</div>
+						<div className="form-group">
+							<label className="col-form-label">Password *</label>
+							<Input
+								type="password"
+								id="id-password"
+								name="password"
+								invalid={this.hasError("formLogin", "password", "required")}
+								onChange={this.validateOnChange}
+								data-validate='["required"]'
+								value={this.state.formLogin.password}
+							/>
+							<span className="invalid-feedback">Field is required</span>
+						</div>
+						<div className="required">* Required fields</div>
+						<Button color="info" type="submit" block className=" mt-3">
+							Login
+						</Button>
+					</form>
+				</CardBody>
+			</Card>
+		);
+	}
+}
+
+const mapStateToProps = (state) => ({ user: state.user });
+const mapDispatchToProps = (dispatch) => ({
+	setUser: (payload) => dispatch({ type: "SET_USER", payload }),
+	setPT: (payload) => dispatch({ type: "SET_PT", payload }),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Login);

+ 37 - 0
components/Main/PermohonanPT.js

@@ -0,0 +1,37 @@
+import Scrollable from "@/components/Common/Scrollable";
+import { Col, FormGroup } from "reactstrap";
+
+function PermohonanPT({ data, title = null }) {
+	return (
+		<>
+			<p className="lead bb">{title || "Permohonan dari PT"}</p>
+			<form className="form-horizontal">
+				<FormGroup row>
+					<Col md="4">Dokumen Permohonan:</Col>
+					<Col md="8">
+						<Scrollable height="120px" className="list-group">
+							<table className="table table-bordered bg-transparent">
+								<tbody>
+									{data.files.map((e) => (
+										<tr>
+											<td>
+												<em className="fa-lg far fa-file-code"></em>
+											</td>
+											<td>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</td>
+										</tr>
+									))}
+								</tbody>
+							</table>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+			</form>
+		</>
+	);
+}
+
+export default PermohonanPT;

+ 81 - 0
components/Main/PublicPage.js

@@ -0,0 +1,81 @@
+import React, { Component } from "react";
+import BasePage from "@/components/Layout/BasePage";
+import { getPT } from "@/actions/PT";
+import { getPelanggaran } from "@/actions/pelanggaran";
+import Select from "react-select";
+import AsyncSelect from "react-select/async";
+import { Row, Col, FormGroup, Input, Card, CardBody, Button, CustomInput, Navbar, NavItem, NavLink, NavbarBrand, NavbarToggler, Nav, Collapse } from "reactstrap";
+import ContentWrapper from "@/components/Layout/ContentWrapper";
+
+const menu = [
+	{
+		title: "Home",
+		path: "/app",
+	},
+	{
+		title: "Membuat Laporan",
+		path: "/laporan/new",
+	},
+	{
+		title: "Pemantauan",
+		path: "/pemantauan",
+	},
+	{
+		title: "Login",
+		path: "/login",
+	},
+];
+class PublicPage extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			isOpen: false,
+			inputValue: "",
+			stat: "Waiting to add files..",
+			pelaporanNumber: Math.floor(Date.now() * Math.random()),
+			nama: "",
+			alamat: "",
+			no_hp: "",
+			email: "",
+			fileIdentitas: null,
+			pelanggaran: [],
+			selectedPerguruanTinggi: {},
+			selectedJenis: [],
+			keteranganLaporan: "",
+			files: [],
+		};
+	}
+
+	render() {
+		return (
+			<div>
+				<Navbar color="info" expand="md" dark>
+					<NavbarBrand href="/">
+						<img className="img-fluid" src="/static/img/logo-single.png" alt="App Logo" /> Aldila Dikti
+					</NavbarBrand>
+					<NavbarToggler onClick={this.toggleCollapse} />
+					<Collapse isOpen={this.state.isOpen} navbar>
+						<Nav className="ml-auto" navbar>
+							{menu.map((e) => (
+								<NavItem active={e.path === this.props.pathname ? true : false}>
+									<NavLink href={e.path}>{e.title}</NavLink>
+								</NavItem>
+							))}
+						</Nav>
+					</Collapse>
+				</Navbar>
+				<ContentWrapper>
+					<Row>
+						<Col lg={8} className="block-center d-block ">
+							{this.props.children}
+						</Col>
+					</Row>
+				</ContentWrapper>
+			</div>
+		);
+	}
+}
+
+PublicPage.Layout = BasePage;
+
+export default PublicPage;

+ 39 - 0
components/Main/RiwayatEvaluasi.js

@@ -0,0 +1,39 @@
+import Datatable from "@/components/Tables/Datatable";
+
+function RiwayatEvaluasi({ listData }) {
+	return (
+		<Datatable options={{ responsive: true }}>
+			<table className="table table-striped my-4 w-100">
+				<thead>
+					<tr>
+						<th>Tanggal</th>
+						<th>Judul Dokumen</th>
+						<th>File Pendukung</th>
+					</tr>
+				</thead>
+				<tbody>
+					{listData.map((data) => (
+						<tr>
+							<td>{data.tanggal}</td>
+							<td>{data.judul_dokumen}</td>
+							<td>
+								{data.file
+									.map((e) => (
+										<>
+											<em className="fa-lg far fa-file-code"></em>
+											<a className="text-muted" href="">
+												database.controller.js
+											</a>
+										</>
+									))
+									.join(",")}
+							</td>
+						</tr>
+					))}
+				</tbody>
+			</table>
+		</Datatable>
+	);
+}
+
+export default RiwayatEvaluasi;

+ 73 - 0
components/Main/TableLaporan.js

@@ -0,0 +1,73 @@
+import Datatable from "@/components/Tables/Datatable";
+import { Button } from "reactstrap";
+import Link from "next/link";
+import moment from "moment";
+
+function TableLaporan({ listData, to, linkName, status = false }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				{listData && (
+					<Datatable options={{ responsive: false }}>
+						<table className="table w-100">
+							<thead>
+								<tr>
+									<th>No.Laporan</th>
+									<th>Deskripsi Laporan</th>
+									{status ? <th>Status</th> : ""}
+									<th>Dibuat Oleh</th>
+									<th>Created</th>
+									<th></th>
+								</tr>
+							</thead>
+							<tbody>
+								{listData.map((data) => {
+									return (
+										<tr key={data._id}>
+											<td>{data._number}</td>
+											<td className="text-nowrap">
+												<div className="media align-items-center">
+													<div className="media-body d-flex">
+														<div>
+															<h4 className="m-0">{data.pt.nama}</h4>
+															<p>{data.description}</p>
+														</div>
+													</div>
+												</div>
+											</td>
+											{status ? (
+												<td>
+													<div className="badge badge-info">{data.status}</div>
+												</td>
+											) : (
+												""
+											)}
+											<td>{data.user_id.isPrivate ? "" : data.user_id.nama}</td>
+											<td>{moment(data.createdAt).fromNow()}</td>
+											<td>
+												<div className="ml-auto">
+													<Link
+														href={{
+															pathname: to,
+															query: { ptId: data.pt_id, number: data._number },
+														}}
+													>
+														<Button color="primary" size="sm">
+															{linkName}
+														</Button>
+													</Link>
+												</div>
+											</td>
+										</tr>
+									);
+								})}
+							</tbody>
+						</table>
+					</Datatable>
+				)}
+			</div>
+		</div>
+	);
+}
+
+export default TableLaporan;

+ 67 - 0
components/Main/TableSanksi.js

@@ -0,0 +1,67 @@
+import Datatable from "@/components/Tables/Datatable";
+import { Button } from "reactstrap";
+import Link from "next/link";
+import moment from "moment";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				{listData && (
+					<Datatable options={{ responsive: false }}>
+						<table className="table w-100">
+							<thead>
+								<tr>
+									<th>Nomor Sanksi</th>
+									<th>Keterangan Sanksi</th>
+									<th>Dibuat Oleh</th>
+									<th>Created</th>
+									<th></th>
+								</tr>
+							</thead>
+							<tbody>
+								{listData.length
+									? listData.map((data) => {
+											return (
+												<tr key={data._id}>
+													<td>{data.sanksi.no_sanksi}</td>
+													<td>
+														<div className="media align-items-center">
+															<div className="media-body d-flex">
+																<div>
+																	<h4 className="m-0">Universitas Satyagama</h4>
+																	<p>{data.sanksi.description}</p>
+																</div>
+															</div>
+														</div>
+													</td>
+													<td>{data.sanksi.user_id.nama}</td>
+													<td>{moment(data.sanksi.createdAt).fromNow()}</td>
+													<td>
+														<div className="ml-auto">
+															<Link
+																href={{
+																	pathname: to,
+																	query: { noSanksi: data.sanksi.no_sanksi, ptId: data.pt_id },
+																}}
+															>
+																<Button color="primary" size="sm">
+																	{linkName}
+																</Button>
+															</Link>
+														</div>
+													</td>
+												</tr>
+											);
+									  })
+									: ""}
+							</tbody>
+						</table>
+					</Datatable>
+				)}
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 72 - 0
components/Main/Timeline.js

@@ -0,0 +1,72 @@
+import moment from "moment";
+
+function Timeline({ data, noFile = false }) {
+	const date = data && [...new Set(data.map((e) => moment(e.createdAt).format("DD MMMM YYYY")))];
+	return (
+		<ul className="timeline-alt">
+			{data &&
+				date.map((value) => (
+					<>
+						<li className="timeline-separator" data-datetime={value}></li>
+						{data
+							.filter((e) => moment(e.createdAt).format("DD MMMM YYYY") === value)
+							.map((data, i) => (
+								<>
+									<li className={data.role === "PT" ? "timeline-inverted" : ""}>
+										<div className={`timeline-badge ${data.role === "PT" ? " danger" : "info"}`}>
+											<em className={`fas fa-${data.role === "PT" ? "graduation-cap" : "file"}`}></em>
+										</div>
+
+										<div className="timeline-card">
+											<div className="popover right">
+												<div className="arrow"></div>
+												<div className="popover-body">
+													<div className="d-flex align-items-center mb-3">
+														<img
+															className="mr-3 rounded-circle thumb48"
+															src={`/static/img${data.role === "PT" ? "/univ-avatar.png" : data.role === "UMUM" ? "/user/user.png" : "/logo-single.png"}`}
+															alt="Avatar"
+														/>
+														<p className="m-0">
+															<strong>{data.role_name}</strong>
+															<br />
+															{data.description}
+															<br />
+															<p className="text-muted">{moment(data.createdAt).format("hh:mm")}</p>
+														</p>
+													</div>
+													{(!noFile || data.for_public) && data.data.files && (
+														<>
+															<p className="text-muted my-2">Dokumen</p>
+															{data.data.files.map((e) => (
+																<div className="media bb p-2">
+																	<div className="media-body">
+																		<p className="m-0">
+																			<a href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+																				<strong>{e.name}</strong>
+																			</a>
+																		</p>
+																	</div>
+																</div>
+															))}
+														</>
+													)}
+												</div>
+											</div>
+										</div>
+									</li>
+								</>
+							))}
+					</>
+				))}
+
+			<li className="timeline-end">
+				<a className="timeline-badge">
+					<em className="fa fa-plus"></em>
+				</a>
+			</li>
+		</ul>
+	);
+}
+
+export default Timeline;

+ 56 - 0
components/Maps/VectorMap.js

@@ -0,0 +1,56 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+
+import './vector-map.scss';
+
+/** Wrapper component for jquery-vectormap plugin */
+class VectorMap extends Component {
+
+    static propTypes = {
+        /** series entry of options object */
+        series: PropTypes.object.isRequired,
+        /** markers entry of options object */
+        markers: PropTypes.array.isRequired,
+        /** jvectormap options object */
+        options: PropTypes.object.isRequired,
+        /** height of the container element */
+        height: PropTypes.string
+    }
+
+    static defaultProps = {
+        height: '300px'
+    }
+
+    componentDidMount() {
+        // jquery Vector Map
+        require('ika.jvectormap/jquery-jvectormap-1.2.2.min.js');
+        require('ika.jvectormap/jquery-jvectormap-world-mill-en.js');
+        require('ika.jvectormap/jquery-jvectormap-us-mill-en.js');
+        require('ika.jvectormap/jquery-jvectormap-1.2.2.css');
+
+        window.requestAnimationFrame(() => this.drawMap());
+
+    }
+
+    drawMap() {
+        this.props.options.markers = this.props.markers;
+        this.props.options.series = this.props.series;
+        $(this.mapElement).vectorMap(this.props.options);
+    }
+
+    componentWillUnmount() {
+        const map = $(this.mapElement).vectorMap('get', 'mapObject');
+        map.remove()
+    }
+
+    setRef = node => this.mapElement = node
+
+    render() {
+        return (
+            <div ref={this.setRef} style={{height: this.props.height}}/>
+        )
+    }
+}
+
+export default VectorMap;

+ 45 - 0
components/Maps/vector-map.scss

@@ -0,0 +1,45 @@
+/* ========================================================================
+     Component: vector-map
+ ========================================================================== */
+
+$vmap-label-bg: #313232;
+$vmap-zoom-ctrl-bg: #515253;
+
+body {
+    // adds priority
+
+    .jvectormap-label {
+        position: absolute;
+        display: none;
+        border: solid 1px $vmap-label-bg;
+        border-radius: 2px;
+        background: $vmap-label-bg;
+        color: white;
+        padding: 3px 6px;
+        opacity: 0.9;
+        z-index: 1100;
+    }
+
+    .jvectormap-zoomin, .jvectormap-zoomout {
+        position: absolute;
+        left: 10px;
+        width: 22px;
+        height: 22px;
+        border-radius: 2px;
+        background: $vmap-zoom-ctrl-bg;
+        padding: 5px;
+        color: white;
+        cursor: pointer;
+        line-height: 10px;
+        text-align: center;
+    }
+
+    .jvectormap-zoomin {
+        top: 10px;
+    }
+
+    .jvectormap-zoomout {
+        top: 30px;
+    }
+
+}

+ 48 - 0
components/PT/CabutSanksi/Riwayat.js

@@ -0,0 +1,48 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	const { cabut_sanksi } = data.sanksi;
+	console.log(data);
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{cabut_sanksi ? (
+								<tr>
+									<td>{moment(data.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{cabut_sanksi.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 55 - 0
components/PT/CabutSanksi/TableSanksiJawaban.js

@@ -0,0 +1,55 @@
+import moment from "moment";
+import { Button, Table } from "reactstrap";
+import Link from "next/link";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				<Table className="table w-100">
+					<thead>
+						<tr>
+							<th>Nomor Sanksi</th>
+							<th>Keterangan Sanksi</th>
+							<th>Created</th>
+							<th>Status</th>
+						</tr>
+					</thead>
+					<tbody>
+						{listData.map((data) => {
+							return (
+								<tr key={data._id}>
+									<td>{data.sanksi.no_sanksi}</td>
+									<td className="text-nowrap">
+										<div className="media align-items-center">
+											{/* <img className="img-fluid rounded thumb64" src="/static/img/dummy-search.png" alt="Dummy" /> */}
+											<div className="media-body d-flex">
+												<div>
+													<h4 className="m-0">Universitas Satyagama</h4>
+													{/* <small className="text-muted">0742/O/1990 - www.satyagama.ac.id - info@satyagama.ac.id</small> */}
+													<p>{data.sanksi.description}</p>
+												</div>
+											</div>
+										</div>
+									</td>
+									<td>{moment(data.sanksi.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{data.sanksi.cabut_sanksi?.jawaban ? (
+											<Link href={{ pathname: to, query: { noSanksi: data.sanksi.no_sanksi } }}>
+												<Button color="primary">{linkName}</Button>
+											</Link>
+										) : (
+											<div className="badge-info badge">Menunggu Jawaban</div>
+										)}
+									</td>
+								</tr>
+							);
+						})}
+					</tbody>
+				</Table>
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 48 - 0
components/PT/DocPerbaikan/Riwayat.js

@@ -0,0 +1,48 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Ketarangan</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{data && data.length
+								? data.map((value) => (
+										<tr>
+											<td>{moment(value.createAt).format("DD MMMM YYYY")}</td>
+											<td>{value.description}</td>
+											<td>
+												{value.files.map((e) => (
+													<>
+														<em className="fa-lg far fa-file-code"></em>
+														<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+															{e.name}
+														</a>
+													</>
+												))}
+											</td>
+										</tr>
+								  ))
+								: ""}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 44 - 0
components/PT/JawabanBanding/DetailJawaban.js

@@ -0,0 +1,44 @@
+import { FormGroup } from "reactstrap";
+import Scrollable from "@/components/Common/Scrollable";
+
+function DetailJawaban({ data }) {
+	const { jawaban } = data.sanksi.banding;
+	return (
+		<>
+			<p className="lead bb">Jawaban Permohonan Banding</p>
+			<form className="form-horizontal">
+				<FormGroup>
+					<label md="4">Jawaban:</label>
+					<div md="8">
+						<h3>{jawaban.status}</h3>
+					</div>
+				</FormGroup>
+				<FormGroup>
+					<label md="4">Dokumen Jawaban:</label>
+					<div md="8">
+						<Scrollable height="120px" className="list-group">
+							<table className="table table-bordered bg-transparent">
+								<tbody>
+									{jawaban.files.map((e) => (
+										<tr>
+											<td>
+												<em className="fa-lg far fa-file-code"></em>
+											</td>
+											<td>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</td>
+										</tr>
+									))}
+								</tbody>
+							</table>
+						</Scrollable>
+					</div>
+				</FormGroup>
+			</form>
+		</>
+	);
+}
+
+export default DetailJawaban;

+ 55 - 0
components/PT/JawabanBanding/TableSanksiJawaban.js

@@ -0,0 +1,55 @@
+import moment from "moment";
+import { Button, Table } from "reactstrap";
+import Link from "next/link";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				<Table className="table w-100">
+					<thead>
+						<tr>
+							<th>Nomor Sanksi</th>
+							<th>Keterangan Sanksi</th>
+							<th>Created</th>
+							<th>Status</th>
+						</tr>
+					</thead>
+					<tbody>
+						{listData.map((data) => {
+							return (
+								<tr key={data._id}>
+									<td>{data.sanksi.no_sanksi}</td>
+									<td className="text-nowrap">
+										<div className="media align-items-center">
+											{/* <img className="img-fluid rounded thumb64" src="/static/img/dummy-search.png" alt="Dummy" /> */}
+											<div className="media-body d-flex">
+												<div>
+													<h4 className="m-0">Universitas Satyagama</h4>
+													{/* <small className="text-muted">0742/O/1990 - www.satyagama.ac.id - info@satyagama.ac.id</small> */}
+													<p>{data.sanksi.description}</p>
+												</div>
+											</div>
+										</div>
+									</td>
+									<td>{moment(data.sanksi.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{data.sanksi.banding?.jawaban ? (
+											<Link href={{ pathname: to, query: { noSanksi: data.sanksi.no_sanksi } }}>
+												<Button color="primary">{linkName}</Button>
+											</Link>
+										) : (
+											<div className="badge-info badge">Menunggu Jawaban</div>
+										)}
+									</td>
+								</tr>
+							);
+						})}
+					</tbody>
+				</Table>
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 50 - 0
components/PT/JawabanKeberatan/DetailJawaban.js

@@ -0,0 +1,50 @@
+import { FormGroup } from "reactstrap";
+import Scrollable from "@/components/Common/Scrollable";
+
+function DetailJawaban({ data }) {
+	const { jawaban } = data.sanksi.keberatan;
+	return (
+		<>
+			<p className="lead bb">Jawaban Permohonan Keberatan</p>
+			<form className="form-horizontal">
+				<FormGroup>
+					<label md="4">Jawaban:</label>
+					<div md="8">
+						<h3>{jawaban.status}</h3>
+					</div>
+				</FormGroup>
+				<FormGroup>
+					<label md="4">Keterangan:</label>
+					<div md="8">
+						<p>{jawaban.description}</p>
+					</div>
+				</FormGroup>
+				<FormGroup>
+					<label md="4">Dokumen Jawaban:</label>
+					<div md="8">
+						<Scrollable height="120px" className="list-group">
+							<table className="table table-bordered bg-transparent">
+								<tbody>
+									{jawaban.files.map((e) => (
+										<tr>
+											<td>
+												<em className="fa-lg far fa-file-code"></em>
+											</td>
+											<td>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</td>
+										</tr>
+									))}
+								</tbody>
+							</table>
+						</Scrollable>
+					</div>
+				</FormGroup>
+			</form>
+		</>
+	);
+}
+
+export default DetailJawaban;

+ 157 - 0
components/PT/JawabanKeberatan/ModalPermohonan.js

@@ -0,0 +1,157 @@
+import React, { Component } from "react";
+import Router from "next/router";
+import { Row, Col, FormGroup, Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
+import { addBanding } from "@/actions/banding";
+import { addDocPerbaikan } from "@/actions/docPerbaikan";
+import { connect } from "react-redux";
+
+let Dropzone = null;
+class DropzoneWrapper extends Component {
+	state = {
+		isClient: false,
+	};
+	componentDidMount = () => {
+		Dropzone = require("react-dropzone").default;
+		this.setState({ isClient: true });
+	};
+	render() {
+		return Dropzone ? <Dropzone {...this.props}>{this.props.children}</Dropzone> : null;
+	}
+}
+
+export class ModalPermohonan extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			modal1: false,
+			files: [],
+		};
+	}
+
+	onDrop = (files) => {
+		this.setState({
+			files: files.map((file) =>
+				Object.assign(file, {
+					preview: URL.createObjectURL(file),
+				})
+			),
+			stat: "Added " + files.length + " file(s)",
+		});
+	};
+
+	uploadFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? "Dropzone ready to upload " + this.state.files.length + " file(s)" : "No files added.",
+		});
+	};
+
+	clearFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? this.state.files.length + " file(s) cleared." : "No files to clear.",
+		});
+		this.setState({
+			files: [],
+		});
+	};
+
+	toggleModal1 = () => {
+		this.props.toggleModal(false);
+		this.setState({
+			modal1: !this.state.modal1,
+		});
+	};
+
+	onSubmit = async (e) => {
+		e.preventDefault();
+		const { user, query } = this.props;
+		const { noSanksi } = query;
+		const formdata = new FormData();
+		if (this.state.files.length > 0) {
+			this.state.files.forEach((e) => {
+				formdata.append("files", e);
+			});
+
+			const added = await addBanding({ noSanksi, ptId: user.peran[0].organisasi.id }, formdata);
+
+			if (added) {
+				Router.push({
+					pathname: "/app/pt/jawaban-keberatan",
+				});
+			}
+		}
+	};
+
+	handleKirim = (e) => {
+		this.setState({
+			modal1: !this.state.modal1,
+		});
+		this.onSubmit(e);
+	};
+
+	render() {
+		const { files } = this.state;
+
+		const thumbs = files.map((file, index) => (
+			<Col md={3} key={index}>
+				<img className="img-fluid mb-2" src={file.preview} alt="Item" />
+			</Col>
+		));
+		return (
+			<>
+				<Modal isOpen={this.props.modal} toggle={this.props.toggleModal}>
+					<ModalBody>Apakah anda akan mengajukan banding?</ModalBody>
+					<ModalFooter>
+						<Button color="primary" onClick={this.toggleModal1}>
+							Ya
+						</Button>{" "}
+						<Button color="secondary" onClick={this.props.toggleModal}>
+							Tidak
+						</Button>
+					</ModalFooter>
+				</Modal>
+				<Modal isOpen={this.state.modal1} toggle={this.toggleModal1}>
+					<ModalHeader toggle={this.toggleModal1}>Upload Dokumen Banding</ModalHeader>
+					<ModalBody>
+						<form className="form-horizontal" method="get" action="/" onSubmit={this.onSubmit}>
+							<FormGroup>
+								<label>Dalam hal mengajukan permohonan banding maka wajib mengunggah surat permohonan banding & dokumen pendukungnya</label>
+								<div>
+									<DropzoneWrapper className="" onDrop={this.onDrop}>
+										{({ getRootProps, getInputProps, isDragActive }) => {
+											return (
+												<div {...getRootProps()} className={"dropzone card p-3 " + (isDragActive ? "dropzone-drag-active" : "")}>
+													<input {...getInputProps()} />
+													<div className="dropzone-previews flex">{this.state.files.length > 0 ? <Row>{thumbs}</Row> : <div className="text-center dz-default dz-message">Drop files here to upload</div>}</div>
+													<div className="d-flex align-items-center">
+														<small className="ml-auto">
+															<button type="button" className="btn btn-link" onClick={this.clearFiles}>
+																Clear files
+															</button>
+														</small>
+													</div>
+												</div>
+											);
+										}}
+									</DropzoneWrapper>
+									<span className="form-text">Multiple files upload</span>
+								</div>
+							</FormGroup>
+						</form>
+					</ModalBody>
+					<ModalFooter>
+						<Button color="primary" onClick={this.handleKirim}>
+							Kirim
+						</Button>
+					</ModalFooter>
+				</Modal>
+			</>
+		);
+	}
+}
+
+const mapStateToProps = (state) => ({ user: state.user });
+export default connect(mapStateToProps)(ModalPermohonan);

+ 47 - 0
components/PT/JawabanKeberatan/Riwayat.js

@@ -0,0 +1,47 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	const { banding } = data.sanksi;
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{banding ? (
+								<tr>
+									<td>{moment(banding.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{banding.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 55 - 0
components/PT/JawabanKeberatan/TableSanksiJawaban.js

@@ -0,0 +1,55 @@
+import moment from "moment";
+import { Button, Table } from "reactstrap";
+import Link from "next/link";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				<Table className="table w-100">
+					<thead>
+						<tr>
+							<th>Nomor Sanksi</th>
+							<th>Keterangan Sanksi</th>
+							<th>Created</th>
+							<th>Status</th>
+						</tr>
+					</thead>
+					<tbody>
+						{listData.map((data) => {
+							return (
+								<tr key={data._id}>
+									<td>{data.sanksi.no_sanksi}</td>
+									<td className="text-nowrap">
+										<div className="media align-items-center">
+											{/* <img className="img-fluid rounded thumb64" src="/static/img/dummy-search.png" alt="Dummy" /> */}
+											<div className="media-body d-flex">
+												<div>
+													<h4 className="m-0">Universitas Satyagama</h4>
+													{/* <small className="text-muted">0742/O/1990 - www.satyagama.ac.id - info@satyagama.ac.id</small> */}
+													<p>{data.sanksi.description}</p>
+												</div>
+											</div>
+										</div>
+									</td>
+									<td>{moment(data.sanksi.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{data.sanksi.keberatan?.jawaban ? (
+											<Link href={{ pathname: to, query: { noSanksi: data.sanksi.no_sanksi } }}>
+												<Button color="primary">{linkName}</Button>
+											</Link>
+										) : (
+											<div className="badge-info badge">Menunggu Jawaban</div>
+										)}
+									</td>
+								</tr>
+							);
+						})}
+					</tbody>
+				</Table>
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 5 - 0
components/PT/JawabanPencabutanSanksi/DetailJawaban.js

@@ -0,0 +1,5 @@
+function DetailJawaban({data}) {
+	return <div>Enter</div>;
+}
+
+export default DetailJawaban;

+ 55 - 0
components/PT/JawabanPencabutanSanksi/TableSanksiJawaban.js

@@ -0,0 +1,55 @@
+import moment from "moment";
+import { Button, Table } from "reactstrap";
+import Link from "next/link";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				<Table className="table w-100">
+					<thead>
+						<tr>
+							<th>Nomor Sanksi</th>
+							<th>Keterangan Sanksi</th>
+							<th>Created</th>
+							<th>Status</th>
+						</tr>
+					</thead>
+					<tbody>
+						{listData.map((data) => {
+							return (
+								<tr key={data._id}>
+									<td>{data.sanksi.no_sanksi}</td>
+									<td className="text-nowrap">
+										<div className="media align-items-center">
+											{/* <img className="img-fluid rounded thumb64" src="/static/img/dummy-search.png" alt="Dummy" /> */}
+											<div className="media-body d-flex">
+												<div>
+													<h4 className="m-0">Universitas Satyagama</h4>
+													{/* <small className="text-muted">0742/O/1990 - www.satyagama.ac.id - info@satyagama.ac.id</small> */}
+													<p>{data.sanksi.description}</p>
+												</div>
+											</div>
+										</div>
+									</td>
+									<td>{moment(data.sanksi.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{data.sanksi.cabut_sanksi?.jawaban ? (
+											<Link href={{ pathname: to, query: { noSanksi: data.sanksi.no_sanksi } }}>
+												<Button color="primary">{linkName}</Button>
+											</Link>
+										) : (
+											<div className="badge-info badge">Menunggu Jawaban</div>
+										)}
+									</td>
+								</tr>
+							);
+						})}
+					</tbody>
+				</Table>
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 160 - 0
components/PT/Keberatan/ModalPermohonan.js

@@ -0,0 +1,160 @@
+import React, { Component } from "react";
+import Router from "next/router";
+import { Row, Col, FormGroup, Button, Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
+import { addKeberatan } from "@/actions/keberatan";
+import { connect } from "react-redux";
+
+let Dropzone = null;
+class DropzoneWrapper extends Component {
+	state = {
+		isClient: false,
+	};
+	componentDidMount = () => {
+		Dropzone = require("react-dropzone").default;
+		this.setState({ isClient: true });
+	};
+	render() {
+		return Dropzone ? <Dropzone {...this.props}>{this.props.children}</Dropzone> : null;
+	}
+}
+
+export class ModalPermohonan extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			modal1: false,
+			files: [],
+		};
+	}
+
+	onDrop = (files) => {
+		this.setState({
+			files: files.map((file) =>
+				Object.assign(file, {
+					preview: URL.createObjectURL(file),
+				})
+			),
+			stat: "Added " + files.length + " file(s)",
+		});
+	};
+
+	uploadFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? "Dropzone ready to upload " + this.state.files.length + " file(s)" : "No files added.",
+		});
+	};
+
+	clearFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? this.state.files.length + " file(s) cleared." : "No files to clear.",
+		});
+		this.setState({
+			files: [],
+		});
+	};
+
+	toggleModal1 = () => {
+		this.props.toggleModal(false);
+		this.setState({
+			modal1: !this.state.modal1,
+		});
+	};
+
+	onSubmit = async (e) => {
+		e.preventDefault();
+		const { user, query } = this.props;
+		const { noSanksi } = query;
+		const formdata = new FormData();
+		if (this.state.files.length > 0) {
+			this.state.files.forEach((e) => {
+				formdata.append("files", e);
+			});
+
+			const added = await addKeberatan({ noSanksi, ptId: user.peran[0].organisasi.id }, formdata);
+			// formdata.append("keberatan", added.add.sanksi.keberatan._id);
+			// formdata.append("data", added.add.sanksi.keberatan._id);
+			// formdata.append("model", "Keberatan");
+			// await addDocPerbaikan({ noSanksi, ptId: "0BCE4DB7-B207-445D-8D03-0C54B7688252" }, formdata);
+			// console.log(added);
+			if (added) {
+				Router.push({
+					pathname: "/app/pt/keberatan",
+				});
+			}
+		}
+	};
+
+	handleKirim = (e) => {
+		this.setState({
+			modal1: !this.state.modal1,
+		});
+		this.onSubmit(e);
+	};
+
+	render() {
+		const { files } = this.state;
+
+		const thumbs = files.map((file, index) => (
+			<Col md={3} key={index}>
+				<img className="img-fluid mb-2" src={file.preview} alt="Item" />
+			</Col>
+		));
+		return (
+			<>
+				<Modal isOpen={this.props.modal} toggle={this.props.toggleModal}>
+					<ModalBody>Apakah anda akan mengajukan permohonan keberatan atas pengenaan sanksi?</ModalBody>
+					<ModalFooter>
+						<Button color="primary" onClick={this.toggleModal1}>
+							Ya
+						</Button>{" "}
+						<Button color="secondary" onClick={this.props.toggleModal}>
+							Tidak
+						</Button>
+					</ModalFooter>
+				</Modal>
+				<Modal isOpen={this.state.modal1} toggle={this.toggleModal1}>
+					<ModalHeader toggle={this.toggleModal1}>Unggah Dokumen Permohonan Keberatan</ModalHeader>
+					<ModalBody>
+						<form className="form-horizontal" method="get" action="/" onSubmit={this.onSubmit}>
+							<FormGroup>
+								<label>Dalam hal mengajukan permohonan keberatan maka wajib mengunggah surat permohonan keberatan atas pengenaan sanksi administratif & dokumen pendukungnya</label>
+								<div>
+									<DropzoneWrapper className="" onDrop={this.onDrop}>
+										{({ getRootProps, getInputProps, isDragActive }) => {
+											return (
+												<div {...getRootProps()} className={"dropzone card p-3 " + (isDragActive ? "dropzone-drag-active" : "")}>
+													<input {...getInputProps()} />
+													<div className="dropzone-previews flex">{this.state.files.length > 0 ? <Row>{thumbs}</Row> : <div className="text-center dz-default dz-message">Drop files here to upload</div>}</div>
+													<div className="d-flex align-items-center">
+														<small className="ml-auto">
+															<button type="button" className="btn btn-link" onClick={this.clearFiles}>
+																Clear files
+															</button>
+														</small>
+													</div>
+												</div>
+											);
+										}}
+									</DropzoneWrapper>
+									<span className="form-text">Multiple files upload</span>
+								</div>
+							</FormGroup>
+						</form>
+					</ModalBody>
+					<ModalFooter>
+						<Button color="primary" onClick={this.handleKirim}>
+							Kirim
+						</Button>
+					</ModalFooter>
+				</Modal>
+			</>
+		);
+	}
+}
+
+const mapStateToProps = (state) => ({ user: state.user });
+export default connect(mapStateToProps)(ModalPermohonan);

+ 47 - 0
components/PT/Keberatan/Riwayat.js

@@ -0,0 +1,47 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	const keberatan = data.sanksi.keberatan;
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{keberatan ? (
+								<tr>
+									<td>{moment(keberatan.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{keberatan.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 62 - 0
components/PT/Riwayat.js

@@ -0,0 +1,62 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{data.length ? (
+								data.map((value) => (
+									<tr>
+										<td>{moment(value.createAt).format("DD MMMM YYYY")}</td>
+										<td>
+											{value.files.map((e) => (
+												<>
+													<em className="fa-lg far fa-file-code"></em>
+													<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+														{e.name}
+													</a>
+												</>
+											))}
+										</td>
+									</tr>
+								))
+							) : data ? (
+								<tr>
+									<td>{moment(data.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{data.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 51 - 0
components/PT/TableSanksi.js

@@ -0,0 +1,51 @@
+import moment from "moment";
+import { Button, Table } from "reactstrap";
+import Link from "next/link";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				<Table className="table w-100">
+					<thead>
+						<tr>
+							<th>Nomor Sanksi</th>
+							<th>Keterangan Sanksi</th>
+							<th>Created</th>
+							<th>Status</th>
+						</tr>
+					</thead>
+					<tbody>
+						{listData.map((data) => {
+							return (
+								<tr key={data._id}>
+									<td>{data.sanksi.no_sanksi}</td>
+									<td className="text-nowrap">
+										<div className="media align-items-center">
+											{/* <img className="img-fluid rounded thumb64" src="/static/img/dummy-search.png" alt="Dummy" /> */}
+											<div className="media-body d-flex">
+												<div>
+													<h4 className="m-0">Universitas Satyagama</h4>
+													{/* <small className="text-muted">0742/O/1990 - www.satyagama.ac.id - info@satyagama.ac.id</small> */}
+													<p>{data.sanksi.description}</p>
+												</div>
+											</div>
+										</div>
+									</td>
+									<td>{moment(data.sanksi.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										<Link href={{ pathname: to, query: { noSanksi: data.sanksi.no_sanksi } }}>
+											<Button color="primary">{linkName}</Button>
+										</Link>
+									</td>
+								</tr>
+							);
+						})}
+					</tbody>
+				</Table>
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 55 - 0
components/PT/TableSanksiJawaban.js

@@ -0,0 +1,55 @@
+import moment from "moment";
+import { Button, Table } from "reactstrap";
+import Link from "next/link";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				<Table className="table w-100">
+					<thead>
+						<tr>
+							<th>Nomor Sanksi</th>
+							<th>Keterangan Sanksi</th>
+							<th>Created</th>
+							<th>Status</th>
+						</tr>
+					</thead>
+					<tbody>
+						{listData.map((data) => {
+							return (
+								<tr key={data._id}>
+									<td>{data.sanksi.no_sanksi}</td>
+									<td className="text-nowrap">
+										<div className="media align-items-center">
+											{/* <img className="img-fluid rounded thumb64" src="/static/img/dummy-search.png" alt="Dummy" /> */}
+											<div className="media-body d-flex">
+												<div>
+													<h4 className="m-0">Universitas Satyagama</h4>
+													{/* <small className="text-muted">0742/O/1990 - www.satyagama.ac.id - info@satyagama.ac.id</small> */}
+													<p>{data.sanksi.description}</p>
+												</div>
+											</div>
+										</div>
+									</td>
+									<td>{moment(data.sanksi.createAt).format("DD MMMM YYYY")}</td>
+									<td>
+										{data.sanksi.keberatan?.jawaban || data.sanksi.banding?.jawaban || data.sanksi.cabut_sanksi?.jawaban ? (
+											<Link href={{ pathname: to, query: { noSanksi: data.sanksi.no_sanksi } }}>
+												<Button color="primary">{linkName}</Button>
+											</Link>
+										) : (
+											<div className="badge-info badge">Menunggu Jawaban</div>
+										)}
+									</td>
+								</tr>
+							);
+						})}
+					</tbody>
+				</Table>
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 70 - 0
components/PT/Timeline.js

@@ -0,0 +1,70 @@
+import moment from "moment";
+
+function Timeline({ data, dataPelaporan }) {
+	const jadwal = dataPelaporan;
+	const date = [...new Set(data.data.map((e) => moment(e.createdAt).format("DD MMMM YYYY")))];
+	return (
+		<ul className="timeline-alt">
+			{date.map((value) => (
+				<>
+					<li className="timeline-separator" data-datetime={value}></li>
+					{data.data
+						.filter((e) => e.for_pt && moment(e.createdAt).format("DD MMMM YYYY") === value)
+						.map((data, i) => (
+							<>
+								<li className={data.role === "PT" ? "timeline-inverted" : ""}>
+									<div className={`timeline-badge ${data.role === "PT" ? " danger" : "info"}`}>
+										<em className={`fas fa-${data.role === "PT" ? "graduation-cap" : "file"}`}></em>
+									</div>
+
+									<div className="timeline-card">
+										<div className={`popover ${data.role === "PT" ? "right" : "left"}`}>
+											<div className="arrow"></div>
+											<div className="popover-body">
+												<div className="d-flex align-items-center mb-3">
+													<img className="mr-3 rounded-circle thumb48" src={`/static/img${data.role === "PT" ? "/univ-avatar.png" : "/logo-single.png"}`} alt="Avatar" />
+													<p className="m-0">
+														<strong>{ data.role_name}</strong>
+														<br />
+														{data.description}
+														<br />
+														<p className="text-muted">{moment(data.createdAt).format("hh:mm")}</p>
+													</p>
+												</div>
+												{data.data.files ? (
+													<>
+														<p className="text-muted my-2">Dokumen</p>
+														{data.data.files.map((e) => (
+															<div className="media bb p-2">
+																<div className="media-body">
+																	<p className="m-0">
+																		<a href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+																			<strong>{e.name}</strong>
+																		</a>
+																	</p>
+																</div>
+															</div>
+														))}
+													</>
+												) : (
+													""
+												)}
+											</div>
+										</div>
+									</div>
+								</li>
+							</>
+						))}
+				</>
+			))}
+
+			<li className="timeline-end">
+				<a className="timeline-badge">
+					<em className="fa fa-plus"></em>
+				</a>
+			</li>
+		</ul>
+	);
+}
+
+export default Timeline;

+ 192 - 0
components/Pelaporan/InputData.js

@@ -0,0 +1,192 @@
+import React, { Component } from "react";
+import Router from "next/router";
+import { getPelanggaran } from "@/actions/pelanggaran";
+import { createPelaporan } from "@/actions/pelaporan";
+import Select from "react-select";
+import { Row, Col, FormGroup, Input } from "reactstrap";
+import { connect } from "react-redux";
+
+let Dropzone = null;
+class DropzoneWrapper extends Component {
+	state = {
+		isClient: false,
+	};
+	componentDidMount = () => {
+		Dropzone = require("react-dropzone").default;
+		this.setState({ isClient: true });
+	};
+	render() {
+		return Dropzone ? <Dropzone {...this.props}>{this.props.children}</Dropzone> : null;
+	}
+}
+
+const selectInstanceId = 1;
+export class InputData extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			dropdownOpen: false,
+			splitButtonOpen: false,
+			selectedOptionMulti: [],
+			stat: "Waiting to add files..",
+			pelaporanNumber: Math.floor(Date.now() * Math.random()),
+			keteranganLaporan: "",
+			files: [],
+			pelanggaran: [],
+		};
+	}
+
+	componentDidMount = async () => {
+		const pelanggaran = await getPelanggaran();
+		this.setState({ pelanggaran });
+	};
+
+	optionsJenisPelanggaran = (pelanggaran) => {
+		return pelanggaran.data.map((e) => ({ value: e._id, label: e.pelanggaran, className: "State-ACT" }));
+	};
+
+	setKeteranganPelaporan = (e) => {
+		this.setState({ keteranganLaporan: e.target.value });
+	};
+
+	toggleDropDown = () => {
+		this.setState({
+			dropdownOpen: !this.state.dropdownOpen,
+		});
+	};
+
+	toggleSplit = () => {
+		this.setState({
+			splitButtonOpen: !this.state.splitButtonOpen,
+		});
+	};
+
+	handleChangeSelectMulti = (selectedOptionMulti) => {
+		this.setState({ selectedOptionMulti });
+	};
+
+	onDrop = (files) => {
+		this.setState({
+			files: files.map((file) =>
+				Object.assign(file, {
+					preview: URL.createObjectURL(file),
+				})
+			),
+			stat: "Added " + files.length + " file(s)",
+		});
+	};
+
+	uploadFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? "Dropzone ready to upload " + this.state.files.length + " file(s)" : "No files added.",
+		});
+	};
+
+	clearFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? this.state.files.length + " file(s) cleared." : "No files to clear.",
+		});
+		this.setState({
+			files: [],
+		});
+	};
+
+	onSubmit = async (e) => {
+		const { user } = this.props;
+		e.preventDefault();
+		const formdata = new FormData();
+		formdata.append("number", this.state.pelaporanNumber);
+		formdata.append("user_id", user._id);
+		formdata.append("pt_id", this.props.query.ptId);
+		formdata.append("description", this.state.keteranganLaporan);
+		formdata.append("is_public", false);
+		formdata.append("pelanggaran", this.state.selectedOptionMulti.map((e) => e.value).join());
+		if (this.state.files.length > 0) {
+			this.state.files.forEach((e) => {
+				formdata.append("files", e);
+			});
+		}
+
+		const create = await createPelaporan(formdata);
+		// console.log(create);
+		// console.log(create);
+		// await this.props.dispatch(createPelaporan(formdata));
+		// this.props.dispatch(listPelaporan());
+		if (create) {
+			Router.push({
+				pathname: "/app/pelaporan",
+			});
+		}
+	};
+
+	render() {
+		const { selectedOptionMulti, files, pelanggaran } = this.state;
+
+		const thumbs = files.map((file, index) => (
+			<Col md={3} key={index}>
+				<img className="img-fluid mb-2" src={file.preview} alt="Item" />
+			</Col>
+		));
+		return (
+			<form className="form-horizontal" method="get" action="/" onSubmit={this.onSubmit}>
+				<FormGroup row>
+					<label className="col-md-2 col-form-label">Nomor Pelaporan</label>
+					<div className="col-md-10">
+						<Input type="text" disabled value={this.state.pelaporanNumber} />
+						<span className="form-text">Nomor pelaporan akan digenerate otomatis dari sistem</span>
+					</div>
+				</FormGroup>
+				<FormGroup row>
+					<label className="col-md-2 col-form-label">Jenis Pelanggaran</label>
+					<div className="col-md-10">
+						<Select instanceId={selectInstanceId + 1} isMulti value={selectedOptionMulti} onChange={this.handleChangeSelectMulti} options={pelanggaran.data ? this.optionsJenisPelanggaran(pelanggaran) : []} required />
+						<span className="form-text">Pilih Jenis Pelanggaran</span>
+					</div>
+				</FormGroup>
+				<FormGroup row>
+					<label className="col-md-2 col-form-label">Keterangan Laporan</label>
+					<div className="col-md-10">
+						<Input type="textarea" value={this.state.keteranganLaporan} onChange={this.setKeteranganPelaporan} required />
+						<span className="form-text">Deskripsi pelaporan minimum karakter 50 maksimum 200 karakter</span>
+					</div>
+				</FormGroup>
+				<FormGroup row>
+					<label className="col-md-2 col-form-label">Upload File Pendukung</label>
+					<div className="col-md-10">
+						<DropzoneWrapper className="" onDrop={this.onDrop}>
+							{({ getRootProps, getInputProps, isDragActive }) => {
+								return (
+									<div {...getRootProps()} className={"dropzone card p-3 " + (isDragActive ? "dropzone-drag-active" : "")}>
+										<input {...getInputProps()} />
+										<div className="dropzone-previews flex">{this.state.files.length > 0 ? <Row>{thumbs}</Row> : <div className="text-center dz-default dz-message">Drop files here to upload</div>}</div>
+										<div className="d-flex align-items-center">
+											<small className="ml-auto">
+												<button type="button" className="btn btn-link" onClick={this.clearFiles}>
+													Clear files
+												</button>
+											</small>
+										</div>
+									</div>
+								);
+							}}
+						</DropzoneWrapper>
+					</div>
+				</FormGroup>
+				<FormGroup row>
+					<div className="col-xl-10">
+						<button className="btn btn-sm btn-primary" type="submit">
+							Submit Laporan
+						</button>
+					</div>
+				</FormGroup>
+			</form>
+		);
+	}
+}
+
+const mapStateToProps = (state) => ({ user: state.user });
+export default connect(mapStateToProps)(InputData);

+ 167 - 0
components/Pemeriksaan/InputEvaluasi.js

@@ -0,0 +1,167 @@
+import React, { Component } from "react";
+import { insertPemeriksaan } from "@/actions/pemeriksaan";
+import Router from "next/router";
+import Datetime from "react-datetime";
+import moment from "moment";
+import { Row, Col, FormGroup, Input } from "reactstrap";
+
+const selectInstanceId = 1;
+let Dropzone = null;
+
+class DropzoneWrapper extends Component {
+	state = {
+		isClient: false,
+	};
+	componentDidMount = () => {
+		Dropzone = require("react-dropzone").default;
+		this.setState({ isClient: true });
+	};
+	render() {
+		return Dropzone ? <Dropzone {...this.props}>{this.props.children}</Dropzone> : null;
+	}
+}
+
+export default class InputEvaluasi extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			dropdownOpen: false,
+			splitButtonOpen: false,
+			judulEvaluasi: "",
+			tanggal: moment().format("D MMMM YYYY"),
+			files: [],
+		};
+	}
+
+	setjudulEvaluasi = (e) => {
+		this.setState({ judulEvaluasi: e.target.value });
+	};
+
+	setTanggal = (moment) => {
+		this.setState({ tanggal: moment.format("D MMMM YYYY") });
+	};
+
+	toggleSplit = () => {
+		this.setState({
+			splitButtonOpen: !this.state.splitButtonOpen,
+		});
+	};
+
+	toggleDropDown = () => {
+		this.setState({
+			dropdownOpen: !this.state.dropdownOpen,
+		});
+	};
+
+	onDrop = (files) => {
+		this.setState({
+			files: files.map((file) =>
+				Object.assign(file, {
+					preview: URL.createObjectURL(file),
+				})
+			),
+			stat: "Added " + files.length + " file(s)",
+		});
+	};
+
+	uploadFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? "Dropzone ready to upload " + this.state.files.length + " file(s)" : "No files added.",
+		});
+	};
+
+	clearFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? this.state.files.length + " file(s) cleared." : "No files to clear.",
+		});
+		this.setState({
+			files: [],
+		});
+	};
+
+	onSubmit = async (e) => {
+		e.preventDefault();
+		const { number, ptId } = this.props.query;
+		const formdata = new FormData();
+		formdata.append("title", this.state.judulEvaluasi);
+		formdata.append("date", this.state.tanggal);
+		if (this.state.files.length > 0) {
+			this.state.files.forEach((e) => {
+				formdata.append("files", e);
+			});
+		}
+
+		const inserted = await insertPemeriksaan({ number, ptId }, formdata);
+		if (inserted) {
+			Router.push({
+				pathname: "/app/pemeriksaan",
+			});
+		}
+	};
+
+	render() {
+		const { files } = this.state;
+
+		const thumbs = files.map((file, index) => (
+			<Col md={3} key={index}>
+				<img className="img-fluid mb-2" src={file.preview} alt="Item" />
+			</Col>
+		));
+		return (
+			<>
+				<p className="lead bb">Evaluasi</p>
+				<form className="form-horizontal" method="get" action="/" onSubmit={this.onSubmit}>
+					<FormGroup>
+						<label>Tanggal Dokumen:</label>
+						<div>
+							<Datetime inputProps={{ className: "form-control" }} value={this.state.tanggal} onChange={this.setTanggal} />
+							{/* <span className="form-text">Tanggal</span> */}
+						</div>
+					</FormGroup>
+					<FormGroup>
+						<label>Judul Dokumen:</label>
+						<div>
+							<Input type="text" value={this.state.judulEvaluasi} onChange={this.setjudulEvaluasi} />
+							{/* <Input type="textarea" value={this.state.keteranganLaporan} onChange={this.setKeteranganPelaporan} /> */}
+							{/* <span className="form-text">Deskripsi pelaporan minimum karakter 50 maksimum 200 karakter</span> */}
+						</div>
+					</FormGroup>
+					<FormGroup>
+						<label>Upload File Pendukung:</label>
+						<div>
+							<DropzoneWrapper className="" onDrop={this.onDrop}>
+								{({ getRootProps, getInputProps, isDragActive }) => {
+									return (
+										<div {...getRootProps()} className={"dropzone card p-3 " + (isDragActive ? "dropzone-drag-active" : "")}>
+											<input {...getInputProps()} />
+											<div className="dropzone-previews flex">{this.state.files.length > 0 ? <Row>{thumbs}</Row> : <div className="text-center dz-default dz-message">Drop files here to upload</div>}</div>
+											<div className="d-flex align-items-center">
+												<small className="ml-auto">
+													<button type="button" className="btn btn-link" onClick={this.clearFiles}>
+														Clear files
+													</button>
+												</small>
+											</div>
+										</div>
+									);
+								}}
+							</DropzoneWrapper>
+							<span className="form-text">Multiple files upload</span>
+						</div>
+					</FormGroup>
+					<FormGroup>
+						<div>
+							<button className="btn btn-sm btn-primary" type="submit">
+								Simpan Evaluasi
+							</button>
+						</div>
+					</FormGroup>
+				</form>
+			</>
+		);
+	}
+}

+ 39 - 0
components/Pemeriksaan/TableRiwayat.js

@@ -0,0 +1,39 @@
+import Datatable from "@/components/Tables/Datatable";
+
+function TableRiwayat({ data }) {
+	return (
+		<Datatable options={{ responsive: true }}>
+			<table className="table table-striped my-4 w-100">
+				<thead>
+					<tr>
+						<th>Tanggal Dibuat</th>
+						<th>Tanggal Dokumen</th>
+						<th>Judul Dokumen</th>
+						<th>File Pendukung</th>
+					</tr>
+				</thead>
+				<tbody>
+					{data.penjadwalan.pemeriksaan.map((e, index) => (
+						<tr key={`riwayatPemeriksaan-${index}`}>
+							<td>{moment(e.createdAt).format("D MMMM YYYY")}</td>
+							<td>{moment(e.date).format("D MMMM YYYY")}</td>
+							<td>{e.title}</td>
+							<td>
+								{e.files.map((e, index) => (
+									<>
+										<em className="fa-lg far fa-file-code"></em>
+										<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+											{e.name}
+										</a>
+									</>
+								))}
+							</td>
+						</tr>
+					))}
+				</tbody>
+			</table>
+		</Datatable>
+	);
+}
+
+export default TableRiwayat;

+ 51 - 0
components/PencabutanSanksi/Riwayat.js

@@ -0,0 +1,51 @@
+import Datatable from "@/components/Tables/Datatable";
+import moment from "moment";
+import { Card, CardHeader, CardBody, CardTitle } from "reactstrap";
+
+function Riwayat({ data }) {
+	const { jawaban } = data.sanksi.cabut_sanksi;
+	return (
+		<Card className="card-default">
+			<CardHeader>
+				<CardTitle>Riwayat</CardTitle>
+			</CardHeader>
+			<CardBody>
+				<Datatable options={{ responsive: true }}>
+					<table className="table table-striped my-4 w-100">
+						<thead>
+							<tr>
+								<th>Tanggal</th>
+								<th>Status</th>
+								<th>Keterangan</th>
+								<th>Dokumen</th>
+							</tr>
+						</thead>
+						<tbody>
+							{jawaban ? (
+								<tr>
+									<td>{moment(jawaban.createAt).format("DD MMMM YYYY")}</td>
+									<td>{jawaban.status}</td>
+									<td>{jawaban.description}</td>
+									<td>
+										{jawaban.files.map((e) => (
+											<>
+												<em className="fa-lg far fa-file-code"></em>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</>
+										))}
+									</td>
+								</tr>
+							) : (
+								""
+							)}
+						</tbody>
+					</table>
+				</Datatable>
+			</CardBody>
+		</Card>
+	);
+}
+
+export default Riwayat;

+ 67 - 0
components/PencabutanSanksi/TableSanksi.js

@@ -0,0 +1,67 @@
+import Datatable from "@/components/Tables/Datatable";
+import { Button } from "reactstrap";
+import Link from "next/link";
+import moment from "moment";
+
+function TableSanksi({ listData, to, linkName }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				{listData && (
+					<Datatable options={{ responsive: true }}>
+						<table className="table w-100">
+							<thead>
+								<tr>
+									<th>Nomor Sanksi</th>
+									<th>Keterangan Sanksi</th>
+									<th>Created</th>
+									<th>Status</th>
+									<th></th>
+								</tr>
+							</thead>
+							<tbody>
+								{listData.length
+									? listData.map((data) => {
+											return (
+												<tr key={data._id}>
+													<td>{data.sanksi.no_sanksi}</td>
+													<td>
+														<div className="media align-items-center">
+															<div className="media-body d-flex">
+																<div>
+																	<h4 className="m-0">Universitas Satyagama</h4>
+																	<p>{data.sanksi.description}</p>
+																</div>
+															</div>
+														</div>
+													</td>
+													<td>{moment(data.sanksi.createdAt).fromNow()}</td>
+													<td>{data.sanksi.cabut_sanksi.jawaban ? <div className="badge badge-info">Sudah Dijawab</div> : <div className="badge badge-danger">Belum Dijawab</div>}</td>
+													<td>
+														<div className="ml-auto">
+															<Link
+																href={{
+																	pathname: to,
+																	query: { noSanksi: data.sanksi.no_sanksi, ptId: data.pt_id },
+																}}
+															>
+																<Button color="primary" size="sm">
+																	{linkName}
+																</Button>
+															</Link>
+														</div>
+													</td>
+												</tr>
+											);
+									  })
+									: ""}
+							</tbody>
+						</table>
+					</Datatable>
+				)}
+			</div>
+		</div>
+	);
+}
+
+export default TableSanksi;

+ 124 - 0
components/Penjadwalan/DetailLaporan.js

@@ -0,0 +1,124 @@
+import React, { Component } from "react";
+import Select from "react-select";
+import Scrollable from "@/components/Common/Scrollable";
+import { addStatus } from "@/actions/pelaporan";
+import { Card, CardBody, CardHeader, CardTitle } from "reactstrap";
+
+const status = [
+	{ value: "Ditindaklanjuti Dikti Ristek", label: "Ditindaklanjuti Dikti Ristek", className: "State-ACT" },
+	{ value: "Delegasi ke LLDIKTI", label: "Delegasi ke LLDIKTI", className: "State-ACT" },
+	{ value: "Ditutup", label: "Ditutup", className: "State-ACT" },
+];
+const selectInstanceId = 1;
+export class DetailLaporan extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			selectedOption: null,
+		};
+	}
+
+	componentDidMount = () => {
+		const { data } = this.props;
+		if (data.status) {
+			const selectedOption = status.filter((e) => e.value === data.status)[0];
+			this.setState({ selectedOption });
+		} else {
+			this.setState({ selectedOption: { value: "Ditindaklanjuti", label: "Ditindaklanjuti Dikti Ristek", className: "State-ACT" } });
+			// const tes = await addStatus({ number, ptId }, { status: data.status || "ditindaklanjuti" });
+		}
+	};
+
+	handleChangeSelect = async (selectedOption) => {
+		const { ptId, number } = this.props.query;
+		this.props.handleChangeSelect(selectedOption);
+		this.setState({ selectedOption });
+		await addStatus({ number, ptId }, { status: selectedOption.value });
+	};
+
+	render() {
+		const { data } = this.props;
+		return (
+			<Card className="card b">
+				<CardHeader>
+					<CardTitle tag="h4">Detail Laporan</CardTitle>
+				</CardHeader>
+				<CardBody>
+					<table className="table">
+						<tbody>
+							<tr>
+								<td>
+									<strong>Status</strong>
+								</td>
+								<td>
+									<Select instanceId={selectInstanceId + 1} value={this.state.selectedOption} onChange={this.handleChangeSelect} options={status} required />
+								</td>
+							</tr>
+							<tr>
+								<td>
+									<strong>Nomor Laporan</strong>
+								</td>
+								<td>{data._number}</td>
+							</tr>
+							<tr>
+								<td>
+									<strong>Perguruan Tinggi</strong>
+								</td>
+								<td>Universitas Satyagama</td>
+							</tr>
+							<tr>
+								<td>
+									<strong>Jenis Pelanggaran</strong>
+								</td>
+								<td>
+									<Scrollable height="75px" className="list-group">
+										<ul>{data.pelanggaran ? data.pelanggaran.map((e) => <li>{e.pelanggaran}</li>) : ""}</ul>
+									</Scrollable>
+								</td>
+							</tr>
+							<tr>
+								<td>
+									<strong>Keterangan Laporan</strong>
+								</td>
+								<td>
+									<Scrollable height="100px" className="list-group">
+										<p>{data.description}</p>
+									</Scrollable>
+								</td>
+							</tr>
+							<tr>
+								<td>
+									<strong>File Pendukung</strong>
+								</td>
+								<td>
+									<Scrollable height="120px" className="list-group">
+										<table className="table table-bordered bg-transparent">
+											<tbody>
+												{data.files
+													? data.files.map((e) => (
+															<tr>
+																<td>
+																	<em className="fa-lg far fa-file-code"></em>
+																</td>
+																<td>
+																	<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+																		{e.name}
+																	</a>
+																</td>
+															</tr>
+													  ))
+													: ""}
+											</tbody>
+										</table>
+									</Scrollable>
+								</td>
+							</tr>
+						</tbody>
+					</table>
+				</CardBody>
+			</Card>
+		);
+	}
+}
+
+export default DetailLaporan;

+ 86 - 0
components/Public/DetailLaporan.js

@@ -0,0 +1,86 @@
+import Scrollable from "@/components/Common/Scrollable";
+import moment from "moment";
+import { Col, FormGroup } from "reactstrap";
+
+function DetailLaporan({ data }) {
+	return (
+		<>
+			<p className="lead bb">Detail Laporan</p>
+			<form className="form-horizontal">
+				<FormGroup row>
+					<Col md="4">Nomor Laporan:</Col>
+					<Col md="8">
+						<strong>{data._number}</strong>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Nama Perguruan Tinggi:</Col>
+					<Col md="8">
+						<strong>{data.pt.nama}</strong>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Jenis Pelanggaran:</Col>
+					<Col md="8">
+						<Scrollable height="125px" className="list-group">
+							<ul>
+								{data.pelanggaran.map((e) => (
+									<li>{e.pelanggaran}</li>
+								))}
+							</ul>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Keterangan Laporan:</Col>
+					<Col md="8">
+						<Scrollable height="100px" className="list-group">
+							<p>{data.description}</p>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+				<FormGroup row>
+					<Col md="4">Dibuat Pada:</Col>
+					<Col md="8">
+						<strong>{moment(data.createdAt).format("D MMMM YYYY")}</strong>
+					</Col>
+				</FormGroup>
+				{data.status ? (
+					<FormGroup row>
+						<Col md="4">Status:</Col>
+						<Col md="8">
+							<div className="badge badge-info">{data.status}</div>
+						</Col>
+					</FormGroup>
+				) : (
+					""
+				)}
+				<FormGroup row>
+					<Col md="4">File Pendukung:</Col>
+					<Col md="8">
+						<Scrollable height="120px" className="list-group">
+							<table className="table table-bordered bg-transparent">
+								<tbody>
+									{data.files.map((e, index) => (
+										<tr key={`files-${index}`}>
+											<td>
+												<em className="fa-lg far fa-file-code"></em>
+											</td>
+											<td>
+												<a className="text-muted" href={`data:${e.type};base64, ${Buffer.from(e.data).toString("base64")}`} download={e.name}>
+													{e.name}
+												</a>
+											</td>
+										</tr>
+									))}
+								</tbody>
+							</table>
+						</Scrollable>
+					</Col>
+				</FormGroup>
+			</form>
+		</>
+	);
+}
+
+export default DetailLaporan;

+ 0 - 0
components/Public/Timeline.js


+ 147 - 0
components/Sanksi/Ringkasan.js

@@ -0,0 +1,147 @@
+import { useEffect, useState } from "react";
+import Scrollable from "@/components/Common/Scrollable";
+import { Card, Row, Col, Table, FormGroup } from "reactstrap";
+
+function Ringkasan({ dataLaporan, dataPelanggaran, dataUpload }) {
+	return (
+		<>
+			<Row>
+				<Col>
+					<p className="lead bb">Detail Laporan</p>
+					<form className="form-horizontal">
+						<FormGroup row>
+							<Col md="4">Nomor Laporan:</Col>
+							<Col md="8">
+								<strong>{dataLaporan._number}</strong>
+							</Col>
+						</FormGroup>
+						<FormGroup row>
+							<Col md="4">Nama Perguruan Tinggi:</Col>
+							<Col md="8">
+								<strong>Universitas Satyagama</strong>
+							</Col>
+						</FormGroup>
+						<FormGroup row>
+							<Col md="4">Jenis Pelanggaran:</Col>
+							<Col md="8">
+								<Scrollable height="125px" className="list-group">
+									<ul>
+										{dataLaporan.pelanggaran.map((e) => (
+											<li>{e.pelanggaran}</li>
+										))}
+									</ul>
+								</Scrollable>
+							</Col>
+						</FormGroup>
+						<FormGroup row>
+							<Col md="4">Keterangan Laporan:</Col>
+							<Col md="8">
+								<Scrollable height="100px" className="list-group">
+									<p>{dataLaporan.description}</p>
+								</Scrollable>
+							</Col>
+						</FormGroup>
+						<FormGroup row>
+							<Col md="4">Dibuat Pada:</Col>
+							<Col md="8">
+								<strong>{moment(dataLaporan.createAt).format("D MMMM YYYY")}</strong>
+							</Col>
+						</FormGroup>
+					</form>
+				</Col>
+			</Row>
+			<Row>
+				<Col>
+					<p className="lead bb">Penetapan Sanksi</p>
+					<Card className="card-default">
+						<Table bordered hover responsive>
+							<thead>
+								<tr>
+									<th>No</th>
+									<th>Jenis Pelanggaran</th>
+									<th>Sanksi</th>
+								</tr>
+							</thead>
+							<tbody>
+								{dataPelanggaran
+									? dataPelanggaran.map((e, i) => (
+											<tr key={e._id}>
+												<td>{++i}</td>
+												<td>
+													<div className="media align-items-center">
+														<div className="media-body d-flex">
+															<div>
+																<p>{e.pelanggaran}</p>
+																<p>TMT : {e.tmt_bulan} Bulan</p>
+																<p>Level Pelanggaran : {e.label_sanksi}</p>
+															</div>
+														</div>
+													</div>
+												</td>
+												<td>
+													<div className="media align-items-center">
+														<div className="media-body d-flex">
+															<div>
+																<p>{e.sanksi}</p>
+																<p>Keterangan : {e.keterangan_sanksi}</p>
+															</div>
+														</div>
+													</div>
+												</td>
+											</tr>
+									  ))
+									: ""}
+							</tbody>
+						</Table>
+					</Card>
+				</Col>
+			</Row>
+			<Row>
+				<Col>
+					<p className="lead bb">Nomor Surat Keputusan Sanksi</p>
+					<form className="form-horizontal">
+						<FormGroup row>
+							<Col md="4">Nomor Surat:</Col>
+							<Col md="8">
+								<strong>{dataUpload ? dataUpload.nomorSanksi : ""}</strong>
+							</Col>
+						</FormGroup>
+						<FormGroup row>
+							<Col md="4">Keterangan:</Col>
+							<Col md="8">
+								<strong>{dataUpload ? dataUpload.keterangan : ""}</strong>
+							</Col>
+						</FormGroup>
+						<FormGroup row>
+							<Col md="4">Surat Sanksi:</Col>
+							<Col md="8">
+								<Scrollable height="120px" className="list-group">
+									<table className="table table-bordered bg-transparent">
+										<tbody>
+											{dataUpload
+												? dataUpload.files.map((e) => (
+														<tr>
+															<td>
+																<em className="fa-lg far fa-file-code"></em>
+															</td>
+															<td>
+																<a className="text-muted" href={e.preview} download={e.name}>
+																	{e.name}
+																</a>
+															</td>
+														</tr>
+												  ))
+												: ""}
+										</tbody>
+									</table>
+								</Scrollable>
+							</Col>
+						</FormGroup>
+					</form>
+				</Col>
+			</Row>
+		</>
+	);
+}
+
+export default Ringkasan;

+ 67 - 0
components/Sanksi/TableLaporan.js

@@ -0,0 +1,67 @@
+import Datatable from "@/components/Tables/Datatable";
+import { Button } from "reactstrap";
+import Link from "next/link";
+import moment from "moment";
+
+function TableLaporan({ listData }) {
+	return (
+		<div className="card b">
+			<div className="card-body">
+				{listData && (
+					<Datatable options={{ responsive: false }}>
+						<table className="table w-100">
+							<thead>
+								<tr>
+									<th>#ID</th>
+									<th>Deskripsi Laporan</th>
+									<th>Status</th>
+									<th>Created</th>
+									<th></th>
+								</tr>
+							</thead>
+							<tbody>
+								{listData.map((data) => {
+									return (
+										<tr key={data._id}>
+											<td>{data._number}</td>
+											<td className="text-nowrap">
+												<div className="media align-items-center">
+													<div className="media-body d-flex">
+														<div>
+															<h4 className="m-0">{data.pt.nama}</h4>
+															<p>{data.description}</p>
+														</div>
+													</div>
+												</div>
+											</td>
+											<td>
+												<div className="badge badge-info">{data.status}</div>
+											</td>
+											<td>{moment(data.createdAt).fromNow()}</td>
+											<td>
+												<div className="ml-auto">
+													<Link
+														href={{
+															pathname: data.sanksi ? "/app/sanksi/detail" : "/app/sanksi/proses",
+															query: { ptId: data.pt_id, number: data._number },
+														}}
+													>
+														<Button color="primary" size="sm">
+															{data.sanksi ? "Detail" : "Proses Sanksi"}
+														</Button>
+													</Link>
+												</div>
+											</td>
+										</tr>
+									);
+								})}
+							</tbody>
+						</table>
+					</Datatable>
+				)}
+			</div>
+		</div>
+	);
+}
+
+export default TableLaporan;

+ 88 - 0
components/Sanksi/TablePenetapanSanksi.js

@@ -0,0 +1,88 @@
+import React, { Component } from "react";
+import { Card, Table } from "reactstrap";
+import { getPelanggaran } from "@/actions/pelanggaran";
+
+export class TablePenetapanSanksi extends Component {
+	checkedData = [];
+
+	constructor(props) {
+		super(props);
+		this.state = {
+			pelanggaran: null,
+			// checkedData: [],
+		};
+	}
+
+	componentDidMount = async () => {
+		const pelanggaran = await getPelanggaran();
+		this.setState({ pelanggaran });
+	};
+
+	onHandleChange = (evt) => {
+		const checked = evt.target.checked;
+		const item = evt.target.value;
+		if (checked) this.checkedData.push(evt.target.value);
+		else this.checkedData = this.checkedData.filter((e) => e != item);
+		this.props.setCheckedData(this.checkedData);
+	};
+
+	render() {
+		const { pelanggaran } = this.state;
+		return (
+			<Card className="card-default">
+				<Table bordered hover responsive>
+					<thead>
+						<tr>
+							<th>No</th>
+							<th>Jenis Pelanggaran</th>
+							<th>Sanksi</th>
+							<th></th>
+						</tr>
+					</thead>
+					<tbody>
+						{pelanggaran
+							? pelanggaran.data.map((jp, index) => (
+									<tr key={jp._id}>
+										<td>
+											<label>{index + 1}</label>
+										</td>
+										<td>
+											<div className="media align-items-center">
+												<div className="media-body d-flex">
+													<div>
+														<p>{jp.pelanggaran}</p>
+														<p>TMT : {jp.tmt_bulan} Bulan</p>
+														<p>Jenis Sanksi Administratif : {jp.label_sanksi}</p>
+													</div>
+												</div>
+											</div>
+										</td>
+										<td>
+											<div className="media align-items-center">
+												<div className="media-body d-flex">
+													<div>
+														<p>{jp.sanksi}</p>
+														<p>Keterangan : {jp.keterangan_sanksi}</p>
+													</div>
+												</div>
+											</div>
+										</td>
+										<td>
+											<div className="checkbox c-checkbox">
+												<label>
+													<input type="checkbox" value={jp._id} onChange={this.onHandleChange} />
+													<span className="fa fa-check"></span>
+												</label>
+											</div>
+										</td>
+									</tr>
+							  ))
+							: ""}
+					</tbody>
+				</Table>
+			</Card>
+		);
+	}
+}
+
+export default TablePenetapanSanksi;

+ 122 - 0
components/Sanksi/UploadSurat.js

@@ -0,0 +1,122 @@
+import React, { Component } from "react";
+import { Row, Col, Input, FormGroup } from "reactstrap";
+
+let Dropzone = null;
+class DropzoneWrapper extends Component {
+	state = {
+		isClient: false,
+	};
+	componentDidMount = () => {
+		Dropzone = require("react-dropzone").default;
+		this.setState({ isClient: true });
+	};
+	render() {
+		return Dropzone ? <Dropzone {...this.props}>{this.props.children}</Dropzone> : null;
+	}
+}
+
+export class UploadSurat extends Component {
+	constructor(props) {
+		super(props);
+		this.state = {
+			files: [],
+			nomorSanksi: "",
+			keterangan: "",
+		};
+	}
+
+	onDrop = (files) => {
+		this.setState({
+			files: files.map((file) =>
+				Object.assign(file, {
+					preview: URL.createObjectURL(file),
+				})
+			),
+			stat: "Added " + files.length + " file(s)",
+		});
+		this.props.setUploadSuratSanksi(this.state);
+	};
+
+	uploadFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? "Dropzone ready to upload " + this.state.files.length + " file(s)" : "No files added.",
+		});
+		this.props.setUploadSuratSanksi(this.state);
+	};
+
+	clearFiles = (e) => {
+		e.preventDefault();
+		e.stopPropagation();
+		this.setState({
+			stat: this.state.files.length ? this.state.files.length + " file(s) cleared." : "No files to clear.",
+		});
+		this.setState({
+			files: [],
+		});
+		this.props.setUploadSuratSanksi(this.state);
+	};
+
+	setNomorSanksi = (e) => {
+		this.setState({ nomorSanksi: e.target.value });
+		this.props.setUploadSuratSanksi(this.state);
+	};
+
+	setKeterangan = (e) => {
+		this.setState({ keterangan: e.target.value });
+		this.props.setUploadSuratSanksi(this.state);
+	};
+
+	render() {
+		const { files } = this.state;
+
+		const thumbs = files.map((file, index) => (
+			<Col md={3} key={index}>
+				<img className="img-fluid mb-2" src={file.preview} alt="Item" />
+			</Col>
+		));
+		return (
+			<form className="form-horizontal" method="get" action="/" onSubmit={this.onSubmit}>
+				<FormGroup row>
+					<label className="col-md-2 col-form-label">Nomor Surat:</label>
+					<div className="col-md-10">
+						<Input type="text" value={this.state.nomorSanksi} onChange={this.setNomorSanksi} />
+					</div>
+				</FormGroup>
+				<FormGroup row className="mt-3">
+					<label className="col-md-2 col-form-label">Keterangan</label>
+					<div className="col-md-10">
+						<Input type="textarea" value={this.state.keterangan} onChange={this.setKeterangan} required />
+						{/* <span className="form-text">Deskripsi pelaporan minimum karakter 50 maksimum 200 karakter</span> */}
+					</div>
+				</FormGroup>
+				<FormGroup row>
+					<label className="col-md-2 col-form-label">Dokumen Surat Sanksi:</label>
+					<div className="col-md-10">
+						<DropzoneWrapper className="" onDrop={this.onDrop}>
+							{({ getRootProps, getInputProps, isDragActive }) => {
+								return (
+									<div {...getRootProps()} className={"dropzone card p-3 " + (isDragActive ? "dropzone-drag-active" : "")}>
+										<input {...getInputProps()} />
+										<div className="dropzone-previews flex">{this.state.files.length > 0 ? <Row>{thumbs}</Row> : <div className="text-center dz-default dz-message">Drop files here to upload</div>}</div>
+										<div className="d-flex align-items-center">
+											<small className="ml-auto">
+												<button type="button" className="btn btn-link" onClick={this.clearFiles}>
+													Clear files
+												</button>
+											</small>
+										</div>
+									</div>
+								);
+							}}
+						</DropzoneWrapper>
+						<span className="form-text">Multiple files upload</span>
+					</div>
+				</FormGroup>
+			</form>
+		);
+	}
+}
+
+export default UploadSurat;

+ 63 - 0
components/Tables/Datatable.js

@@ -0,0 +1,63 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import $ from 'jquery';
+
+/**
+ * Wrapper component for dataTable plugin
+ * Only DOM child elements, componets are not supported (e.g. <Table>)
+ */
+export default class Datatable extends Component {
+
+    static propTypes = {
+        /** datatables options object */
+        options: PropTypes.object,
+        /** only one children allowed */
+        children: PropTypes.element.isRequired,
+        /** callback that receives the datatable instance as param */
+        dtInstance: PropTypes.func
+    }
+
+    static defaultProps = {
+        options: {}
+    }
+
+    componentDidMount() {
+        // Datatables
+        require('datatables.net-bs')
+        require('datatables.net-bs4/js/dataTables.bootstrap4.js')
+        require('datatables.net-bs4/css/dataTables.bootstrap4.css')
+        require('datatables.net-buttons')
+        require('datatables.net-buttons-bs')
+        require('datatables.net-responsive')
+        require('datatables.net-responsive-bs')
+        require('datatables.net-responsive-bs/css/responsive.bootstrap.css')
+        require('datatables.net-buttons/js/buttons.colVis.js') // Column visibility
+        require('datatables.net-buttons/js/buttons.html5.js') // HTML 5 file export
+        require('datatables.net-buttons/js/buttons.flash.js') // Flash file export
+        require('datatables.net-buttons/js/buttons.print.js') // Print view button
+        require('datatables.net-keytable');
+        require('datatables.net-keytable-bs/css/keyTable.bootstrap.css')
+        require('jszip/dist/jszip.js');
+        require('pdfmake/build/pdfmake.js');
+        require('pdfmake/build/vfs_fonts.js');
+
+        const dtInstance = $(this.tableElement).dataTable(this.props.options);
+
+        if(this.props.dtInstance)
+            this.props.dtInstance(dtInstance)
+    }
+
+    componentWillUnmount() {
+        $(this.tableElement).dataTable({destroy: true});
+    }
+
+    setRef = node => this.tableElement = node;
+
+    render() {
+        return (
+            React.cloneElement(React.Children.only(this.props.children), {
+                ref: this.setRef
+            })
+        )
+    }
+}

+ 41 - 0
config/axios.js

@@ -0,0 +1,41 @@
+import axios from "axios";
+// import jwt_decode from "jwt-decode";
+
+const axiosAPI = axios.create({
+	baseURL: "http://localhost:5000",
+	withCredentials: true,
+});
+
+// axiosJWT.interceptors.request.use(
+// 	async (config) => {
+// 		// const response = await refreshToken();
+// 		// const decoded = jwt_decode(response.access_token);
+// 		// const expire = decoded.exp;
+// 		// const currentDate = Date.now();
+// 		// if (expire * 1000 < currentDate) {
+// 		const response = await refreshToken();
+// 		// console.log(response);
+// 		if (response.success) config.headers.Authorization = `Bearer ${response.access_token}`;
+// 		// config.withCredentials = true;
+// 		return config;
+// 		// }
+// 	},
+// 	async (error) => {
+// 		const originalConfig = err.config;
+// 		// if (error.response.status === 403 || error.response.status === 401) {
+// 		try {
+// 			const response = await refreshToken();
+// 			if (response.success) {
+// 				// axiosJWT.defaults.headers.Authorization = `Bearer ${response.access_token}`;
+// 				originalConfig.headers.Authorization = `Bearer ${response.access_token}`;
+// 				return axiosJWT(originalConfig);
+// 			}
+// 		} catch (error) {
+// 			return Promise.reject(error);
+// 		}
+// 		// }
+// 		return Promise.reject(error);
+// 	}
+// );
+
+export default axiosAPI;

+ 68 - 0
config/request.js

@@ -0,0 +1,68 @@
+import axiosAPI from "./axios";
+import { refreshToken } from "@/actions/auth";
+
+const handleRequest = async (request) => {
+	try {
+		return await request();
+	} catch (error) {
+		if (error?.response?.status === 401) {
+			try {
+				const token = await refreshToken();
+				if (token.success) {
+					axiosAPI.defaults.headers.Authorization = `Bearer ${token.access_token}`;
+					return await request();
+				}
+				return false;
+			} catch (error) {
+				console.log(error);
+				return false;
+			}
+		}
+		console.log(error);
+		return false;
+	}
+};
+
+export const get = async (url, config = null) => {
+	try {
+		const request = () => axiosAPI.get(url, config);
+		const res = await handleRequest(request);
+		return res;
+	} catch (error) {
+		console.log(error);
+		return false;
+	}
+};
+
+export const post = async (url, data, config = null) => {
+	try {
+		const request = () => axiosAPI.post(url, data, config);
+		const res = await handleRequest(request);
+		return res;
+	} catch (error) {
+		console.log(error);
+		return false;
+	}
+};
+
+export const put = async (url, data, config = null) => {
+	try {
+		const request = () => axiosAPI.put(url, data, config);
+		const res = await handleRequest(request);
+		return res;
+	} catch (error) {
+		console.log(error);
+		return false;
+	}
+};
+
+export const del = async (url, config = null) => {
+	try {
+		const request = () => axiosAPI.delete(url, config);
+		const res = await handleRequest(request);
+		return res;
+	} catch (error) {
+		console.log(error);
+		return false;
+	}
+};

+ 56 - 0
json/dataUser.js

@@ -0,0 +1,56 @@
+module.exports = [
+	{
+		id: "2A080F42-AE7F-407B-976E-DE5FA87BD277",
+		username: "dikti",
+		password: "123",
+		nama: "Dikti Ristek/LLDIKTI",
+		no_telp: "0211111222233",
+		no_hp: "197903302008011007",
+		jabatan: null,
+		alamat: "Jln. Jenderal Sudirman Pintu 1, Senayan",
+		id_sdm_pengguna: null,
+		peran: [
+			{
+				id: "96EBA4CC-5DE1-4429-A836-56E32A63FC67",
+				organisasi: {
+					id: "86942CDF-44F1-446E-8E9E-CB37BBBB16E6",
+					nama: "Semua Unit",
+					id_lembaga_asal: "86942CDF-44F1-446E-8E9E-CB37BBBB16E6",
+					id_jenis_lembaga: 21,
+				},
+				peran: {
+					id: 1,
+					nama: "Admin Dikti",
+					menu: null,
+				},
+			},
+		],
+	},
+	{
+		id: "2A080F42-AE7F-407B-976E-DE5FA87BD278",
+		username: "satyagama",
+		password: "123",
+		nama: "Universitas Satyagama",
+		no_telp: "02157946063",
+		no_hp: "197903302008011007",
+		jabatan: null,
+		alamat: "Jln. Jenderal Sudirman Pintu 1, Senayan",
+		id_sdm_pengguna: null,
+		peran: [
+			{
+				id: "96EBA4CC-5DE1-4429-A836-56E32A63FC69",
+				organisasi: {
+					id: "86942CDF-44F1-446E-8E9E-CB37BBBB16E6",
+					nama: "Semua Unit",
+					id_lembaga_asal: "86942CDF-44F1-446E-8E9E-CB37BBBB16E6",
+					id_jenis_lembaga: 21,
+				},
+				peran: {
+					id: 2,
+					nama: "Perguruan Tinggi",
+					menu: null,
+				},
+			},
+		],
+	},
+];

+ 10 - 0
keys-note.txt

@@ -0,0 +1,10 @@
+It's pretty simple when you think about it:
+
+Store - Is what holds all the data your application uses.
+Reducer - is what manipulates that data when it recieves an action.
+Action - is what tells reducer to manipulate the store data, it carries the name and (not required) some data.
+
+Reducer is usually in a format of a switch statement, 
+that switches between all possible Actions (Cases) and 
+then manipulates the Store data based on action. 
+When a reducer data changes within the redux, the properties in your components are changed and then the re-render ocurrs.

Some files were not shown because too many files changed in this diff