diff --git a/Modules/Client/Http/Controllers/Api/AuthController.php b/Modules/Client/Http/Controllers/Api/AuthController.php index 8e36cde6..825fc3eb 100755 --- a/Modules/Client/Http/Controllers/Api/AuthController.php +++ b/Modules/Client/Http/Controllers/Api/AuthController.php @@ -2,45 +2,97 @@ namespace Modules\Client\Http\Controllers\Api; +use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; +use Symfony\Component\HttpFoundation\Response; class AuthController extends Controller { public function login(Request $request) { $request->validate([ - 'email' => 'required|email', - 'password' => 'required' + 'phoneOrEmail' => 'required' ]); $user = User::query() - ->where('email', $request->email) - ->first(); + ->when(filter_var($request->phoneOrEmail, FILTER_VALIDATE_EMAIL), function (Builder $query) use ($request) { + $query->getQuery()->where('email', $request->phoneOrEmail); + }, function (Builder $query) use ($request) { + $query->getQuery()->where('phone', $request->phoneOrEmail); + }) + ->first(); if (!$user) { - return response(['message' => 'User Tidak Ditemukan'], 404); + $message = filter_var($request->phoneOrEmail, FILTER_VALIDATE_EMAIL) ? "User dengan alamat email " . $request->phoneOrEmail . " tidak ditemukan" : "User dengan nomor telepon " . $request->phoneOrEmail . " tidak ditemukan"; + + return Helper::responseJson(statusCode: Response::HTTP_NOT_FOUND, message: $message); } - if (!Hash::check($request->password, $user->password)) { - return response(['message' => 'Password Salah'], 403); + if (filter_var($request->phoneOrEmail, FILTER_VALIDATE_EMAIL)) { + User::query()->find($user->id)->update([ + 'email' => $request->phoneOrEmail, + 'otp' => rand(1000, 9999), + 'otp_created_at' => now() + ]); + } else { + User::query()->find($user->id)->update([ + 'phone' => $request->phoneOrEmail, + 'otp' => rand(1000, 9999), + 'otp_created_at' => now() + ]); } - return response([ - 'message' => 'Selamat Datang', - 'user' => $user, - 'token' => $user->createToken('app')->plainTextToken + // TODO Send the OTP + if (filter_var($request->phoneOrEmail, FILTER_VALIDATE_EMAIL)) { + // Send Email + } else { + // Send Whatsapp + } + + return Helper::responseJson(message: 'OTP Terkirim'); + } + + public function validateOtp(Request $request) + { + echo '
';
+        var_dump($_POST, $_GET);
+        echo '
'; + exit; + + $request->validate([ + 'phoneOrEmail' => 'required', + 'otp' => 'required' ]); + + $user = User::query() + ->when(filter_var($request->phoneOrEmail, FILTER_VALIDATE_EMAIL), function (Builder $query) use ($request) { + $query->getQuery()->where('email', $request->phoneOrEmail); + }, function (Builder $query) use ($request) { + $query->getQuery()->where('phone', $request->phoneOrEmail); + }) + ->first(); + + if ($user->otp == $request->otp) { + return Helper::responseJson( + data: [ + 'token' => $user->createToken('app')->plainTextToken, + 'user' => $user, + ], + message: 'Selamat Datang' + ); + } + + return Helper::responseJson(message: 'OTP yang anda masukan salah!'); } public function logout(Request $request) { - $token = $request->bearerToken(); - Auth::user()->tokens()->where('id', $token)->delete(); + $request->user()->currentAccessToken()->delete(); - return response(['message' => 'Berhasil Logout.']); + return Helper::responseJson(message: 'Berhasil Logout.'); } } diff --git a/Modules/Client/Routes/api.php b/Modules/Client/Routes/api.php index 09b10e41..418acb51 100755 --- a/Modules/Client/Routes/api.php +++ b/Modules/Client/Routes/api.php @@ -18,19 +18,18 @@ use Modules\Client\Http\Controllers\Api\UserController; */ Route::prefix('client')->group(function () { - - Route::post('login', [AuthController::class, 'login'])->name('login'); - Route::post('forget-password', [AuthController::class, 'forgetPassword'])->name('forget-password'); - Route::post('verify-email', [AuthController::class, 'verifyEmail'])->name('verify-email'); + + Route::controller(AuthController::class)->group(function () { + Route::post('login', 'login'); + Route::post('verify-code', 'validateOtp'); + }); Route::middleware('auth:sanctum')->group(function () { - + Route::post('logout', [AuthController::class, 'logout'])->name('logout'); Route::get('/user', [UserController::class, 'index']); Route::get('dashboard', [DashboardController::class, 'index']); Route::get('members', [MemberController::class, 'index']); - }); - }); diff --git a/frontend/client-portal/public/images/login-image.gif b/frontend/client-portal/public/images/login-image.gif new file mode 100644 index 00000000..5f123cf8 Binary files /dev/null and b/frontend/client-portal/public/images/login-image.gif differ diff --git a/frontend/client-portal/src/@types/auth.ts b/frontend/client-portal/src/@types/auth.ts index 3df73c76..dbc15390 100755 --- a/frontend/client-portal/src/@types/auth.ts +++ b/frontend/client-portal/src/@types/auth.ts @@ -1,4 +1,4 @@ -import { UserCredential } from 'firebase/auth'; +// import { UserCredential } from 'firebase/auth'; // ---------------------------------------------------------------------- @@ -26,20 +26,20 @@ export type JWTContextType = { isInitialized: boolean; user: AuthUser; method: 'jwt'; - login: (email: string, password: string) => Promise; - register: (email: string, password: string, firstName: string, lastName: string) => Promise; + login: (phoneOrEmail: string) => Promise; + validateOtp: (phoneOrEmail: string, otp: string) => Promise logout: () => Promise; }; -export type FirebaseContextType = { - isAuthenticated: boolean; - isInitialized: boolean; - user: AuthUser; - method: 'firebase'; - login: (email: string, password: string) => Promise; - register: (email: string, password: string, firstName: string, lastName: string) => Promise; - logout: () => Promise; -}; +// export type FirebaseContextType = { +// isAuthenticated: boolean; +// isInitialized: boolean; +// user: AuthUser; +// method: 'firebase'; +// login: (email: string, password: string) => Promise; +// register: (email: string, password: string, firstName: string, lastName: string) => Promise; +// logout: () => Promise; +// }; export type AWSCognitoContextType = { isAuthenticated: boolean; diff --git a/frontend/client-portal/src/components/nav-section/type.ts b/frontend/client-portal/src/components/nav-section/type.ts index 870407d8..f8a9ea44 100755 --- a/frontend/client-portal/src/components/nav-section/type.ts +++ b/frontend/client-portal/src/components/nav-section/type.ts @@ -29,7 +29,7 @@ export type NavItemProps = { export interface NavSectionProps extends BoxProps { isCollapse?: boolean; navConfig: { - subheader: string; + subheader?: string; items: NavListProps[]; }[]; } diff --git a/frontend/client-portal/src/contexts/LaravelAuthContext.tsx b/frontend/client-portal/src/contexts/LaravelAuthContext.tsx index d30559b5..f78d2a81 100755 --- a/frontend/client-portal/src/contexts/LaravelAuthContext.tsx +++ b/frontend/client-portal/src/contexts/LaravelAuthContext.tsx @@ -1,18 +1,16 @@ import { createContext, ReactNode, useEffect, useReducer } from 'react'; // utils import axios from '../utils/axios'; -// import { isValidToken, setSession } from '../utils/jwt'; -import { setSession, getSession, getUser } from '../utils/token'; +import { setSession, getSession } from '../utils/token'; // @types import { ActionMap, AuthState, AuthUser, JWTContextType } from '../@types/auth'; // ---------------------------------------------------------------------- -import { Navigate, useLocation } from 'react-router-dom'; enum Types { Initial = 'INITIALIZE', Login = 'LOGIN', + ValidateOtp = 'VALIDATE-OTP', Logout = 'LOGOUT', - Register = 'REGISTER', } type JWTAuthPayload = { @@ -20,13 +18,11 @@ type JWTAuthPayload = { isAuthenticated: boolean; user: AuthUser; }; - [Types.Login]: { + [Types.Login]: undefined; + [Types.ValidateOtp]: { user: AuthUser; }; [Types.Logout]: undefined; - [Types.Register]: { - user: AuthUser; - }; }; export type JWTActions = ActionMap[keyof ActionMap]; @@ -46,6 +42,12 @@ const JWTReducer = (state: AuthState, action: JWTActions) => { user: action.payload.user, }; case 'LOGIN': + return { + ...state, + isAuthenticated: false, + user: null, + }; + case 'VALIDATE-OTP': return { ...state, isAuthenticated: true, @@ -57,14 +59,6 @@ const JWTReducer = (state: AuthState, action: JWTActions) => { isAuthenticated: false, user: null, }; - - case 'REGISTER': - return { - ...state, - isAuthenticated: true, - user: action.payload.user, - }; - default: return state; } @@ -80,15 +74,15 @@ type AuthProviderProps = { function AuthProvider({ children }: AuthProviderProps) { const [state, dispatch] = useReducer(JWTReducer, initialState); - let location = useLocation(); useEffect(() => { - const initialize = async () => { - console.log('initialize', state) + (async () => { + console.log('initialize', state); + try { const accessToken = getSession(); - - console.log('') + + console.log(''); if (accessToken) { setSession(accessToken); @@ -120,61 +114,55 @@ function AuthProvider({ children }: AuthProviderProps) { }, }); } - }; - - initialize(); + })(); }, []); - const login = async (email: string, password: string) => { - return axios - .post('/login', { email, password }) - .then((response) => { - const { user, token } = response.data; - setSession(token); + const login = async (phoneOrEmail: string) => + axios + .post('/login', { phoneOrEmail }) + .then(() => { + dispatch({ + type: Types.Login, + }); + }) + .catch((error) => { + if (error.response.status !== 404) throw error.response; + if (error.response.status !== 422) throw error.response; + }); - dispatch({ - type: Types.Login, - payload: { - user, - } - }); - }) - .catch(error => { - if (error.response.status !== 404) throw error.response - if (error.response.status !== 422) throw error.response - }) - }; + const validateOtp = async (phoneOrEmail: string, otp: string) => + axios + .post('/verify-code', { otp }) + .then((response) => { + const { user, token } = response.data.data; + setSession(token); - const register = async (email: string, password: string, firstName: string, lastName: string) => { - const response = await axios.post('/api/register', { - email, - password, - firstName, - lastName, - }); - const { accessToken, user } = response.data; - - window.localStorage.setItem('accessToken', accessToken); - dispatch({ - type: Types.Register, - payload: { - user, - }, - }); - }; + dispatch({ + type: Types.ValidateOtp, + payload: { + user, + }, + }); + }) + .catch((error) => { + if (error.response.status !== 404) throw error.response; + if (error.response.status !== 422) throw error.response; + }); const logout = async () => { + await axios.post('/logout'); setSession(null); dispatch({ type: Types.Logout }); }; - return ( {children} @@ -182,9 +170,9 @@ function AuthProvider({ children }: AuthProviderProps) { ); // if (state.isInitialized) { - // return (!state.isAuthenticated && location.pathname !== '/auth/login') ? + // return (!state.isAuthenticated && location.pathname !== '/auth/login') ? // () - // : false && location.pathname == '/auth/login' ? + // : false && location.pathname == '/auth/login' ? // () // : ( // PRIME CENTER - ) - : ( + + ) : ( diff --git a/frontend/client-portal/src/pages/AlarmCenter/Index.tsx b/frontend/client-portal/src/pages/AlarmCenter/Index.tsx new file mode 100755 index 00000000..793014eb --- /dev/null +++ b/frontend/client-portal/src/pages/AlarmCenter/Index.tsx @@ -0,0 +1,34 @@ +// mui +import { Container, Grid, Card } from '@mui/material'; +// components +import Page from '../../components/Page'; +// utils +import useSettings from '../../hooks/useSettings'; +// sections +// import ListTable from '../../sections/claimreports/ListTable'; +// import ClaimStatusCard from '../../sections/claimreports/ClaimStatusCard'; + +import List from '../Members/List'; + +export default function Drugs() { + const { themeStretch } = useSettings(); + + // const { corporate_id } = useParams(); + + return ( + + + + + {/* */} + + + + + + + + + + ); +} diff --git a/frontend/client-portal/src/pages/Members/Create.tsx b/frontend/client-portal/src/pages/Members/Create.tsx index e0527248..90df6205 100755 --- a/frontend/client-portal/src/pages/Members/Create.tsx +++ b/frontend/client-portal/src/pages/Members/Create.tsx @@ -1,15 +1,14 @@ import * as Yup from 'yup'; -import { yupResolver } from "@hookform/resolvers/yup"; -import { Card, Collapse, Divider, Grid, Stack, Typography } from "@mui/material"; -import { useForm } from "react-hook-form"; -import { useParams } from "react-router-dom"; -import HeaderBreadcrumbs from "../../../components/HeaderBreadcrumbs"; -import { FormProvider, RHFCheckbox, RHFSelect, RHFTextField } from "../../../components/hook-form"; -import Page from "../../../components/Page"; -import useSettings from "../../../hooks/useSettings"; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Card, Collapse, Divider, Grid, Stack, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; +import HeaderBreadcrumbs from '../../components/HeaderBreadcrumbs'; +import { FormProvider, RHFCheckbox, RHFSelect, RHFTextField } from '../../components/hook-form'; +import Page from '../../components/Page'; +import useSettings from '../../hooks/useSettings'; import { useMemo, useState } from 'react'; -import Form from "./Form"; - +import Form from './Form'; export default function Divisions() { const { themeStretch } = useSettings(); @@ -48,13 +47,12 @@ export default function Divisions() { } = methods; const onSubmit = async (data: any) => { - console.log(data) + console.log(data); }; const pageTitle = 'Create Formularium'; return ( - - -
+ diff --git a/frontend/client-portal/src/pages/Members/Form.tsx b/frontend/client-portal/src/pages/Members/Form.tsx index 9ec18471..da9803d0 100755 --- a/frontend/client-portal/src/pages/Members/Form.tsx +++ b/frontend/client-portal/src/pages/Members/Form.tsx @@ -33,10 +33,10 @@ import { RHFMultiCheckbox, RHFCheckbox, RHFCustomMultiCheckbox, -} from '../../../components/hook-form'; -import { Corporate } from '../../../@types/corporates'; -import axios from '../../../utils/axios'; -import { fCurrency } from '../../../utils/formatNumber'; +} from '../../components/hook-form'; +import { Corporate } from '../../@types/corporates'; +import axios from '../../utils/axios'; +import { fCurrency } from '../../utils/formatNumber'; const LabelStyle = styled(Typography)(({ theme }) => ({ ...theme.typography.subtitle2, @@ -110,26 +110,31 @@ export default function FormulariumForm({ isEdit, currentFormularium }: Props) { if (!isEdit) { const response = await axios.post('/master/formulariums', data); } else { - const response = await axios.put('/master/formulariums/' + currentFormularium?.id ?? '', data); + const response = await axios.put( + '/master/formulariums/' + currentFormularium?.id ?? '', + data + ); } reset(); - enqueueSnackbar(!isEdit ? 'Formularium Created Successfully!' : 'Formularium Udpated Successfully!', { variant: 'success' }); + enqueueSnackbar( + !isEdit ? 'Formularium Created Successfully!' : 'Formularium Udpated Successfully!', + { variant: 'success' } + ); navigate('/master/formularium'); } catch (error: any) { if (error && error.response.status === 422) { - for (const [key, value] of Object.entries(error.response.data.errors)) { + for (const [key, value] of Object.entries(error.response.data.errors)) { setError(key, { message: value[0] }); enqueueSnackbar(value[0] ?? 'Failed Processing Request', { variant: 'error' }); } - } - else { + } else { enqueueSnackbar(error.message ?? 'Failed Processing Request', { variant: 'error' }); } } - const ascent = document?.querySelector("ascent"); + const ascent = document?.querySelector('ascent'); if (ascent != null) { - ascent.innerHTML = ""; + ascent.innerHTML = ''; } }; @@ -151,35 +156,34 @@ export default function FormulariumForm({ isEdit, currentFormularium }: Props) { setValue('logo', null); }; - const linking_rules_checkbox_name = "linking_rules" + const linking_rules_checkbox_name = 'linking_rules'; const linking_tools = [ { - "value" : "nrik", - "label" : "No. KTP" + value: 'nrik', + label: 'No. KTP', }, { - "value" : "nik", - "label" : "Nomor Induk Karyawan (NIK)" + value: 'nik', + label: 'Nomor Induk Karyawan (NIK)', }, { - "value" : "member_id", - "label" : "Member ID" + value: 'member_id', + label: 'Member ID', }, { - "value" : "phone", - "label" : "Nomor Telepon" + value: 'phone', + label: 'Nomor Telepon', }, { - "value" : "email", - "label" : "E-Mail" + value: 'email', + label: 'E-Mail', }, - ] - + ]; - const importForm = useRef(null) + const importForm = useRef(null); const [anchorEl, setAnchorEl] = useState(null); - const [currentImportFileName, setCurrentImportFileName] = useState(null); - + const [currentImportFileName, setCurrentImportFileName] = useState(null); + const handleClose = () => { setAnchorEl(null); }; @@ -189,51 +193,73 @@ export default function FormulariumForm({ isEdit, currentFormularium }: Props) { handleClose(); importForm.current ? importForm.current.click() : console.log('No File selected'); } else { - alert('No file selected') + alert('No file selected'); } - } + }; const handleCancelImportButton = () => { - importForm.current.value = ""; - importForm.current.dispatchEvent(new Event("change", { bubbles: true })); - } + importForm.current.value = ''; + importForm.current.dispatchEvent(new Event('change', { bubbles: true })); + }; const handleImportChange = (event: any) => { if (event.target.files[0]) { - setCurrentImportFileName(event.target.files[0].name) + setCurrentImportFileName(event.target.files[0].name); } else { setCurrentImportFileName(null); } - } - + }; return ( - - Formularium Detail
- - {(!(currentFormularium?.id) && Will be generated if empty)} + + {!currentFormularium?.id && ( + Will be generated if empty + )}
Formularium Drug List Import - + - - {(currentImportFileName && )} + + {currentImportFileName && ( + + )} - + {!isEdit ? 'Save New Corporate' : 'Save Update'} -
); -}; +} diff --git a/frontend/client-portal/src/pages/auth/Login.tsx b/frontend/client-portal/src/pages/auth/Login.tsx index a3ec2f16..ca0ae3ea 100755 --- a/frontend/client-portal/src/pages/auth/Login.tsx +++ b/frontend/client-portal/src/pages/auth/Login.tsx @@ -1,19 +1,15 @@ -import { capitalCase } from 'change-case'; -import { Link as RouterLink } from 'react-router-dom'; // @mui import { styled } from '@mui/material/styles'; -import { Box, Card, Stack, Link, Alert, Tooltip, Container, Typography } from '@mui/material'; -// routes -import { PATH_AUTH } from '../../routes/paths'; -// hooks -import useAuth from '../../hooks/useAuth'; -import useResponsive from '../../hooks/useResponsive'; +import { Box, Card, Divider, Grid, Link, Stack, Typography } from '@mui/material'; // components import Page from '../../components/Page'; -import Logo from '../../components/Logo'; import Image from '../../components/Image'; +import Logo from '../../components/Logo'; +// react +import { useEffect, useState } from 'react'; // sections -import { LoginForm } from '../../sections/auth/login'; +import { LoginEmailForm, LoginPhoneForm } from '../../sections/auth/login'; +import { useLocation } from 'react-router'; // ---------------------------------------------------------------------- @@ -21,117 +17,88 @@ const RootStyle = styled('div')(({ theme }) => ({ [theme.breakpoints.up('md')]: { display: 'flex', }, -})); - -const HeaderStyle = styled('header')(({ theme }) => ({ - top: 0, - zIndex: 9, - lineHeight: 0, - width: '100%', - display: 'flex', - alignItems: 'center', - position: 'absolute', - padding: theme.spacing(3), - justifyContent: 'space-between', - [theme.breakpoints.up('md')]: { - alignItems: 'flex-start', - padding: theme.spacing(7, 5, 0, 7), - }, -})); - -const SectionStyle = styled(Card)(({ theme }) => ({ - width: '100%', - maxWidth: 464, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - margin: theme.spacing(2, 0, 2, 2), -})); - -const ContentStyle = styled('div')(({ theme }) => ({ - maxWidth: 480, - margin: 'auto', - display: 'flex', minHeight: '100vh', flexDirection: 'column', justifyContent: 'center', - padding: theme.spacing(12, 0), + alignItems: 'center', +})); + +const ContentStyle = styled(Card)(({ theme }) => ({ + [theme.breakpoints.up('md')]: { + maxHeight: '600px', + maxWidth: '1000px', + }, })); // ---------------------------------------------------------------------- export default function Login() { - const { method } = useAuth(); + const { state } = useLocation(); + const [formPhone, setFormPhone] = useState(false); - const smUp = useResponsive('up', 'sm'); + const handlerChange = (event: any, setForm: boolean) => { + event.preventDefault(); + setFormPhone(setForm); + }; - const mdUp = useResponsive('up', 'md'); + useEffect(() => { + // if (state !== null) { + // setFormPhone(state.formPhone); + // } + // console.log(state); + }, [state]); return ( - - - {smUp && ( - - Has problem with your account? {''} - { - window.location.href = "mailto:admin@linksehat.com"; - e.preventDefault(); - }}> - Contact Us - - - )} - + + + + login + + + + + + + Sign in to LinkSehat + + + Enter your details below. + + + - {/* {mdUp && ( - - - Hi, Welcome Back - - login - - )} */} + {formPhone ? ( + + ) : ( + + )} - - - - - - Sign in to LinkSehat - - Enter your details below. - + Atau - - <> - - - - - - - - {false && !smUp && ( - - Don’t have an account?{' '} - - Get started - - - )} - - + + {formPhone ? ( + handlerChange(event, false)} + > + Masuk menggunakan email + + ) : ( + handlerChange(event, true)} + > + Masuk menggunakan nomor handphone + + )} + + + + ); diff --git a/frontend/client-portal/src/pages/auth/Register.tsx b/frontend/client-portal/src/pages/auth/Register.tsx deleted file mode 100755 index 89a364d3..00000000 --- a/frontend/client-portal/src/pages/auth/Register.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { capitalCase } from 'change-case'; -import { Link as RouterLink } from 'react-router-dom'; -// @mui -import { styled } from '@mui/material/styles'; -import { Box, Card, Link, Container, Typography, Tooltip } from '@mui/material'; -// hooks -import useAuth from '../../hooks/useAuth'; -import useResponsive from '../../hooks/useResponsive'; -// routes -import { PATH_AUTH } from '../../routes/paths'; -// components -import Page from '../../components/Page'; -import Logo from '../../components/Logo'; -import Image from '../../components/Image'; -// sections -import { RegisterForm } from '../../sections/auth/register'; - -// ---------------------------------------------------------------------- - -const RootStyle = styled('div')(({ theme }) => ({ - [theme.breakpoints.up('md')]: { - display: 'flex', - }, -})); - -const HeaderStyle = styled('header')(({ theme }) => ({ - top: 0, - zIndex: 9, - lineHeight: 0, - width: '100%', - display: 'flex', - alignItems: 'center', - position: 'absolute', - padding: theme.spacing(3), - justifyContent: 'space-between', - [theme.breakpoints.up('md')]: { - alignItems: 'flex-start', - padding: theme.spacing(7, 5, 0, 7), - }, -})); - -const SectionStyle = styled(Card)(({ theme }) => ({ - width: '100%', - maxWidth: 464, - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - margin: theme.spacing(2, 0, 2, 2), -})); - -const ContentStyle = styled('div')(({ theme }) => ({ - maxWidth: 480, - margin: 'auto', - display: 'flex', - minHeight: '100vh', - flexDirection: 'column', - justifyContent: 'center', - padding: theme.spacing(12, 0), -})); - -// ---------------------------------------------------------------------- - -export default function Register() { - const { method } = useAuth(); - - const smUp = useResponsive('up', 'sm'); - - const mdUp = useResponsive('up', 'md'); - - return ( - - - - - {smUp && ( - - Already have an account? {''} - - Login - - - )} - - - {mdUp && ( - - - Manage the job more effectively with Minimal - - register - - )} - - - - - - - Get started absolutely free. - - - Free forever. No credit card needed. - - - - <> - - - - - - - - - By registering, I agree to Minimal  - - Terms of Service - - {''}and{''} - - Privacy Policy - - . - - - {!smUp && ( - - Already have an account?{' '} - - Login - - - )} - - - - - ); -} diff --git a/frontend/client-portal/src/pages/auth/ResetPassword.tsx b/frontend/client-portal/src/pages/auth/ResetPassword.tsx deleted file mode 100755 index bd306e90..00000000 --- a/frontend/client-portal/src/pages/auth/ResetPassword.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; -// @mui -import { styled } from '@mui/material/styles'; -import { Box, Button, Container, Typography } from '@mui/material'; -// layouts -import LogoOnlyLayout from '../../layouts/LogoOnlyLayout'; -// routes -import { PATH_AUTH } from '../../routes/paths'; -// components -import Page from '../../components/Page'; -// sections -import { ResetPasswordForm } from '../../sections/auth/reset-password'; -// assets -import { SentIcon } from '../../assets'; - -// ---------------------------------------------------------------------- - -const RootStyle = styled('div')(({ theme }) => ({ - display: 'flex', - minHeight: '100%', - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(12, 0), -})); - -// ---------------------------------------------------------------------- - -export default function ResetPassword() { - const [email, setEmail] = useState(''); - - const [sent, setSent] = useState(false); - - return ( - - - - - - - {!sent ? ( - <> - - Forgot your password? - - - - Please enter the email address associated with your account and We will email you - a link to reset your password. - - - setSent(true)} - onGetEmail={(value) => setEmail(value)} - /> - - - - ) : ( - - - - - Request sent successfully - - - - We have sent a confirmation email to   - {email} -
- Please check your email. -
- - -
- )} -
-
-
-
- ); -} diff --git a/frontend/client-portal/src/pages/auth/VerifyCode.tsx b/frontend/client-portal/src/pages/auth/VerifyCode.tsx index 1ca75352..43cfacca 100755 --- a/frontend/client-portal/src/pages/auth/VerifyCode.tsx +++ b/frontend/client-portal/src/pages/auth/VerifyCode.tsx @@ -1,66 +1,109 @@ -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom'; // @mui import { styled } from '@mui/material/styles'; -import { Box, Button, Link, Container, Typography } from '@mui/material'; -// layouts -import LogoOnlyLayout from '../../layouts/LogoOnlyLayout'; -// routes -import { PATH_AUTH } from '../../routes/paths'; +import { Box, Card, Divider, IconButton, Grid, Link, Stack, Typography } from '@mui/material'; // components import Page from '../../components/Page'; +import Image from '../../components/Image'; import Iconify from '../../components/Iconify'; // sections import { VerifyCodeForm } from '../../sections/auth/verify-code'; +import { useEffect } from 'react'; // ---------------------------------------------------------------------- const RootStyle = styled('div')(({ theme }) => ({ - display: 'flex', - height: '100%', + [theme.breakpoints.up('md')]: { + display: 'flex', + }, + minHeight: '100vh', + flexDirection: 'column', + justifyContent: 'center', alignItems: 'center', - padding: theme.spacing(12, 0), })); +const ContentStyle = styled(Card)(({ theme }) => ({ + [theme.breakpoints.up('md')]: { + maxHeight: '600px', + maxWidth: '1000px', + }, +})); // ---------------------------------------------------------------------- export default function VerifyCode() { + const { state } = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + if (state === null) { + navigate('/auth/login'); + } else { + console.log(state); + } + }, [state, navigate]); + return ( - + - + + + + login + + + + + + navigate('/auth/login', { state: { formPhone: state.formPhone } }) + } + > + + + + Verifikasi OTP + + + + + Masukkan kode OTP anda disini + + + - - - - - - Please check your email! - - - We have emailed a 6-digit confirmation code to acb@domain, please enter the code in - below box to verify your email. - - - - - - Don’t have a code?   - {}}> - Resend code - - - - + Atau + + + {state.formPhone === false ? ( + + navigate('/auth/login', { state: { formPhone: state.formPhone } }) + } + > + Masuk menggunakan nomor handphone + + ) : ( + + navigate('/auth/login', { state: { formPhone: state.formPhone } }) + } + > + Masuk menggunakan email + + )} + + + + ); diff --git a/frontend/client-portal/src/routes/index.tsx b/frontend/client-portal/src/routes/index.tsx index 671f5a36..789cca00 100755 --- a/frontend/client-portal/src/routes/index.tsx +++ b/frontend/client-portal/src/routes/index.tsx @@ -6,10 +6,6 @@ import LogoOnlyLayout from '../layouts/LogoOnlyLayout'; // components import LoadingScreen from '../components/LoadingScreen'; import GuestGuard from '../guards/GuestGuard'; -import { RegisterForm } from '../sections/auth/register'; -import Register from '../pages/auth/Register'; -import ResetPassword from '../pages/auth/ResetPassword'; -import VerifyCode from '../pages/auth/VerifyCode'; import { AuthProvider } from '../contexts/LaravelAuthContext'; import AuthGuard from '../guards/AuthGuard'; @@ -42,11 +38,11 @@ export default function Router() { ), }, { - path: 'register', + path: 'verify-code', element: ( - + ), @@ -54,7 +50,6 @@ export default function Router() { // { path: 'login-unprotected', element: }, // { path: 'register-unprotected', element: }, // { path: 'reset-password', element: }, - // { path: 'verify', element: }, ], }, // { @@ -68,29 +63,46 @@ export default function Router() { - ), - children:[ + + ), + children: [ { element: , index: true }, { path: 'dashboard', element: , }, + ], + }, + { + path: '/members', + element: ( + + + + + + ), + children: [ { - path: 'members', element: , + index: true, }, - ] + { + path: 'create', + element: , + }, + ], }, // { // path: '/dashboard', // element: , // children: [ // { element: , index: true }, - // { path: 'one', element: + // { path: 'one', element: // }, - // { path: 'two', element: + // { path: 'two', element: // }, - // { path: 'three', element: + // { path: 'three', element: // }, // { // path: 'user', @@ -114,7 +126,9 @@ export default function Router() { ]); } +// Auth const Login = Loadable(lazy(() => import('../pages/auth/Login'))); +const VerifyCode = Loadable(lazy(() => import('../pages/auth/VerifyCode'))); // Dashboard const Dashboard = Loadable(lazy(() => import('../pages/Dashboard'))); @@ -122,4 +136,7 @@ const NotFound = Loadable(lazy(() => import('../pages/Page404'))); // Members const Members = Loadable(lazy(() => import('../pages/Members/Index'))); -const MedicinesCreate = Loadable(lazy(() => import('../pages/Medicines/Create'))); +const MembersCreate = Loadable(lazy(() => import('../pages/Members/Create'))); + +// Alarm Center +// const AlarmCenter = Loadable(lazy(() => import('../pages/AlarmCenter/Index'))); diff --git a/frontend/client-portal/src/sections/auth/login/LoginEmailForm.tsx b/frontend/client-portal/src/sections/auth/login/LoginEmailForm.tsx new file mode 100755 index 00000000..73366a36 --- /dev/null +++ b/frontend/client-portal/src/sections/auth/login/LoginEmailForm.tsx @@ -0,0 +1,88 @@ +import * as Yup from 'yup'; +import { useNavigate } from 'react-router-dom'; +// form +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +// @mui +import { Stack, Alert } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +// hooks +import useAuth from '../../../hooks/useAuth'; +import useIsMountedRef from '../../../hooks/useIsMountedRef'; +// components +import { FormProvider, RHFTextField } from '../../../components/hook-form'; + +// ---------------------------------------------------------------------- + +type FormValuesProps = { + email: string; + afterSubmit?: string; +}; + +interface Props { + formPhone: boolean; +} + +// ---------------------------------------------------------------------- + +export default function LoginForm({ formPhone }: Props) { + const { login } = useAuth(); + const navigate = useNavigate(); + + const isMountedRef = useIsMountedRef(); + + const LoginSchema = Yup.object().shape({ + email: Yup.string().email('Email must be a valid email address').required('Email is required'), + }); + + const defaultValues = { + email: '', + }; + + const methods = useForm({ + resolver: yupResolver(LoginSchema), + defaultValues, + }); + + const { + reset, + setError, + handleSubmit, + formState: { errors, isSubmitting }, + } = methods; + + const onSubmit = async (data: FormValuesProps) => { + try { + await login(data.email); + navigate('/auth/verify-code', { state: { phoneOrEmail: data.email, formPhone } }); + } catch (error: any) { + reset(); + + if (isMountedRef.current) { + setError('afterSubmit', { ...error, message: error.data.message }); + } + } + }; + + return ( + + + Masukkan akun yang telah terdaftar + {!!errors.afterSubmit && {errors.afterSubmit.message}} + + + + + + Login + + + ); +} diff --git a/frontend/client-portal/src/sections/auth/login/LoginForm.tsx b/frontend/client-portal/src/sections/auth/login/LoginForm.tsx deleted file mode 100755 index 8fba3741..00000000 --- a/frontend/client-portal/src/sections/auth/login/LoginForm.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as Yup from 'yup'; -import { useState } from 'react'; -import { Link as RouterLink, useNavigate } from 'react-router-dom'; -// form -import { useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -// @mui -import { Link, Stack, Alert, IconButton, InputAdornment } from '@mui/material'; -import { LoadingButton } from '@mui/lab'; -// routes -import { PATH_AUTH } from '../../../routes/paths'; -// hooks -import useAuth from '../../../hooks/useAuth'; -import useIsMountedRef from '../../../hooks/useIsMountedRef'; -// components -import Iconify from '../../../components/Iconify'; -import { FormProvider, RHFTextField, RHFCheckbox } from '../../../components/hook-form'; - -// ---------------------------------------------------------------------- - -type FormValuesProps = { - email: string; - password: string; - remember: boolean; - afterSubmit?: string; -}; - -export default function LoginForm() { - const { login } = useAuth(); - const navigate = useNavigate(); - - const isMountedRef = useIsMountedRef(); - - const [showPassword, setShowPassword] = useState(false); - - const LoginSchema = Yup.object().shape({ - email: Yup.string().email('Email must be a valid email address').required('Email is required'), - password: Yup.string().required('Password is required'), - }); - - const defaultValues = { - email: 'admin@linksehat.dev', - password: 'password', - remember: true, - }; - - const methods = useForm({ - resolver: yupResolver(LoginSchema), - defaultValues, - }); - - const { - reset, - setError, - handleSubmit, - formState: { errors, isSubmitting }, - } = methods; - - const onSubmit = async (data: FormValuesProps) => { - try { - const loginResult = await login(data.email, data.password ); - - navigate('/dashboard'); - } catch (error) { - console.error(error); - - reset(); - - if (isMountedRef.current) { - setError('afterSubmit', { ...error, message: error.data.message }); - } - } - }; - - return ( - - - Email : admin@linksehat.dev & Password : password - {!!errors.afterSubmit && {errors.afterSubmit.message}} - - - - - setShowPassword(!showPassword)} edge="end"> - - - - ), - }} - /> - - - - - - Forgot password? - - - - - Login - - - ); -} diff --git a/frontend/client-portal/src/sections/auth/login/LoginPhoneForm.tsx b/frontend/client-portal/src/sections/auth/login/LoginPhoneForm.tsx new file mode 100755 index 00000000..1ab770b2 --- /dev/null +++ b/frontend/client-portal/src/sections/auth/login/LoginPhoneForm.tsx @@ -0,0 +1,86 @@ +import * as Yup from 'yup'; +import { useNavigate } from 'react-router-dom'; +// form +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +// @mui +import { Stack, Alert, InputAdornment } from '@mui/material'; +import { LoadingButton } from '@mui/lab'; +// components +import { FormProvider, RHFTextField } from '../../../components/hook-form'; +import useAuth from '../../../hooks/useAuth'; + +// ---------------------------------------------------------------------- + +type FormValuesProps = { + phone: string; + afterSubmit?: string; +}; + +interface Props { + formPhone: boolean; +} + +export default function LoginPhoneForm({ formPhone }: Props) { + const { login } = useAuth(); + const navigate = useNavigate(); + + const LoginSchema = Yup.object().shape({ + phone: Yup.string().required('Phone is required'), + }); + + const defaultValues = { + phone: '', + }; + + const methods = useForm({ + resolver: yupResolver(LoginSchema), + defaultValues, + }); + + const { + reset, + setError, + handleSubmit, + formState: { errors, isSubmitting }, + } = methods; + + const onSubmit = async (data: FormValuesProps) => { + try { + await login(0 + data.phone); + navigate('/auth/verify-code', { state: { phoneOrEmail: 0 + data.phone, formPhone } }); + } catch (error: any) { + reset(); + setError('afterSubmit', { ...error, message: error.response.data.message }); + } + }; + + return ( + + + Masukkan akun yang telah terdaftar + {!!errors.afterSubmit && {errors.afterSubmit.message}} + + +62, + }} + /> + + + + Login + + + ); +} diff --git a/frontend/client-portal/src/sections/auth/login/index.ts b/frontend/client-portal/src/sections/auth/login/index.ts index 5c49358d..9722682f 100755 --- a/frontend/client-portal/src/sections/auth/login/index.ts +++ b/frontend/client-portal/src/sections/auth/login/index.ts @@ -1 +1,2 @@ -export { default as LoginForm } from './LoginForm'; +export { default as LoginEmailForm } from './LoginEmailForm'; +export { default as LoginPhoneForm } from './LoginPhoneForm'; \ No newline at end of file diff --git a/frontend/client-portal/src/sections/auth/verify-code/VerifyCodeForm.tsx b/frontend/client-portal/src/sections/auth/verify-code/VerifyCodeForm.tsx index cfe2ec6a..3af436dc 100755 --- a/frontend/client-portal/src/sections/auth/verify-code/VerifyCodeForm.tsx +++ b/frontend/client-portal/src/sections/auth/verify-code/VerifyCodeForm.tsx @@ -8,6 +8,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; // @mui import { OutlinedInput, Stack } from '@mui/material'; import { LoadingButton } from '@mui/lab'; +import useAuth from '../../../hooks/useAuth'; // routes // import { PATH_DASHBOARD } from '../../../routes/paths'; @@ -18,14 +19,13 @@ type FormValuesProps = { code2: string; code3: string; code4: string; - code5: string; - code6: string; }; -type ValueNames = 'code1' | 'code2' | 'code3' | 'code4' | 'code5' | 'code6'; +type ValueNames = 'code1' | 'code2' | 'code3' | 'code4'; -export default function VerifyCodeForm() { +export default function VerifyCodeForm({ phoneOrEmail }: any) { const navigate = useNavigate(); + const { validateOtp } = useAuth(); const { enqueueSnackbar } = useSnackbar(); @@ -34,8 +34,6 @@ export default function VerifyCodeForm() { code2: Yup.string().required('Code is required'), code3: Yup.string().required('Code is required'), code4: Yup.string().required('Code is required'), - code5: Yup.string().required('Code is required'), - code6: Yup.string().required('Code is required'), }); const defaultValues = { @@ -43,8 +41,6 @@ export default function VerifyCodeForm() { code2: '', code3: '', code4: '', - code5: '', - code6: '', }; const { @@ -68,12 +64,10 @@ export default function VerifyCodeForm() { const onSubmit = async (data: FormValuesProps) => { try { - await new Promise((resolve) => setTimeout(resolve, 500)); - console.log('code:', Object.values(data).join('')); - + await new Promise((resolve) => setTimeout(resolve, 750)); + await validateOtp(phoneOrEmail, Object.values(data).join('')); enqueueSnackbar('Verify success!'); - - navigate('/dashboard', { replace: true }); + navigate('/dashboard'); } catch (error) { console.error(error); } @@ -114,8 +108,8 @@ export default function VerifyCodeForm() { }; return ( - - + + {Object.keys(values).map((name, index) => ( ) => handleChangeWithNextField(event, field.onChange) } @@ -144,18 +138,6 @@ export default function VerifyCodeForm() { /> ))} - - - Verify - ); }