diff options
| author | trisusilo48 <tri.susilo@altama.co.id> | 2024-09-24 09:33:10 +0700 |
|---|---|---|
| committer | trisusilo48 <tri.susilo@altama.co.id> | 2024-09-24 09:33:10 +0700 |
| commit | d4cb977d050a54b9daa1658b6de6e82878ca4c9b (patch) | |
| tree | c58569a165721c6e60aad9c1ed9fcf8f8525b6b0 | |
| parent | 1475593324319d1faf377f2d00a22a4b3caa3faa (diff) | |
| parent | cf42512eb11b1a96c99ced8d1f867aeb8c2dcbc1 (diff) | |
Merge branch 'release' into CR/product_detail
29 files changed, 2279 insertions, 647 deletions
diff --git a/public/images/NO-SPPKP-FORMAT-TEMPLATE.jpg b/public/images/NO-SPPKP-FORMAT-TEMPLATE.jpg Binary files differnew file mode 100644 index 00000000..63001bb5 --- /dev/null +++ b/public/images/NO-SPPKP-FORMAT-TEMPLATE.jpg diff --git a/src-migrate/modules/register/components/Form.tsx b/src-migrate/modules/register/components/Form.tsx index b834f97a..38e9c810 100644 --- a/src-migrate/modules/register/components/Form.tsx +++ b/src-migrate/modules/register/components/Form.tsx @@ -1,179 +1,169 @@ -import { ChangeEvent, useMemo } from "react"; -import { useMutation } from "react-query"; -import { useRegisterStore } from "../stores/useRegisterStore"; -import { RegisterProps } from "~/types/auth"; -import { registerUser } from "~/services/auth"; -import TermCondition from "./TermCondition"; -import FormCaptcha from "./FormCaptcha"; -import { useRouter } from "next/router"; -import { UseToastOptions, useToast } from "@chakra-ui/react"; -import Link from "next/link"; - -const Form = () => { - const { - form, - isCheckedTNC, - isValidCaptcha, - errors, - updateForm, - validate, - } = useRegisterStore() - - const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]) - - const router = useRouter() - const toast = useToast() +import { ChangeEvent, useMemo, useEffect, useRef, useState } from 'react'; +import { useMutation } from 'react-query'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import TermCondition from './TermCondition'; +import FormCaptcha from './FormCaptcha'; +import { useRouter } from 'next/router'; +import { UseToastOptions, useToast } from '@chakra-ui/react'; +import Link from 'next/link'; + +interface FormProps { + type: string; + required: boolean; + isBisnisRegist: boolean; + chekValid: boolean; + buttonSubmitClick: boolean; +} + +const Form: React.FC<FormProps> = ({ + type, + required, + isBisnisRegist = false, + chekValid, + buttonSubmitClick, +}) => { + const { form, isCheckedTNC, isValidCaptcha, errors, updateForm, validate } = + useRegisterStore(); + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + + const router = useRouter(); + const toast = useToast(); + + const emailRef = useRef<HTMLInputElement>(null); + const nameRef = useRef<HTMLInputElement>(null); + const passwordRef = useRef<HTMLInputElement>(null); + const phoneRef = useRef<HTMLInputElement>(null); const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { const { name, value } = event.target; - updateForm(name, value) - validate() - } - - const mutation = useMutation({ - mutationFn: (data: RegisterProps) => registerUser(data) - }) - - const handleSubmit = async (e: ChangeEvent<HTMLFormElement>) => { - e.preventDefault() - - const response = await mutation.mutateAsync(form) - - if (response?.register === true) { - const urlParams = new URLSearchParams({ - activation: 'otp', - email: form.email, - redirect: (router.query?.next || '/') as string - }) - router.push(`${router.route}?${urlParams}`) - } - - const toastProps: UseToastOptions = { - duration: 5000, - isClosable: true - } - - switch (response?.reason) { - case 'EMAIL_USED': - toast({ - ...toastProps, - title: 'Email sudah digunakan', - status: 'warning' - }) - break; - case 'NOT_ACTIVE': - const activationUrl = `${router.route}?activation=email` - toast({ - ...toastProps, - title: 'Akun belum aktif', - description: <>Akun sudah terdaftar namun belum aktif. <Link href={activationUrl} className="underline">Klik untuk aktivasi akun</Link></>, - status: 'warning' - }) - break - } - } + updateForm(name, value); + validate(); + }; + useEffect(() => { + const loadIndustries = async () => { + if (!isFormValid) { + const options: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + }; + + if (errors.email_partner && emailRef.current) { + emailRef.current.scrollIntoView(options); + return; + } + if (errors.name && nameRef.current) { + nameRef.current.scrollIntoView(options); + return; + } + + if (errors.password && passwordRef.current) { + passwordRef.current.scrollIntoView(options); + return; + } + + if (errors.phone && phoneRef.current) { + phoneRef.current.scrollIntoView(options); + return; + } + } + }; + loadIndustries(); + }, [buttonSubmitClick, chekValid]); return ( - <form className="mt-6 grid grid-cols-1 gap-y-4" onSubmit={handleSubmit}> + <form className='mt-6 grid grid-cols-1 gap-y-4'> <div> - <label htmlFor="company"> - Nama Perusahaan <span className='text-gray_r-11'>(opsional)</span> + <label htmlFor='name' className='text-black font-bold'> + Nama Lengkap </label> <input - type="text" - name="company" - id="company" - className="form-input mt-3" - placeholder="cth: INDOTEKNIK DOTCOM GEMILANG" - autoCapitalize="true" - value={form.company} - onChange={handleInputChange} - /> - </div> - - <div> - <label htmlFor='name'>Nama Lengkap</label> - - <input type='text' id='name' name='name' - className='form-input mt-3' + className='form-input mt-3 transition-all duration-700' placeholder='Masukan nama lengkap anda' value={form.name} + ref={nameRef} onChange={handleInputChange} - aria-invalid={!!errors.name} - /> - - {!!errors.name && <span className="form-msg-danger">{errors.name}</span>} - </div> - - <div> - <label htmlFor='phone'>No Handphone</label> - - <input - type='tel' - id='phone' - name='phone' - className='form-input mt-3' - placeholder='08xxxxxxxx' - value={form.phone} - onChange={handleInputChange} - aria-invalid={!!errors.phone} + aria-invalid={chekValid && !!errors.name} /> - {!!errors.phone && <span className="form-msg-danger">{errors.phone}</span>} + {chekValid && !!errors.name && ( + <span className='form-msg-danger'>{errors.name}</span> + )} </div> <div> - <label htmlFor='email'>Alamat Email</label> + <label htmlFor='email' className='text-black font-bold'> + Alamat Email + </label> <input type='text' id='email' name='email' - className='form-input mt-3' + className='form-input mt-3 transition-all duration-500' placeholder='Masukan alamat email anda' value={form.email} + ref={emailRef} onChange={handleInputChange} - autoComplete="username" - aria-invalid={!!errors.email} + autoComplete='username' + aria-invalid={chekValid && !!errors.email} /> - {!!errors.email && <span className="form-msg-danger">{errors.email}</span>} + {chekValid && !!errors.email && ( + <span className='form-msg-danger'>{errors.email}</span> + )} </div> <div> - <label htmlFor='password'>Kata Sandi</label> + <label htmlFor='password' className='text-black font-bold'> + Kata Sandi + </label> <input type='password' name='password' id='password' - className='form-input mt-3' + className='form-input mt-3 transition-all duration-500' placeholder='••••••••••••' value={form.password} + ref={passwordRef} onChange={handleInputChange} - autoComplete="current-password" - aria-invalid={!!errors.password} + autoComplete='current-password' + aria-invalid={chekValid && !!errors.password} /> - {!!errors.password && <span className="form-msg-danger">{errors.password}</span>} + {chekValid && !!errors.password && ( + <span className='form-msg-danger'>{errors.password}</span> + )} </div> - <FormCaptcha /> + <div> + <label htmlFor='phone' className='text-black font-bold'> + No Handphone + </label> - <TermCondition /> + <input + type='tel' + id='phone' + name='phone' + className='form-input mt-3 transition-all duration-500' + placeholder='08xxxxxxxx' + value={form.phone} + ref={phoneRef} + onChange={handleInputChange} + aria-invalid={chekValid && !!errors.phone} + /> - <button - type="submit" - className="btn-yellow w-full mt-2" - disabled={!isFormValid || !isCheckedTNC || mutation.isLoading || !isValidCaptcha} - > - {mutation.isLoading ? 'Loading...' : 'Daftar'} - </button> + {chekValid && !!errors.phone && ( + <span className='form-msg-danger'>{errors.phone}</span> + )} + </div> </form> - ) -} + ); +}; -export default Form
\ No newline at end of file +export default Form; diff --git a/src-migrate/modules/register/components/FormBisnis.tsx b/src-migrate/modules/register/components/FormBisnis.tsx new file mode 100644 index 00000000..5ee19e58 --- /dev/null +++ b/src-migrate/modules/register/components/FormBisnis.tsx @@ -0,0 +1,715 @@ +import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { useMutation } from 'react-query'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import { useRouter } from 'next/router'; +import { + Button, + Checkbox, + UseToastOptions, + color, + useToast, +} from '@chakra-ui/react'; +import Link from 'next/link'; +import getFileBase64 from '@/core/utils/getFileBase64'; +import { Controller, useForm } from 'react-hook-form'; +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; +import odooApi from '~/libs/odooApi'; +import { toast } from 'react-hot-toast'; +import { EyeIcon } from '@heroicons/react/24/outline'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import Image from 'next/image'; +import useDevice from '@/core/hooks/useDevice'; +interface FormProps { + type: string; + required: boolean; + isPKP: boolean; + chekValid: boolean; + buttonSubmitClick: boolean; +} + +interface industry_id { + label: string; + value: string; + category: string; +} + +interface companyType { + value: string; + label: string; +} + +const form: React.FC<FormProps> = ({ + type, + required, + isPKP, + chekValid, + buttonSubmitClick, +}) => { + const { form, errors, updateForm, validate } = useRegisterStore(); + const { control, watch, setValue } = useForm(); + const [selectedCategory, setSelectedCategory] = useState<string>(''); + const [isChekBox, setIsChekBox] = useState<boolean>(false); + const [isExample, setIsExample] = useState<boolean>(false); + const { isDesktop, isMobile } = useDevice(); + // Inside your component + const [formattedNpwp, setFormattedNpwp] = useState<string>(''); // State for formatted NPWP + const [unformattedNpwp, setUnformattedNpwp] = useState<string>(''); // State for unformatted NPWP + + const [industries, setIndustries] = useState<industry_id[]>([]); + const [companyTypes, setCompanyTypes] = useState<companyType[]>([]); + + const router = useRouter(); + const toast = useToast(); + + const emailRef = useRef<HTMLInputElement>(null); + const businessNameRef = useRef<HTMLInputElement>(null); + const companyTypeRef = useRef<HTMLInputElement>(null); + const industryRef = useRef<HTMLDivElement>(null); + const addressRef = useRef<HTMLInputElement>(null); + const namaWajibPajakRef = useRef<HTMLInputElement>(null); + const alamatWajibPajakRef = useRef<HTMLInputElement>(null); + const npwpRef = useRef<HTMLInputElement>(null); + const sppkpRef = useRef<HTMLInputElement>(null); + const docsSppkpRef = useRef<HTMLInputElement>(null); + const docsNpwpRef = useRef<HTMLInputElement>(null); + + useEffect(() => { + const loadCompanyTypes = async () => { + const dataCompanyTypes = await odooApi( + 'GET', + '/api/v1/partner/company_type' + ); + setCompanyTypes( + dataCompanyTypes?.map((o: { id: any; name: any }) => ({ + value: o.id, + label: o.name, + })) + ); + }; + loadCompanyTypes(); + }, []); + + useEffect(() => { + const selectedCompanyType = companyTypes.find( + (company) => company.value === watch('companyType') + ); + if (selectedCompanyType) { + updateForm('company_type_id', `${selectedCompanyType?.value}`); + validate(); + } + }, [watch('companyType'), companyTypes]); + + useEffect(() => { + const selectedIndustryType = industries.find( + (industry) => industry.value === watch('industry_id') + ); + if (selectedIndustryType) { + updateForm('industry_id', `${selectedIndustryType?.value}`); + setSelectedCategory(selectedIndustryType.category); + validate(); + } + }, [watch('industry_id'), industries]); + + useEffect(() => { + const loadIndustries = async () => { + const dataIndustries = await odooApi('GET', '/api/v1/partner/industry'); + setIndustries( + dataIndustries?.map((o: { id: any; name: any; category: any }) => ({ + value: o.id, + label: o.name, + category: o.category, + })) + ); + }; + loadIndustries(); + }, []); + + const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => { + const { name, value } = event.target; + + updateForm('type_acc', 'business'); + updateForm('is_pkp', `${isPKP}`); + + // Update form dengan nilai terbaru dari input yang berubah + updateForm(name, value); + + // Jika checkbox aktif, sinkronisasi alamat_wajib_pajak dengan alamat_bisnis + if (isChekBox) { + if (name === 'alamat_wajib_pajak') { + updateForm('alamat_bisnis', value); + } else if (name === 'alamat_bisnis') { + updateForm('alamat_wajib_pajak', value); + } + } + + // Validasi setelah perubahan dilakukan + validate(); + }; + + const handleInputChangeNpwp = (event: ChangeEvent<HTMLInputElement>) => { + const { name, value } = event.target; + updateForm('type_acc', `business`); + updateForm('is_pkp', `${isPKP}`); + updateForm('npwp', value); + validate(); + }; + + const handleChange = (event: ChangeEvent<HTMLInputElement>) => { + setIsChekBox(!isChekBox); + }; + + const formatNpwp = (value: string) => { + try { + const cleaned = ('' + value).replace(/\D/g, ''); + let match; + if (cleaned.length <= 15) { + match = cleaned.match( + /(\d{0,2})?(\d{0,3})?(\d{0,3})?(\d{0,1})?(\d{0,3})?(\d{0,3})$/ + ); + } else { + match = cleaned.match( + /(\d{0,3})?(\d{0,3})?(\d{0,3})?(\d{0,1})?(\d{0,3})?(\d{0,3})$/ + ); + } + + if (match) { + return [ + match[1], + match[2] ? '.' : '', + match[2], + match[3] ? '.' : '', + match[3], + match[4] ? '.' : '', + match[4], + match[5] ? '-' : '', + match[5], + match[6] ? '.' : '', + match[6], + ].join(''); + } + + // If match is null, return the original cleaned string or handle as needed + return cleaned; + } catch (error) { + // Handle error or return a default value + console.error('Error formatting NPWP:', error); + return value; + } + }; + + useEffect(() => { + if (isChekBox) { + updateForm('isChekBox', 'true'); + updateForm('alamat_wajib_pajak', `${form.alamat_bisnis}`); + validate(); + } else { + updateForm('isChekBox', 'false'); + validate(); + } + }, [isChekBox]); + + const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => { + const toastProps: UseToastOptions = { + duration: 5000, + isClosable: true, + position: 'top', + }; + + let fileBase64 = ''; + const { name } = event.target; + const file = event.target.files?.[0]; + + // Allowed file extensions + const allowedExtensions = ['pdf', 'doc', 'docx', 'png', 'jpg', 'jpeg']; + + if (file) { + const fileExtension = file.name.split('.').pop()?.toLowerCase(); // Extract file extension + + // Check if the file extension is allowed + if (!fileExtension || !allowedExtensions.includes(fileExtension)) { + toast({ + ...toastProps, + title: + 'Format file yang diijinkan adalah .pdf, .doc, .docx, .png, .jpg, atau .jpeg', + status: 'error', + }); + event.target.value = ''; + return; + } + + // Check for file size + if (file.size > 5000000) { + toast({ + ...toastProps, + title: 'Maksimal ukuran file adalah 5MB', + status: 'error', + }); + event.target.value = ''; + return; + } + + // Convert file to Base64 + fileBase64 = await getFileBase64(file); + updateForm(name, fileBase64); // Update form with the Base64 string + validate(); // Perform form validation + } + }; + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + + useEffect(() => { + const loadIndustries = async () => { + if (!isFormValid) { + const options: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + }; + if (errors.email_partner && emailRef.current) { + emailRef.current.scrollIntoView(options); + return; + } + if (errors.company_type_id && companyTypeRef.current) { + companyTypeRef.current.scrollIntoView(options); + return; + } + + if (errors.business_name && businessNameRef.current) { + businessNameRef.current.scrollIntoView(options); + return; + } + + if (errors.industry_id && industryRef.current) { + industryRef.current.scrollIntoView(options); + return; + } + + if (errors.alamat_bisnis && addressRef.current) { + addressRef.current.scrollIntoView(options); + return; + } + + if (errors.npwp && npwpRef.current) { + npwpRef.current.scrollIntoView(options); + return; + } + + if (errors.sppkp && sppkpRef.current) { + sppkpRef.current.scrollIntoView(options); + return; + } + if (errors.sppkp_document && docsSppkpRef.current) { + docsSppkpRef.current.scrollIntoView(options); + return; + } + if (errors.npwp_document && docsNpwpRef.current) { + docsNpwpRef.current.scrollIntoView(options); + return; + } + } + }; + loadIndustries(); + }, [buttonSubmitClick, chekValid]); + return ( + <> + <BottomPopup + className='' + title='Contoh SPPKP' + active={isExample} + close={() => setIsExample(false)} + > + <div className='flex p-2'> + <Image + src='/images/NO-SPPKP-FORMAT-TEMPLATE.jpg' + alt='Contoh SPPKP' + className='w-full h-full ' + width={800} + height={800} + quality={100} + /> + </div> + </BottomPopup> + <form + className={` ${ + type === 'bisnis' + ? 'mt-6 grid grid-cols-1 gap-y-4' + : 'mt-6 grid grid-cols-2 gap-x-4 gap-y-2' + }`} + > + <div> + <label htmlFor='email' className='font-bold'> + Email Bisnis{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='text' + id='email_partner' + name='email_partner' + placeholder='example@email.com' + value={!required ? form.email_partner : ''} + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + onChange={handleInputChange} + autoComplete='username' + ref={emailRef} + aria-invalid={ + chekValid && !required && isPKP && !!errors.email_partner + } + /> + + {chekValid && !required && isPKP && !!errors.email_partner && ( + <span className='form-msg-danger'>{errors.email_partner}</span> + )} + </div> + + <div> + <label className='font-bold' htmlFor='company'> + Nama Bisnis + </label> + <div className='flex justify-between items-start gap-2 max-h-12 min-h-12 text-sm mt-3'> + <div + className='w-4/5 pr-1 max-h-11 transition-all duration-500' + ref={companyTypeRef} + > + <Controller + name='companyType' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={companyTypes} + disabled={required} + placeholder='Badan Usaha' + /> + )} + /> + {chekValid && !required && !!errors.company_type_id && ( + <span className='form-msg-danger'> + {errors.company_type_id} + </span> + )} + </div> + <div className='w-[120%] '> + <input + type='text' + name='business_name' + id='business_name' + className='form-input max-h-11 transition-all duration-500' + placeholder='Nama Perusahaan' + autoCapitalize='true' + value={form.business_name} + ref={businessNameRef} + aria-invalid={chekValid && !!errors.business_name} + onChange={handleInputChange} + /> + + {chekValid && !!errors.business_name && ( + <span className='form-msg-danger'>{errors.business_name}</span> + )} + </div> + </div> + </div> + + <div className='mt-8 md:mt-0'> + <label className='font-bold' htmlFor='business_name'> + Klasifikasi Jenis Usaha + </label> + <div + className='max-h-10 transition-all duration-500' + ref={industryRef} + > + <Controller + name='industry_id' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={industries} + disabled={required} + placeholder={'Select industry'} + /> + )} + /> + </div> + {selectedCategory && ( + <span className='text-gray_r-11 text-xs'> + Kategori : {selectedCategory} + </span> + )} + {chekValid && !required && !!errors.industry_id && ( + <span className='form-msg-danger'>{errors.industry_id}</span> + )} + </div> + + <div> + <label htmlFor='alamat_bisnis' className='font-bold'> + Alamat Bisnis + </label> + + <input + type='text' + id='alamat_bisnis' + name='alamat_bisnis' + placeholder='Masukan alamat bisnis anda' + value={!required ? form.alamat_bisnis : ''} + className={`form-input mt-3 max-h-11 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + ref={addressRef} + onChange={handleInputChange} + aria-invalid={chekValid && !required && !!errors.alamat_bisnis} + /> + + {chekValid && !required && !!errors.alamat_bisnis && ( + <span className='form-msg-danger'>{errors.alamat_bisnis}</span> + )} + </div> + + <div> + <label htmlFor='nama_wajib_pajak' className='font-bold'> + Nama Wajib Pajak{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='text' + id='nama_wajib_pajak' + name='nama_wajib_pajak' + placeholder='Masukan nama lengkap anda' + value={!required ? form.nama_wajib_pajak : ''} + className={`form-input mt-3 max-h-11 transition-all duration-500${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + onChange={handleInputChange} + ref={namaWajibPajakRef} + aria-invalid={ + chekValid && isPKP && !required && !!errors.nama_wajib_pajak + } + /> + + {chekValid && isPKP && !required && !!errors.nama_wajib_pajak && ( + <span className='form-msg-danger'>{errors.nama_wajib_pajak}</span> + )} + </div> + + <div> + <label + htmlFor='alamat_wajib_pajak' + className='font-bold flex items-center' + > + <p> + Alamat Wajib Pajak{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </p> + <div className='flex items-center ml-2 mt-1 '> + <Checkbox + borderColor='gray.600' + colorScheme='red' + size='md' + isChecked={isChekBox} + onChange={handleChange} + /> + <span className='text-caption-2 ml-2 font-normal italic'> + sama dengan alamat bisnis? + </span> + </div> + </label> + + <input + type='text' + id='alamat_wajib_pajak' + name='alamat_wajib_pajak' + placeholder='Masukan alamat wajib pajak anda' + value={ + !required + ? isChekBox + ? form.alamat_bisnis + : form.alamat_wajib_pajak + : '' + } + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={isChekBox || required} + contentEditable={required} + readOnly={required} + onChange={handleInputChange} + ref={alamatWajibPajakRef} + aria-invalid={ + chekValid && isPKP && !required && !!errors.alamat_wajib_pajak + } + /> + + {chekValid && isPKP && !required && !!errors.alamat_wajib_pajak && ( + <span className='form-msg-danger'>{errors.alamat_wajib_pajak}</span> + )} + </div> + + <div> + <label htmlFor='npwp' className='font-bold'> + Nomor NPWP{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='tel' + id='npwp' + name='npwp' + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + ref={npwpRef} + placeholder='000.000.000.0-000.000' + value={!required ? formattedNpwp : ''} + maxLength={21} // Set maximum length to 16 characters + onChange={(e) => { + if (!required) { + const unformatted = e.target.value.replace(/\D/g, ''); // Remove all non-digit characters + const formattedValue = formatNpwp(unformatted); // Format the value + setUnformattedNpwp(unformatted); // Store unformatted value + setFormattedNpwp(formattedValue); // Store formatted value + handleInputChangeNpwp({ + ...e, + target: { ...e.target, value: unformatted }, + }); // Update form state with unformatted value + } + }} + aria-invalid={chekValid && !required && !!errors.npwp} + /> + + {chekValid && !required && !!errors.npwp && ( + <span className='form-msg-danger'>{errors.npwp}</span> + )} + </div> + + <div> + <label + htmlFor='sppkp' + className='font-bold flex flex-row items-center justify-between' + > + <div className='flex flex-row items-center'> + Nomor SPPKP{' '} + {!required && ( + <span className='ml-2 font-normal text-gray_r-11'> + (opsional){' '} + </span> + )} + </div> + { + <div + onClick={() => setIsExample(!isExample)} + className='rounded text-white p-2 flex flex-row bg-red-500 hover:cursor-pointer hover:bg-red-400' + > + <EyeIcon className={`w-4 ${isDesktop && 'mr-2'}`} /> + {isDesktop && ( + <p className='font-light text-xs'>Lihat Contoh</p> + )} + </div> + } + </label> + + <input + type='tel' + id='sppkp' + name='sppkp' + className={`form-input max-h-11 mt-3 transition-all duration-500 ${ + required ? 'cursor-no-drop' : '' + }`} + disabled={required} + contentEditable={required} + readOnly={required} + ref={sppkpRef} + placeholder='X-XXXPKP/WJPXXX/XX.XXXX/XXXX' + onChange={handleInputChange} + value={!required ? form.sppkp : ''} + aria-invalid={chekValid && !required && !!errors.sppkp} + /> + + {chekValid && !required && !!errors.sppkp && ( + <span className='form-msg-danger'>{errors.sppkp}</span> + )} + </div> + + <div> + <label htmlFor='npwp_document' className='font-bold'> + Dokumen NPWP{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='file' + id='npwp_document' + name='npwp_document' + className={`form-input transition-all duration-500 ${ + type === 'bisnis' ? '' : 'border-none' + } mt-3 ${required ? 'cursor-no-drop' : ''}`} + disabled={required} + contentEditable={required} + ref={docsNpwpRef} + readOnly={required} + onChange={handleFileChange} + accept='.pdf,.doc,.docx,.png,.jpg,.jpeg' // Filter file types + /> + + {chekValid && isPKP && !required && !!errors.npwp_document && ( + <span className='form-msg-danger'>{errors.npwp_document}</span> + )} + </div> + + <div> + <label htmlFor='sppkp_document' className='font-bold'> + Dokumen SPPKP{' '} + {!isPKP && !required && ( + <span className='font-normal text-gray_r-11'>(opsional)</span> + )} + </label> + + <input + type='file' + id='sppkp_document' + name='sppkp_document' + className={`form-input transition-all duration-500 ${ + type === 'bisnis' ? '' : 'border-none' + } mt-3 ${required ? 'cursor-no-drop' : ''}`} + disabled={required} + contentEditable={required} + ref={docsSppkpRef} + readOnly={required} + onChange={handleFileChange} + accept='.pdf,.doc,.docx,.png,.jpg,.jpeg' // Filter file types + /> + + {chekValid && isPKP && !required && !!errors.sppkp_document && ( + <span className='form-msg-danger'>{errors.sppkp_document}</span> + )} + </div> + </form> + </> + ); +}; + +export default form; diff --git a/src-migrate/modules/register/components/RegistrasiBisnis.tsx b/src-migrate/modules/register/components/RegistrasiBisnis.tsx new file mode 100644 index 00000000..ce4d3972 --- /dev/null +++ b/src-migrate/modules/register/components/RegistrasiBisnis.tsx @@ -0,0 +1,197 @@ +import { ChangeEvent, useEffect, useMemo, useState } from 'react'; +import FormBisnis from './FormBisnis'; +import Form from './Form'; +import TermCondition from './TermCondition'; +import FormCaptcha from './FormCaptcha'; +import { Radio, RadioGroup, Stack, Divider, Button } from '@chakra-ui/react'; +import React from 'react'; +import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { useMutation } from 'react-query'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import router from 'next/router'; +import { useRouter } from 'next/router'; +import { UseToastOptions, useToast } from '@chakra-ui/react'; +import Link from 'next/link'; +interface FormProps { + chekValid: boolean; + buttonSubmitClick: boolean; +} +const RegistrasiBisnis: React.FC<FormProps> = ({ + chekValid, + buttonSubmitClick, +}) => { + const [isPKP, setIsPKP] = useState(true); + const [isTerdaftar, setIsTerdaftar] = useState(false); + const [isDropIndividu, setIsDropIndividu] = useState(true); + const [isBisnisClicked, setisBisnisClicked] = useState(true); + const [selectedValue, setSelectedValue] = useState('PKP'); + const [selectedValueBisnis, setSelectedValueBisnis] = useState('false'); + const { form, isCheckedTNC, isValidCaptcha, errors, validate, updateForm } = + useRegisterStore(); + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + const toast = useToast(); + const mutation = useMutation({ + mutationFn: (data: RegisterProps) => registerUser(data), + }); + + useEffect(() => { + if (selectedValue === 'PKP') { + updateForm('is_pkp', 'true'); + validate(); + } else { + updateForm('is_pkp', 'false'); + validate(); + } + }, [selectedValue]); + + useEffect(() => { + if (isTerdaftar) { + updateForm('is_terdaftar', 'true'); + validate(); + } else { + updateForm('is_terdaftar', 'false'); + validate(); + } + }, [isTerdaftar]); + + const handleChange = (value: string) => { + setSelectedValue(value); + if (value === 'PKP') { + validate(); + setIsPKP(true); + } else { + validate(); + setIsPKP(false); + setIsPKP(false); + } + }; + + const handleChangeBisnis = (value: string) => { + setSelectedValueBisnis(value); + if (value === 'true') { + validate(); + setIsTerdaftar(true); + } else { + validate(); + setIsTerdaftar(false); + } + }; + + const handleClick = () => { + setIsDropIndividu(!isDropIndividu); + }; + + const handleClickBisnis = () => { + setisBisnisClicked(!isBisnisClicked); + }; + + return ( + <> + <div className='mt-4 border'> + <div className='p-4'> + <div onClick={handleClick} className='flex justify-between'> + <p className='text-2xl font-semibold text-center md:text-left'> + Data Akun + </p> + {isDropIndividu ? ( + <div className='flex'> + <ChevronDownIcon + onClick={handleClick} + className='h-6 w-6 text-black' + /> + </div> + ) : ( + <ChevronRightIcon + onClick={handleClick} + className='h-6 w-6 text-black' + /> + )} + </div> + {isDropIndividu && ( + <div> + <Divider my={4} /> + <Form + type='bisnis' + required={true} + isBisnisRegist={true} + chekValid={chekValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + </div> + </div> + <div className='mt-4 border'> + <div className='p-4'> + <div onClick={handleClickBisnis} className='flex justify-between'> + <p className='text-2xl font-semibold text-center md:text-left'> + Data Bisnis + </p> + {isBisnisClicked ? ( + <div className='flex'> + <ChevronDownIcon + onClick={handleClickBisnis} + className='h-6 w-6 text-black' + /> + </div> + ) : ( + <ChevronRightIcon + onClick={handleClickBisnis} + className='h-6 w-6 text-black' + /> + )} + </div> + {isBisnisClicked && ( + <div> + <Divider my={4} /> + <div> + <p className='text-black font-bold mb-2'> + Bisnis Terdaftar di Indoteknik? + </p> + <RadioGroup + onChange={handleChangeBisnis} + value={selectedValueBisnis} + > + <Stack direction='row'> + <Radio colorScheme='red' value='true'> + Sudah Terdaftar + </Radio> + <Radio colorScheme='red' value='false' className='ml-2'> + Belum Terdaftar + </Radio> + </Stack> + </RadioGroup> + </div> + <div className='mt-4'> + <p className='text-black font-bold mb-2'>Tipe Bisnis</p> + <RadioGroup onChange={handleChange} value={selectedValue}> + <Stack direction='row' className='font-bold'> + <Radio colorScheme='red' value='PKP'> + PKP + </Radio> + <Radio colorScheme='red' value='Non-PKP' className='ml-4'> + Non-PKP + </Radio> + </Stack> + </RadioGroup> + </div> + <FormBisnis + type='bisnis' + required={isTerdaftar} + isPKP={isPKP} + chekValid={chekValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + </div> + </div> + + <h1 className=''></h1> + </> + ); +}; + +export default RegistrasiBisnis; diff --git a/src-migrate/modules/register/components/RegistrasiIndividu.tsx b/src-migrate/modules/register/components/RegistrasiIndividu.tsx new file mode 100644 index 00000000..84049065 --- /dev/null +++ b/src-migrate/modules/register/components/RegistrasiIndividu.tsx @@ -0,0 +1,34 @@ +import Form from './Form'; +import { useRegisterStore } from '../stores/useRegisterStore'; +import { useEffect } from 'react'; +interface FormProps { + chekValid: boolean; + buttonSubmitClick: boolean; +} +const RegistrasiIndividu: React.FC<FormProps> = ({ + chekValid, + buttonSubmitClick, +}) => { + const { form, errors, updateForm, validate } = useRegisterStore(); + + useEffect(() => { + updateForm('is_pkp', 'false'); + updateForm('is_terdaftar', 'false'); + updateForm('type_acc', 'individu'); + validate(); + }, []); + + return ( + <> + <Form + type='individu' + required={false} + isBisnisRegist={false} + chekValid={chekValid} + buttonSubmitClick={buttonSubmitClick} + /> + </> + ); +}; + +export default RegistrasiIndividu; diff --git a/src-migrate/modules/register/components/TermCondition.tsx b/src-migrate/modules/register/components/TermCondition.tsx index b7729deb..d54fe921 100644 --- a/src-migrate/modules/register/components/TermCondition.tsx +++ b/src-migrate/modules/register/components/TermCondition.tsx @@ -10,7 +10,7 @@ const TermCondition = () => { return ( <> <div className="mt-4 flex items-center gap-x-2"> - <Checkbox id='tnc' name='tnc' isChecked={isCheckedTNC} onChange={toggleCheckTNC} /> + <Checkbox id='tnc' name='tnc' colorScheme='red' isChecked={isCheckedTNC} onChange={toggleCheckTNC} /> <div> <label htmlFor="tnc" className="cursor-pointer">Dengan ini saya menyetujui</label> {' '} diff --git a/src-migrate/modules/register/index.tsx b/src-migrate/modules/register/index.tsx index 00931284..da41fd8b 100644 --- a/src-migrate/modules/register/index.tsx +++ b/src-migrate/modules/register/index.tsx @@ -1,54 +1,212 @@ -import PageContent from "~/modules/page-content" -import Form from "./components/Form" -import Link from "next/link" -import Image from "next/image" -import IndoteknikLogo from "~/images/logo.png" -import AccountActivation from "../account-activation" +import PageContent from '~/modules/page-content'; +import RegistrasiIndividu from './components/RegistrasiIndividu'; +import RegistrasiBisnis from './components/RegistrasiBisnis'; +import Link from 'next/link'; +import Image from 'next/image'; +import IndoteknikLogo from '~/images/logo.png'; +import AccountActivation from '../account-activation'; +import { useMemo, useState } from 'react'; +import { useRegisterStore } from './stores/useRegisterStore'; +import FormCaptcha from './components/FormCaptcha'; +import TermCondition from './components/TermCondition'; +import { Button } from '@chakra-ui/react'; +import { useMutation } from 'react-query'; +import { UseToastOptions, useToast } from '@chakra-ui/react'; +import { RegisterProps } from '~/types/auth'; +import { registerUser } from '~/services/auth'; +import { useRouter } from 'next/router'; const LOGO_WIDTH = 150; const LOGO_HEIGHT = LOGO_WIDTH / 3; const Register = () => { + const [isIndividuClicked, setIsIndividuClicked] = useState(true); + const [notValid, setNotValid] = useState(false); + const [buttonSubmitClick, setButtonSubmitClick] = useState(false); + const [isBisnisClicked, setIsBisnisClicked] = useState(false); + const { form, isCheckedTNC, isValidCaptcha, resetForm, errors, updateForm } = + useRegisterStore(); + + const isFormValid = useMemo(() => Object.keys(errors).length === 0, [errors]); + const toast = useToast(); + const router = useRouter(); + const mutation = useMutation({ + mutationFn: (data: RegisterProps) => registerUser(data), + }); + + const handleIndividuClick = () => { + resetForm(); + setIsIndividuClicked(true); + setIsBisnisClicked(false); + }; + + const handleBisnisClick = () => { + resetForm(); + updateForm('is_terdaftar', 'false'); + updateForm('type_acc', 'business'); + setIsIndividuClicked(false); + setIsBisnisClicked(true); + }; + const handleSubmit = async () => { + if (!isFormValid) { + setNotValid(true); + setButtonSubmitClick(!buttonSubmitClick); + return; + } else { + setButtonSubmitClick(!buttonSubmitClick); + setNotValid(false); + } + const response = await mutation.mutateAsync(form); + if (response?.register === true) { + const urlParams = new URLSearchParams({ + activation: 'otp', + email: form.email, + redirect: (router.query?.next || '/') as string, + }); + router.push(`${router.route}?${urlParams}`); + } + + const toastProps: UseToastOptions = { + duration: 5000, + isClosable: true, + position: 'top', + }; + + switch (response?.reason) { + case 'EMAIL_USED': + toast({ + ...toastProps, + title: 'Email sudah digunakan', + status: 'warning', + }); + break; + case 'NOT_ACTIVE': + const activationUrl = `${router.route}?activation=email`; + toast({ + ...toastProps, + title: 'Akun belum aktif', + description: ( + <> + Akun sudah terdaftar namun belum aktif.{' '} + <Link href={activationUrl} className='underline'> + Klik untuk aktivasi akun + </Link> + </> + ), + status: 'warning', + }); + break; + } + }; return ( - <div className="container"> - <div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 pt-10 px-2 md:pt-16"> - <section> - <Link href='/' className="block md:hidden"> - <Image src={IndoteknikLogo} alt='Logo Indoteknik' width={LOGO_WIDTH} height={LOGO_HEIGHT} className="mx-auto mb-4 w-auto h-auto" priority /> - </Link> - - <h1 className="text-2xl font-semibold text-center md:text-left"> - Daftar Akun Indoteknik - </h1> - <h2 className="text-gray_r-11 mt-1 mb-10 text-center md:text-left"> - Buat akun sekarang lebih mudah dan terverifikasi - </h2> - - <Form /> - - <div className='text-gray_r-11 mt-4 text-center md:text-left'> - Sudah punya akun Indoteknik?{' '} - <Link href='/login' className='inline font-medium text-danger-500'> - Masuk + <div className='container'> + <div className='grid grid-cols-1 md:grid-cols-2 gap-x-8 pt-10 px-2 md:pt-16'> + <section className=''> + <div className='px-8 py-4 border'> + <Link href='/' className='block md:hidden'> + <Image + src={IndoteknikLogo} + alt='Logo Indoteknik' + width={LOGO_WIDTH} + height={LOGO_HEIGHT} + className='mx-auto mb-4 w-auto h-auto' + priority + /> </Link> - </div> - <div className='text-gray_r-11 mt-4 text-center md:text-left'> - Akun anda belum aktif?{' '} - <Link href='/register?activation=email' className='inline font-medium text-danger-500'> - Aktivasi - </Link> + <h1 className='text-2xl font-semibold text-center md:text-left'> + Daftar Akun Indoteknik + </h1> + <h2 className='text-gray_r-11 mt-1 mb-4 text-center md:text-left'> + Buat akun sekarang lebih mudah dan terverifikasi + </h2> + + <label htmlFor='name' className='text-black font-bold'> + Tipe Akun + </label> + <div className='grid grid-cols-2 gap-x-3 mt-2 h-14 font-bold text-black hover:cursor-pointer'> + <div + className={` border rounded-md flex justify-center items-center transition-colors duration-300 ease-in-out ${ + isIndividuClicked ? 'bg-red-500 text-white' : '' + }`} + onClick={handleIndividuClick} + > + <p>Individu</p> + </div> + <div + className={` border rounded-md flex justify-center items-center transition-colors duration-300 ease-in-out ${ + isBisnisClicked ? 'bg-red-500 text-white' : '' + }`} + onClick={handleBisnisClick} + > + <p>Bisnis</p> + </div> + </div> + <div className='transition-opacity duration-300 ease-in-out'> + {isIndividuClicked && ( + <div className='opacity-100'> + <RegistrasiIndividu + chekValid={notValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + {isBisnisClicked && ( + <div className='opacity-100'> + <RegistrasiBisnis + chekValid={notValid} + buttonSubmitClick={buttonSubmitClick} + /> + </div> + )} + </div> + <section className='mt-2'> + <FormCaptcha /> + <TermCondition /> + <Button + type='submit' + colorScheme='red' + className='w-full mt-2' + size='lg' + onClick={handleSubmit} + isDisabled={ + !isCheckedTNC || mutation.isLoading || !isValidCaptcha + } + > + {mutation.isLoading ? 'Loading...' : 'Daftar'} + </Button> + </section> + <section className='flex justify-center items-center flex-col'> + <div className='text-gray_r-11 mt-4 text-center md:text-left'> + Sudah punya akun Indoteknik?{' '} + <Link + href='/login' + className='inline font-medium text-danger-500' + > + Masuk + </Link> + </div> + <div className='text-gray_r-11 mt-4 text-center md:text-left'> + Akun anda belum aktif?{' '} + <Link + href='/register?activation=email' + className='inline font-medium text-danger-500' + > + Aktivasi + </Link> + </div> + </section> </div> </section> - <section className="my-10 md:my-0"> - <PageContent path="/register" /> + <section className='my-10 md:my-0'> + <PageContent path='/register' /> </section> </div> <AccountActivation /> </div> - ) -} + ); +}; -export default Register
\ No newline at end of file +export default Register; diff --git a/src-migrate/modules/register/stores/useRegisterStore.ts b/src-migrate/modules/register/stores/useRegisterStore.ts index d8abf52b..273472be 100644 --- a/src-migrate/modules/register/stores/useRegisterStore.ts +++ b/src-migrate/modules/register/stores/useRegisterStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { RegisterProps } from '~/types/auth'; import { registerSchema } from '~/validations/auth'; -import { ZodError } from 'zod'; +import { boolean, ZodError } from 'zod'; type State = { form: RegisterProps; @@ -20,18 +20,33 @@ type Action = { openTNC: () => void; closeTNC: () => void; validate: () => void; + resetForm: () => void; }; export const useRegisterStore = create<State & Action>((set, get) => ({ form: { - company: '', + company_type_id: '', + business_name: '', name: '', + nama_wajib_pajak : '', email: '', + email_partner: '', password: '', phone: '', + sppkp_document: '', + npwp_document: '', + industry_id: '', + npwp: '', + sppkp: '', + is_pkp: '', + type_acc:'', + is_terdaftar:'', + alamat_bisnis:'', + alamat_wajib_pajak:'', }, updateForm: (name, value) => set((state) => ({ form: { ...state.form, [name]: value } })), + errors: {}, validate: () => { @@ -48,6 +63,7 @@ export const useRegisterStore = create<State & Action>((set, get) => ({ } } }, + isCheckedTNC: false, toggleCheckTNC: () => set((state) => ({ isCheckedTNC: !state.isCheckedTNC })), @@ -57,4 +73,27 @@ export const useRegisterStore = create<State & Action>((set, get) => ({ isValidCaptcha: false, updateValidCaptcha: (value) => set(() => ({ isValidCaptcha: value })), + + resetForm: () => set({ + form: { + company_type_id: '', + business_name: '', + name: '', + nama_wajib_pajak : '', + email: '', + email_partner: '', + password: '', + phone: '', + sppkp_document: '', + npwp_document: '', + industry_id: '', + npwp: '', + sppkp: '', + is_pkp: '', + type_acc:'', + is_terdaftar:'', + alamat_bisnis:'', + alamat_wajib_pajak:'', + }, + }), })); diff --git a/src-migrate/types/auth.ts b/src-migrate/types/auth.ts index e93a475a..593e120f 100644 --- a/src-migrate/types/auth.ts +++ b/src-migrate/types/auth.ts @@ -10,6 +10,7 @@ export type AuthProps = { name: string; email: string; phone: string; + npwp: string; mobile: string; external: boolean; company: boolean; diff --git a/src-migrate/validations/auth.ts b/src-migrate/validations/auth.ts index 78fc5e71..3abdfb57 100644 --- a/src-migrate/validations/auth.ts +++ b/src-migrate/validations/auth.ts @@ -1,17 +1,147 @@ import { z } from 'zod'; -export const registerSchema = z.object({ - name: z.string().min(1, { message: 'Nama harus diisi' }), - email: z - .string() - .min(1, { message: 'Email harus diisi' }) - .email({ message: 'Email harus menggunakan format example@mail.com' }), - password: z.string().min(6, { message: 'Password minimal 6 karakter' }), - company: z.string().optional(), - phone: z - .string() - .min(1, { message: 'Nomor telepon harus diisi' }) - .refine((val) => /^\d{10,12}$/.test(val), { - message: 'Format nomor telepon tidak valid, contoh: 081234567890', +export const registerSchema = z + .object({ + name: z.string().min(1, { message: 'Nama harus diisi' }), + email: z + .string() + .min(1, { message: 'Email harus diisi' }) + .email({ message: 'Email harus menggunakan format example@mail.com' }), + password: z.string().min(6, { message: 'Password minimal 6 karakter' }), + phone: z + .string() + .min(1, { message: 'Nomor telepon harus diisi' }) + .refine((val) => /^\d{10,12}$/.test(val), { + message: 'Format nomor telepon tidak valid, contoh: 081234567890', + }), + type_acc: z.string().optional(), + nama_wajib_pajak: z.string().optional(), + alamat_bisnis: z.string().optional(), + alamat_wajib_pajak: z.string().optional(), + is_pkp: z.string(), + is_terdaftar: z.string(), + sppkp_document: z.string().optional(), + npwp_document: z.string().optional(), + industry_id: z.string().optional(), + email_partner: z.string().optional(), + business_name: z.string().optional(), + company_type_id: z.string().optional(), + isChekBox: z.string().optional(), + npwp: z.string().optional().refine((val) => !val || /^\d{15,16}$/.test(val), { + message: 'Format NPWP tidak valid, NPWP harus terdiri dari 15-16 digit angka.', }), -}); + sppkp: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.type_acc === 'business') { + if (data.is_terdaftar === 'false') { + if (data.is_pkp === 'true') { + const requiredFields: { field: keyof typeof data; message: string }[] = [ + { field: 'business_name', message: 'Nama perusahaan harus diisi' }, + { field: 'alamat_bisnis', message: 'Alamat perusahaan harus diisi' }, + // { field: 'alamat_wajib_pajak', message: 'Alamat wajib pajak harus diisi' }, + { field: 'company_type_id', message: 'Badan usaha wajib dipilih' }, + { field: 'industry_id', message: 'Jenis usaha harus dipilih' }, + { field: 'sppkp_document', message: 'Document harus diisi' }, + { field: 'npwp_document', message: 'Document harus diisi' }, + { field: 'npwp', message: 'Format NPWP tidak valid, NPWP harus terdiri dari 15-16 digit angka.' }, + { field: 'nama_wajib_pajak', message: 'Nama wajib pajak harus diisi' }, + ]; + + requiredFields.forEach(({ field, message }) => { + if (!data[field]) { + ctx.addIssue({ + code: 'custom', + path: [field], + message, + }); + } + }); + + if (!data.email_partner || !z.string().email().safeParse(data.email_partner).success) { + ctx.addIssue({ + code: 'custom', + path: ['email_partner'], + message: 'Email partner harus diisi dengan format example@mail.com', + }); + } + if(data.isChekBox === 'false'){ + if (!data.alamat_wajib_pajak) { + ctx.addIssue({ + code: 'custom', + path: ['alamat_wajib_pajak'], + message: 'Alamat wajib pajak harus diisi', + }); + } + } + + } else { + if (!data.business_name) { + ctx.addIssue({ + code: 'custom', + path: ['business_name'], + message: 'Nama perusahaan harus diisi', + }); + } + if (!data.alamat_bisnis) { + ctx.addIssue({ + code: 'custom', + path: ['alamat_bisnis'], + message: 'Alamat perusahaan harus diisi', + }); + } + + if (!data.company_type_id) { + ctx.addIssue({ + code: 'custom', + path: ['company_type_id'], + message: 'Badan usaha wajib dipilih', + }); + } + if (!data.industry_id) { + ctx.addIssue({ + code: 'custom', + path: ['industry_id'], + message: 'Jenis usaha harus dipilih', + }); + } + + if (data.npwp && !/^\d{15,16}$/.test(data.npwp)) { + ctx.addIssue({ + code: 'custom', + path: ['npwp'], + message: 'Format NPWP tidak valid, NPWP harus terdiri dari 15-16 digit angka.', + }); + } + + } + }else{ + if (data.is_pkp === 'true') { + if (!data.business_name) { + ctx.addIssue({ + code: 'custom', + path: ['business_name'], + message: 'Nama perusahaan harus diisi', + }); + } + } else { + if (!data.business_name) { + ctx.addIssue({ + code: 'custom', + path: ['business_name'], + message: 'Nama perusahaan harus diisi', + }); + } + } + } + + // Remove this unconditional issue addition to prevent blocking form submission + // ctx.addIssue({ + // code: 'custom', + // path: ['business_name'], + // message: 'Nama perusahaan harus diisi', + // }); + }else{ + + } + }); diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index ebbcf857..2a51c41f 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -293,7 +293,7 @@ const NavbarDesktop = () => { /> </div> )} - <p className="absolute inset-0 flex justify-center items-center group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200 z-10">Semua Promo</p> + <span className="absolute inset-0 flex justify-center items-center group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200 z-10">Semua Promo</span> </Link> {/* {showPopup && router.pathname === '/' && ( <div className={`fixed ${isTop ? 'top-[170px]' : 'top-[90px]'} rounded-3xl left-[700px] w-fit object-center bg-green-50 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20 text-center p-2 z-50 transition-all duration-300`}> @@ -312,7 +312,7 @@ const NavbarDesktop = () => { target='_blank' rel='noreferrer' > - <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Semua Brand</p> + <span className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Semua Brand</span> </Link> <Link href='/shop/search?orderBy=stock' @@ -323,7 +323,7 @@ const NavbarDesktop = () => { target='_blank' rel='noreferrer' > - <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Ready Stock</p> + <span className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Ready Stock</span> </Link> <Link href='https://blog.indoteknik.com/' @@ -331,7 +331,7 @@ const NavbarDesktop = () => { target='_blank' rel='noreferrer noopener' > - <p className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Blog Indoteknik</p> + <span className="group-hover:scale-105 group-hover:text-red-500 transition-transform duration-200">Blog Indoteknik</span> </Link> {/* <Link href='/video' diff --git a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx index 42bdc12a..c0698b6e 100644 --- a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx +++ b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx @@ -1,37 +1,38 @@ -import { deleteAuth } from '@/core/utils/auth' -import Link from '../Link/Link' -import { useRouter } from 'next/router' -import { signOut, useSession } from 'next-auth/react' -import useAuth from '@/core/hooks/useAuth' +import { deleteAuth } from '@/core/utils/auth'; +import Link from '../Link/Link'; +import { useRouter } from 'next/router'; +import { signOut, useSession } from 'next-auth/react'; +import useAuth from '@/core/hooks/useAuth'; const NavbarUserDropdown = () => { - const router = useRouter() - const atuh = useAuth() + const router = useRouter(); + const atuh = useAuth(); const logout = async () => { deleteAuth().then(() => { - router.push('/login') - }) - } + router.push('/login'); + }); + }; return ( <div className='navbar-user-dropdown-wrapper'> <div className='navbar-user-dropdown'> + <Link href='/my/profile'>Profil Saya</Link> <Link href='/my/quotations'>Daftar Quotation</Link> <Link href='/my/transactions'>Daftar Transaksi</Link> <Link href='/my/shipments'>Daftar Pengiriman</Link> <Link href='/my/invoices'>Invoice & Faktur Pajak</Link> <Link href='/my/wishlist'>Wishlist</Link> <Link href='/my/address'>Daftar Alamat</Link> - {!atuh?.external && + {!atuh?.external && ( <Link href='/my/recomendation'>Dashboard Recomendation</Link> - } + )} <button type='button' onClick={logout}> Keluar Akun </button> </div> </div> - ) -} + ); +}; -export default NavbarUserDropdown +export default NavbarUserDropdown; diff --git a/src/core/components/layouts/AppLayout.jsx b/src/core/components/layouts/AppLayout.jsx index ebbc1ad5..ec61ca06 100644 --- a/src/core/components/layouts/AppLayout.jsx +++ b/src/core/components/layouts/AppLayout.jsx @@ -10,13 +10,15 @@ const BasicFooter = dynamic(() => import('../elements/Footer/BasicFooter'), { const AppLayout = ({ children, title, withFooter = true }) => { return ( - <> - <AnimationLayout> - <AppBar title={title} /> - {children} - </AnimationLayout> - {withFooter && <BasicFooter />} - </> + <div className='flex flex-col min-h-screen max-h-screen overflow-y-auto'> + <AppBar title={title} /> + <div className='flex-grow p-4'>{children}</div> + {withFooter && ( + <div className='mt-auto'> + <BasicFooter /> + </div> + )} + </div> ); }; diff --git a/src/lib/address/components/Addresses.jsx b/src/lib/address/components/Addresses.jsx index a610d371..9ca617ae 100644 --- a/src/lib/address/components/Addresses.jsx +++ b/src/lib/address/components/Addresses.jsx @@ -1,34 +1,72 @@ -import Link from '@/core/components/elements/Link/Link' -import Spinner from '@/core/components/elements/Spinner/Spinner' -import useAuth from '@/core/hooks/useAuth' -import { getItemAddress, updateItemAddress } from '@/core/utils/address' -import { useRouter } from 'next/router' -import useAddresses from '../hooks/useAddresses' -import MobileView from '@/core/components/views/MobileView' -import DesktopView from '@/core/components/views/DesktopView' -import Menu from '@/lib/auth/components/Menu' +import { useState } from 'react'; +import Link from '@/core/components/elements/Link/Link'; +import Spinner from '@/core/components/elements/Spinner/Spinner'; +import useAuth from '@/core/hooks/useAuth'; +import { getItemAddress, updateItemAddress } from '@/core/utils/address'; +import { useRouter } from 'next/router'; +import useAddresses from '../hooks/useAddresses'; +import MobileView from '@/core/components/views/MobileView'; +import DesktopView from '@/core/components/views/DesktopView'; +import Menu from '@/lib/auth/components/Menu'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; const Addresses = () => { - const router = useRouter() - const { select = null } = router.query - const { addresses } = useAddresses() - const selectedAddress = getItemAddress(select || '') + const router = useRouter(); + const { select = null } = router.query; + const { addresses } = useAddresses(); + const selectedAddress = getItemAddress(select || ''); + const [changeConfirmation, setChangeConfirmation] = useState(false); + const [selectedForChange, setSelectedForChange] = useState(null); // State baru untuk simpan alamat yang akan diubah + const changeSelectedAddress = (id) => { - if (!select) return - updateItemAddress(select, id) - router.back() - } + if (!select) return; + updateItemAddress(select, id); + router.back(); + }; + + const handleConfirmSubmit = () => { + setChangeConfirmation(false); + if (selectedForChange) { + router.push(`/my/address/${selectedForChange}/edit`); + } + }; if (addresses.isLoading) { return ( <div className='flex justify-center my-6'> <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> </div> - ) + ); } return ( <> + <BottomPopup + active={changeConfirmation} + close={() => setChangeConfirmation(false)} // Menutup popup + title='Ubah alamat Bisnis' + > + <div className='leading-7 text-gray_r-12/80'> + Anda akan mengubah alamat utama bisnis? + </div> + <div className='flex mt-6 gap-x-4 md:justify-end'> + <button + className='btn-solid-red flex-1 md:flex-none' + type='button' + onClick={handleConfirmSubmit} + > + Yakin + </button> + <button + className='btn-light flex-1 md:flex-none' + type='button' + onClick={() => setChangeConfirmation(false)} + > + Batal + </button> + </div> + </BottomPopup> + <MobileView> <div className='p-4'> <div className='text-right'> @@ -37,7 +75,10 @@ const Addresses = () => { <div className='grid gap-y-4 mt-4'> {addresses.data?.map((address, index) => { - const type = address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address' + const type = + address.type.charAt(0).toUpperCase() + + address.type.slice(1) + + ' Address'; return ( <AddressCard key={index} @@ -45,9 +86,11 @@ const Addresses = () => { type={type} changeSelectedAddress={changeSelectedAddress} selectedAddress={selectedAddress} + setChangeConfirmation={setChangeConfirmation} // Memanggil popup + setSelectedForChange={setSelectedForChange} // Simpan id address yang akan diubah select={select} /> - ) + ); })} </div> </div> @@ -72,7 +115,9 @@ const Addresses = () => { <div className='grid grid-cols-2 gap-4'> {addresses.data?.map((address, index) => { const type = - address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address' + address.type.charAt(0).toUpperCase() + + address.type.slice(1) + + ' Address'; return ( <AddressCard key={index} @@ -80,20 +125,31 @@ const Addresses = () => { type={type} changeSelectedAddress={changeSelectedAddress} selectedAddress={selectedAddress} + setChangeConfirmation={setChangeConfirmation} + setSelectedForChange={setSelectedForChange} select={select} /> - ) + ); })} </div> </div> </div> </DesktopView> </> - ) -} + ); +}; -const AddressCard = ({ address, selectedAddress, changeSelectedAddress, type, select }) => { - const auth = useAuth() +const AddressCard = ({ + address, + selectedAddress, + changeSelectedAddress, + type, + select, + setChangeConfirmation, + setSelectedForChange, +}) => { + const auth = useAuth(); + const router = useRouter(); return ( <div @@ -106,23 +162,37 @@ const AddressCard = ({ address, selectedAddress, changeSelectedAddress, type, se (select && 'cursor-pointer hover:bg-gray_r-4 transition') }`} > - <div onClick={() => changeSelectedAddress(address.id)} className={select && 'cursor-pointer'}> + <div + onClick={() => changeSelectedAddress(address.id)} + className={select && 'cursor-pointer'} + > <div className='flex gap-x-2'> <div className='badge-red'>{type}</div> - {auth?.partnerId == address.id && <div className='badge-green'>Utama</div>} + {auth?.partnerId == address.id && ( + <div className='badge-green'>Utama</div> + )} </div> <p className='font-medium mt-2'>{address.name}</p> - {address.mobile && <p className='mt-2 text-gray_r-11'>{address.mobile}</p>} + {address.mobile && ( + <p className='mt-2 text-gray_r-11'>{address.mobile}</p> + )} <p className='mt-1 leading-6 text-gray_r-11'>{address.street}</p> </div> - <Link - href={`/my/address/${address.id}/edit`} + <button + onClick={() => { + if (type == 'Contact Address' && auth.parentId) { + setSelectedForChange(address.id); // Set alamat yang dipilih + setChangeConfirmation(true); // Tampilkan popup konfirmasi + } else { + router.push(`/my/address/${address.id}/edit`); + } + }} className='btn-light bg-white mt-3 w-full !text-gray_r-11' > Ubah Alamat - </Link> + </button> </div> - ) -} + ); +}; -export default Addresses +export default Addresses; diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 86519147..e315affe 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -1,76 +1,101 @@ -import HookFormSelect from '@/core/components/elements/Select/HookFormSelect' -import useAuth from '@/core/hooks/useAuth' -import { useRouter } from 'next/router' -import { Controller, useForm } from 'react-hook-form' -import * as Yup from 'yup' -import cityApi from '../api/cityApi' -import districtApi from '../api/districtApi' -import subDistrictApi from '../api/subDistrictApi' -import { useEffect, useState } from 'react' -import createAddressApi from '../api/createAddressApi' -import { toast } from 'react-hot-toast' -import { yupResolver } from '@hookform/resolvers/yup' -import Menu from '@/lib/auth/components/Menu' +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; +import useAuth from '@/core/hooks/useAuth'; +import { useRouter } from 'next/router'; +import { Controller, useForm } from 'react-hook-form'; +import * as Yup from 'yup'; +import cityApi from '../api/cityApi'; +import districtApi from '../api/districtApi'; +import subDistrictApi from '../api/subDistrictApi'; +import { useEffect, useState } from 'react'; +import createAddressApi from '../api/createAddressApi'; +import { toast } from 'react-hot-toast'; +import { yupResolver } from '@hookform/resolvers/yup'; +import Menu from '@/lib/auth/components/Menu'; +import useAddresses from '../hooks/useAddresses'; const CreateAddress = () => { - const auth = useAuth() - const router = useRouter() + const auth = useAuth(); + const router = useRouter(); const { register, formState: { errors }, handleSubmit, watch, setValue, - control + control, } = useForm({ resolver: yupResolver(validationSchema), - defaultValues - }) - - const [cities, setCities] = useState([]) - const [districts, setDistricts] = useState([]) - const [subDistricts, setSubDistricts] = useState([]) + defaultValues, + }); + const { addresses = [] } = useAddresses(); // Ensure addresses is an array + const [cities, setCities] = useState([]); + const [districts, setDistricts] = useState([]); + const [subDistricts, setSubDistricts] = useState([]); + const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types useEffect(() => { const loadCities = async () => { - let dataCities = await cityApi() - dataCities = dataCities.map((city) => ({ value: city.id, label: city.name })) - setCities(dataCities) + let dataCities = await cityApi(); + dataCities = dataCities.map((city) => ({ + value: city.id, + label: city.name, + })); + setCities(dataCities); + }; + loadCities(); + }, []); + + useEffect(() => { + if (addresses) { + let hasContactAddress = false; + + for (let i = 0; i < addresses?.data?.length; i++) { + if (addresses.data[i].type === 'contact') { + hasContactAddress = true; + break; + } + } + if (hasContactAddress) { + setFilteredTypes(types.filter((type) => type.value !== 'contact')); + } else { + setFilteredTypes(types); + } } - loadCities() - }, []) + }, [auth]); - const watchCity = watch('city') + const watchCity = watch('city'); useEffect(() => { - setValue('district', '') + setValue('district', ''); if (watchCity) { const loadDistricts = async () => { - let dataDistricts = await districtApi({ cityId: watchCity }) + let dataDistricts = await districtApi({ cityId: watchCity }); dataDistricts = dataDistricts.map((district) => ({ value: district.id, - label: district.name - })) - setDistricts(dataDistricts) - } - loadDistricts() + label: district.name, + })); + setDistricts(dataDistricts); + }; + loadDistricts(); } - }, [watchCity, setValue]) + }, [watchCity, setValue]); - const watchDistrict = watch('district') + const watchDistrict = watch('district'); useEffect(() => { - setValue('subDistrict', '') + setValue('subDistrict', ''); if (watchDistrict) { const loadSubDistricts = async () => { - let dataSubDistricts = await subDistrictApi({ districtId: watchDistrict }) + let dataSubDistricts = await subDistrictApi({ + districtId: watchDistrict, + }); dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, - label: district.name - })) - setSubDistricts(dataSubDistricts) - } - loadSubDistricts() + label: district.name, + })); + setSubDistricts(dataSubDistricts); + }; + loadSubDistricts(); } - }, [watchDistrict, setValue]) + }, [watchDistrict, setValue]); const onSubmitHandler = async (values) => { const data = { @@ -78,15 +103,15 @@ const CreateAddress = () => { city_id: values.city, district_id: values.district, sub_district_id: values.subDistrict, - parent_id: auth.partnerId - } + parent_id: auth.partnerId, + }; - const address = await createAddressApi({ data }) + const address = await createAddressApi({ data }); if (address?.id) { - toast.success('Berhasil menambahkan alamat') - router.back() + toast.success('Berhasil menambahkan alamat'); + router.back(); } - } + }; return ( <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> @@ -102,10 +127,16 @@ const CreateAddress = () => { name='type' control={control} render={(props) => ( - <HookFormSelect {...props} isSearchable={false} options={types} /> + <HookFormSelect + {...props} + isSearchable={false} + options={filteredTypes} + /> )} /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.type?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.type?.message} + </div> </div> <div> @@ -116,7 +147,9 @@ const CreateAddress = () => { type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.name?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> </div> <div> @@ -127,7 +160,9 @@ const CreateAddress = () => { type='email' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.email?.message} + </div> </div> <div> @@ -138,7 +173,9 @@ const CreateAddress = () => { type='tel' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.mobile?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.mobile?.message} + </div> </div> <div> @@ -149,7 +186,9 @@ const CreateAddress = () => { type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.street?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.street?.message} + </div> </div> <div> @@ -160,7 +199,9 @@ const CreateAddress = () => { type='number' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.zip?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.zip?.message} + </div> </div> <div> @@ -168,9 +209,13 @@ const CreateAddress = () => { <Controller name='city' control={control} - render={(props) => <HookFormSelect {...props} options={cities} />} + render={(props) => ( + <HookFormSelect {...props} options={cities} /> + )} /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.city?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.city?.message} + </div> </div> <div> @@ -179,10 +224,16 @@ const CreateAddress = () => { name='district' control={control} render={(props) => ( - <HookFormSelect {...props} options={districts} disabled={!watchCity} /> + <HookFormSelect + {...props} + options={districts} + disabled={!watchCity} + /> )} /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.district?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.district?.message} + </div> </div> <div> @@ -191,31 +242,37 @@ const CreateAddress = () => { name='subDistrict' control={control} render={(props) => ( - <HookFormSelect {...props} options={subDistricts} disabled={!watchDistrict} /> + <HookFormSelect + {...props} + options={subDistricts} + disabled={!watchDistrict} + /> )} /> </div> </div> - <button type='submit' className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto'> + <button + type='submit' + className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' + > Simpan </button> </form> </div> </div> - ) -} + ); +}; const validationSchema = Yup.object().shape({ type: Yup.string().required('Harus di-pilih'), name: Yup.string().min(3, 'Minimal 3 karakter').required('Harus di-isi'), - // email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), mobile: Yup.string().required('Harus di-isi'), street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), city: Yup.string().required('Harus di-pilih'), - district: Yup.string().required('Harus di-pilih') -}) + district: Yup.string().required('Harus di-pilih'), +}); const defaultValues = { type: '', @@ -226,14 +283,14 @@ const defaultValues = { city: '', district: '', subDistrict: '', - zip: '' -} + zip: '', +}; const types = [ { value: 'contact', label: 'Contact Address' }, { value: 'invoice', label: 'Invoice Address' }, { value: 'delivery', label: 'Delivery Address' }, - { value: 'other', label: 'Other Address' } -] + { value: 'other', label: 'Other Address' }, +]; -export default CreateAddress +export default CreateAddress; diff --git a/src/lib/address/components/EditAddress.jsx b/src/lib/address/components/EditAddress.jsx index 520bba51..ff6b1f12 100644 --- a/src/lib/address/components/EditAddress.jsx +++ b/src/lib/address/components/EditAddress.jsx @@ -1,18 +1,22 @@ -import { yupResolver } from '@hookform/resolvers/yup' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import * as Yup from 'yup' -import cityApi from '../api/cityApi' -import { Controller, useForm } from 'react-hook-form' -import districtApi from '../api/districtApi' -import subDistrictApi from '../api/subDistrictApi' -import editAddressApi from '../api/editAddressApi' -import HookFormSelect from '@/core/components/elements/Select/HookFormSelect' -import { toast } from 'react-hot-toast' -import Menu from '@/lib/auth/components/Menu' +import { yupResolver } from '@hookform/resolvers/yup'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import * as Yup from 'yup'; +import cityApi from '../api/cityApi'; +import { Controller, useForm } from 'react-hook-form'; +import districtApi from '../api/districtApi'; +import subDistrictApi from '../api/subDistrictApi'; +import addressApi from '@/lib/address/api/addressApi'; +import editAddressApi from '../api/editAddressApi'; +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; +import { toast } from 'react-hot-toast'; +import Menu from '@/lib/auth/components/Menu'; +import useAuth from '@/core/hooks/useAuth'; +import odooApi from '@/core/api/odooApi'; const EditAddress = ({ id, defaultValues }) => { - const router = useRouter() + const auth = useAuth(); + const router = useRouter(); const { register, formState: { errors }, @@ -20,205 +24,283 @@ const EditAddress = ({ id, defaultValues }) => { watch, setValue, getValues, - control + control, } = useForm({ resolver: yupResolver(validationSchema), - defaultValues - }) + defaultValues, + }); + const [cities, setCities] = useState([]); + const [districts, setDistricts] = useState([]); + const [subDistricts, setSubDistricts] = useState([]); - const [cities, setCities] = useState([]) - const [districts, setDistricts] = useState([]) - const [subDistricts, setSubDistricts] = useState([]) + useEffect(() => { + const loadProfile = async () => { + const dataProfile = await addressApi({ id: auth.parentId }); + setValue('industry', dataProfile.industryId); + setValue('companyType', dataProfile.companyTypeId); + setValue('taxName', dataProfile.taxName); + setValue('npwp', dataProfile.npwp); + setValue('alamat_wajib_pajak', dataProfile.alamatWajibPajak); + setValue('alamat_bisnis', dataProfile.alamatBisnis); + setValue('business_name', dataProfile.name); + }; + if (auth) loadProfile(); + }, [auth, setValue]); useEffect(() => { const loadCities = async () => { - let dataCities = await cityApi() + let dataCities = await cityApi(); dataCities = dataCities.map((city) => ({ value: city.id, - label: city.name - })) - setCities(dataCities) - } - loadCities() - }, []) + label: city.name, + })); + setCities(dataCities); + }; + loadCities(); + }, []); - const watchCity = watch('city') + const watchCity = watch('city'); useEffect(() => { - setValue('district', '') + setValue('district', ''); if (watchCity) { const loadDistricts = async () => { - let dataDistricts = await districtApi({ cityId: watchCity }) + let dataDistricts = await districtApi({ cityId: watchCity }); dataDistricts = dataDistricts.map((district) => ({ value: district.id, - label: district.name - })) - setDistricts(dataDistricts) - let oldDistrict = getValues('oldDistrict') + label: district.name, + })); + setDistricts(dataDistricts); + let oldDistrict = getValues('oldDistrict'); if (oldDistrict) { - setValue('district', oldDistrict) - setValue('oldDistrict', '') + setValue('district', oldDistrict); + setValue('oldDistrict', ''); } - } - loadDistricts() + }; + loadDistricts(); } - }, [watchCity, setValue, getValues]) + }, [watchCity, setValue, getValues]); - const watchDistrict = watch('district') + const watchDistrict = watch('district'); useEffect(() => { - setValue('subDistrict', '') + setValue('subDistrict', ''); if (watchDistrict) { const loadSubDistricts = async () => { let dataSubDistricts = await subDistrictApi({ - districtId: watchDistrict - }) + districtId: watchDistrict, + }); dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, - label: district.name - })) - setSubDistricts(dataSubDistricts) - let oldSubDistrict = getValues('oldSubDistrict') + label: district.name, + })); + setSubDistricts(dataSubDistricts); + let oldSubDistrict = getValues('oldSubDistrict'); if (oldSubDistrict) { - setValue('subDistrict', oldSubDistrict) - setValue('oldSubDistrict', '') + setValue('subDistrict', oldSubDistrict); + setValue('oldSubDistrict', ''); } - } - loadSubDistricts() + }; + loadSubDistricts(); } - }, [watchDistrict, setValue, getValues]) - + }, [watchDistrict, setValue, getValues]); const onSubmitHandler = async (values) => { const data = { ...values, + phone: values.mobile, city_id: values.city, district_id: values.district, - sub_district_id: values.subDistrict + sub_district_id: values.subDistrict, + }; + const address = await editAddressApi({ id, data }); + let dataAlamat; + let isUpdated = true; + if (auth?.partnerId == id) { + dataAlamat = { + id_user: auth.partnerId, + company_type_id: values.companyType, + industry_id: values.industry, + tax_name: values.taxName, + alamat_lengkap_text: values.alamat_wajib_pajak, + street: values.street, + business_name: values.business_name, + name: values.business_name, + npwp: values.npwp, + }; + isUpdated = await odooApi( + 'PUT', + `/api/v1/partner/${auth.parentId}`, + dataAlamat + ); } - const address = await editAddressApi({ id, data }) - if (address?.id) { - toast.success('Berhasil mengubah alamat') - router.back() + // if (isUpdated?.id) { + if (address?.id && isUpdated?.id) { + toast.success('Berhasil mengubah alamat'); + router.back(); + } else { + toast.error('Terjadi kesalahan internal'); + router.back(); } - } + }; return ( - <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> - <div className='hidden md:block w-3/12 pr-4'> - <Menu /> - </div> - <div className='w-full md:w-9/12 p-4 bg-white border border-gray_r-6 rounded'> - <h1 className='text-title-sm font-semibold mb-6 hidden md:block'>Ubah Alamat</h1> - <form onSubmit={handleSubmit(onSubmitHandler)}> - <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> - <div> - <label className='form-label mb-2'>Label Alamat</label> - <Controller - name='type' - control={control} - render={(props) => ( - <HookFormSelect {...props} isSearchable={false} options={types} /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.type?.message}</div> - </div> + <> + <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> + <div className='hidden md:block w-3/12 pr-4'> + <Menu /> + </div> + <div className='w-full md:w-9/12 p-4 bg-white border border-gray_r-6 rounded'> + <div className='flex justify-start items-center mb-6'> + <h1 className='text-title-sm font-semibold hidden md:block mr-2'> + Ubah Alamat + </h1> + {auth?.partnerId == id && <div className='badge-green'>Utama</div>} + </div> + <form onSubmit={handleSubmit(onSubmitHandler)}> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> + <div> + <label className='form-label mb-2'>Label Alamat</label> + <Controller + name='type' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + isSearchable={false} + options={types} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.type?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Nama</label> - <input - {...register('name')} - placeholder='John Doe' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.name?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Nama</label> + <input + {...register('name')} + placeholder='John Doe' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Email</label> - <input - {...register('email')} - placeholder='johndoe@example.com' - type='email' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Email</label> + <input + {...register('email')} + placeholder='johndoe@example.com' + type='email' + className='form-input' + disabled={auth?.partnerId == id && true} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.email?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Mobile</label> - <input - {...register('mobile')} - placeholder='08xxxxxxxx' - type='tel' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.mobile?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Mobile</label> + <input + {...register('mobile')} + placeholder='08xxxxxxxx' + type='tel' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.mobile?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Alamat</label> - <input - {...register('street')} - placeholder='Jl. Bandengan Utara 85A' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.street?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Alamat</label> + <input + {...register('street')} + placeholder='Jl. Bandengan Utara 85A' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.street?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Kode Pos</label> - <input - {...register('zip')} - placeholder='10100' - type='number' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.zip?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Kode Pos</label> + <input + {...register('zip')} + placeholder='10100' + type='number' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.zip?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Kota</label> - <Controller - name='city' - control={control} - render={(props) => <HookFormSelect {...props} options={cities} />} - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.city?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Kota</label> + <Controller + name='city' + control={control} + render={(props) => ( + <HookFormSelect {...props} options={cities} /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.city?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Kecamatan</label> - <Controller - name='district' - control={control} - render={(props) => ( - <HookFormSelect {...props} options={districts} disabled={!watchCity} /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.district?.message}</div> - </div> + <div> + <label className='form-label mb-2'>Kecamatan</label> + <Controller + name='district' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={districts} + disabled={!watchCity} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.district?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Kelurahan</label> - <Controller - name='subDistrict' - control={control} - render={(props) => ( - <HookFormSelect {...props} options={subDistricts} disabled={!watchDistrict} /> - )} - /> + <div> + <label className='form-label mb-2'>Kelurahan</label> + <Controller + name='subDistrict' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={subDistricts} + disabled={!watchDistrict} + /> + )} + /> + </div> </div> - </div> - <button type='submit' className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto'> - Simpan - </button> - </form> + <button + type='submit' + className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' + > + Simpan + </button> + </form> + </div> </div> - </div> - ) -} + </> + ); +}; const validationSchema = Yup.object().shape({ type: Yup.string().required('Harus di-pilih'), @@ -228,14 +310,14 @@ const validationSchema = Yup.object().shape({ street: Yup.string().required('Harus di-isi'), zip: Yup.string().required('Harus di-isi'), city: Yup.string().required('Harus di-pilih'), - district: Yup.string().required('Harus di-pilih') -}) + district: Yup.string().required('Harus di-pilih'), +}); const types = [ { value: 'contact', label: 'Contact Address' }, { value: 'invoice', label: 'Invoice Address' }, { value: 'delivery', label: 'Delivery Address' }, - { value: 'other', label: 'Other Address' } -] + { value: 'other', label: 'Other Address' }, +]; -export default EditAddress +export default EditAddress; diff --git a/src/lib/auth/components/CompanyProfile.jsx b/src/lib/auth/components/CompanyProfile.jsx index 2faede9b..7bda992f 100644 --- a/src/lib/auth/components/CompanyProfile.jsx +++ b/src/lib/auth/components/CompanyProfile.jsx @@ -1,78 +1,136 @@ -import odooApi from '@/core/api/odooApi' -import HookFormSelect from '@/core/components/elements/Select/HookFormSelect' -import useAuth from '@/core/hooks/useAuth' -import addressApi from '@/lib/address/api/addressApi' -import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline' -import { useEffect, useState } from 'react' -import { Controller, useForm } from 'react-hook-form' -import { toast } from 'react-hot-toast' +import odooApi from '@/core/api/odooApi'; +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; +import useAuth from '@/core/hooks/useAuth'; +import addressApi from '@/lib/address/api/addressApi'; +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'; +import { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { toast } from 'react-hot-toast'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as Yup from 'yup'; const CompanyProfile = () => { - const auth = useAuth() - const [isOpen, setIsOpen] = useState(false) - const toggle = () => setIsOpen(!isOpen) - const { register, setValue, control, handleSubmit } = useForm({ - defaultValues: { - industry: '', - companyType: '', - name: '', - taxName: '', - npwp: '' - } - }) + const [changeConfirmation, setChangeConfirmation] = useState(false); + const auth = useAuth(); + const [isOpen, setIsOpen] = useState(false); + const toggle = () => setIsOpen(!isOpen); + const { + register, + formState: { errors }, + setValue, + control, + handleSubmit, + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues, + }); - const [industries, setIndustries] = useState([]) + const [industries, setIndustries] = useState([]); useEffect(() => { const loadIndustries = async () => { - const dataIndustries = await odooApi('GET', '/api/v1/partner/industry') - setIndustries(dataIndustries?.map((o) => ({ value: o.id, label: o.name }))) - } - loadIndustries() - }, []) + const dataIndustries = await odooApi('GET', '/api/v1/partner/industry'); + setIndustries( + dataIndustries?.map((o) => ({ value: o.id, label: o.name })) + ); + }; + loadIndustries(); + }, []); - const [companyTypes, setCompanyTypes] = useState([]) + const [companyTypes, setCompanyTypes] = useState([]); useEffect(() => { const loadCompanyTypes = async () => { - const dataCompanyTypes = await odooApi('GET', '/api/v1/partner/company_type') - setCompanyTypes(dataCompanyTypes?.map((o) => ({ value: o.id, label: o.name }))) - } - loadCompanyTypes() - }, []) + const dataCompanyTypes = await odooApi( + 'GET', + '/api/v1/partner/company_type' + ); + setCompanyTypes( + dataCompanyTypes?.map((o) => ({ value: o.id, label: o.name })) + ); + }; + loadCompanyTypes(); + }, []); useEffect(() => { const loadProfile = async () => { - const dataProfile = await addressApi({ id: auth.parentId }) - setValue('name', dataProfile.name) - setValue('industry', dataProfile.industryId) - setValue('companyType', dataProfile.companyTypeId) - setValue('taxName', dataProfile.taxName) - setValue('npwp', dataProfile.npwp) - } - if (auth) loadProfile() - }, [auth, setValue]) + const dataProfile = await addressApi({ id: auth.parentId }); + setValue('name', dataProfile.name); + setValue('industry', dataProfile.industryId); + setValue('companyType', dataProfile.companyTypeId); + setValue('taxName', dataProfile.taxName); + setValue('npwp', dataProfile.npwp); + setValue('alamat_wajib_pajak', dataProfile.alamatWajibPajak); + setValue('alamat_bisnis', dataProfile.alamatBisnis); + }; + if (auth) loadProfile(); + }, [auth, setValue]); const onSubmitHandler = async (values) => { - const data = { - ...values, - company_type_id: values.companyType, - industry_id: values.industry, - tax_name: values.taxName + if (changeConfirmation) { + const data = { + ...values, + id_user: auth.partnerId, + company_type_id: values.companyType, + industry_id: values.industry, + tax_name: values.taxName, + alamat_lengkap_text: values.alamat_wajib_pajak, + street: values.alamat_bisnis, + }; + const isUpdated = await odooApi( + 'PUT', + `/api/v1/partner/${auth.parentId}`, + data + ); + if (isUpdated?.id) { + toast.success('Berhasil mengubah profil', { duration: 1500 }); + return; + } + toast.error('Terjadi kesalahan internal'); } - const isUpdated = await odooApi('PUT', `/api/v1/partner/${auth.parentId}`, data) - if (isUpdated?.id) { - toast.success('Berhasil mengubah profil', { duration: 1500 }) - return - } - toast.error('Terjadi kesalahan internal') - } + }; + + const handleConfirmSubmit = () => { + setChangeConfirmation(false); + handleSubmit(onSubmitHandler)(); + }; return ( <> - <button type='button' onClick={toggle} className='p-4 flex items-center text-left w-full'> + <BottomPopup + active={changeConfirmation} + close={() => setChangeConfirmation(true)} + title='Ubah profil Bisnis' + > + <div className='leading-7 text-gray_r-12/80'> + Apakah anda yakin mengubah data bisnis? + </div> + <div className='flex mt-6 gap-x-4 md:justify-end'> + <button + className='btn-solid-red flex-1 md:flex-none' + type='button' + onClick={handleConfirmSubmit} + > + Ya, Ubah + </button> + <button + className='btn-light flex-1 md:flex-none' + type='button' + onClick={() => setChangeConfirmation(false)} + > + Batal + </button> + </div> + </BottomPopup> + <button + type='button' + onClick={toggle} + className='p-4 flex items-center text-left w-full' + > <div> <div className='font-semibold mb-2'>Informasi Usaha</div> <div className='text-gray_r-11'> - Dibawah ini adalah data usaha yang anda masukkan, periksa kembali data usaha anda. + Dibawah ini adalah data usaha yang anda masukkan, periksa kembali + data usaha anda. </div> </div> <div className='ml-auto p-2 bg-gray_r-3 rounded'> @@ -82,15 +140,26 @@ const CompanyProfile = () => { </button> {isOpen && ( - <form className='p-4 border-t border-gray_r-6' onSubmit={handleSubmit(onSubmitHandler)}> + <form + className='p-4 border-t border-gray_r-6' + onSubmit={(e) => { + e.preventDefault(); + setChangeConfirmation(true); + }} + > <div className='grid grid-cols-1 sm:grid-cols-2 gap-4'> <div> <label className='block mb-3'>Klasifikasi Jenis Usaha</label> <Controller name='industry' control={control} - render={(props) => <HookFormSelect {...props} options={industries} />} + render={(props) => ( + <HookFormSelect {...props} options={industries} /> + )} /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.industry?.message} + </div> </div> <div className='flex flex-wrap'> <div className='w-full mb-3'>Badan Usaha</div> @@ -98,8 +167,13 @@ const CompanyProfile = () => { <Controller name='companyType' control={control} - render={(props) => <HookFormSelect {...props} options={companyTypes} />} + render={(props) => ( + <HookFormSelect {...props} options={companyTypes} /> + )} /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.companyType?.message} + </div> </div> <div className='w-9/12 pl-1'> <input @@ -108,15 +182,55 @@ const CompanyProfile = () => { className='form-input' placeholder='Cth: Indoteknik Dotcom Gemilang' /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> </div> </div> <div> <label>Nama Wajib Pajak</label> - <input {...register('taxName')} type='text' className='form-input mt-3' /> + <input + {...register('taxName')} + type='text' + className='form-input mt-3' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.taxName?.message} + </div> + </div> + <div> + <label>Alamat Wajib Pajak</label> + <input + {...register('alamat_wajib_pajak')} + type='text' + className='form-input mt-3' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.alamat_wajib_pajak?.message} + </div> + </div> + <div> + <label>Alamat Bisnis</label> + <input + {...register('alamat_bisnis')} + type='text' + className='form-input mt-3' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.alamat_bisnis?.message} + </div> </div> <div> <label>Nomor NPWP</label> - <input {...register('npwp')} type='text' className='form-input mt-3' /> + <input + {...register('npwp')} + type='text' + className='form-input mt-3' + maxLength={16} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.npwp?.message} + </div> </div> </div> <button type='submit' className='btn-yellow w-full mt-6'> @@ -125,7 +239,27 @@ const CompanyProfile = () => { </form> )} </> - ) -} + ); +}; + +export default CompanyProfile; + +const validationSchema = Yup.object().shape({ + alamat_bisnis: Yup.string().required('Harus di-isi'), + alamat_wajib_pajak: Yup.string().required('Harus di-isi'), + taxName: Yup.string().required('Harus di-isi'), + npwp: Yup.string().required('Harus di-isi'), + name: Yup.string().required('Harus di-isi'), + industry: Yup.string().required('Harus di-pilih'), + companyType: Yup.string().required('Harus di-pilih'), +}); -export default CompanyProfile +const defaultValues = { + industry: '', + companyType: '', + name: '', + taxName: '', + npwp: '', + alamat_wajib_pajak: '', + alamat_bisnis: '', +}; diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index f63ef457..4c7e852f 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -77,6 +77,9 @@ const Checkout = () => { if (!addresses) return; const matchAddress = (key) => { + if (key === 'invoicing') { + key = 'invoice'; + } const addressToMatch = getItemAddress(key); const foundAddress = addresses.filter( (address) => address.id == addressToMatch diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index b30fa5c9..eefced60 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -9,7 +9,7 @@ import Link from '@/core/components/elements/Link/Link' import axios from 'axios' const PreferredBrand = () => { - let query = 'level_s' + let query = '' let params = 'prioritas' const [isLoading, setIsLoading] = useState(true) const [startWith, setStartWith] = useState(null) @@ -18,7 +18,7 @@ const PreferredBrand = () => { const loadBrand = useCallback(async () => { setIsLoading(true) const name = startWith ? `${startWith}*` : '' - const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?params=${name}`) + const result = await axios(`${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/brands?rows=20`) setIsLoading(false) setManufactures((manufactures) => [...result.data]) @@ -35,9 +35,9 @@ const PreferredBrand = () => { useEffect(() => { loadBrand() - }, [loadBrand]) + }, []) - const { preferredBrands } = usePreferredBrand(query) + // const { preferredBrands } = usePreferredBrand(query) const { isMobile, isDesktop } = useDevice() const swiperBanner = { modules:[Navigation, Pagination, Autoplay], @@ -59,7 +59,7 @@ const PreferredBrand = () => { return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4'> - <div className='font-semibold sm:text-h-lg'>Brand Pilihan</div> + <h1 className='font-semibold text-[14px] sm:text-h-lg'><Link href='/shop/brands' className='!text-black font-semibold'>Brand Pilihan</Link></h1> {isDesktop && ( <Link href='/shop/brands' className='!text-red-500 font-semibold'> Lihat Semua diff --git a/src/lib/home/components/PromotionProgram.jsx b/src/lib/home/components/PromotionProgram.jsx index c2f76069..ae8d5d6f 100644 --- a/src/lib/home/components/PromotionProgram.jsx +++ b/src/lib/home/components/PromotionProgram.jsx @@ -16,7 +16,7 @@ const BannerSection = () => { return ( <div className='px-4 sm:px-0'> <div className='flex justify-between items-center mb-4 '> - <div className='font-semibold sm:text-h-lg'>Promo Tersedia</div> + <h1 className='font-semibold text-[14px] sm:text-h-lg'> <Link href='/shop/promo' className='!text-black font-semibold' >Promo Tersedia</Link></h1> {isDesktop && ( <Link href='/shop/promo' className='!text-red-500 font-semibold'> Lihat Semua diff --git a/src/lib/product/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js index ecd6724a..51cc17fa 100644 --- a/src/lib/product/api/productSimilarApi.js +++ b/src/lib/product/api/productSimilarApi.js @@ -19,7 +19,7 @@ const productSimilarApi = async ({ query, source }) => { } } const dataProductSimilar = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular-weekly&operation=OR&priceFrom=1` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular-weekly&operation=OR&priceFrom=1&source=similar` ) if (dataflashSale) { dataProductSimilar.data.response.products = [ diff --git a/src/pages/api/activation-request.js b/src/pages/api/activation-request.js index 98d27f78..2b8ccec3 100644 --- a/src/pages/api/activation-request.js +++ b/src/pages/api/activation-request.js @@ -1,27 +1,29 @@ -import odooApi from '@/core/api/odooApi' -import mailer from '@/core/utils/mailer' +import odooApi from '@/core/api/odooApi'; +import mailer from '@/core/utils/mailer'; export default async function handler(req, res) { try { - const { email } = req.body - let result = await odooApi('POST', '/api/v1/user/activation-request', { email }) + const { email } = req.body; + let result = await odooApi('POST', '/api/v1/user/activation-request', { + email, + }); if (result.activationRequest) { mailer.sendMail({ - from: 'Indoteknik.com <noreply@indoteknik.com>', + from: 'noreply@indoteknik.com', to: result.user.email, subject: 'Permintaan Aktivasi Akun Indoteknik', html: ` <h1>Permintaan Aktivasi Akun Indoteknik</h1> <br> <p>Aktivasi akun anda melalui link berikut: <a href="${process.env.NEXT_PUBLIC_SELF_HOST}/activate?token=${result.token}">Aktivasi Akun</a></p> - ` - }) + `, + }); } - delete result.user - delete result.token - res.status(200).json(result) + delete result.user; + delete result.token; + res.status(200).json(result); } catch (error) { - console.log(error) - res.status(400).json({ error: error.message }) + console.log(error); + res.status(400).json({ error: error.message }); } } diff --git a/src/pages/api/shop/brands.js b/src/pages/api/shop/brands.js index cc64a7e7..9c2824b3 100644 --- a/src/pages/api/shop/brands.js +++ b/src/pages/api/shop/brands.js @@ -24,6 +24,8 @@ export default async function handler(req, res) { params = `name_s:${req.query.params}`.toLowerCase(); } } + if(req.query.rows) rows = req.query.rows; + const url = `${SOLR_HOST}/solr/brands/select?q=${params}&q.op=OR&indent=true&rows=${rows}&${sort}`; let brands = await axios(url); let dataBrands = responseMap(brands.data.response.docs); diff --git a/src/pages/api/shop/finish-checkout.js b/src/pages/api/shop/finish-checkout.js index 9eaa36db..4dcc915c 100644 --- a/src/pages/api/shop/finish-checkout.js +++ b/src/pages/api/shop/finish-checkout.js @@ -1,60 +1,66 @@ -import odooApi from '@/core/api/odooApi' -import mailer from '@/core/utils/mailer' -import FinishCheckoutEmail from '@/lib/checkout/email/FinishCheckoutEmail' -import { render } from '@react-email/render' -import axios from 'axios' -import camelcaseObjectDeep from 'camelcase-object-deep' +import odooApi from '@/core/api/odooApi'; +import mailer from '@/core/utils/mailer'; +import FinishCheckoutEmail from '@/lib/checkout/email/FinishCheckoutEmail'; +import { render } from '@react-email/render'; +import axios from 'axios'; +import camelcaseObjectDeep from 'camelcase-object-deep'; export default async function handler(req, res) { - const { orderName = null } = req.query + const { orderName = null } = req.query; if (!orderName) { - return res.status(422).json({ error: 'parameter missing' }) + return res.status(422).json({ error: 'parameter missing' }); } - let { auth } = req.cookies + let { auth } = req.cookies; if (!auth) { - return res.status(401).json({ error: 'Unauthorized' }) + return res.status(401).json({ error: 'Unauthorized' }); } - auth = JSON.parse(auth) + auth = JSON.parse(auth); - const midtransAuthKey = btoa(process.env.MIDTRANS_SERVER_KEY + ':') + const midtransAuthKey = btoa(process.env.MIDTRANS_SERVER_KEY + ':'); const midtransHeaders = { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Basic ${midtransAuthKey}` - } - let midtransStatus = {} + Authorization: `Basic ${midtransAuthKey}`, + }; + let midtransStatus = {}; try { - midtransStatus = await axios.get(`${process.env.MIDTRANS_HOST}/v2/${orderName}/status`, { - headers: midtransHeaders - }) - midtransStatus = camelcaseObjectDeep(midtransStatus.data) + midtransStatus = await axios.get( + `${process.env.MIDTRANS_HOST}/v2/${orderName}/status`, + { + headers: midtransHeaders, + } + ); + midtransStatus = camelcaseObjectDeep(midtransStatus.data); } catch (error) { - console.log(error) + console.log(error); } - let statusPayment = 'manual' + let statusPayment = 'manual'; if (midtransStatus?.orderId) { - const transactionStatus = midtransStatus.transactionStatus - statusPayment = 'failed' + const transactionStatus = midtransStatus.transactionStatus; + statusPayment = 'failed'; if (['capture', 'settlement'].includes(transactionStatus)) { - statusPayment = 'success' + statusPayment = 'success'; } else if (transactionStatus == 'pending') { - statusPayment = 'pending' + statusPayment = 'pending'; } } - const query = `name=${orderName.replaceAll('-', '/')}&limit=1&context=quotation` + const query = `name=${orderName.replaceAll( + '-', + '/' + )}&limit=1&context=quotation`; const searchTransaction = await odooApi( 'GET', `/api/v1/partner/${auth.partnerId}/sale_order?${query}`, {}, { Token: auth.token } - ) + ); if (searchTransaction.saleOrderTotal == 0) { - return res.status(400).json({ error: 'Transaction Not Found' }) + return res.status(400).json({ error: 'Transaction Not Found' }); } let transaction = await odooApi( @@ -62,17 +68,17 @@ export default async function handler(req, res) { `/api/v1/partner/${auth.partnerId}/sale_order/${searchTransaction.saleOrders[0].id}`, {}, { Token: auth.token } - ) + ); if (!transaction?.id) { - return res.status(400).json({ error: 'Transaction Detail Not Found' }) + return res.status(400).json({ error: 'Transaction Detail Not Found' }); } - transaction.subtotal = 0 - transaction.discountTotal = 0 + transaction.subtotal = 0; + transaction.discountTotal = 0; for (const product of transaction.products) { - transaction.subtotal += product.price.price * product.quantity + transaction.subtotal += product.price.price * product.quantity; transaction.discountTotal -= - (product.price.price - product.price.priceDiscount) * product.quantity + (product.price.price - product.price.priceDiscount) * product.quantity; } const emailMessage = render( @@ -81,14 +87,14 @@ export default async function handler(req, res) { payment={midtransStatus} statusPayment={statusPayment} /> - ) + ); mailer.sendMail({ - from: 'sales@indoteknik.com', + from: 'noreply@indoteknik.com', to: transaction.address.customer.email, subject: 'Pembelian di Indoteknik.com', - html: emailMessage - }) + html: emailMessage, + }); - return res.status(200).json({ description: 'success' }) + return res.status(200).json({ description: 'success' }); } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 0e87205e..6077c192 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -18,7 +18,7 @@ import { getAuth } from '~/libs/auth'; import useProductDetail from '~/modules/product-detail/stores/useProductDetail'; const BasicLayout = dynamic(() => - import('@/core/components/layouts/BasicLayout') + import('@/core/components/layouts/BasicLayout'),{ssr: false} ); const HeroBanner = dynamic(() => import('@/components/ui/HeroBanner'), { loading: () => <HeroBannerSkeleton />, @@ -55,24 +55,24 @@ const ProgramPromotion = dynamic(() => ); const BannerSection = dynamic(() => - import('@/lib/home/components/BannerSection') + import('@/lib/home/components/BannerSection'), {ssr: false} ); const CategoryHomeId = dynamic(() => - import('@/lib/home/components/CategoryHomeId') + import('@/lib/home/components/CategoryHomeId'), {ssr: false} ); const CategoryDynamic = dynamic(() => - import('@/lib/home/components/CategoryDynamic') + import('@/lib/home/components/CategoryDynamic'), {ssr: false} ); const CategoryDynamicMobile = dynamic(() => -import('@/lib/home/components/CategoryDynamicMobile') +import('@/lib/home/components/CategoryDynamicMobile'), {ssr: false} ); const CustomerReviews = dynamic(() => - import('@/lib/review/components/CustomerReviews') + import('@/lib/review/components/CustomerReviews'), {ssr: false} ); // need to ssr:false -const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList')); // need to ssr: false +const ServiceList = dynamic(() => import('@/lib/home/components/ServiceList'), {ssr: false}); // need to ssr: false @@ -148,11 +148,11 @@ export default function Home({categoryId}) { </DelayRender> </> )} - <PromotinProgram /> + {/* <PromotinProgram /> */} {dataCategories &&( <CategoryPilihan categories={dataCategories} /> )} - <CategoryDynamic /> + <CategoryDynamic /> <CategoryHomeId /> <BannerSection /> <CustomerReviews /> @@ -183,7 +183,7 @@ export default function Home({categoryId}) { </> )} <DelayRender renderAfter={600}> - <PromotinProgram /> + {/* <PromotinProgram /> */} </DelayRender> <DelayRender renderAfter={600}> {dataCategories &&( diff --git a/src/pages/login.jsx b/src/pages/login.jsx index 9a1aa85b..07d13784 100644 --- a/src/pages/login.jsx +++ b/src/pages/login.jsx @@ -1,3 +1,5 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; import Seo from '@/core/components/Seo'; import SimpleFooter from '@/core/components/elements/Footer/SimpleFooter'; import BasicLayout from '@/core/components/layouts/BasicLayout'; @@ -5,8 +7,18 @@ import DesktopView from '@/core/components/views/DesktopView'; import MobileView from '@/core/components/views/MobileView'; import LoginComponent from '@/lib/auth/components/Login'; import AccountActivation from '~/modules/account-activation'; +import useAuth from '@/core/hooks/useAuth'; export default function Login() { + const router = useRouter(); + const auth = useAuth(); + + useEffect(() => { + if (auth) { + router.push('/'); + } + }, [auth, router]); + return ( <> <Seo title='Login - Indoteknik.com' /> diff --git a/src/pages/my/address/[id]/edit.jsx b/src/pages/my/address/[id]/edit.jsx index bd680b90..c552659b 100644 --- a/src/pages/my/address/[id]/edit.jsx +++ b/src/pages/my/address/[id]/edit.jsx @@ -1,11 +1,11 @@ -import Seo from '@/core/components/Seo' -import AppLayout from '@/core/components/layouts/AppLayout' -import BasicLayout from '@/core/components/layouts/BasicLayout' -import DesktopView from '@/core/components/views/DesktopView' -import MobileView from '@/core/components/views/MobileView' -import addressApi from '@/lib/address/api/addressApi' -import EditAddressComponent from '@/lib/address/components/EditAddress' -import IsAuth from '@/lib/auth/components/IsAuth' +import Seo from '@/core/components/Seo'; +import AppLayout from '@/core/components/layouts/AppLayout'; +import BasicLayout from '@/core/components/layouts/BasicLayout'; +import DesktopView from '@/core/components/views/DesktopView'; +import MobileView from '@/core/components/views/MobileView'; +import addressApi from '@/lib/address/api/addressApi'; +import EditAddressComponent from '@/lib/address/components/EditAddress'; +import IsAuth from '@/lib/auth/components/IsAuth'; export default function EditAddress({ id, defaultValues }) { return ( @@ -24,12 +24,12 @@ export default function EditAddress({ id, defaultValues }) { </BasicLayout> </DesktopView> </IsAuth> - ) + ); } export async function getServerSideProps(context) { - const { id } = context.query - const address = await addressApi({ id }) + const { id } = context.query; + const address = await addressApi({ id }); const defaultValues = { type: address.type, name: address.name, @@ -41,7 +41,8 @@ export async function getServerSideProps(context) { oldDistrict: address.district?.id || '', district: '', oldSubDistrict: address.subDistrict?.id || '', - subDistrict: '' - } - return { props: { id, defaultValues } } + subDistrict: '', + business_name: '', + }; + return { props: { id, defaultValues } }; } diff --git a/src/styles/globals.css b/src/styles/globals.css index b3ca85f4..6447284e 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -98,7 +98,7 @@ button { border text-gray_r-12 border-gray_r-7 - !bg-white + bg-white bg-transparent w-full leading-none @@ -117,7 +117,7 @@ button { } .form-msg-danger { - @apply text-danger-600 mt-2 block; + @apply text-danger-600 mt-2 block text-sm; } .btn-yellow, diff --git a/tsconfig.json b/tsconfig.json index 3afcd9ea..96670169 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES5", - "lib": [ - "DOM", - "DOM.Iterable", - "ESNext" - ], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -33,10 +29,10 @@ "**/*.ts", "**/*.tsx", "**/*.jsx", - ".next/types/**/*.ts" -, "src/pages/shop/promo/index.tsx", "src/pages/shop/promo/[slug].jsx", "src/lib/promo/hooks/usePromotionSearch.js" ], - "exclude": [ - "node_modules", - "src" - ] + ".next/types/**/*.ts", + "src-migrate/**/*", + "src/pages/shop/promo/index.tsx", + "src/pages/shop/promo/[slug].jsx" + ], + "exclude": ["node_modules", "src"] } |
