diff --git a/package.json b/package.json
index 3d1fe52..4ddb8a5 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
"react": "^18",
"react-dom": "^18",
"react-redux": "^9.1.2",
- "react-tooltip": "^5.28.0"
+ "react-tooltip": "^5.28.0",
+ "redux-persist": "^6.0.0"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/public/images/background.jpg b/public/images/background.jpg
new file mode 100644
index 0000000..9067154
Binary files /dev/null and b/public/images/background.jpg differ
diff --git a/public/images/logo-login.png b/public/images/logo-login.png
new file mode 100644
index 0000000..c98ecb3
Binary files /dev/null and b/public/images/logo-login.png differ
diff --git a/public/images/midsuit-login.png b/public/images/midsuit-login.png
new file mode 100644
index 0000000..692cb8c
Binary files /dev/null and b/public/images/midsuit-login.png differ
diff --git a/src/app/(dashboard)/absensi/page.tsx b/src/app/(dashboard)/absensi/page.tsx
index 059f7d7..039789c 100644
--- a/src/app/(dashboard)/absensi/page.tsx
+++ b/src/app/(dashboard)/absensi/page.tsx
@@ -2,7 +2,7 @@
import { useAppSelector } from "@/lib/hooks";
import { useGetAttendanceRangeQuery, useGetMonthlyAttendanceQuery, useGetOrganizationAttendanceQuery } from "@/services/api";
import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
-import { BarChart, ChartsTooltip, LineChart, pieArcClasses, pieArcLabelClasses, PieChart } from "@mui/x-charts";
+import { BarChart, ChartsAxisTooltipContent, ChartsTooltip, LineChart, pieArcClasses, pieArcLabelClasses, PieChart } from "@mui/x-charts";
import { ChartsNoDataOverlay } from "@mui/x-charts/ChartsOverlay";
import { formatDate } from "date-fns";
@@ -74,19 +74,22 @@ export default function AbsensiPage() {
Data Kehadiran Karyawan Setiap Perusahaan
- {attendanceSummary && v === "tooltip" ? "Attendance" : undefined!, color: "#2385DE", stack: "attendance", stackOffset: "expand", valueFormatter: (v) => v + " mandays"},
- {dataKey: "absent", label: (v) => v === "tooltip" ? "Absent" : undefined!, color: "#F7B500", stack: "attendance", stackOffset: "expand", valueFormatter: (v) => v + " mandays"},
+ undefined!, color: "#2385DE", stack: "attendance", stackOffset: "expand", valueFormatter: (v) => undefined!},
+ {dataKey: "absent", label: (v) => undefined!, color: "#F7B500", stack: "attendance", stackOffset: "expand", valueFormatter: (v) => undefined!},
]} xAxis={[
- {dataKey: "organization_code", label: "Nama Perusahaan", scaleType: "band", valueFormatter: (v, context) => context.location === "tooltip" ? attendanceSummary.find(e => e.organization_code === v)?.organization_name : v}
+ {dataKey: "organization_code", label: "Nama Perusahaan", scaleType: "band", valueFormatter: (v, context) => context.location === "tooltip" ? attendanceSummary?.find(e => e.organization_code === v)?.organization_name + ` (${Math.round(attendanceSummary?.find(e => e.organization_code === v)?.count!/attendanceSummary?.find(e => e.organization_code === v)?.workdays! * 100)}%)` : v }
]}
yAxis={[
{dataKey: "count",scaleType: "linear", valueFormatter: (v) => (v*100)+"%"}
]}
+ tooltip={{
+ trigger: "axis",
+ }}
slots={{
noDataOverlay: ()=> ,
}}
- />}
+ />
diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx
index ed9b6ad..a41559a 100644
--- a/src/app/(dashboard)/layout.tsx
+++ b/src/app/(dashboard)/layout.tsx
@@ -14,6 +14,7 @@ import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { setFilter } from "@/lib/slice/filter";
import { format } from "date-fns";
import { useGetFilterOptionsQuery } from "@/services/api";
+import { NeedLoginRoute } from "../RouteGuard";
export default function DashboardLayout({children}:{children: React.ReactNode}) {
@@ -25,128 +26,130 @@ export default function DashboardLayout({children}:{children: React.ReactNode})
const [region, setRegion] = React.useState("");
return (
-
-
-
-
-
-
-
-
Halaman
-
-
-
Data Karyawan
-
-
-
-
Absensi
-
-
-
-
Turn Over Rate
-
-
-
-
Produktifitas Karyawan
-
-
-
-
HR Cost
-
-
-
-
Log Out
-
-
-
Powered by
-
-
+
+
+
+
+
+
+
+
+
Halaman
+
+
+
Data Karyawan
+
+
+
+
Absensi
+
+
+
+
Turn Over Rate
+
+
+
+
Produktifitas Karyawan
+
+
+
+
HR Cost
+
+
+
+
Log Out
+
+
-
-
-
-
-
- dispatch(setFilter({...filter, start_date: format(date ?? new Date(), "yyyy-MM-dd")}))}
- />
- dispatch(setFilter({...filter, end_date: format(date ?? new Date(), "yyyy-MM-dd")}))}
- />
-
-
-
option.name}
- renderInput={(params) => }
- onChange={(e, value) => {
- dispatch(setFilter({...filter, organization_code: value?.codes ?? ""}));
- setRegion(value?.codes ?? "");
- }}
- />
-
+
+
+
+ dispatch(setFilter({...filter, start_date: format(date ?? new Date(), "yyyy-MM-dd")}))}
+ />
+ dispatch(setFilter({...filter, end_date: format(date ?? new Date(), "yyyy-MM-dd")}))}
+ />
+
+
+
option.name}
- renderInput={(params) => }
- onChange={(e, value) => {
- dispatch(setFilter({...filter, organization_code: value?.code ?? ""}));
- if (value?.code == undefined) dispatch(setFilter({...filter, organization_code: region}));
- }}
- />
- option.name}
- renderInput={(params) => }
- onChange={(e, value) => dispatch(setFilter({...filter, estate_name: value?.name ?? ""}))}
- />
+ options={filterOptions?.regions ?? []}
+ getOptionLabel={(option) => option.name}
+ renderInput={(params) => }
+ onChange={(e, value) => {
+ dispatch(setFilter({...filter, organization_code: value?.codes ?? ""}));
+ setRegion(value?.codes ?? "");
+ }}
+ />
+ option.name}
+ renderInput={(params) => }
+ onChange={(e, value) => {
+ dispatch(setFilter({...filter, organization_code: value?.code ?? ""}));
+ if (value?.code == undefined) dispatch(setFilter({...filter, organization_code: region}));
+ }}
+ />
+ option.name}
+ renderInput={(params) => }
+ onChange={(e, value) => dispatch(setFilter({...filter, estate_name: value?.name ?? ""}))}
+ />
+
+
+ dispatch(setFilter({...filter, job_name: e.target.value}))}>
+
+
+
+
+
+
+
-
-
dispatch(setFilter({...filter, job_name: e.target.value}))}>
-
-
-
-
-
-
+
+ {children}
-
- {children}
-
-
+
)
}
\ No newline at end of file
diff --git a/src/app/AppProvider.tsx b/src/app/AppProvider.tsx
index 9ae6a15..517601f 100644
--- a/src/app/AppProvider.tsx
+++ b/src/app/AppProvider.tsx
@@ -1,23 +1,23 @@
"use client";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
-import { makeStore, AppStore } from '../lib/store'
+import { AppStore, persistor, store } from '../lib/store'
import { useRef } from "react";
import { Provider } from "react-redux";
import { StyledEngineProvider } from "@mui/material";
+import { PersistGate } from "redux-persist/integration/react";
+import { GuardedRoute } from "./RouteGuard";
export default function AppProvider({children}:{children: React.ReactNode}) {
- const storeRef = useRef
()
- if (!storeRef.current) {
- // Create the store instance the first time this renders
- storeRef.current = makeStore()
- }
-
return (
-
-
- {children}
-
+
+
+
+
+ {children}
+
+
+
)
diff --git a/src/app/RouteGuard.tsx b/src/app/RouteGuard.tsx
new file mode 100644
index 0000000..5f2cc92
--- /dev/null
+++ b/src/app/RouteGuard.tsx
@@ -0,0 +1,36 @@
+"use client";
+import React from "react";
+
+import { useAppSelector } from "@/lib/hooks";
+import { usePathname, useRouter } from "next/navigation";
+
+// set route for disabled user to come, if user has login
+const IF_LOGIN = ["/login", "/register"];
+
+export function GuardedRoute({ children }: React.PropsWithChildren) {
+ const router = useRouter();
+ const pathname = usePathname();
+
+ const auth = useAppSelector((state) => state.auth);
+ console.log("GuardedRoute", auth, pathname);
+
+ return children;
+}
+
+interface IGuardedRoute {
+ children: React.ReactNode;
+ role: string;
+}
+
+export function NeedLoginRoute({ children, role }: IGuardedRoute) {
+ const router = useRouter();
+
+ const auth = useAppSelector((state) => state.auth);
+
+ if (auth.token && auth.roles.includes(role)) {
+ return children;
+ }
+ console.log("NeedLoginRoute", auth, role);
+ router.push("/login");
+ return;
+}
\ No newline at end of file
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
new file mode 100644
index 0000000..9d90a0c
--- /dev/null
+++ b/src/app/login/page.tsx
@@ -0,0 +1,63 @@
+"use client"
+import Image from "next/image";
+import background from "../../../public/images/background.jpg";
+import loginImg from "../../../public/images/logo-login.png";
+import midsuit from "../../../public/images/midsuit-login.png";
+import { useEffect, useState } from "react";
+import { useLoginMutation } from "@/services/api";
+import { useAppDispatch } from "@/lib/hooks";
+import { setRoles, setToken, setUser } from "@/lib/slice/auth";
+import { useRouter } from "next/navigation";
+
+export default function LoginPage(){
+ const [login, {data: loginData, error: loginError}] = useLoginMutation();
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const dispatch = useAppDispatch();
+ const router = useRouter();
+
+
+ useEffect(() => {
+ if(loginData){
+ dispatch(setUser(loginData.user))
+ dispatch(setToken(loginData.token))
+ dispatch(setRoles(loginData.roles))
+ router.push("/karyawan")
+ }
+ }, [loginData, loginError])
+
+ return (
+
+
+
+
+
+
+
+
+
HRM Dashboard
+
Masukan Username & Password
+
+
+ setUsername(e.target.value)}/>
+ setPassword(e.target.value)}/>
+
+
+ {loginError &&
{(loginError as any).data.message}
}
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/logout/page.tsx b/src/app/logout/page.tsx
new file mode 100644
index 0000000..7949cd5
--- /dev/null
+++ b/src/app/logout/page.tsx
@@ -0,0 +1,18 @@
+"use client"
+import { useAppDispatch } from "@/lib/hooks";
+import { setRoles, setToken, setUser } from "@/lib/slice/auth";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+export default function LogoutPage() {
+ const dispatch = useAppDispatch();
+ const router = useRouter();
+ useEffect(() => {
+ dispatch(setUser(null));
+ dispatch(setToken(null));
+ dispatch(setRoles([]));
+ router.push("/login");
+ }, []);
+
+ return
+}
\ No newline at end of file
diff --git a/src/lib/slice/auth.ts b/src/lib/slice/auth.ts
new file mode 100644
index 0000000..23d5f74
--- /dev/null
+++ b/src/lib/slice/auth.ts
@@ -0,0 +1,37 @@
+import { User } from "@/services/types";
+import { createSlice } from "@reduxjs/toolkit/react";
+
+interface AuthState {
+ user: User | null;
+ token: string | null;
+ roles: string[];
+ isLoading: boolean;
+ }
+
+ const initialState: AuthState = {
+ user: null,
+ token: null,
+ roles: [],
+ isLoading: false,
+ };
+
+ export const authSlice = createSlice({
+ name: "auth",
+ initialState,
+ reducers: {
+ setUser: (state, action) => {
+ state.user = action.payload;
+ },
+ setToken: (state, action) => {
+ state.token = action.payload;
+ },
+ setRoles: (state, action) => {
+ state.roles = action.payload;
+ },
+ setIsLoading: (state, action) => {
+ state.isLoading = action.payload;
+ },
+ },
+});
+
+export const { setUser, setToken, setRoles, setIsLoading } = authSlice.actions;
\ No newline at end of file
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
new file mode 100644
index 0000000..354474b
--- /dev/null
+++ b/src/lib/storage.ts
@@ -0,0 +1,23 @@
+"use client";
+import createWebStorage from "redux-persist/lib/storage/createWebStorage";
+
+const createNoopStorage = () => {
+ return {
+ getItem(_key: any) {
+ return Promise.resolve(null);
+ },
+ setItem(_key: any, value: any) {
+ return Promise.resolve(value);
+ },
+ removeItem(_key: any) {
+ return Promise.resolve();
+ },
+ };
+};
+
+const storage =
+ typeof window !== "undefined"
+ ? createWebStorage("local")
+ : createNoopStorage();
+
+export default storage;
\ No newline at end of file
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 3de5a2f..c59e66e 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -1,20 +1,33 @@
import { api } from '@/services/api'
import { configureStore } from '@reduxjs/toolkit'
import filterReducer from '@/lib/slice/filter'
+import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from "redux-persist";
+import { authSlice } from './slice/auth';
+import storage from './storage';
-export const makeStore = () => {
- return configureStore({
- reducer: {
- [api.reducerPath]: api.reducer,
- filter: filterReducer,
- },
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware().concat(api.middleware),
- })
-}
+const persistAuth = persistReducer({
+ key: 'auth',
+ storage: storage,
+}, authSlice.reducer)
+export const store = configureStore({
+ reducer: {
+ [api.reducerPath]: api.reducer,
+ auth: persistAuth,
+ filter: filterReducer,
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware({
+ serializableCheck: {
+ ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
+ },
+ }).concat(api.middleware),
+})
+
+
+export const persistor = persistStore(store)
// Infer the type of makeStore
-export type AppStore = ReturnType
+export type AppStore = typeof store
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType
export type AppDispatch = AppStore['dispatch']
\ No newline at end of file
diff --git a/src/services/api.ts b/src/services/api.ts
index 6bff3bb..8e8b86b 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -1,10 +1,38 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
-import { AttendanceRange, AttendanceSummary, EmployeeSummary, FilterOptions, MonthlyAttendance, MonthlyEmployee, ResignationCategory, ResignationReason, ResignationType as ResignationType, ResignSummary } from './types'
+import { AttendanceRange, AttendanceSummary, EmployeeSummary, FilterOptions, MonthlyAttendance, MonthlyEmployee, ResignationCategory, ResignationReason, ResignationType as ResignationType, ResignSummary, User } from './types'
import { Response , Filter} from './types'
export const api = createApi({
- baseQuery: fetchBaseQuery({ baseUrl: 'https://erp.julongindonesia.com:8443/api' }),
+ baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8080' }),
endpoints: (builder) => ({
+ login: builder.mutation<{
+ user: User;
+ token: string;
+ roles: string[];
+ }, { username: string, password: string }>({
+ query: ({ username, password }) => ({
+ url: '/login',
+ method: 'POST',
+ body: { username, password },
+ }),
+ transformResponse: (response: Response) => {
+ if (response.status === "success") {
+ return response.data!;
+ }
+ },
+ }),
+ authCheck: builder.query<{
+ user: User;
+ token: string;
+ roles: string[];
+ }, void>({
+ query: () => ({ url: '/auth-check' }),
+ transformResponse: (response: Response) => {
+ if (response.status === "success") {
+ return response.data!;
+ }
+ },
+ }),
getFilterOptions: builder.query({
query: (params) => ({ url: '/dashboard/filter-options', params }),
transformResponse: (response: Response) => {
@@ -90,4 +118,5 @@ export const api = createApi({
export const { useGetFilterOptionsQuery, useGetEmployeeSummaryQuery, useGetMonthlyEmployeeQuery, useGetMonthlyAttendanceQuery,
useGetOrganizationAttendanceQuery, useGetAttendanceRangeQuery, useGetResignSummaryQuery, useGetResignTypeQuery,
+ useLoginMutation, useAuthCheckQuery,
useGetResignCategoryQuery, useGetResignReasonQuery } = api
\ No newline at end of file
diff --git a/src/services/types.ts b/src/services/types.ts
index 708259d..98a8535 100644
--- a/src/services/types.ts
+++ b/src/services/types.ts
@@ -86,4 +86,12 @@ export type ResignationCategory = {
export type ResignationReason = {
reason: string;
count: number;
+}
+
+export type User = {
+ ad_user_id: number;
+ name: string;
+ username: string;
+ password: string;
+ hc_employee_id: number;
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index b1695f5..854da73 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1508,6 +1508,11 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+redux-persist@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
+ integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
+
redux-thunk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"