summaryrefslogtreecommitdiff
path: root/src2/pages
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2023-03-01 09:18:52 +0000
committerIT Fixcomart <it@fixcomart.co.id>2023-03-01 09:18:52 +0000
commita7abbf4ddc70068620e9f44b74dc162ce2e16ee2 (patch)
tree74f66253717515d364ce74bd8275015c1f829cbc /src2/pages
parent90e1edab9b6a8ccc09a49fed3addbec2cbc4e4c3 (diff)
parenta1b9b647a6c4bda1f5db63879639d44543f9557e (diff)
Merged in refactor (pull request #1)
Refactor
Diffstat (limited to 'src2/pages')
-rw-r--r--src2/pages/404.js27
-rw-r--r--src2/pages/_app.js31
-rw-r--r--src2/pages/_error.js11
-rw-r--r--src2/pages/activate.js111
-rw-r--r--src2/pages/api/activation-request.js31
-rw-r--r--src2/pages/api/activation.js16
-rw-r--r--src2/pages/api/login.js15
-rw-r--r--src2/pages/api/register.js15
-rw-r--r--src2/pages/api/shop/search.js96
-rw-r--r--src2/pages/api/shop/suggest.js12
-rw-r--r--src2/pages/api/token.js10
-rw-r--r--src2/pages/faqs.js91
-rw-r--r--src2/pages/index.js106
-rw-r--r--src2/pages/login.js97
-rw-r--r--src2/pages/logout.js14
-rw-r--r--src2/pages/my/address/[id]/edit.js249
-rw-r--r--src2/pages/my/address/create.js234
-rw-r--r--src2/pages/my/address/index.js84
-rw-r--r--src2/pages/my/invoice/[id].js149
-rw-r--r--src2/pages/my/invoices.js180
-rw-r--r--src2/pages/my/menu.js82
-rw-r--r--src2/pages/my/profile.js134
-rw-r--r--src2/pages/my/transaction/[id].js265
-rw-r--r--src2/pages/my/transactions.js198
-rw-r--r--src2/pages/my/wishlist.js60
-rw-r--r--src2/pages/register.js100
-rw-r--r--src2/pages/shop/brands/[slug].js178
-rw-r--r--src2/pages/shop/brands/index.js79
-rw-r--r--src2/pages/shop/cart.js282
-rw-r--r--src2/pages/shop/checkout/finish.js47
-rw-r--r--src2/pages/shop/checkout/index.js325
-rw-r--r--src2/pages/shop/product/[slug].js305
-rw-r--r--src2/pages/shop/quotation/finish.js39
-rw-r--r--src2/pages/shop/quotation/index.js140
-rw-r--r--src2/pages/shop/search.js125
35 files changed, 3938 insertions, 0 deletions
diff --git a/src2/pages/404.js b/src2/pages/404.js
new file mode 100644
index 00000000..1e1850f2
--- /dev/null
+++ b/src2/pages/404.js
@@ -0,0 +1,27 @@
+import Image from "next/image";
+import Link from "@/components/elements/Link";
+import Header from "@/components/layouts/Header";
+import Layout from "@/components/layouts/Layout";
+import PageNotFoundImage from "../images/page-not-found.png";
+
+export default function PageNotFound() {
+ return (
+ <>
+ <Header title="Halaman Tidak Ditemukan - Indoteknik" />
+ <Layout>
+ <main className="pb-8">
+ <Image src={PageNotFoundImage} alt="Halaman Tidak Ditemukan - Indoteknik" className="w-full" />
+ <p className="mt-3 h1 text-center">Halaman tidak ditemukan</p>
+ <div className="mt-6 flex px-4 gap-x-3">
+ <Link href="/" className="btn-light text-gray_r-12 flex-1">
+ Kembali ke beranda
+ </Link>
+ <a href="https://send.whatsapp.com" className="btn-yellow text-gray_r-12 flex-1 h-fit">
+ Tanya admin
+ </a>
+ </div>
+ </main>
+ </Layout>
+ </>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/_app.js b/src2/pages/_app.js
new file mode 100644
index 00000000..6a40f4e6
--- /dev/null
+++ b/src2/pages/_app.js
@@ -0,0 +1,31 @@
+import '../styles/globals.css';
+import NextProgress from 'next-progress';
+import { useRouter } from 'next/router';
+import { AnimatePresence } from 'framer-motion';
+import { Toaster } from "react-hot-toast";
+
+function MyApp({ Component, pageProps }) {
+ const router = useRouter();
+
+ return (
+ <>
+ <Toaster
+ position="top-center"
+ toastOptions={{
+ duration: 3000,
+ className: 'border border-gray_r-8'
+ }}
+ />
+ <NextProgress color="#F01C21" options={{ showSpinner: false }} />
+ <AnimatePresence
+ mode='wait'
+ initial={false}
+ onExitComplete={() => window.scrollTo(0, 0)}
+ >
+ <Component {...pageProps} key={router.asPath} />
+ </AnimatePresence>
+ </>
+ )
+}
+
+export default MyApp
diff --git a/src2/pages/_error.js b/src2/pages/_error.js
new file mode 100644
index 00000000..107ddf46
--- /dev/null
+++ b/src2/pages/_error.js
@@ -0,0 +1,11 @@
+import Header from "@/components/layouts/Header";
+import Layout from "@/components/layouts/Layout";
+
+export default function Error() {
+ return (
+ <Layout>
+ <Header title="Kesalahan Internal"/>
+
+ </Layout>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/activate.js b/src2/pages/activate.js
new file mode 100644
index 00000000..d9b41bf4
--- /dev/null
+++ b/src2/pages/activate.js
@@ -0,0 +1,111 @@
+import axios from "axios";
+import Head from "next/head";
+import Image from "next/image";
+import Link from "@/components/elements/Link";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+import Alert from "@/components/elements/Alert";
+import Layout from "@/components/layouts/Layout";
+import Spinner from "@/components/elements/Spinner";
+import { setAuth } from "@/core/utils/auth";
+import Logo from "@/images/logo.png";
+
+export default function Activate() {
+ const [email, setEmail] = useState('');
+ const [isInputFulfilled, setIsInputFulfilled] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [alert, setAlert] = useState();
+ const router = useRouter();
+ const { token } = router.query;
+
+ useEffect(() => {
+ if (router.query.email) setEmail(router.query.email);
+ }, [router])
+
+ useEffect(() => {
+ const activateIfTokenExist = async () => {
+ if (token) {
+ let activation = await axios.post(`${process.env.SELF_HOST}/api/activation`, {token});
+ if (activation.data.activation) {
+ setAuth(activation.data.user);
+ setAlert({
+ component: <>Selamat, akun anda berhasil diaktifkan, <Link className="text-gray_r-12" href="/">kembali ke beranda</Link>.</>,
+ type: 'success'
+ });
+ } else {
+ setAlert({
+ component: <>Mohon maaf token sudah tidak aktif, lakukan permintaan aktivasi akun kembali atau <Link className="text-gray_r-12" href="/login">masuk</Link> jika sudah memiliki akun.</>,
+ type: 'info'
+ });
+ }
+ }
+ }
+ activateIfTokenExist();
+ }, [token]);
+
+ useEffect(() => {
+ setIsInputFulfilled(email != '');
+ }, [email]);
+
+ const activationRequest = async (e) => {
+ e.preventDefault();
+ setIsLoading(true);
+ let activationRequest = await axios.post(`${process.env.SELF_HOST}/api/activation-request`, {email});
+ if (activationRequest.data.activation_request) {
+ setAlert({
+ component: <>Mohon cek email anda untuk aktivasi akun Indoteknik</>,
+ type: 'success'
+ });
+ } else {
+ switch (activationRequest.data.reason) {
+ case 'NOT_FOUND':
+ setAlert({
+ component: <>Email tersebut belum terdaftar, <Link className="text-gray_r-12" href="/register">daftar sekarang</Link>.</>,
+ type: 'info'
+ });
+ break;
+ case 'ACTIVE':
+ setAlert({
+ component: <>Email tersebut sudah terdaftar dan sudah aktif, <Link className="text-gray_r-12" href="/login">masuk sekarang</Link>.</>,
+ type: 'info'
+ });
+ break;
+ }
+ }
+ setIsLoading(false);
+ }
+ return (
+ <>
+ <Head>
+ <title>Aktivasi Akun Indoteknik</title>
+ </Head>
+ <Layout className="max-w-lg mx-auto flex flex-col items-center px-4 pb-8">
+ <Link href="/" className="mt-16">
+ <Image src={Logo} alt="Logo Indoteknik" width={165} height={42} />
+ </Link>
+ <h1 className="text-2xl text-gray_r-12 mt-4 text-center">Aktivasi Akun Indoteknik Anda</h1>
+ <h2 className="text-gray-800 mt-2 mb-4 text-center">Link aktivasi akan dikirimkan melalui email</h2>
+ {alert ? (
+ <Alert className="text-center" type={alert.type}>{alert.component}</Alert>
+ ) : ''}
+ <form onSubmit={activationRequest} className="w-full">
+ <input
+ type="text"
+ className="form-input bg-gray-100 mt-4 focus:ring-1 focus:ring-yellow-900"
+ placeholder="johndoe@gmail.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ autoFocus
+ />
+ <button type="submit" disabled={!isInputFulfilled} className="btn-yellow font-semibold mt-4 w-full">
+ {isLoading ? (
+ <div className="flex justify-center items-center gap-x-2">
+ <Spinner className="w-4 h-4 text-gray-600 fill-gray-900" /> <span>Loading...</span>
+ </div>
+ ) : 'Kirim Email'}
+ </button>
+ </form>
+ </Layout>
+ </>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/api/activation-request.js b/src2/pages/api/activation-request.js
new file mode 100644
index 00000000..3f33875c
--- /dev/null
+++ b/src2/pages/api/activation-request.js
@@ -0,0 +1,31 @@
+import apiOdoo from "@/core/utils/apiOdoo";
+import mailer from "@/core/utils/mailer";
+
+export default async function handler(req, res) {
+ try {
+ const { email } = req.body;
+ let result = await apiOdoo(
+ 'POST',
+ '/api/v1/user/activation-request',
+ {email}
+ );
+ if (result.activation_request) {
+ mailer.sendMail({
+ from: 'sales@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.SELF_HOST}/activate?token=${result.token}">Aktivasi Akun</a></p>
+ `
+ });
+ }
+ delete result.user;
+ delete result.token;
+ res.status(200).json(result);
+ } catch (error) {
+ console.log(error);
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/activation.js b/src2/pages/api/activation.js
new file mode 100644
index 00000000..8b22af8d
--- /dev/null
+++ b/src2/pages/api/activation.js
@@ -0,0 +1,16 @@
+import apiOdoo from "@/core/utils/apiOdoo";
+
+export default async function handler(req, res) {
+ try {
+ const { token } = req.body;
+ let result = await apiOdoo(
+ 'POST',
+ '/api/v1/user/activation',
+ {token}
+ );
+ res.status(200).json(result);
+ } catch (error) {
+ console.log(error);
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/login.js b/src2/pages/api/login.js
new file mode 100644
index 00000000..e02a73cb
--- /dev/null
+++ b/src2/pages/api/login.js
@@ -0,0 +1,15 @@
+import apiOdoo from "@/core/utils/apiOdoo";
+
+export default async function handler(req, res) {
+ try {
+ const { email, password } = req.body;
+ let result = await apiOdoo(
+ 'POST',
+ '/api/v1/user/login',
+ {email, password}
+ );
+ res.status(200).json(result);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/register.js b/src2/pages/api/register.js
new file mode 100644
index 00000000..7c8d8b39
--- /dev/null
+++ b/src2/pages/api/register.js
@@ -0,0 +1,15 @@
+import apiOdoo from "@/core/utils/apiOdoo";
+
+export default async function handler(req, res) {
+ try {
+ const { email, name, password } = req.body;
+ let result = await apiOdoo(
+ 'POST',
+ '/api/v1/user/register',
+ {email, name, password}
+ );
+ res.status(200).json(result);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/shop/search.js b/src2/pages/api/shop/search.js
new file mode 100644
index 00000000..ad986c86
--- /dev/null
+++ b/src2/pages/api/shop/search.js
@@ -0,0 +1,96 @@
+import axios from "axios";
+
+const productResponseMap = (products) => {
+ return products.map((product) => {
+ let productMapped = {
+ id: product.product_id ? product.product_id[0] : '',
+ image: product.image ? product.image[0] : '',
+ code: product.default_code ? product.default_code[0] : '',
+ name: product.product_name ? product.product_name[0] : '',
+ lowest_price: {
+ price: product.price ? product.price[0] : 0,
+ price_discount: product.price_discount ? product.price_discount[0] : 0,
+ discount_percentage: product.discount ? product.discount[0] : 0,
+ },
+ variant_total: product.variant_total ? product.variant_total[0] : 0,
+ stock_total: product.stock_total ? product.stock_total[0] : 0,
+ weight: product.weight ? product.weight[0] : 0,
+ manufacture: {},
+ categories: [],
+ };
+
+ if (product.manufacture_id && product.brand) {
+ productMapped.manufacture = {
+ id: product.manufacture_id ? product.manufacture_id[0] : '',
+ name: product.brand ? product.brand[0] : '',
+ };
+ }
+
+ productMapped.categories = [
+ {
+ id: product.category_id ? product.category_id[0] : '',
+ name: product.category_name ? product.category_name[0] : '',
+ }
+ ];
+
+ return productMapped;
+ });
+}
+
+export default async function handler(req, res) {
+ const {
+ q,
+ page = 1,
+ brand = '',
+ category = '',
+ price_from = 0,
+ price_to = 0,
+ order_by = ''
+ } = req.query;
+
+ let paramOrderBy = '';
+ switch (order_by) {
+ case 'price-asc':
+ paramOrderBy = ', price_discount ASC';
+ break;
+ case 'price-desc':
+ paramOrderBy = ', price_discount DESC';
+ break;
+ case 'popular':
+ paramOrderBy = ', search_rank DESC';
+ break;
+ case 'stock':
+ paramOrderBy = ', stock_total DESC';
+ break;
+ }
+
+ let limit = 30;
+ let offset = (page - 1) * limit;
+ let parameter = [
+ `facet.query=${q}`,
+ 'facet=true',
+ 'indent=true',
+ 'q.op=AND',
+ `q=${q}`,
+ 'facet.field=brand_str',
+ 'facet.field=category_name_str',
+ `start=${offset}`,
+ `rows=${limit}`,
+ `sort=product_rating DESC ${paramOrderBy}`,
+ `fq=price_discount:[${price_from == '' ? '*' : price_from} TO ${price_to == '' ? '*' : price_to}]`
+ ];
+
+ if (brand) parameter.push(`fq=brand:${brand}`);
+ if (category) parameter.push(`fq=category_name:${category}`);
+
+ let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'));
+ try {
+ result.data.response.products = productResponseMap(result.data.response.docs);
+ result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start);
+ result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows);
+ delete result.data.response.docs;
+ res.status(200).json(result.data);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/shop/suggest.js b/src2/pages/api/shop/suggest.js
new file mode 100644
index 00000000..6db1a851
--- /dev/null
+++ b/src2/pages/api/shop/suggest.js
@@ -0,0 +1,12 @@
+import axios from "axios";
+
+export default async function handler(req, res) {
+ const { q } = req.query;
+
+ let result = await axios(process.env.SOLR_HOST + `/solr/products/suggest?suggest=true&suggest.dictionary=mySuggester&suggest.q=${q}`);
+ try {
+ res.status(200).json(result.data);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/token.js b/src2/pages/api/token.js
new file mode 100644
index 00000000..ec048158
--- /dev/null
+++ b/src2/pages/api/token.js
@@ -0,0 +1,10 @@
+import axios from "axios";
+
+export default async function handler(req, res) {
+ try {
+ let result = await axios.get(process.env.ODOO_HOST + '/api/token');
+ res.status(200).json(result.data.result);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/faqs.js b/src2/pages/faqs.js
new file mode 100644
index 00000000..cdb8ef52
--- /dev/null
+++ b/src2/pages/faqs.js
@@ -0,0 +1,91 @@
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
+import { useEffect, useState } from "react";
+
+const dataFaqs = [
+ {
+ id: 1,
+ name: 'Akun',
+ description: 'Bantuan tentang pengelolaan fitur dan akun'
+ },
+ {
+ id: 2,
+ name: 'Pembelian',
+ description: 'Bantuan seputar status stock, layanan pengiriman & asuransi hingga seluruh indonesia'
+ },
+ {
+ id: 3,
+ name: 'Metode Pembayaran',
+ description: 'Bantuan terkait layanan metode pembayaran'
+ },
+ {
+ id: 4,
+ name: 'Quotation',
+ description: 'Bantuan fitur RFQ & quotation Express'
+ },
+ {
+ id: 5,
+ name: 'Faktur Pajak & Invoice',
+ description: 'Bantuan seputar layanan terbit faktur pajak & invoice'
+ },
+ {
+ id: 6,
+ name: 'Pengembalian & Garansi',
+ description: 'Bantuan cara pengembalian produk & garansi produk'
+ }
+];
+
+export default function Faqs() {
+ const [ faqs, setFaqs ] = useState([]);
+
+ useEffect(() => {
+ if (faqs.length == 0) {
+ setFaqs(dataFaqs.map((dataFaq) => ({
+ ...dataFaq,
+ isOpen: false
+ })));
+ }
+ }, [ faqs ]);
+
+ const toggleFaq = (id) => {
+ const faqsToUpdate = faqs.map(faq => {
+ if (faq.id == id) faq.isOpen = !faq.isOpen;
+ return faq;
+ });
+ setFaqs(faqsToUpdate);
+ };
+
+ return (
+ <Layout>
+ <AppBar title="FAQ's" />
+
+ <div className="divide-y divide-gray_r-6">
+ { faqs.map((faq, index) => (
+ <div className="p-4" key={index}>
+ <div className="flex gap-x-3 items-center">
+ <div className="flex-1">
+ <p className="font-medium mb-1">{ faq.name }</p>
+ <p className="text-caption-1 text-gray_r-11">
+ { faq.description }
+ </p>
+ </div>
+ <button type="button" className="p-2 rounded bg-gray_r-4 h-fit" onClick={() => toggleFaq(faq.id)}>
+ { faq.isOpen ? (
+ <ChevronUpIcon className="w-5"/>
+ ) : (
+ <ChevronDownIcon className="w-5"/>
+ ) }
+ </button>
+ </div>
+ { faq.isOpen && (
+ <p className="text-caption-1 text-gray_r-11 leading-7 mt-4">
+ { faq?.content || 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.' }
+ </p>
+ ) }
+ </div>
+ )) }
+ </div>
+ </Layout>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/index.js b/src2/pages/index.js
new file mode 100644
index 00000000..65999ff6
--- /dev/null
+++ b/src2/pages/index.js
@@ -0,0 +1,106 @@
+import { useEffect, useState } from "react";
+import { Pagination, Autoplay } from "swiper";
+import axios from "axios";
+import { Swiper, SwiperSlide } from "swiper/react";
+import "swiper/css";
+import "swiper/css/pagination";
+import "swiper/css/autoplay";
+
+// Helpers
+import apiOdoo from "@/core/utils/apiOdoo";
+
+// Components
+import Header from "@/components/layouts/Header";
+import ProductSlider from "@/components/products/ProductSlider";
+import Layout from "@/components/layouts/Layout";
+import ManufactureCard from "@/components/manufactures/ManufactureCard";
+import Footer from "@/components/layouts/Footer";
+import Image from "@/components/elements/Image";
+import ProductCategories from "@/components/products/ProductCategories";
+
+const swiperBanner = {
+ pagination: { dynamicBullets: true },
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ modules: [Pagination, Autoplay]
+}
+
+export async function getServerSideProps() {
+ const heroBanners = await apiOdoo('GET', `/api/v1/banner?type=index-a-1`);
+
+ return { props: { heroBanners } };
+}
+
+export default function Home({ heroBanners }) {
+ const [manufactures, setManufactures] = useState(null);
+ const [popularProducts, setPopularProducts] = useState(null);
+
+ useEffect(() => {
+ const getManufactures = async () => {
+ const dataManufactures = await apiOdoo('GET', `/api/v1/manufacture?level=prioritas`);
+ setManufactures(dataManufactures);
+ }
+ getManufactures();
+
+ const getPopularProducts = async () => {
+ const dataPopularProducts = await axios(`${process.env.SELF_HOST}/api/shop/search?q=*&page=1&order_by=popular`);
+ setPopularProducts(dataPopularProducts.data.response);
+ }
+ getPopularProducts();
+ }, []);
+
+ return (
+ <>
+ <Header title='Home - Indoteknik' />
+ <Layout>
+ <Swiper
+ slidesPerView={1}
+ pagination={swiperBanner.pagination}
+ modules={swiperBanner.modules}
+ autoplay={swiperBanner.autoplay}
+ >
+ {
+ heroBanners?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ className="w-full h-auto"
+ />
+ </SwiperSlide>
+ ))
+ }
+ </Swiper>
+ <div className="mt-6 px-4">
+ <h2 className="mb-3">Brand Pilihan</h2>
+ <Swiper slidesPerView={4} freeMode={true} spaceBetween={16}>
+ {
+ manufactures?.manufactures?.map((manufacture, index) => (
+ <SwiperSlide key={index}>
+ <ManufactureCard data={manufacture} key={index} />
+ </SwiperSlide>
+ ))
+ }
+ </Swiper>
+ </div>
+ <div className="my-6 p-4 py-0">
+ <h2 className="mb-4">Produk Populer</h2>
+ <ProductSlider products={popularProducts} simpleProductTitleLine />
+ </div>
+
+ <ProductCategories />
+
+ <div className="px-4">
+ <h5 className="h2 mb-2">Platform Belanja B2B Alat Teknik & Industri di Indonesia</h5>
+ <p className="text-gray_r-11 leading-6 text-caption-2 mb-4">
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+ </p>
+ </div>
+
+ <Footer />
+ </Layout>
+ </>
+ )
+}
diff --git a/src2/pages/login.js b/src2/pages/login.js
new file mode 100644
index 00000000..e80de44e
--- /dev/null
+++ b/src2/pages/login.js
@@ -0,0 +1,97 @@
+import axios from "axios";
+import Head from "next/head";
+import Image from "next/image";
+import Link from "@/components/elements/Link";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+import Alert from "@/components/elements/Alert";
+import Layout from "@/components/layouts/Layout";
+import Spinner from "@/components/elements/Spinner";
+import { setAuth } from "@/core/utils/auth";
+import Logo from "@/images/logo.png";
+
+export default function Login() {
+ const router = useRouter();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isInputFulfilled, setIsInputFulfilled] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [alert, setAlert] = useState();
+
+ useEffect(() => {
+ setIsInputFulfilled(email && password);
+ }, [email, password]);
+
+ const login = async (e) => {
+ e.preventDefault();
+ setIsLoading(true);
+ let login = await axios.post(`${process.env.SELF_HOST}/api/login`, {email, password});
+ if (login.data.is_auth) {
+ setAuth(login.data.user);
+ router.push('/');
+ } else {
+ switch (login.data.reason) {
+ case 'NOT_FOUND':
+ setAlert({
+ component: <>Email atau password tidak cocok</>,
+ type: 'info'
+ });
+ break;
+ case 'NOT_ACTIVE':
+ setAlert({
+ component: <>Email belum diaktivasi, <Link className="text-gray-900" href={`/activate?email=${email}`}>aktivasi sekarang</Link></>,
+ type: 'info'
+ });
+ break;
+ }
+ setIsLoading(false);
+ }
+ }
+
+ return (
+ <>
+ <Head>
+ <title>Masuk - Indoteknik</title>
+ </Head>
+ <Layout className="max-w-lg mx-auto flex flex-col items-center px-4 pb-8">
+ <Link href="/" className="mt-16">
+ <Image src={Logo} alt="Logo Indoteknik" width={165} height={42} />
+ </Link>
+ <h1 className="text-2xl mt-4 text-center">Mulai Belanja Sekarang</h1>
+ <h2 className="text-gray_r-11 font-normal mt-2 mb-4">Masuk ke akun kamu untuk belanja</h2>
+ {alert ? (
+ <Alert className="text-center" type={alert.type}>{alert.component}</Alert>
+ ) : ''}
+ <form onSubmit={login} className="w-full">
+ <label className="form-label mt-4 mb-2">Alamat Email</label>
+ <input
+ type="email"
+ className="form-input bg-gray_r-2"
+ placeholder="johndoe@gmail.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ />
+ <label className="form-label mt-4 mb-2">Kata Sandi</label>
+ <input
+ type="password"
+ className="form-input bg-gray_r-2"
+ placeholder="••••••••"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ />
+ <div className="flex justify-end mt-4 w-full">
+ <Link href="/forgot-password">Lupa kata sandi</Link>
+ </div>
+ <button type="submit" disabled={!isInputFulfilled} className="btn-yellow font-semibold mt-4 w-full">
+ {isLoading ? (
+ <div className="flex justify-center items-center gap-x-2">
+ <Spinner className="w-4 h-4 text-gray-600 fill-gray-900" /> <span>Loading...</span>
+ </div>
+ ) : 'Masuk'}
+ </button>
+ </form>
+ <p className="text-gray-700 mt-4">Belum punya akun Indoteknik? <Link href="/register">Daftar</Link></p>
+ </Layout>
+ </>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/logout.js b/src2/pages/logout.js
new file mode 100644
index 00000000..8ea21fab
--- /dev/null
+++ b/src2/pages/logout.js
@@ -0,0 +1,14 @@
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+import { deleteAuth } from "@/core/utils/auth";
+
+export default function Logout() {
+ const router = useRouter();
+
+ useEffect(() => {
+ deleteAuth();
+ router.replace('/login');
+ }, [router]);
+
+ return null;
+} \ No newline at end of file
diff --git a/src2/pages/my/address/[id]/edit.js b/src2/pages/my/address/[id]/edit.js
new file mode 100644
index 00000000..838d39e7
--- /dev/null
+++ b/src2/pages/my/address/[id]/edit.js
@@ -0,0 +1,249 @@
+import { Controller, useForm } from "react-hook-form"
+import WithAuth from "@/components/auth/WithAuth";
+import Layout from "@/components/layouts/Layout";
+import AppBar from "@/components/layouts/AppBar";
+import { yupResolver } from "@hookform/resolvers/yup";
+import * as Yup from "yup";
+import { Select } from "@/components/elements/Fields";
+import { useEffect, useState } from "react";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { toast } from "react-hot-toast";
+import { useRouter } from "next/router";
+
+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 johndoe@example.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'),
+});
+
+const types = [
+ { value: 'contact', label: 'Contact Address' },
+ { value: 'invoice', label: 'Invoice Address' },
+ { value: 'delivery', label: 'Delivery Address' },
+ { value: 'other', label: 'Other Address' },
+];
+
+export async function getServerSideProps( context ) {
+ const { id } = context.query;
+ const address = await apiOdoo('GET', `/api/v1/partner/${id}/address`);
+ let defaultValues = {
+ type: address.type,
+ name: address.name,
+ email: address.email,
+ mobile: address.mobile,
+ street: address.street,
+ zip: address.zip,
+ city: address.city?.id || '',
+ oldDistrict: address.district?.id || '',
+ district: '',
+ oldSubDistrict: address.sub_district?.id || '',
+ subDistrict: '',
+ };
+ return { props: { id, defaultValues } };
+}
+
+export default function EditAddress({ id, defaultValues }) {
+ const router = useRouter();
+ const {
+ register,
+ formState: { errors },
+ handleSubmit,
+ watch,
+ setValue,
+ getValues,
+ control,
+ } = useForm({
+ resolver: yupResolver(validationSchema),
+ defaultValues
+ });
+
+ const [ cities, setCities ] = useState([]);
+ const [ districts, setDistricts ] = useState([]);
+ const [ subDistricts, setSubDistricts ] = useState([]);
+
+ useEffect(() => {
+ const loadCities = async () => {
+ let dataCities = await apiOdoo('GET', '/api/v1/city');
+ dataCities = dataCities.map((city) => ({ value: city.id, label: city.name }));
+ setCities(dataCities);
+ };
+ loadCities();
+ }, []);
+
+ const watchCity = watch('city');
+ useEffect(() => {
+ setValue('district', '');
+ if (watchCity) {
+ const loadDistricts = async () => {
+ let dataDistricts = await apiOdoo('GET', `/api/v1/district?city_id=${watchCity}`);
+ dataDistricts = dataDistricts.map((district) => ({ value: district.id, label: district.name }));
+ setDistricts(dataDistricts);
+ let oldDistrict = getValues('oldDistrict');
+ if (oldDistrict) {
+ setValue('district', oldDistrict);
+ setValue('oldDistrict', '');
+ }
+ };
+ loadDistricts();
+ }
+ }, [ watchCity, setValue, getValues ]);
+
+ const watchDistrict = watch('district');
+ useEffect(() => {
+ setValue('subDistrict', '');
+ if (watchDistrict) {
+ const loadSubDistricts = async () => {
+ let dataSubDistricts = await apiOdoo('GET', `/api/v1/sub_district?district_id=${watchDistrict}`);
+ dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, label: district.name }));
+ setSubDistricts(dataSubDistricts);
+ let oldSubDistrict = getValues('oldSubDistrict');
+ if (oldSubDistrict) {
+ setValue('subDistrict', oldSubDistrict);
+ setValue('oldSubDistrict', '');
+ }
+ };
+ loadSubDistricts();
+ }
+ }, [ watchDistrict, setValue, getValues ])
+
+ const onSubmitHandler = async (values) => {
+ const parameters = {
+ ...values,
+ city_id: values.city,
+ district_id: values.district,
+ sub_district_id: values.subDistrict
+ }
+
+ const address = await apiOdoo('PUT', `/api/v1/partner/${id}/address`, parameters);
+ if (address?.id) {
+ toast.success('Berhasil mengubah alamat');
+ router.back();
+ }
+ };
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Ubah Alamat" />
+
+ <form className="p-4 flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmitHandler)}>
+ <div>
+ <label className="form-label mb-2">Label Alamat</label>
+ <Controller
+ name="type"
+ control={control}
+ render={props => <Select {...props} isSearchable={false} options={types} />}
+ />
+ <div className="text-caption-2 text-red_r-11 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-red_r-11 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-red_r-11 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-red_r-11 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-red_r-11 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-red_r-11 mt-1">{ errors.zip?.message }</div>
+ </div>
+
+ <div>
+ <label className="form-label mb-2">Kota</label>
+ <Controller
+ name="city"
+ control={control}
+ render={props => <Select {...props} options={cities} />}
+ />
+ <div className="text-caption-2 text-red_r-11 mt-1">{ errors.city?.message }</div>
+ </div>
+
+ <div>
+ <label className="form-label mb-2">Kecamatan</label>
+ <Controller
+ name="district"
+ control={control}
+ render={props => (
+ <Select
+ {...props}
+ options={districts}
+ disabled={!watchCity}
+ />
+ )}
+ />
+ </div>
+
+ <div>
+ <label className="form-label mb-2">Kelurahan</label>
+ <Controller
+ name="subDistrict"
+ control={control}
+ render={props => (
+ <Select
+ {...props}
+ options={subDistricts}
+ disabled={!watchDistrict}
+ />
+ )}
+ />
+ </div>
+
+ <button
+ type="submit"
+ className="btn-yellow mt-2 w-full"
+ >
+ Simpan
+ </button>
+ </form>
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/my/address/create.js b/src2/pages/my/address/create.js
new file mode 100644
index 00000000..42cd117c
--- /dev/null
+++ b/src2/pages/my/address/create.js
@@ -0,0 +1,234 @@
+import { Controller, useForm } from "react-hook-form"
+import WithAuth from "@/components/auth/WithAuth";
+import Layout from "@/components/layouts/Layout";
+import AppBar from "@/components/layouts/AppBar";
+import { yupResolver } from "@hookform/resolvers/yup";
+import * as Yup from "yup";
+import { Select } from "@/components/elements/Fields";
+import { useEffect, useState } from "react";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useAuth } from "@/core/utils/auth";
+import { toast } from "react-hot-toast";
+import { useRouter } from "next/router";
+
+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 johndoe@example.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'),
+});
+
+const defaultValues = {
+ type: '',
+ name: '',
+ email: '',
+ mobile: '',
+ street: '',
+ city: '',
+ district: '',
+ subDistrict: '',
+ zip: '',
+};
+
+const types = [
+ { value: 'contact', label: 'Contact Address' },
+ { value: 'invoice', label: 'Invoice Address' },
+ { value: 'delivery', label: 'Delivery Address' },
+ { value: 'other', label: 'Other Address' },
+];
+
+export default function CreateAddress() {
+ const [ auth ] = useAuth();
+ const router = useRouter();
+ const {
+ register,
+ formState: { errors },
+ handleSubmit,
+ watch,
+ setValue,
+ control,
+ } = useForm({
+ resolver: yupResolver(validationSchema),
+ defaultValues
+ });
+
+ const [ cities, setCities ] = useState([]);
+ const [ districts, setDistricts ] = useState([]);
+ const [ subDistricts, setSubDistricts ] = useState([]);
+
+ useEffect(() => {
+ const loadCities = async () => {
+ let dataCities = await apiOdoo('GET', '/api/v1/city');
+ dataCities = dataCities.map((city) => ({ value: city.id, label: city.name }));
+ setCities(dataCities);
+ };
+ loadCities();
+ }, []);
+
+ const watchCity = watch('city');
+ useEffect(() => {
+ setValue('district', '');
+ if (watchCity) {
+ const loadDistricts = async () => {
+ let dataDistricts = await apiOdoo('GET', `/api/v1/district?city_id=${watchCity}`);
+ dataDistricts = dataDistricts.map((district) => ({ value: district.id, label: district.name }));
+ setDistricts(dataDistricts);
+ };
+ loadDistricts();
+ }
+ }, [ watchCity, setValue ]);
+
+ const watchDistrict = watch('district');
+ useEffect(() => {
+ setValue('subDistrict', '');
+ if (watchDistrict) {
+ const loadSubDistricts = async () => {
+ let dataSubDistricts = await apiOdoo('GET', `/api/v1/sub_district?district_id=${watchDistrict}`);
+ dataSubDistricts = dataSubDistricts.map((district) => ({ value: district.id, label: district.name }));
+ setSubDistricts(dataSubDistricts);
+ };
+ loadSubDistricts();
+ }
+ }, [ watchDistrict, setValue ])
+
+ const onSubmitHandler = async (values) => {
+ const parameters = {
+ ...values,
+ city_id: values.city,
+ district_id: values.district,
+ sub_district_id: values.subDistrict,
+ parent_id: auth.partner_id
+ };
+
+ const address = await apiOdoo('POST', '/api/v1/partner/address', parameters);
+ if (address?.id) {
+ toast.success('Berhasil menambahkan alamat');
+ router.back();
+ }
+ };
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Tambah Alamat" />
+
+ <form className="p-4 flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmitHandler)}>
+ <div>
+ <label className="form-label mb-2">Label Alamat</label>
+ <Controller
+ name="type"
+ control={control}
+ render={props => <Select {...props} isSearchable={false} options={types} />}
+ />
+ <div className="text-caption-2 text-red_r-11 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-red_r-11 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-red_r-11 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-red_r-11 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-red_r-11 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-red_r-11 mt-1">{ errors.zip?.message }</div>
+ </div>
+
+ <div>
+ <label className="form-label mb-2">Kota</label>
+ <Controller
+ name="city"
+ control={control}
+ render={props => <Select {...props} options={cities} />}
+ />
+ <div className="text-caption-2 text-red_r-11 mt-1">{ errors.city?.message }</div>
+ </div>
+
+ <div>
+ <label className="form-label mb-2">Kecamatan</label>
+ <Controller
+ name="district"
+ control={control}
+ render={props => (
+ <Select
+ {...props}
+ options={districts}
+ disabled={!watchCity}
+ />
+ )}
+ />
+ </div>
+
+ <div>
+ <label className="form-label mb-2">Kelurahan</label>
+ <Controller
+ name="subDistrict"
+ control={control}
+ render={props => (
+ <Select
+ {...props}
+ options={subDistricts}
+ disabled={!watchDistrict}
+ />
+ )}
+ />
+ </div>
+
+ <button
+ type="submit"
+ className="btn-yellow mt-2 w-full"
+ >
+ Simpan
+ </button>
+ </form>
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/my/address/index.js b/src2/pages/my/address/index.js
new file mode 100644
index 00000000..5cad4410
--- /dev/null
+++ b/src2/pages/my/address/index.js
@@ -0,0 +1,84 @@
+import { useEffect, useState } from "react";
+import { useRouter } from "next/router";
+
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import Link from "@/components/elements/Link";
+import WithAuth from "@/components/auth/WithAuth";
+
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useAuth } from "@/core/utils/auth";
+import { createOrUpdateItemAddress, getItemAddress } from "@/core/utils/address";
+import { toast } from "react-hot-toast";
+
+export default function Address() {
+ const router = useRouter();
+ const { select } = router.query;
+ const [ auth ] = useAuth();
+ const [ addresses, setAddresses ] = useState(null);
+ const [ selectedAdress, setSelectedAdress ] = useState(null);
+
+ useEffect(() => {
+ const getAddress = async () => {
+ if (auth) {
+ const dataAddress = await apiOdoo('GET', `/api/v1/user/${auth.id}/address`);
+ setAddresses(dataAddress);
+ }
+ };
+ getAddress();
+ }, [auth]);
+
+ useEffect(() => {
+ if (select) {
+ setSelectedAdress(getItemAddress(select));
+ }
+ }, [select]);
+
+ const changeSelectedAddress = (id) => {
+ if (select) {
+ createOrUpdateItemAddress(select, id);
+ router.back();
+ }
+ };
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Daftar Alamat" />
+
+ <div className="text-right mt-4 px-4">
+ <Link href="/my/address/create">Tambah Alamat</Link>
+ </div>
+
+ <div className="grid gap-y-4 p-4">
+ { auth && addresses && addresses.map((address, index) => {
+ let type = address.type.charAt(0).toUpperCase() + address.type.slice(1) + ' Address';
+ return (
+ <div
+ key={index}
+ className={"p-4 rounded-md border " + (selectedAdress && selectedAdress == address.id ? "bg-gray_r-4" : "border-gray_r-7") }
+ >
+ <div onClick={() => changeSelectedAddress(address.id)}>
+ <div className="flex gap-x-2" >
+ <div className="badge-red">{ type }</div>
+ { auth?.partner_id == 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>
+ ) }
+ <p className={`mt-1 leading-6 ${selectedAdress && selectedAdress == address.id ? "text-gray_r-12" : "text-gray_r-11"}`}>
+ { address.street }
+ </p>
+ </div>
+ <Link href={`/my/address/${address.id}/edit`} className="btn-light bg-white mt-3 w-full text-gray_r-11">Ubah Alamat</Link>
+ </div>
+ );
+ }) }
+ </div>
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/my/invoice/[id].js b/src2/pages/my/invoice/[id].js
new file mode 100644
index 00000000..820c9af8
--- /dev/null
+++ b/src2/pages/my/invoice/[id].js
@@ -0,0 +1,149 @@
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import LineDivider from "@/components/elements/LineDivider";
+import WithAuth from "@/components/auth/WithAuth";
+import { useEffect, useState } from "react";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useRouter } from "next/router";
+import { useAuth } from "@/core/utils/auth";
+import VariantCard from "@/components/variants/VariantCard";
+import currencyFormat from "@/core/utils/currencyFormat";
+import Disclosure from "@/components/elements/Disclosure";
+import DescriptionRow from "@/components/elements/DescriptionRow";
+import { SkeletonList } from "@/components/elements/Skeleton";
+import VariantGroupCard from "@/components/variants/VariantGroupCard";
+
+export default function DetailInvoice() {
+ const router = useRouter();
+ const { id } = router.query;
+ const [ auth ] = useAuth();
+ const [ invoice, setInvoice ] = useState(null);
+
+ useEffect(() => {
+ if (auth && id) {
+ const loadInvoice = async () => {
+ const dataInvoice = await apiOdoo('GET', `/api/v1/partner/${auth?.partner_id}/invoice/${id}`);
+ setInvoice(dataInvoice);
+ }
+ loadInvoice();
+ }
+ }, [ auth, id ]);
+
+ const Customer = () => {
+ const customer = invoice?.customer;
+ const fullAddress = [];
+ if (customer?.street) fullAddress.push(customer.street);
+ if (customer?.sub_district?.name) fullAddress.push(customer.sub_district.name);
+ if (customer?.district?.name) fullAddress.push(customer.district.name);
+ if (customer?.city?.name) fullAddress.push(customer.city.name);
+
+ return (
+ <div className="p-4 pt-0 flex flex-col gap-y-4">
+ <DescriptionRow label="Nama">{ invoice?.customer?.name }</DescriptionRow>
+ <DescriptionRow label="Email">{ invoice?.customer?.email || '-' }</DescriptionRow>
+ <DescriptionRow label="No Telepon">{ invoice?.customer?.mobile || '-' }</DescriptionRow>
+ <DescriptionRow label="Alamat">{ fullAddress.join(', ') }</DescriptionRow>
+ </div>
+ );
+ };
+
+ const downloadTaxInvoice = () => {
+ window.open(`${process.env.ODOO_HOST}/api/v1/download/tax-invoice/${invoice.id}/${invoice.token}`, 'Download')
+ }
+
+ const downloadInvoice = () => {
+ window.open(`${process.env.ODOO_HOST}/api/v1/download/invoice/${invoice.id}/${invoice.token}`, 'Download')
+ }
+
+ return (
+ <WithAuth>
+ <Layout className="pb-4">
+ <AppBar title="Detail Invoice" />
+
+ { invoice ? (
+ <>
+ <div className="p-4 flex flex-col gap-y-4">
+ <DescriptionRow label="No Invoice">
+ { invoice?.name }
+ </DescriptionRow>
+ <DescriptionRow label="Status Transaksi">
+ { invoice?.amount_residual > 0 ? (
+ <span className="badge-solid-red">Belum Lunas</span>
+ ) : (
+ <span className="badge-solid-green">Lunas</span>
+ ) }
+ </DescriptionRow>
+ <DescriptionRow label="Purchase Order">
+ { invoice?.purchase_order_name || '-' }
+ </DescriptionRow>
+ <DescriptionRow label="Ketentuan Pembayaran">
+ { invoice?.payment_term }
+ </DescriptionRow>
+ { invoice?.amount_residual > 0 && invoice.invoice_date != invoice.invoice_date_due && (
+ <DescriptionRow label="Tanggal Jatuh Tempo">
+ { invoice?.invoice_date_due }
+ </DescriptionRow>
+ ) }
+ <DescriptionRow label="Nama Sales">
+ { invoice?.sales }
+ </DescriptionRow>
+ <DescriptionRow label="Tanggal Invoice">
+ { invoice?.invoice_date }
+ </DescriptionRow>
+ <div className="flex items-center">
+ <p className="text-gray_r-11 leading-none">Faktur Pembelian</p>
+ <button
+ type="button"
+ className="btn-light py-1.5 px-3 ml-auto"
+ onClick={downloadInvoice}
+ >
+ Download
+ </button>
+ </div>
+ <div className="flex items-center">
+ <p className="text-gray_r-11 leading-none">Faktur Pajak</p>
+ <button
+ type="button"
+ className="btn-light py-1.5 px-3 ml-auto"
+ onClick={downloadTaxInvoice}
+ disabled={!invoice.efaktur}
+ >
+ Download
+ </button>
+ </div>
+ </div>
+
+ <LineDivider />
+
+ <Disclosure
+ label="Detail Penagihan"
+ />
+
+ <Customer />
+
+ <LineDivider />
+
+ <Disclosure
+ label="Detail Produk"
+ />
+
+ <div className="mt-2 p-4 pt-0 flex flex-col gap-y-3">
+ <VariantGroupCard
+ variants={invoice?.products}
+ buyMore
+ />
+ <div className="flex justify-between mt-3 font-medium">
+ <p className="text-gray_r-11">Total Belanja</p>
+ <p>{ currencyFormat(invoice?.amount_total || 0) }</p>
+ </div>
+ </div>
+ </>
+ ) : (
+ <div className="p-4 py-6">
+ <SkeletonList number={12} />
+ </div>
+ ) }
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/my/invoices.js b/src2/pages/my/invoices.js
new file mode 100644
index 00000000..9b2e77dc
--- /dev/null
+++ b/src2/pages/my/invoices.js
@@ -0,0 +1,180 @@
+import WithAuth from "@/components/auth/WithAuth"
+import Alert from "@/components/elements/Alert"
+import Link from "@/components/elements/Link"
+import Pagination from "@/components/elements/Pagination"
+import AppBar from "@/components/layouts/AppBar"
+import Layout from "@/components/layouts/Layout"
+import apiOdoo from "@/core/utils/apiOdoo"
+import { useAuth } from "@/core/utils/auth"
+import currencyFormat from "@/core/utils/currencyFormat"
+import useBottomPopup from "@/lib/elements/hooks/useBottomPopup"
+import { CheckIcon, ClockIcon, EllipsisVerticalIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"
+import { useRouter } from "next/router"
+import { useEffect, useRef, useState } from "react"
+
+export default function Invoices() {
+ const [ auth ] = useAuth()
+ const router = useRouter()
+ const {
+ q,
+ page = 1
+ } = router.query
+
+ const [ invoices, setInvoices ] = useState([])
+
+ const [ pageCount, setPageCount ] = useState(0)
+ const [ isLoading, setIsLoading ] = useState(true)
+
+ const searchQueryRef = useRef()
+
+ useEffect(() => {
+ const loadInvoices = async () => {
+ if (auth) {
+ const limit = 10
+ let offset = (page - 1) * 10
+ let queryParams = [`limit=${limit}`, `offset=${offset}`]
+ if (q) queryParams.push(`name=${q}`)
+ queryParams = queryParams.join('&')
+ queryParams = queryParams ? '?' + queryParams : ''
+
+ const dataInvoices = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/invoice${queryParams}`)
+ setInvoices(dataInvoices)
+ setPageCount(Math.ceil(dataInvoices.sale_order_total / limit))
+ setIsLoading(false)
+ }
+ }
+ loadInvoices()
+ }, [ auth, q, page ])
+
+ const actionSearch = (e) => {
+ e.preventDefault()
+ let queryParams = []
+ if (searchQueryRef.current.value) queryParams.push(`q=${searchQueryRef.current.value}`)
+ queryParams = queryParams.join('&')
+ queryParams = queryParams ? `?${queryParams}` : ''
+ router.push(`/my/invoices${queryParams}`)
+ }
+
+ const downloadInvoice = (data) => {
+ const url = `${process.env.ODOO_HOST}/api/v1/download/invoice/${data.id}/${data.token}`
+ window.open(url, 'download')
+ closePopup()
+ }
+
+ const downloadTaxInvoice = (data) => {
+ const url = `${process.env.ODOO_HOST}/api/v1/download/tax-invoice/${data.id}/${data.token}`
+ window.open(url, 'download')
+ closePopup()
+ }
+
+ const childrenPopup = (data) => (
+ <div className="flex flex-col gap-y-6">
+ <button
+ className="text-left disabled:opacity-60"
+ onClick={() => downloadInvoice(data)}
+ >
+ Download Faktur Pembelian
+ </button>
+ <button
+ className="text-left disabled:opacity-60"
+ disabled={!data?.efaktur}
+ onClick={() => downloadTaxInvoice(data)}
+ >
+ Download Faktur Pajak
+ </button>
+ </div>
+ )
+
+ const {
+ closePopup,
+ openPopup,
+ BottomPopup
+ } = useBottomPopup({
+ title: 'Lainnya',
+ children: childrenPopup
+ })
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Invoice" />
+
+ <form onSubmit={actionSearch} className="p-4 pb-0 flex gap-x-4">
+ <input
+ type="text"
+ className="form-input"
+ placeholder="Cari Transaksi"
+ ref={searchQueryRef}
+ defaultValue={q}
+ />
+ <button type="submit" className="border border-gray_r-7 rounded px-3">
+ <MagnifyingGlassIcon className="w-5"/>
+ </button>
+ </form>
+
+ <div className="p-4 flex flex-col gap-y-5">
+ { invoices?.invoice_total === 0 && !isLoading && (
+ <Alert type="info" className="text-center">
+ Invoice tidak ditemukan
+ </Alert>
+ ) }
+ { invoices?.invoices?.map((invoice, index) => (
+ <div className="p-4 shadow border border-gray_r-3 rounded-md" key={index}>
+ <div className="grid grid-cols-2">
+ <Link href={`/my/invoice/${invoice.id}`}>
+ <span className="text-caption-2 text-gray_r-11">No. Invoice</span>
+ <h2 className="text-red_r-11 mt-1">{ invoice.name }</h2>
+ </Link>
+ <div className="flex gap-x-1 justify-end">
+ { invoice.amount_residual > 0 ? (
+ <div className="badge-solid-red h-fit ml-auto">Belum Lunas</div>
+ ) : (
+ <div className="badge-solid-green h-fit ml-auto">Lunas</div>
+ ) }
+ <EllipsisVerticalIcon className="w-5 h-5" onClick={() => openPopup(invoice)} />
+ </div>
+ </div>
+ <Link href={`/my/invoice/${invoice.id}`}>
+ <div className="grid grid-cols-2 text-caption-2 text-gray_r-11 mt-2 font-normal">
+ <p>
+ { invoice.invoice_date }
+ </p>
+ <p className="text-right">
+ { invoice.payment_term }
+ </p>
+ </div>
+ <hr className="my-3"/>
+ <div className="grid grid-cols-2">
+ <div>
+ <span className="text-caption-2 text-gray_r-11">No. Purchase Order</span>
+ <p className="mt-1 font-medium text-gray_r-12">{ invoice.purchase_order_name || '-' }</p>
+ </div>
+ <div className="text-right">
+ <span className="text-caption-2 text-gray_r-11">Total Invoice</span>
+ <p className="mt-1 font-medium text-gray_r-12">{ currencyFormat(invoice.amount_total) }</p>
+ </div>
+ </div>
+ </Link>
+ { invoice.efaktur ? (
+ <div className="badge-green h-fit mt-3 ml-auto flex items-center gap-x-0.5">
+ <CheckIcon className="w-4 stroke-2" />
+ Faktur Pajak
+ </div>
+ ) : (
+ <div className="badge-red h-fit mt-3 ml-auto flex items-center gap-x-0.5">
+ <ClockIcon className="w-4 stroke-2" />
+ Faktur Pajak
+ </div>
+ ) }
+ </div>
+ )) }
+ </div>
+
+ <div className="pb-6 pt-2">
+ <Pagination currentPage={page} pageCount={pageCount} url={`/my/invoices${q ? `?q=${q}` : ''}`} />
+ </div>
+ { BottomPopup }
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/my/menu.js b/src2/pages/my/menu.js
new file mode 100644
index 00000000..ae6c2af8
--- /dev/null
+++ b/src2/pages/my/menu.js
@@ -0,0 +1,82 @@
+
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import Link from "@/components/elements/Link";
+import { useAuth } from "@/core/utils/auth";
+import {
+ ArrowRightOnRectangleIcon,
+ ChatBubbleLeftRightIcon,
+ ChevronRightIcon,
+ MapIcon,
+ PaperClipIcon,
+ PencilSquareIcon,
+ QuestionMarkCircleIcon,
+ ReceiptPercentIcon,
+ UserIcon,
+ HeartIcon
+} from "@heroicons/react/24/outline";
+import WithAuth from "@/components/auth/WithAuth";
+
+const Menu = ({ icon, name, url }) => {
+ return (
+ <Link href={url} className="text-gray_r-11 font-normal flex gap-x-2 items-center py-4 border-b border-gray_r-6">
+ <span className="flex gap-x-2">
+ { icon }
+ { name }
+ </span>
+ <ChevronRightIcon className="w-5 ml-auto"/>
+ </Link>
+ );
+};
+
+export default function MyMenu() {
+ const [auth] = useAuth();
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Menu Utama" />
+
+ <div className="p-4 flex gap-x-2 items-center">
+ <div className="flex-1 flex gap-x-3 items-center">
+ <div className="p-2 bg-gray_r-4 rounded-full h-fit">
+ <UserIcon className="w-6" />
+ </div>
+ <div>
+ <h2>{ auth?.name }</h2>
+ { auth?.company ? (
+ <div className="badge-red font-normal text-xs">Akun Bisnis</div>
+ ) : (
+ <div className="badge-gray font-normal text-xs">Akun Individu</div>
+ ) }
+ </div>
+ </div>
+ <Link href="/my/profile">
+ <PencilSquareIcon className="w-6 text-yellow_r-12"/>
+ </Link>
+ </div>
+
+ <div className="px-4 mt-4">
+ <p className="font-medium mb-2">Aktivitas Pembelian</p>
+ <div className="flex flex-col mb-6">
+ <Menu icon={<ReceiptPercentIcon className="w-5" />} name="Daftar Transaksi" url="/my/transactions" />
+ <Menu icon={<PaperClipIcon className="w-5" />} name="Invoice & Faktur Pajak" url="/my/invoices" />
+ <Menu icon={<HeartIcon className="w-5" />} name="Wishlist" url="/my/wishlist" />
+ </div>
+
+ <p className="font-medium mb-2">Pusat Bantuan</p>
+ <div className="flex flex-col mb-6">
+ <Menu icon={<ChatBubbleLeftRightIcon className="w-5"/>} name="Layanan Pelanggan" url="/" />
+ <Menu icon={<QuestionMarkCircleIcon className="w-5"/>} name="F.A.Q" url="/faqs" />
+ </div>
+
+ <p className="font-medium mb-2">Pengaturan Akun</p>
+ <div className="flex flex-col mb-6">
+ <Menu icon={<MapIcon className="w-5" />} name="Daftar Alamat" url="/my/address" />
+ <Menu icon={<ArrowRightOnRectangleIcon className="w-5" />} name="Keluar Akun" url="/logout" />
+ </div>
+ </div>
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/my/profile.js b/src2/pages/my/profile.js
new file mode 100644
index 00000000..97891259
--- /dev/null
+++ b/src2/pages/my/profile.js
@@ -0,0 +1,134 @@
+import { useState } from "react";
+import { toast } from "react-hot-toast";
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import WithAuth from "@/components/auth/WithAuth";
+import apiOdoo from "@/core/utils/apiOdoo";
+import {
+ useAuth,
+ setAuth as setAuthCookie,
+ getAuth
+} from "@/core/utils/auth";
+
+export default function MyProfile() {
+ const [auth, setAuth] = useAuth();
+ const [editMode, setEditMode] = useState(false);
+ const [password, setPassword] = useState('');
+
+ const update = async (e) => {
+ e.preventDefault();
+ let dataToUpdate = {
+ name: auth.name,
+ phone: auth.phone,
+ mobile: auth.mobile
+ };
+ if (password) dataToUpdate.password = password;
+ let update = await apiOdoo('PUT', `/api/v1/user/${auth.id}`, dataToUpdate);
+ setAuthCookie(update.user);
+ cancelEdit();
+ toast.success('Berhasil mengubah profil', { duration: 1500 });
+ };
+
+ const handleInput = (e) => {
+ let authToUpdate = auth;
+ authToUpdate[e.target.name] = e.target.value;
+ setAuth({ ...authToUpdate });
+ };
+
+ const cancelEdit = () => {
+ setEditMode(false);
+ setAuth(getAuth());
+ setPassword('');
+ }
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Akun Saya" />
+
+ <form onSubmit={update} className="w-full px-4">
+ { auth && (
+ <>
+ <label className="form-label mt-4 mb-2">Email</label>
+ <input
+ type="text"
+ className="form-input"
+ placeholder="johndoe@gmail.com"
+ name="email"
+ value={auth.email}
+ onChange={handleInput}
+ disabled={true}
+ />
+
+ <label className="form-label mt-4 mb-2">Nama Lengkap</label>
+ <input
+ type="text"
+ className="form-input"
+ placeholder="John Doe"
+ name="name"
+ value={auth.name}
+ onChange={handleInput}
+ disabled={!editMode}
+ />
+
+ <label className="form-label mt-4 mb-2">No Telepon</label>
+ <input
+ type="tel"
+ className="form-input"
+ placeholder="08xxxxxxxx"
+ name="phone"
+ value={auth.phone}
+ onChange={handleInput}
+ disabled={!editMode}
+ />
+
+ <label className="form-label mt-4 mb-2">No Handphone</label>
+ <input
+ type="tel"
+ className="form-input"
+ placeholder="08xxxxxxxx"
+ name="mobile"
+ value={auth.mobile}
+ onChange={handleInput}
+ disabled={!editMode}
+ />
+
+ <label className="form-label mt-4 mb-2">Kata Sandi</label>
+ <input
+ type="password"
+ className="form-input"
+ placeholder="••••••••"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ disabled={!editMode}
+ />
+ </>
+ ) }
+
+ { editMode && (
+ <div className="flex gap-x-3 mt-6">
+ <button
+ type="button"
+ className="btn-light flex-1 float-right"
+ onClick={cancelEdit}
+ >
+ Batal
+ </button>
+ <button type="submit" className="btn-yellow flex-1 float-right">Simpan</button>
+ </div>
+ ) }
+
+ { !editMode && (
+ <button
+ type="button"
+ className="btn-light float-right mt-6 w-full"
+ onClick={() => setEditMode(true)}
+ >
+ Ubah Profil
+ </button>
+ ) }
+ </form>
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/my/transaction/[id].js b/src2/pages/my/transaction/[id].js
new file mode 100644
index 00000000..fb806aa4
--- /dev/null
+++ b/src2/pages/my/transaction/[id].js
@@ -0,0 +1,265 @@
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import LineDivider from "@/components/elements/LineDivider";
+import WithAuth from "@/components/auth/WithAuth";
+import { useCallback, useEffect, useRef, useState } from "react";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useRouter } from "next/router";
+import { useAuth } from "@/core/utils/auth";
+import currencyFormat from "@/core/utils/currencyFormat";
+import DescriptionRow from "@/components/elements/DescriptionRow";
+import { TransactionDetailAddress } from "@/components/transactions/TransactionDetail";
+import { SkeletonList } from "@/components/elements/Skeleton";
+import Link from "@/components/elements/Link";
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import Alert from "@/components/elements/Alert";
+import TransactionStatusBadge from "@/components/transactions/TransactionStatusBadge";
+import useConfirmAlert from "@/lib/elements/hooks/useConfirmAlert";
+import { toast } from "react-hot-toast";
+import useBottomPopup from "@/lib/elements/hooks/useBottomPopup";
+import getFileBase64 from "@/core/utils/getFileBase64";
+import VariantGroupCard from "@/components/variants/VariantGroupCard";
+
+export default function DetailTransaction() {
+ const router = useRouter();
+ const { id } = router.query;
+ const [ auth ] = useAuth();
+ const [ transaction, setTransaction ] = useState(null);
+
+ const loadTransaction = useCallback(async () => {
+ if (auth && id) {
+ const dataTransaction = await apiOdoo('GET', `/api/v1/partner/${auth?.partner_id}/sale_order/${id}`);
+ setTransaction(dataTransaction);
+ }
+ }, [ auth, id ]);
+
+ useEffect(() => {
+ loadTransaction();
+ }, [ loadTransaction ]);
+
+ const submitCancelTransaction = async (data) => {
+ const isCancelled = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/cancel`);
+ if (isCancelled) {
+ toast.success('Berhasil batalkan transaksi');
+ loadTransaction();
+ }
+ }
+
+ const {
+ openConfirmAlert,
+ ConfirmAlert
+ } = useConfirmAlert({
+ title: 'Batalkan Transaksi',
+ caption: 'Apakah anda yakin untuk membatalkan transaksi?',
+ closeText: 'Tidak',
+ submitText: 'Iya, batalkan',
+ onSubmit: submitCancelTransaction
+ });
+
+ const UploadPurchaseOrder = () => {
+ const nameRef = useRef('');
+ const fileRef = useRef('');
+
+ const submitUploadPurchaseOrder = async (e) => {
+ e.preventDefault();
+ const file = fileRef.current.files[0];
+ const name = nameRef.current.value;
+ if (file.size > 5000000) {
+ toast.error('Maksimal ukuran file adalah 5MB', {
+ position: 'bottom-center'
+ });
+ return;
+ }
+ const parameter = {
+ name,
+ file: await getFileBase64(file)
+ };
+ const isUploaded = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/${transaction.id}/upload_po`, parameter);
+ if (isUploaded) {
+ toast.success('Berhasil upload PO');
+ loadTransaction();
+ closePopup();
+ }
+ };
+
+ return (
+ <form className="flex flex-col gap-y-4" onSubmit={submitUploadPurchaseOrder}>
+ <div>
+ <label className="form-label mb-2">Nama PO</label>
+ <input className="form-input" type="text" ref={nameRef} required />
+ </div>
+ <div>
+ <label className="form-label mb-2">Dokumen PO</label>
+ <input className="form-input" type="file" ref={fileRef} required />
+ </div>
+ <button type="submit" className="btn-yellow w-full mt-2">Upload</button>
+ </form>
+ );
+ }
+
+ const {
+ closePopup,
+ BottomPopup,
+ openPopup
+ } = useBottomPopup({
+ title: 'Upload PO',
+ children: UploadPurchaseOrder
+ });
+
+ const downloadPurchaseOrder = () => {
+ const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${transaction.id}/download_po/${transaction.token}`;
+ window.open(url, 'download')
+ };
+
+ const downloadQuotation = () => {
+ const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${transaction.id}/download/${transaction.token}`;
+ window.open(url, 'download')
+ };
+
+ const checkout = async () => {
+ if (!transaction.purchase_order_file) {
+ toast.error('Mohon upload dokumen PO anda sebelum melanjutkan pesanan')
+ return
+ }
+ await apiOdoo('POST', `/api/v1/partner/${auth?.partner_id}/sale_order/${id}/checkout`)
+ toast.success('Berhasil melanjutkan pesanan')
+ loadTransaction()
+ }
+
+ return (
+ <WithAuth>
+ <Layout className="pb-4">
+ <AppBar title="Detail Transaksi" />
+
+ { transaction ? (
+ <>
+ <div className="p-4 flex flex-col gap-y-4">
+ <DescriptionRow label="Status Transaksi">
+ <div className="flex justify-end">
+ <TransactionStatusBadge status={transaction?.status} />
+ </div>
+ </DescriptionRow>
+ <DescriptionRow label="No Transaksi">
+ { transaction?.name }
+ </DescriptionRow>
+ <DescriptionRow label="Ketentuan Pembayaran">
+ { transaction?.payment_term }
+ </DescriptionRow>
+ <DescriptionRow label="Nama Sales">
+ { transaction?.sales }
+ </DescriptionRow>
+ <DescriptionRow label="Waktu Transaksi">
+ { transaction?.date_order }
+ </DescriptionRow>
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4 flex flex-col gap-y-4">
+ <DescriptionRow label="Purchase Order">
+ { transaction?.purchase_order_name || '-' }
+ </DescriptionRow>
+ <div className="flex items-center">
+ <p className="text-gray_r-11 leading-none">Dokumen PO</p>
+ <button
+ type="button"
+ className="btn-light py-1.5 px-3 ml-auto"
+ onClick={transaction?.purchase_order_file ? downloadPurchaseOrder : openPopup}
+ >
+ { transaction?.purchase_order_file ? 'Download' : 'Upload' }
+ </button>
+ </div>
+ </div>
+
+ <LineDivider />
+
+ <p className="h2 p-4">Detail Produk</p>
+
+ <div className="mt-2 p-4 pt-0 flex flex-col gap-y-3">
+ <VariantGroupCard
+ variants={transaction?.products}
+ buyMore
+ />
+ <div className="flex justify-between mt-3 font-medium">
+ <p>Total Belanja</p>
+ <p>{ currencyFormat(transaction?.amount_total || 0) }</p>
+ </div>
+ </div>
+
+ <LineDivider />
+
+ <TransactionDetailAddress transaction={transaction} />
+
+ <LineDivider />
+
+ <div className="p-4">
+ <p className="h2">Invoice</p>
+ <div className="flex flex-col gap-y-3 mt-4">
+ { transaction?.invoices?.map((invoice, index) => (
+ <Link href={`/my/invoice/${invoice.id}`} key={index}>
+ <div className="shadow rounded-md p-4 text-gray_r-12 font-normal flex justify-between">
+ <div>
+ <p className="mb-2">{ invoice?.name }</p>
+ <div className="flex items-center gap-x-1">
+ { invoice.amount_residual > 0 ? (
+ <div className="badge-red">Belum Lunas</div>
+ ) : (
+ <div className="badge-green">Lunas</div>
+ ) }
+ <p className="text-caption-2 text-gray_r-11">
+ { currencyFormat(invoice.amount_total) }
+ </p>
+ </div>
+ </div>
+ <ChevronRightIcon className="w-5 stroke-2" />
+ </div>
+ </Link>
+ )) }
+ { transaction?.invoices?.length === 0 && (
+ <Alert type='info' className='text-center'>
+ Belum ada Invoice
+ </Alert>
+ ) }
+ </div>
+ </div>
+
+ <LineDivider />
+
+ <div className="px-4">
+ { transaction?.status == 'draft' && (
+ <button
+ className="btn-yellow w-full mt-4"
+ onClick={checkout}
+ >
+ Lanjutkan Transaksi
+ </button>
+ ) }
+ <button
+ className="btn-light w-full mt-4"
+ disabled={transaction?.status != 'draft'}
+ onClick={downloadQuotation}
+ >
+ Download Quotation
+ </button>
+ { transaction?.status != 'draft' && (
+ <button
+ className="btn-light w-full mt-4"
+ disabled={transaction?.status != 'waiting'}
+ onClick={() => openConfirmAlert(transaction)}
+ >
+ Batalkan Transaksi
+ </button>
+ ) }
+ </div>
+ </>
+ ) : (
+ <div className="p-4 py-6">
+ <SkeletonList number={12} />
+ </div>
+ ) }
+ { ConfirmAlert }
+ { BottomPopup }
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/my/transactions.js b/src2/pages/my/transactions.js
new file mode 100644
index 00000000..8be43af7
--- /dev/null
+++ b/src2/pages/my/transactions.js
@@ -0,0 +1,198 @@
+import { useRouter } from "next/router";
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import WithAuth from "@/components/auth/WithAuth";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useAuth } from "@/core/utils/auth";
+import apiOdoo from "@/core/utils/apiOdoo";
+import currencyFormat from "@/core/utils/currencyFormat";
+import { EllipsisVerticalIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
+import Link from "@/components/elements/Link";
+import Pagination from "@/components/elements/Pagination";
+import Alert from "@/components/elements/Alert";
+import TransactionStatusBadge from "@/components/transactions/TransactionStatusBadge";
+import { toast } from "react-hot-toast";
+import useConfirmAlert from "@/lib/elements/hooks/useConfirmAlert";
+import useBottomPopup from "@/lib/elements/hooks/useBottomPopup";
+
+export default function Transactions() {
+ const [ auth ] = useAuth();
+ const router = useRouter();
+ const {
+ q,
+ page = 1
+ } = router.query;
+
+ const [ transactions, setTransactions ] = useState([]);
+
+ const [ pageCount, setPageCount ] = useState(0);
+ const [ isLoading, setIsLoading ] = useState(true);
+
+ const searchQueryRef = useRef();
+ const loadTransactions = useCallback(async () => {
+ if (auth) {
+ const limit = 10;
+ let offset = (page - 1) * 10;
+ let queryParams = [`limit=${limit}`, `offset=${offset}`];
+ if (q) queryParams.push(`name=${q}`);
+ queryParams = queryParams.join('&');
+ queryParams = queryParams ? '?' + queryParams : '';
+
+ const dataTransactions = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/sale_order${queryParams}`);
+ setTransactions(dataTransactions);
+ setPageCount(Math.ceil(dataTransactions?.sale_order_total / limit));
+ setIsLoading(false);
+ };
+ }, [ auth, q, page ]);
+
+ useEffect(() => {
+ loadTransactions();
+ }, [ loadTransactions ]);
+
+ const actionSearch = (e) => {
+ e.preventDefault();
+ let queryParams = [];
+ if (searchQueryRef.current.value) queryParams.push(`q=${searchQueryRef.current.value}`);
+ queryParams = queryParams.join('&');
+ queryParams = queryParams ? `?${queryParams}` : '';
+ router.push(`/my/transactions${queryParams}`);
+ };
+
+ const downloadPurchaseOrder = (data) => {
+ const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/download_po/${data.token}`;
+ window.open(url, 'download');
+ closePopup();
+ };
+
+ const downloadQuotation = (data) => {
+ const url = `${process.env.ODOO_HOST}/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/download/${data.token}`;
+ window.open(url, 'download');
+ closePopup();
+ };
+
+ const childrenPopup = (data) => (
+ <div className="flex flex-col gap-y-6">
+ <button
+ className="text-left disabled:opacity-60"
+ disabled={!data?.purchase_order_file}
+ onClick={() => downloadPurchaseOrder(data)}
+ >
+ Download PO
+ </button>
+ <button
+ className="text-left disabled:opacity-60"
+ disabled={data?.status != 'draft'}
+ onClick={() => downloadQuotation(data)}
+ >
+ Download Quotation
+ </button>
+ <button
+ className="text-left disabled:opacity-60"
+ disabled={ data?.status != 'waiting' }
+ onClick={() => {openConfirmAlert(data); closePopup()}}
+ >
+ Batalkan Transaksi
+ </button>
+ </div>
+ );
+
+ const {
+ closePopup,
+ openPopup,
+ BottomPopup
+ } = useBottomPopup({
+ title: 'Lainnya',
+ children: childrenPopup
+ });
+
+ const submitCancelTransaction = async (data) => {
+ const isCancelled = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/${data.id}/cancel`);
+ if (isCancelled) {
+ toast.success('Berhasil batalkan transaksi');
+ loadTransactions();
+ }
+ }
+
+ const {
+ openConfirmAlert,
+ ConfirmAlert
+ } = useConfirmAlert({
+ title: 'Batalkan Transaksi',
+ caption: 'Apakah anda yakin untuk membatalkan transaksi?',
+ closeText: 'Tidak',
+ submitText: 'Ya, Batalkan',
+ onSubmit: submitCancelTransaction
+ });
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Transaksi" />
+
+ <form onSubmit={actionSearch} className="p-4 pb-0 flex gap-x-4">
+ <input
+ type="text"
+ className="form-input"
+ placeholder="Cari Transaksi"
+ ref={searchQueryRef}
+ defaultValue={q}
+ />
+ <button type="submit" className="border border-gray_r-7 rounded px-3">
+ <MagnifyingGlassIcon className="w-5"/>
+ </button>
+ </form>
+
+ <div className="p-4 flex flex-col gap-y-5">
+ { transactions?.sale_order_total === 0 && !isLoading && (
+ <Alert type="info" className="text-center">
+ Transaksi tidak ditemukan
+ </Alert>
+ ) }
+ { transactions?.sale_orders?.map((transaction, index) => (
+ <div className="p-4 shadow border border-gray_r-3 rounded-md" key={index}>
+ <div className="grid grid-cols-2">
+ <Link href={`/my/transaction/${transaction.id}`}>
+ <span className="text-caption-2 text-gray_r-11">No. Transaksi</span>
+ <h2 className="text-red_r-11 mt-1">{ transaction.name }</h2>
+ </Link>
+ <div className="flex gap-x-1 justify-end">
+ <TransactionStatusBadge status={transaction.status} />
+ <EllipsisVerticalIcon className="w-5 h-5" onClick={() => openPopup(transaction)} />
+ </div>
+ </div>
+ <Link href={`/my/transaction/${transaction.id}`}>
+ <div className="grid grid-cols-2 mt-3">
+ <div>
+ <span className="text-caption-2 text-gray_r-11">No. Purchase Order</span>
+ <p className="mt-1 font-medium text-gray_r-12">{ transaction.purchase_order_name || '-' }</p>
+ </div>
+ <div className="text-right">
+ <span className="text-caption-2 text-gray_r-11">Total Invoice</span>
+ <p className="mt-1 font-medium text-gray_r-12">{ transaction.invoice_count } Invoice</p>
+ </div>
+ </div>
+ <div className="grid grid-cols-2 mt-3">
+ <div>
+ <span className="text-caption-2 text-gray_r-11">Sales</span>
+ <p className="mt-1 font-medium text-gray_r-12">{ transaction.sales }</p>
+ </div>
+ <div className="text-right">
+ <span className="text-caption-2 text-gray_r-11">Total Harga</span>
+ <p className="mt-1 font-medium text-gray_r-12">{ currencyFormat(transaction.amount_total) }</p>
+ </div>
+ </div>
+ </Link>
+ </div>
+ )) }
+ </div>
+
+ <div className="pb-6 pt-2">
+ <Pagination currentPage={page} pageCount={pageCount} url={`/my/transactions${q ? `?q=${q}` : ''}`} />
+ </div>
+
+ { ConfirmAlert }
+ { BottomPopup }
+ </Layout>
+ </WithAuth>
+ );
+}; \ No newline at end of file
diff --git a/src2/pages/my/wishlist.js b/src2/pages/my/wishlist.js
new file mode 100644
index 00000000..3d479802
--- /dev/null
+++ b/src2/pages/my/wishlist.js
@@ -0,0 +1,60 @@
+import WithAuth from "@/components/auth/WithAuth";
+import Alert from "@/components/elements/Alert";
+import Pagination from "@/components/elements/Pagination";
+import Spinner from "@/components/elements/Spinner";
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import ProductCard from "@/components/products/ProductCard";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useAuth } from "@/core/utils/auth";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+
+export default function Wishlist() {
+ const [ auth ] = useAuth();
+ const router = useRouter();
+ const { page = 1 } = router.query;
+ const [ wishlists, setWishlists ] = useState(null);
+ const [ pageCount, setPageCount ] = useState(0);
+
+ useEffect(() => {
+ const loadWishlist = async () => {
+ const limit = 10;
+ const offset = (page - 1) * limit;
+ if (auth) {
+ const dataWishlist = await apiOdoo('GET', `/api/v1/user/${auth.id}/wishlist?limit=${limit}&offset=${offset}`);
+ setWishlists(dataWishlist);
+ setPageCount(Math.ceil(dataWishlist.product_total / limit));
+ }
+ }
+ loadWishlist();
+ }, [ auth, page ]);
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title='Wishlist' />
+
+ <div className="px-4 py-6">
+ { !wishlists && (
+ <Spinner className="w-6 h-6 text-gray-600 fill-gray-900 mx-auto" />
+ ) }
+ { wishlists?.products?.length == 0 && (
+ <Alert type='info' className='text-center'>
+ Wishlist anda masih kosong
+ </Alert>
+ ) }
+ <div className="grid grid-cols-2 gap-3">
+ {wishlists?.products.map((product) => (
+ <ProductCard key={product.id} data={product} />
+ ))}
+ </div>
+
+ <div className="mt-6">
+ <Pagination currentPage={page} pageCount={pageCount} url={`/my/wishlist`} />
+ </div>
+ </div>
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/register.js b/src2/pages/register.js
new file mode 100644
index 00000000..39bd137f
--- /dev/null
+++ b/src2/pages/register.js
@@ -0,0 +1,100 @@
+import axios from "axios";
+import Head from "next/head";
+import Image from "next/image";
+import Link from "@/components/elements/Link";
+import { useEffect, useState } from "react";
+import Alert from "@/components/elements/Alert";
+import Layout from "@/components/layouts/Layout";
+import Spinner from "@/components/elements/Spinner";
+import Logo from "@/images/logo.png";
+
+export default function Login() {
+ const [email, setEmail] = useState('');
+ const [name, setName] = useState('');
+ const [password, setPassword] = useState('');
+ const [isInputFulfilled, setIsInputFulfilled] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [alert, setAlert] = useState();
+
+ useEffect(() => {
+ setIsInputFulfilled(email && name && password);
+ }, [email, name, password]);
+
+ const register = async (e) => {
+ e.preventDefault();
+ setIsLoading(true);
+ let register = await axios.post(`${process.env.SELF_HOST}/api/register`, {email, name, password});
+ if (register.data.register) {
+ await axios.post(`${process.env.SELF_HOST}/api/activation-request`, {email});
+ setAlert({
+ component: <>Berhasil mendaftarkan akun anda, cek email untuk melakukan aktivasi akun</>,
+ type: 'success'
+ });
+ setEmail('');
+ setName('');
+ setPassword('');
+ } else {
+ switch (register.data.reason) {
+ case 'EMAIL_USED':
+ setAlert({
+ component: <>Email telah digunakan</>,
+ type: 'info'
+ });
+ break;
+ }
+ }
+ setIsLoading(false);
+ }
+
+ return (
+ <>
+ <Head>
+ <title>Daftar - Indoteknik</title>
+ </Head>
+ <Layout className="max-w-lg mx-auto flex flex-col items-center px-4 pb-8">
+ <Link href="/" className="mt-16">
+ <Image src={Logo} alt="Logo Indoteknik" width={165} height={42} />
+ </Link>
+ <h1 className="text-2xl mt-4 text-center">Mudahkan Pembelian dengan Indoteknik</h1>
+ <h2 className="text-gray_r-11 font-normal mt-2 mb-4">Daftar untuk melanjutkan belanja</h2>
+ {alert ? (
+ <Alert className="text-center" type={alert.type}>{alert.component}</Alert>
+ ) : ''}
+ <form onSubmit={register} className="w-full">
+ <label className="form-label mt-4 mb-2">Alamat Email</label>
+ <input
+ type="email"
+ className="form-input bg-gray_r-2"
+ placeholder="johndoe@gmail.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ />
+ <label className="form-label mt-4 mb-2">Nama Lengkap</label>
+ <input
+ type="text"
+ className="form-input bg-gray_r-2"
+ placeholder="John Doe"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ />
+ <label className="form-label mt-4 mb-2">Kata Sandi</label>
+ <input
+ type="password"
+ className="form-input bg-gray_r-2"
+ placeholder="••••••••"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ />
+ <button type="submit" disabled={!isInputFulfilled} className="btn-yellow font-semibold mt-4 w-full">
+ {isLoading ? (
+ <div className="flex justify-center items-center gap-x-2">
+ <Spinner className="w-4 h-4 text-gray-600 fill-gray-900" /> <span>Loading...</span>
+ </div>
+ ) : 'Daftar'}
+ </button>
+ </form>
+ <p className="text-gray-700 mt-4">Sudah punya akun Indoteknik? <Link href="/login">Masuk</Link></p>
+ </Layout>
+ </>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/shop/brands/[slug].js b/src2/pages/shop/brands/[slug].js
new file mode 100644
index 00000000..a387e55d
--- /dev/null
+++ b/src2/pages/shop/brands/[slug].js
@@ -0,0 +1,178 @@
+import axios from "axios";
+import { useEffect, useState } from "react";
+import Filter from "@/components/elements/Filter";
+import Footer from "@/components/layouts/Footer";
+import Header from "@/components/layouts/Header";
+import Layout from "@/components/layouts/Layout";
+import Pagination from "@/components/elements/Pagination";
+import ProductCard from "@/components/products/ProductCard";
+import { getIdFromSlug, getNameFromSlug } from "@/core/utils/slug";
+import FilterIcon from "@/icons/filter.svg";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { Swiper, SwiperSlide } from "swiper/react";
+import "swiper/css";
+import "swiper/css/pagination";
+import "swiper/css/autoplay";
+import { Pagination as SwiperPagination } from "swiper";
+import Image from "@/components/elements/Image";
+import LineDivider from "@/components/elements/LineDivider";
+
+export async function getServerSideProps(context) {
+ const {
+ slug,
+ page = 1,
+ category = '',
+ price_from = '',
+ price_to = '',
+ order_by = '',
+ } = context.query;
+
+ let urlParameter = [
+ 'q=*',
+ `page=${page}`,
+ `brand=${getNameFromSlug(slug)}`,
+ `category=${category}`,
+ `price_from=${price_from}`,
+ `price_to=${price_to}`,
+ `order_by=${order_by}`
+ ].join('&');
+ let searchResults = await axios(`${process.env.SELF_HOST}/api/shop/search?${urlParameter}`);
+ searchResults = searchResults.data;
+
+ const manufacture = await apiOdoo('GET', `/api/v1/manufacture/${getIdFromSlug(slug)}`);
+
+ return {
+ props: {
+ searchResults,
+ page,
+ slug,
+ category,
+ price_from,
+ price_to,
+ order_by,
+ manufacture
+ }
+ };
+}
+
+export default function BrandDetail({
+ searchResults,
+ page,
+ slug,
+ category,
+ price_from,
+ price_to,
+ order_by,
+ manufacture
+}) {
+ const pageCount = Math.ceil(searchResults.response.numFound / searchResults.responseHeader.params.rows);
+ const productStart = searchResults.responseHeader.params.start;
+ const productRows = searchResults.responseHeader.params.rows;
+ const productFound = searchResults.response.numFound;
+
+ const [activeFilter, setActiveFilter] = useState(false);
+ const [filterCount, setFilterCount] = useState(0);
+
+ const route = () => {
+ let route = `/shop/brands/${slug}`;
+ if (category) route += `&category=${category}`;
+ if (price_from) route += `&price_from=${price_from}`;
+ if (price_to) route += `&price_to=${price_to}`;
+ if (order_by) route += `&order_by=${order_by}`;
+ return route;
+ }
+
+ useEffect(() => {
+ let calculateFilterCount = 0;
+ if (category) calculateFilterCount++;
+ if (price_from || price_to) calculateFilterCount++;
+ if (order_by) calculateFilterCount++;
+ setFilterCount(calculateFilterCount);
+ }, [category, price_from, price_to, order_by]);
+
+ return (
+ <>
+ <Header title={`Distributor ${getNameFromSlug(slug)} Indonesia Harga Official - Indoteknik`} />
+ <Filter
+ defaultRoute={`/shop/brands/${slug}`}
+ isActive={activeFilter}
+ closeFilter={() => setActiveFilter(false)}
+ defaultPriceFrom={price_from}
+ defaultPriceTo={price_to}
+ defaultBrand=''
+ defaultCategory={category}
+ defaultOrderBy={order_by}
+ searchResults={searchResults}
+ disableFilter={['brand']}
+ />
+ <Layout>
+ <Swiper slidesPerView={1} pagination={{dynamicBullets: true}} modules={[SwiperPagination]}>
+ {
+ manufacture.banners?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner}
+ alt={`Banner ${manufacture.name}`}
+ className="w-full h-auto border-b border-gray_r-6"
+ />
+ </SwiperSlide>
+ ))
+ }
+ </Swiper>
+ <div className="p-4 grid grid-cols-2">
+ <div>
+ <p className="text-caption-2 text-gray_r-11 mb-2">Produk dari brand:</p>
+ { manufacture.logo ? (
+ <div className="w-8/12">
+ <Image src={manufacture?.logo} alt={manufacture.name} className="border border-gray_r-6 rounded p-3" />
+ </div>
+ ) : (
+ <p className="badge-solid-red text-caption-1">{ manufacture.name }</p>
+ ) }
+ </div>
+ <div className="text-right">
+ <p className="text-caption-2 text-gray_r-11 mb-2">Jumlah Produk:</p>
+ <p>{ searchResults.response.numFound }</p>
+ </div>
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4">
+ <h1 className="mb-2">Produk</h1>
+ <div className="text-caption-1 mb-4">
+ {productFound > 0 ? (
+ <>
+ Menampilkan&nbsp;
+ {pageCount > 1 ? (
+ <>
+ {productStart + 1}-{
+ (productStart + productRows) > productFound ? productFound : productStart + productRows
+ }
+ &nbsp;dari&nbsp;
+ </>
+ ) : ''}
+ {searchResults.response.numFound}
+ &nbsp;produk untuk brand <span className="font-semibold">{getNameFromSlug(slug)}</span>
+ </>
+ ) : 'Mungkin yang anda cari'}
+ </div>
+ <button className="btn-light py-2 flex items-center gap-x-2 mb-4" onClick={() => setActiveFilter(true)}>
+ <FilterIcon className="w-4 h-4" /> <span>Filter {filterCount > 0 ? `(${filterCount})` : ''}</span>
+ </button>
+ <div className="grid grid-cols-2 gap-3">
+ {searchResults.response.products.map((product) => (
+ <ProductCard key={product.id} data={product} />
+ ))}
+ </div>
+
+ <div className="mt-4">
+ <Pagination pageCount={pageCount} currentPage={parseInt(page)} url={route()} />
+ </div>
+ </div>
+
+ <Footer />
+ </Layout>
+ </>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/shop/brands/index.js b/src2/pages/shop/brands/index.js
new file mode 100644
index 00000000..bfdcd403
--- /dev/null
+++ b/src2/pages/shop/brands/index.js
@@ -0,0 +1,79 @@
+import Header from "@/components/layouts/Header";
+import apiOdoo from "@/core/utils/apiOdoo";
+import InfiniteScroll from "react-infinite-scroll-component";
+import { useCallback, useEffect, useState } from "react";
+import Spinner from "@/components/elements/Spinner";
+import Layout from "@/components/layouts/Layout";
+import ManufactureCard from "@/components/manufactures/ManufactureCard";
+import Footer from "@/components/layouts/Footer";
+
+export async function getServerSideProps() {
+ let initialManufactures = await apiOdoo('GET', '/api/v1/manufacture?limit=31');
+ return {props: {initialManufactures}};
+}
+
+export default function Brands({ initialManufactures }) {
+ const [manufactures, setManufactures] = useState(initialManufactures.manufactures);
+ const [hasMoreManufacture, setHasMoreManufacture] = useState(true);
+ const [manufactureStartwith, setManufactureStartWith] = useState('');
+
+ const alpha = Array.from(Array(26)).map((e, i) => i + 65);
+ const alphabets = alpha.map((x) => String.fromCharCode(x));
+
+ const getMoreManufactures = useCallback(async () => {
+ const name = manufactureStartwith != '' ? `${manufactureStartwith}%` : '';
+ const result = await apiOdoo('GET', `/api/v1/manufacture?limit=30&offset=${manufactures.length}&name=${name}`);
+ setHasMoreManufacture(manufactures.length + 30 < result.manufacture_total)
+ setManufactures((manufactures) => [...manufactures, ...result.manufactures]);
+ }, [ manufactureStartwith ]);
+
+ const filterManufactureStartWith = (character) => {
+ setManufactures([]);
+ if (manufactureStartwith == character) {
+ setManufactureStartWith('');
+ } else {
+ setManufactureStartWith(character);
+ }
+ };
+
+ useEffect(() => {
+ getMoreManufactures();
+ }, [ getMoreManufactures ]);
+
+ return (
+ <>
+ <Header title='Semua Brand di Indoteknik' />
+ <Layout>
+ <div className="p-4">
+ <h1>Semua Brand di Indoteknik</h1>
+ <div className="flex overflow-x-auto gap-x-2 py-2">
+ {alphabets.map((alphabet, index) => (
+ <button key={index} className={"p-2 py-1 border bg-white border-gray_r-6 rounded w-10 flex-shrink-0" + (manufactureStartwith == alphabet ? ' !bg-yellow_r-9 border-yellow_r-9 ' : '')} onClick={() => filterManufactureStartWith(alphabet)}>
+ {alphabet}
+ </button>
+ ))}
+ </div>
+ <InfiniteScroll
+ dataLength={manufactures.length}
+ next={getMoreManufactures}
+ hasMore={hasMoreManufacture}
+ className="grid grid-cols-4 gap-4 mt-6 !overflow-x-hidden"
+ loader={
+ <div className="flex justify-center items-center border border-gray-300 p-2 rounded h-14">
+ <Spinner className="w-6 h-6 text-gray-600 fill-gray-900"/>
+ </div>
+ }
+ >
+ {manufactures?.map((manufacture, index) => (
+ manufacture.name ? (
+ <ManufactureCard data={manufacture} key={index} />
+ ) : ''
+ ))}
+ </InfiniteScroll>
+ </div>
+
+ <Footer />
+ </Layout>
+ </>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/shop/cart.js b/src2/pages/shop/cart.js
new file mode 100644
index 00000000..1178781b
--- /dev/null
+++ b/src2/pages/shop/cart.js
@@ -0,0 +1,282 @@
+import { useEffect, useState } from "react";
+import { toast } from "react-hot-toast";
+import {
+ TrashIcon,
+ PlusIcon,
+ MinusIcon,
+ ExclamationCircleIcon,
+} from "@heroicons/react/24/solid";
+import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
+
+// Helpers
+import {
+ createOrUpdateItemCart,
+ deleteItemCart,
+ getCart
+} from "@/core/utils/cart";
+import { createSlug } from "@/core/utils/slug";
+import apiOdoo from "@/core/utils/apiOdoo";
+import currencyFormat from "@/core/utils/currencyFormat";
+
+// Components
+import Image from "@/components/elements/Image";
+import Layout from "@/components/layouts/Layout";
+import Link from "@/components/elements/Link";
+import Alert from "@/components/elements/Alert";
+import Spinner from "@/components/elements/Spinner";
+import AppBar from "@/components/layouts/AppBar";
+import ProgressBar from "@/components/elements/ProgressBar";
+import LineDivider from "@/components/elements/LineDivider";
+import useConfirmAlert from "@/lib/elements/hooks/useConfirmAlert";
+
+export default function Cart() {
+ const router = useRouter();
+ const [isLoadingProducts, setIsLoadingProducts] = useState(true);
+ const [products, setProducts] = useState([]);
+ const [totalPriceBeforeTax, setTotalPriceBeforeTax] = useState(0);
+ const [totalTaxAmount, setTotalTaxAmount] = useState(0);
+ const [totalDiscountAmount, setTotalDiscountAmount] = useState(0);
+
+ useEffect(() => {
+ const getProducts = async () => {
+ let cart = getCart();
+ let productIds = Object.keys(cart);
+ if (productIds.length > 0) {
+ productIds = productIds.join(',');
+ let dataProducts = await apiOdoo('GET', `/api/v1/product_variant/${productIds}`);
+ dataProducts = dataProducts.map((product) => ({
+ ...product,
+ quantity: cart[product.id].quantity,
+ selected: cart[product.id].selected,
+ }));
+ setProducts(dataProducts);
+ }
+ setIsLoadingProducts(false);
+ }
+ getProducts();
+ }, []);
+
+ useEffect(() => {
+ for (const product of products) {
+ if (product.quantity != '') createOrUpdateItemCart(product.id, product.quantity, product.selected);
+ }
+ const productsSelected = products.filter((product) => product.selected == true);
+ let calculateTotalPriceBeforeTax = 0;
+ let calculateTotalTaxAmount = 0;
+ let calculateTotalDiscountAmount = 0;
+ productsSelected.forEach(product => {
+ let priceBeforeTax = product.price.price / 1.11;
+ calculateTotalPriceBeforeTax += priceBeforeTax * product.quantity;
+ calculateTotalTaxAmount += (product.price.price - priceBeforeTax) * product.quantity;
+ calculateTotalDiscountAmount += (product.price.price - product.price.price_discount) * product.quantity;
+ });
+ setTotalPriceBeforeTax(calculateTotalPriceBeforeTax);
+ setTotalTaxAmount(calculateTotalTaxAmount);
+ setTotalDiscountAmount(calculateTotalDiscountAmount);
+ }, [products]);
+
+ const getProductsSelected = () => {
+ return products.filter((product) => product.selected == true);
+ }
+
+ const updateCart = (productId, quantity) => {
+ let productIndexToUpdate = products.findIndex((product) => product.id == productId);
+ let productsToUpdate = products;
+ productsToUpdate[productIndexToUpdate].quantity = quantity;
+ setProducts([...productsToUpdate]);
+ };
+
+ const blurQuantity = (productId, quantity) => {
+ quantity = quantity == ('' || 0) ? 1 : parseInt(quantity);
+ if (typeof quantity === 'number') {
+ quantity = parseInt(quantity);
+ quantity = Math.floor(quantity);
+ }
+ updateCart(productId, quantity);
+ };
+
+ const updateQuantity = (productId, quantity) => {
+ quantity = quantity == '' ? '' : parseInt(quantity);
+ updateCart(productId, quantity);
+ };
+
+ const plusQuantity = (productId) => {
+ let productIndexToUpdate = products.findIndex((product) => product.id == productId);
+ let quantity = products[productIndexToUpdate].quantity + 1;
+ updateCart(productId, quantity);
+ }
+
+ const minusQuantity = (productId) => {
+ let productIndexToUpdate = products.findIndex((product) => product.id == productId);
+ let quantity = products[productIndexToUpdate].quantity - 1;
+ updateCart(productId, quantity);
+ }
+
+ const toggleProductSelected = (productId) => {
+ let productIndexToUpdate = products.findIndex((product) => product.id == productId);
+ let productsToUpdate = products;
+ productsToUpdate[productIndexToUpdate].selected = !productsToUpdate[productIndexToUpdate].selected;
+ setProducts([...productsToUpdate]);
+ }
+
+ const deleteItem = (productId) => {
+ let productIndexToUpdate = products.findIndex((product) => product.id == productId);
+ let productsToUpdate = products;
+ productsToUpdate.splice(productIndexToUpdate, 1);
+ setProducts([...productsToUpdate]);
+ deleteItemCart(productId);
+ toast.success('Berhasil menghapus 1 barang dari keranjang', { duration: 1500 });
+ }
+
+ const {
+ openConfirmAlert,
+ ConfirmAlert
+ } = useConfirmAlert({
+ title: 'Hapus barang dari keranjang',
+ caption:'Apakah anda yakin menghapus barang dari keranjang?',
+ closeText: 'Batal',
+ submitText: 'Hapus',
+ onSubmit: deleteItem
+ })
+
+ return (
+ <>
+ { ConfirmAlert }
+
+ <Layout>
+ <AppBar title="Keranjang Saya" />
+
+ {isLoadingProducts && (
+ <div className="flex justify-center items-center gap-x-3 mt-14">
+ <Spinner className="w-10 text-gray_r-8 fill-gray_r-12" />
+ </div>
+ ) }
+
+ { !isLoadingProducts && products.length == 0 && (
+ <div className="text-center mt-14">
+ <ExclamationTriangleIcon className="w-12 mx-auto"/>
+ <p className="mt-2 h2">Keranjang belanja anda masih kosong.</p>
+ <Link href="/" className="btn-yellow text-gray_r-12 mx-auto mt-4">Mulai Belanja</Link>
+ </div>
+ ) }
+
+ { !isLoadingProducts && products.length > 0 && (
+ <>
+ <ProgressBar
+ current={1}
+ labels={['Keranjang', 'Pembayaran', 'Selesai']}
+ />
+
+ <LineDivider />
+
+ <div className="p-4">
+ <Alert type="warning" className="text-caption-2 flex gap-x-3 items-center">
+ <div>
+ <ExclamationCircleIcon className="w-8 text-yellow_r-11"/>
+ </div>
+ <span>Mohon dicek kembali & pastikan pesanan kamu sudah sesuai dengan yang kamu butuhkan. Atau bisa hubungi kami.</span>
+ </Alert>
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4 flex flex-col gap-y-6">
+ <div className="flex justify-between items-center">
+ <h2>Daftar Produk Belanja</h2>
+ <Link href="/" className="text-caption-1">Cari Produk Lain</Link>
+ </div>
+ {products.map((product, index) => (
+ <div className="flex gap-x-3" key={index}>
+ <div className="w-4/12 flex items-center gap-x-2" onClick={() => toggleProductSelected(product.id)}>
+ <button
+ className={'p-2 rounded border-2 ' + (product.selected ? 'border-yellow_r-9 bg-yellow_r-9' : 'border-gray_r-12')}
+ ></button>
+ <Image
+ src={product.parent.image}
+ alt={product.parent.name}
+ className="object-contain object-center border border-gray_r-6 h-32 w-full rounded-md"
+ />
+ </div>
+ <div className="w-8/12 flex flex-col">
+ <Link href={'/shop/product/' + createSlug(product.parent.name, product.parent.id)} className="product-card__title wrap-line-ellipsis-2">
+ {product.parent.name}
+ </Link>
+ <p className="text-caption-2 text-gray_r-11 mt-1">
+ {product.code || '-'}
+ {product.attributes.length > 0 ? ` | ${product.attributes.join(', ')}` : ''}
+ </p>
+ <div className="flex flex-wrap gap-x-1 items-center mb-2 mt-auto">
+ {product.price.discount_percentage > 0 && (
+ <>
+ <p className="text-caption-2 text-gray_r-11 line-through">{currencyFormat(product.price.price)}</p>
+ <span className="badge-red">{product.price.discount_percentage}%</span>
+ </>
+ )}
+ <p className="text-caption-2 text-gray_r-12">{currencyFormat(product.price.price_discount)}</p>
+ </div>
+ <div className="flex items-center">
+ <p className="mr-auto text-caption-2 text-gray_r-12 font-bold">{currencyFormat(product.quantity * product.price.price_discount)}</p>
+ <div className="flex gap-x-2 items-center">
+ <button
+ className="btn-red p-2 rounded"
+ onClick={() => openConfirmAlert(product.id)}
+ >
+ <TrashIcon className="text-red_r-11 w-3"/>
+ </button>
+ <button
+ className="btn-light p-2 rounded"
+ disabled={product.quantity == 1}
+ onClick={() => minusQuantity(product.id)}
+ >
+ <MinusIcon className={'text-gray_r-12 w-3' + (product.quantity == 1 ? ' text-gray_r-11' : '')}/>
+ </button>
+ <input
+ type="number"
+ className="bg-transparent border-none w-6 text-center outline-none"
+ onBlur={(e) => blurQuantity(product.id, e.target.value)}
+ onChange={(e) => updateQuantity(product.id, e.target.value)}
+ value={product.quantity}
+ />
+ <button className="btn-light p-2 rounded" onClick={() => plusQuantity(product.id)}>
+ <PlusIcon className="text-gray_r-12 w-3"/>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+
+ <div className="p-4 bg-gray_r-1 sticky bottom-0 border-t-4 border-gray_r-4">
+ <div className="flex">
+ <p>Total</p>
+ <p className="text-gray_r-11 ml-1">{getProductsSelected().length > 0 && (
+ <>({ getProductsSelected().length } Barang)</>
+ )}</p>
+ <p className="font-semibold text-red_r-11 ml-auto">{currencyFormat(totalPriceBeforeTax + totalTaxAmount - totalDiscountAmount)}</p>
+ </div>
+
+ <div className="flex gap-x-3 mt-4">
+ <button
+ className="flex-1 btn-light"
+ disabled={getProductsSelected().length == 0}
+ onClick={() => router.push('/shop/quotation')}
+ >
+ Quotation {getProductsSelected().length > 0 && `(${getProductsSelected().length})`}
+ </button>
+ <button
+ className="flex-1 btn-yellow"
+ disabled={getProductsSelected().length == 0}
+ onClick={() => router.push('/shop/checkout')}
+ >
+ Checkout {getProductsSelected().length > 0 && `(${getProductsSelected().length})`}
+ </button>
+ </div>
+ </div>
+ </>
+ ) }
+ </Layout>
+ </>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/shop/checkout/finish.js b/src2/pages/shop/checkout/finish.js
new file mode 100644
index 00000000..df284f8a
--- /dev/null
+++ b/src2/pages/shop/checkout/finish.js
@@ -0,0 +1,47 @@
+import WithAuth from "@/components/auth/WithAuth";
+import Link from "@/components/elements/Link";
+import AppBar from "@/components/layouts/AppBar";
+import Header from "@/components/layouts/Header";
+import Layout from "@/components/layouts/Layout";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useAuth } from "@/core/utils/auth";
+import { EnvelopeIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+
+export default function FinishCheckout() {
+ const router = useRouter();
+ const { id } = router.query;
+ const [ auth ] = useAuth();
+ const [ transaction, setTransactions ] = useState(null);
+
+ useEffect(() => {
+ const loadTransaction = async () => {
+ if (auth && id) {
+ const dataTransaction = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/sale_order/${id}`);
+ setTransactions(dataTransaction);
+ }
+ };
+ loadTransaction();
+ });
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Pembelian Berhasil" />
+
+ <div className="m-4 rounded-xl bg-yellow_r-4 text-center border border-yellow_r-7">
+ <div className="px-4 py-6 text-yellow_r-12">
+ <p className="h2 mb-2">Terima Kasih atas Pembelian Anda</p>
+ <p className="text-yellow_r-11 mb-4 leading-6">Rincian belanja sudah kami kirimkan ke email anda. Mohon dicek kembali. jika tidak menerima email, anda dapat menghubungi kami disini.</p>
+ <p className="mb-2 font-medium">{ transaction?.name }</p>
+ <p className="text-caption-2 text-yellow_r-11">No. Transaksi</p>
+ </div>
+ <Link href={transaction?.id ? `/my/transaction/${transaction.id}` : '/'} className="bg-yellow_r-6 text-yellow_r-12 rounded-b-xl py-4 block">
+ Lihat detail pembelian Anda disini
+ </Link>
+ </div>
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/shop/checkout/index.js b/src2/pages/shop/checkout/index.js
new file mode 100644
index 00000000..0a77ebed
--- /dev/null
+++ b/src2/pages/shop/checkout/index.js
@@ -0,0 +1,325 @@
+import { ExclamationCircleIcon } from "@heroicons/react/24/solid"
+import { useEffect, useState } from "react"
+import Alert from "@/components/elements/Alert"
+import AppBar from "@/components/layouts/AppBar"
+import Layout from "@/components/layouts/Layout"
+import LineDivider from "@/components/elements/LineDivider"
+import Link from "@/components/elements/Link"
+import ProgressBar from "@/components/elements/ProgressBar"
+import Spinner from "@/components/elements/Spinner"
+import apiOdoo from "@/core/utils/apiOdoo"
+import { useAuth } from "@/core/utils/auth"
+import { deleteItemCart, getCart } from "@/core/utils/cart"
+import currencyFormat from "@/core/utils/currencyFormat"
+import { getItemAddress } from "@/core/utils/address"
+import { useRouter } from "next/router"
+import WithAuth from "@/components/auth/WithAuth"
+import { toast } from "react-hot-toast"
+import getFileBase64 from "@/core/utils/getFileBase64"
+import VariantCard from "@/components/variants/VariantCard"
+
+export default function Checkout() {
+ const router = useRouter()
+ const { product_id, qty } = router.query
+ const [ auth ] = useAuth()
+ const [ addresses, setAddresses ] = useState(null)
+ const [ poNumber, setPoNumber ] = useState('')
+ const [ poFile, setPoFile ] = useState('')
+ const [ selectedAddress, setSelectedAddress ] = useState({
+ shipping: null,
+ invoicing: null
+ })
+ const [ selectedPayment, setSelectedPayment ] = useState(null)
+ const [ products, setProducts ] = useState(null)
+ const [ totalAmount, setTotalAmount ] = useState(0)
+ const [ totalDiscountAmount, setTotalDiscountAmount ] = useState(0)
+
+ const [ isLoading, setIsLoading ] = useState(false)
+
+ const payments = [
+ { name: 'BCA', number: '8870-4000-81' },
+ { name: 'MANDIRI', number: '155-0067-6869-75' },
+ ]
+
+ useEffect(() => {
+ const getAddresses = async () => {
+ if (auth) {
+ const dataAddresses = await apiOdoo('GET', `/api/v1/user/${auth.id}/address`)
+ setAddresses(dataAddresses)
+ }
+ }
+ getAddresses()
+ }, [auth])
+
+ useEffect(() => {
+ const getProducts = async () => {
+ let cart = getCart()
+ let productIds = []
+ if (product_id) {
+ productIds = [parseInt(product_id)]
+ } else {
+ productIds = Object
+ .values(cart)
+ .filter((itemCart) => itemCart.selected == true)
+ .map((itemCart) => itemCart.product_id)
+ }
+ if (productIds.length > 0) {
+ productIds = productIds.join(',')
+ let dataProducts = await apiOdoo('GET', `/api/v1/product_variant/${productIds}`)
+ dataProducts = dataProducts.map((product) => {
+ if (product_id) {
+ product.quantity = 1
+ if (qty) product.quantity = parseInt(qty)
+ } else {
+ product.quantity = cart[product.id].quantity
+ }
+ return product
+ })
+ setProducts(dataProducts)
+ }
+ }
+ getProducts()
+ }, [router, auth, product_id, qty])
+
+ useEffect(() => {
+ if (addresses) {
+ const matchAddress = (key) => {
+ const addressToMatch = getItemAddress(key)
+ let foundAddress = addresses.filter((address) => address.id == addressToMatch)
+ if (foundAddress.length > 0) {
+ return foundAddress[0]
+ }
+ return addresses[0]
+ }
+ setSelectedAddress({
+ shipping: matchAddress('shipping'),
+ invoicing: matchAddress('invoicing'),
+ })
+ }
+ }, [addresses])
+
+ useEffect(() => {
+ if (products) {
+ let calculateTotalAmount = 0
+ let calculateTotalDiscountAmount = 0
+ products.forEach(product => {
+ calculateTotalAmount += product.price.price * product.quantity
+ calculateTotalDiscountAmount += (product.price.price - product.price.price_discount) * product.quantity
+ })
+ setTotalAmount(calculateTotalAmount)
+ setTotalDiscountAmount(calculateTotalDiscountAmount)
+ }
+ }, [products])
+
+ const checkout = async () => {
+ if (!selectedPayment) {
+ toast.error('Mohon pilih metode pembayaran', {
+ position: 'bottom-center'
+ })
+ return
+ }
+ if (poFile && poFile.size > 5000000) {
+ toast.error('Maksimal ukuran file adalah 5MB', {
+ position: 'bottom-center'
+ })
+ return
+ }
+ setIsLoading(true)
+ let productOrder = products.map((product) => ({ 'product_id': product.id, 'quantity': product.quantity }))
+ let data = {
+ 'partner_shipping_id': selectedAddress.shipping.id,
+ 'partner_invoice_id': selectedAddress.invoicing.id,
+ 'order_line': JSON.stringify(productOrder),
+ 'type': 'sale_order'
+ }
+ if (poNumber) data.po_number = poNumber
+ if (poFile) data.po_file = await getFileBase64(poFile)
+
+ const checkoutToOdoo = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/checkout`, data)
+ for (const product of products) {
+ deleteItemCart(product.id)
+ }
+ router.push(`/shop/checkout/finish?id=${checkoutToOdoo.id}`)
+ setIsLoading(false)
+ }
+
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title={"Checkout"} />
+ { !products && !addresses && (
+ <div className="flex justify-center items-center gap-x-3 mt-14">
+ <Spinner className="w-10 text-gray_r-8 fill-gray_r-12" />
+ </div>
+ ) }
+
+ { products && addresses && (
+ <>
+ <ProgressBar
+ current={2}
+ labels={['Keranjang', 'Pembayaran', 'Selesai']}
+ />
+
+ <LineDivider/>
+
+ <div className="p-4">
+ <Alert type="info" className="text-caption-2 flex gap-x-3 items-center">
+ <div>
+ <ExclamationCircleIcon className="w-6 text-blue-700"/>
+ </div>
+ <span>Jika mengalami kesulitan dalam melakukan pembelian di website Indoteknik. Hubungi kami disini</span>
+ </Alert>
+ </div>
+
+ <LineDivider/>
+
+ <div className="p-4">
+ <div className="flex justify-between items-center">
+ <h2>Alamat Pengiriman</h2>
+ <Link className="text-caption-1" href="/my/address?select=shipping">Pilih Alamat Lain</Link>
+ </div>
+
+ { selectedAddress.shipping && (
+ <div className="mt-4 text-caption-1">
+ <div className="badge-red mb-2">{ selectedAddress.shipping.type.charAt(0).toUpperCase() + selectedAddress.shipping.type.slice(1) + ' Address' }</div>
+ <p className="font-medium">{ selectedAddress.shipping.name }</p>
+ <p className="mt-2 text-gray_r-11">{ selectedAddress.shipping.mobile }</p>
+ <p className="mt-1 text-gray_r-11">{ selectedAddress.shipping.street }, { selectedAddress.shipping?.city?.name }</p>
+ </div>
+ ) }
+ </div>
+
+ <LineDivider/>
+
+ <div className="p-4 flex flex-col gap-y-4">
+ {products.map((product, index) => (
+ <VariantCard
+ data={product}
+ openOnClick={false}
+ key={index}
+ />
+ ))}
+ </div>
+
+ <LineDivider/>
+
+ <div className="p-4">
+ <div className="flex justify-between items-center">
+ <h2>Ringkasan Pesanan</h2>
+ <p className="text-gray_r-11 text-caption-1">{products.length} Barang</p>
+ </div>
+ <hr className="my-4 border-gray_r-6"/>
+ <div className="flex flex-col gap-y-4">
+ <div className="flex gap-x-2 justify-between">
+ <p>Total Belanja</p>
+ <p className="font-medium">{currencyFormat(totalAmount)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>Total Diskon</p>
+ <p className="font-medium text-red_r-11">- {currencyFormat(totalDiscountAmount)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>Subtotal</p>
+ <p className="font-medium">{currencyFormat(totalAmount - totalDiscountAmount)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>PPN 11% (Incl.)</p>
+ <p className="font-medium">{currencyFormat((totalAmount - totalDiscountAmount) * 0.11)}</p>
+ </div>
+ </div>
+ <hr className="my-4 border-gray_r-6"/>
+ <div className="flex gap-x-2 justify-between mb-4">
+ <p>Grand Total</p>
+ <p className="font-medium text-yellow_r-11">{currencyFormat(totalAmount - totalDiscountAmount)}</p>
+ </div>
+ <p className="text-caption-2 text-gray_r-10 mb-2">*) Belum termasuk biaya pengiriman</p>
+ <p className="text-caption-2 text-gray_r-10 leading-5">
+ Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui <Link href="/">Syarat & Ketentuan</Link> yang berlaku
+ </p>
+ </div>
+
+ <LineDivider/>
+
+ <div className="p-4">
+ <div className="flex justify-between items-center">
+ <h2>Alamat Penagihan</h2>
+ <Link className="text-caption-1" href="/my/address?select=invoicing">Pilih Alamat Lain</Link>
+ </div>
+
+ { selectedAddress.invoicing && (
+ <div className="mt-4 text-caption-1">
+ <div className="badge-red mb-2">{ selectedAddress.invoicing.type.charAt(0).toUpperCase() + selectedAddress.invoicing.type.slice(1) + ' Address' }</div>
+ <p className="font-medium">{ selectedAddress.invoicing.name }</p>
+ <p className="mt-2 text-gray_r-11">{ selectedAddress.invoicing.mobile }</p>
+ <p className="mt-1 text-gray_r-11">{ selectedAddress.invoicing.street } { selectedAddress.invoicing.street2 }</p>
+ </div>
+ ) }
+ </div>
+
+ <LineDivider/>
+
+ <div className="p-4">
+ <h2>Metode Pembayaran <span className="font-normal text-gray_r-11">(Wajib dipilih)</span></h2>
+ <div className="grid gap-y-3 mt-4">
+ { payments.map((payment, index) => (
+ <button
+ type="button"
+ className={"text-left border border-gray_r-6 rounded-md p-3 " + (selectedPayment == payment.name && 'border-yellow_r-10 bg-yellow_r-3')}
+ onClick={() => setSelectedPayment(payment.name)}
+ key={index}
+ >
+ <p>{payment.name} - {payment.number}</p>
+ <p className="mt-1 text-gray_r-11">PT. Indoteknik Dotcom Gemilang</p>
+ </button>
+ )) }
+ </div>
+ </div>
+
+ <LineDivider/>
+
+ <div className="p-4">
+ <h2>Purchase Order</h2>
+
+ <div className="mt-4 flex gap-x-3">
+ <div className="w-6/12">
+ <label className="form-label font-normal">
+ Dokumen PO
+ </label>
+ <input
+ type="file"
+ className="form-input mt-2 h-12"
+ accept="image/*,application/pdf"
+ onChange={(e) => setPoFile(e.target.files[0])}
+ />
+ </div>
+ <div className="w-6/12">
+ <label className="form-label font-normal">Nomor PO</label>
+ <input
+ type="text"
+ className="form-input mt-2 h-12"
+ value={poNumber}
+ onChange={(e) => setPoNumber(e.target.value)}
+ />
+ </div>
+ </div>
+ <p className="text-caption-2 text-gray_r-11 mt-2">Ukuran dokumen PO Maksimal 5MB</p>
+ </div>
+
+ <LineDivider/>
+
+ <div className="flex gap-x-3 p-4">
+ <button
+ className="flex-1 btn-yellow"
+ onClick={checkout}
+ disabled={isLoading}
+ >
+ { isLoading && 'Loading...' }
+ { !isLoading && 'Bayar' }
+ </button>
+ </div>
+ </>
+ ) }
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/shop/product/[slug].js b/src2/pages/shop/product/[slug].js
new file mode 100644
index 00000000..61692c1c
--- /dev/null
+++ b/src2/pages/shop/product/[slug].js
@@ -0,0 +1,305 @@
+import Link from "@/components/elements/Link"
+import { useRouter } from "next/router"
+import { useEffect, useState } from "react"
+import Header from "@/components/layouts/Header"
+import apiOdoo from "@/core/utils/apiOdoo"
+import { createSlug, getIdFromSlug } from "@/core/utils/slug"
+import currencyFormat from "@/core/utils/currencyFormat"
+import Layout from "@/components/layouts/Layout"
+import { createOrUpdateItemCart } from "@/core/utils/cart"
+import toast from "react-hot-toast"
+import Footer from "@/components/layouts/Footer"
+import Image from "@/components/elements/Image"
+import LineDivider from "@/components/elements/LineDivider"
+import { HeartIcon as HeartIconSolid } from "@heroicons/react/24/solid"
+import { useAuth } from "@/core/utils/auth"
+import { HeartIcon } from "@heroicons/react/24/outline"
+import LazyLoad from "react-lazy-load"
+import ProductSimilar from "@/components/products/ProductSimilar"
+
+export async function getServerSideProps( context ) {
+ const { slug } = context.query
+ let product = await apiOdoo('GET', '/api/v1/product/' + getIdFromSlug(slug))
+ if (product?.length == 1) {
+ product = product[0]
+ product.description = product.description.replaceAll('<p>', '||p||')
+ product.description = product.description.replaceAll('</p>', '||/p||')
+ product.description = product.description.replace(/(<([^>]+)>)/gi, ' ')
+ product.description = product.description.replaceAll('||p||', '<p>')
+ product.description = product.description.replaceAll('||/p||', '</p>')
+ product.description = product.description.trim()
+ }
+ return { props: { product } }
+}
+
+export default function ProductDetail({ product }) {
+ const [ auth ] = useAuth()
+ const router = useRouter()
+ const { slug } = router.query
+ const [selectedVariant, setSelectedVariant] = useState("")
+ const [quantity, setQuantity] = useState("1")
+ const [activeVariant, setActiveVariant] = useState({
+ id: product.id,
+ code: product.code,
+ price: product.lowest_price,
+ stock: product.stock_total,
+ weight: product.weight,
+ attributes: '',
+ })
+
+ const [ isAddedToWishlist, setAddedToWishlist ] = useState(false)
+ const [ activeTab, setActiveTab ] = useState('specification')
+
+ const addOrDeleteWishlist = async () => {
+ if (auth) {
+ await apiOdoo('POST', `/api/v1/user/${auth.id}/wishlist/create-or-delete`, {
+ product_id: product.id
+ })
+ if (isAddedToWishlist) {
+ toast.success('Berhasil menghapus dari wishlist')
+ } else {
+ toast.success('Berhasil menambahkan ke wishlist')
+ }
+ setAddedToWishlist(!isAddedToWishlist)
+ } else {
+ toast.error('Login terlebih dahulu untuk melanjutkan')
+ router.push('/login')
+ }
+ }
+
+ useEffect(() => {
+ if (auth) {
+ const checkWishlist = async () => {
+ const wishlist = await apiOdoo('GET', `/api/v1/user/${auth.id}/wishlist?product_id=${product.id}`)
+ setAddedToWishlist(wishlist.product_total > 0 ? true : false)
+ }
+ checkWishlist()
+ }
+ }, [ auth, product ])
+
+ useEffect(() => {
+ if (product.variants.length == 1) {
+ setSelectedVariant(product.variants[0].id)
+ }
+ }, [ product ])
+
+ useEffect(() => {
+ if (selectedVariant != '') {
+ let newActiveVariant = product.variants.filter((variant) => {
+ return variant.id == selectedVariant
+ })
+
+ if (newActiveVariant.length == 1) {
+ newActiveVariant = newActiveVariant[0]
+ setActiveVariant({
+ id: newActiveVariant.id,
+ code: newActiveVariant.code,
+ price: newActiveVariant.price,
+ stock: newActiveVariant.stock,
+ weight: newActiveVariant.weight,
+ attributes: newActiveVariant.attributes.join(', '),
+ })
+ }
+ }
+ }, [selectedVariant, product])
+
+ const onchangeVariant = (e) => {
+ setSelectedVariant(e.target.value)
+ }
+
+ const onChangeQuantity = (e) => {
+ let inputValue = e.target.value
+ inputValue = parseInt(inputValue)
+ inputValue = Math.floor(inputValue)
+ setQuantity(inputValue)
+ }
+
+ const addItemToCart = () => {
+ if (product.variant_total > 1 && !selectedVariant) {
+ toast.error('Pilih varian terlebih dahulu untuk menambahkan ke keranjang', { duration: 2000 })
+ return false
+ }
+
+ if (quantity > 0) {
+ toast.success('Berhasil menambahkan ke keranjang', { duration: 1500 })
+ createOrUpdateItemCart(activeVariant.id, parseInt(quantity))
+ } else {
+ toast.error('Jumlah barang yang ditambahkan minimal 1 pcs', { duration: 2000 })
+ }
+
+ return true
+ }
+
+ const checkoutProduct = () => {
+ if (!auth) {
+ toast.error('Login terlebih dahulu untuk melanjutkan', { duration: 2000 })
+ router.push('/login')
+ return
+ }
+ if (product.variant_total > 1 && !selectedVariant) {
+ toast.error('Pilih varian terlebih dahulu untuk melanjutkan pembelian', { duration: 2000 })
+ return
+ }
+ if (quantity < 0) {
+ toast.error('Jumlah barang yang ditambahkan minimal 1 pcs', { duration: 2000 })
+ return
+ }
+ router.push(`/shop/checkout?product_id=${activeVariant.id}&qty=${quantity}`)
+ }
+
+ const TabButton = ({ children, name }) => (
+ <button
+ type="button"
+ className={`font-medium pb-1 ${activeTab == name ? 'text-red_r-11 border-b border-red_r-10' : 'text-gray_r-11'}`}
+ onClick={() => setActiveTab(name)}
+ >
+ { children }
+ </button>
+ )
+
+ return (
+ <>
+ <Header title={`${product.name} - Indoteknik`}/>
+ <Layout>
+ <Image
+ src={product.image}
+ alt={product.name}
+ className="border-b border-gray_r-6 w-full h-[300px] object-contain object-center bg-white"
+ />
+
+ <div className="p-4">
+ <div className="flex justify-between gap-x-3">
+ <div>
+ <Link href={'/shop/brands/' + createSlug(product.manufacture.name, product.manufacture.id)}>
+ {product.manufacture.name ?? '-'}
+ </Link>
+ <h1 className="h2 mt-2 mb-3">{product.name}{activeVariant.attributes ? ' - ' + activeVariant.attributes : ''}</h1>
+ </div>
+ <button className="h-fit" onClick={addOrDeleteWishlist}>
+ { isAddedToWishlist && (
+ <HeartIconSolid className="w-6 text-red_r-10" />
+ ) }
+ { !isAddedToWishlist && (
+ <HeartIcon className="w-6" />
+ ) }
+ </button>
+ </div>
+
+ {product.variant_total > 1 && !selectedVariant && product.lowest_price.price > 0 ? (
+ <p className="text-caption-2 text-gray-800 mb-1">Harga mulai dari:</p>
+ ) : ''}
+
+ {product.lowest_price.discount_percentage > 0 ? (
+ <div className="flex gap-x-1 items-center mb-1">
+ <p className="text-caption-2 text-gray_r-11 line-through">{currencyFormat(activeVariant.price.price)}</p>
+ <span className="badge-solid-red">{activeVariant.price.discount_percentage}%</span>
+ </div>
+ ) : ''}
+
+ {product.lowest_price.price > 0 ? (
+ <p className="text-body-lg font-semibold">{currencyFormat(activeVariant.price.price_discount)}</p>
+ ) : (
+ <p className="text-gray_r-11">Dapatkan harga terbaik, <a href="">hubungi kami.</a></p>
+ )}
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4">
+ <div className="">
+ <label className="form-label mb-2">Pilih: <span className="text-gray_r-11 font-normal">{product.variant_total} Varian</span></label>
+ <select name="variant" className="form-input" value={selectedVariant} onChange={onchangeVariant} >
+ <option value="" disabled={selectedVariant != "" ? true : false}>Pilih Varian...</option>
+ {product.variants.length > 1 ? (
+ product.variants.map((variant) => {
+ return (
+ <option key={variant.id} value={variant.id}>{variant.attributes.join(', ')}</option>
+ )
+ })
+ ) : (
+ <option key={product.variants[0].id} value={product.variants[0].id}>{product.variants[0].name}</option>
+ )}
+ </select>
+ </div>
+
+ <label htmlFor="quantity" className="form-label mb-1 mt-3">Jumlah</label>
+ <div className="flex gap-x-2 mt-2">
+ <input type="number" name="quantity" id="quantity" className="form-input h-full w-5/12 text-center" value={quantity} onChange={onChangeQuantity} />
+
+ <button
+ className="btn-yellow w-full"
+ onClick={addItemToCart}
+ disabled={(product.lowest_price.price == 0 ? true : false)}
+ >
+ Keranjang
+ </button>
+ <button
+ onClick={checkoutProduct}
+ className="btn-solid-red w-full"
+ >
+ Beli
+ </button>
+ </div>
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4">
+ <h2 className="font-bold mb-4">Informasi Produk</h2>
+ <div className="flex gap-x-3 mb-4">
+ <TabButton name="specification">Spesifikasi</TabButton>
+ <TabButton name="description">Deskripsi</TabButton>
+ <TabButton name="information">Info Penting</TabButton>
+ </div>
+
+ <div className={`border border-gray_r-6 rounded divide-y ${activeTab == 'specification' ? 'block' : 'hidden'}`}>
+ <ProductSpecification label="Jumlah Varian">
+ <p className="text-gray-800">{product.variant_total} Varian</p>
+ </ProductSpecification>
+ <ProductSpecification label="Nomor SKU">
+ <p className="text-gray-800" id="sku_number">SKU-{activeVariant.id}</p>
+ </ProductSpecification>
+ <ProductSpecification label="Part Number">
+ <p className="text-gray-800" id="part_number">{activeVariant.code}</p>
+ </ProductSpecification>
+ <ProductSpecification label="Stok">
+ <div className="flex gap-x-2" id="stock">
+ {activeVariant.stock > 0 ? (activeVariant.stock > 5 && (
+ <>
+ <div className="badge-solid-red">Ready Stock</div>
+ <div className="badge-gray">{activeVariant.stock > 5 ? '> 5' : '< 5'}</div>
+ </>
+ )) : '0'}
+ </div>
+ </ProductSpecification>
+ <ProductSpecification label="Part Number">
+ <p className="text-gray-800" id="weight">{activeVariant.weight > 0 ? activeVariant.weight : '1'} KG</p>
+ </ProductSpecification>
+ </div>
+
+ <div
+ className={`text-gray-800 leading-7 ${activeTab == 'description' ? 'block' : 'hidden'}`}
+ dangerouslySetInnerHTML={{__html: (product.description != '' ? product.description : 'Belum ada deskripsi produk.')}}
+ ></div>
+ </div>
+
+ <LineDivider />
+
+ <LazyLoad>
+ <ProductSimilar productId={getIdFromSlug(slug || '')} />
+ </LazyLoad>
+
+ <Footer />
+ </Layout>
+ </>
+ )
+}
+
+const ProductSpecification = ({ children, ...props }) => {
+ return (
+ <div className="flex p-3 justify-between items-center gap-x-1">
+ <h3 className="text-gray-900">{ props.label }</h3>
+ { children }
+ </div>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/shop/quotation/finish.js b/src2/pages/shop/quotation/finish.js
new file mode 100644
index 00000000..f7983fef
--- /dev/null
+++ b/src2/pages/shop/quotation/finish.js
@@ -0,0 +1,39 @@
+import WithAuth from "@/components/auth/WithAuth";
+import Link from "@/components/elements/Link";
+import Header from "@/components/layouts/Header";
+import Layout from "@/components/layouts/Layout";
+import { useAuth } from "@/core/utils/auth";
+import { EnvelopeIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
+
+export default function FinishQuotation() {
+ const router = useRouter();
+ const { id } = router.query;
+ const [ auth ] = useAuth();
+
+ return (
+ <WithAuth>
+ <Layout>
+ <Header title="Penawaran Harga" />
+
+ <div className="m-4 px-4 py-6 shadow-md border border-gray_r-3">
+ <div className="flex">
+ <span className="p-3 mx-auto bg-yellow_r-3 border border-yellow_r-6 rounded">
+ <EnvelopeIcon className="w-8 text-yellow_r-11" />
+ </span>
+ </div>
+ <p className="h2 text-center mt-6">
+ Terima Kasih { auth?.name }
+ </p>
+ <p className="text-center mt-3 leading-6 text-gray_r-11">
+ Penawaran harga kamu di Indoteknik.com berhasil dikirimkan, tim kami akan segera menghubungi anda.
+ </p>
+ { id && (
+ <Link href={`/my/transaction/${id}`} className="btn-yellow text-gray_r-12 mt-6 w-full">Lihat Penawaran</Link>
+ )}
+ <Link href="/" className="btn-light text-gray_r-12 mt-2 w-full">Ke Halaman Utama</Link>
+ </div>
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src2/pages/shop/quotation/index.js b/src2/pages/shop/quotation/index.js
new file mode 100644
index 00000000..e1c196db
--- /dev/null
+++ b/src2/pages/shop/quotation/index.js
@@ -0,0 +1,140 @@
+import WithAuth from "@/components/auth/WithAuth";
+import LineDivider from "@/components/elements/LineDivider";
+import Link from "@/components/elements/Link";
+import AppBar from "@/components/layouts/AppBar";
+import Layout from "@/components/layouts/Layout";
+import VariantCard from "@/components/variants/VariantCard";
+import apiOdoo from "@/core/utils/apiOdoo";
+import { useAuth } from "@/core/utils/auth";
+import { deleteItemCart, getCart } from "@/core/utils/cart";
+import currencyFormat from "@/core/utils/currencyFormat";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+import { toast } from "react-hot-toast";
+
+export default function Quotation() {
+ const router = useRouter();
+ const [ auth ] = useAuth();
+ const [ products, setProducts ] = useState([]);
+ const [ totalAmount, setTotalAmount ] = useState(0);
+ const [ totalDiscountAmount, setTotalDiscountAmount ] = useState(0);
+
+ useEffect(() => {
+ const getProducts = async () => {
+ let cart = getCart();
+ let productIds = Object
+ .values(cart)
+ .filter((itemCart) => itemCart.selected == true)
+ .map((itemCart) => itemCart.product_id);
+ if (productIds.length > 0) {
+ productIds = productIds.join(',');
+ let dataProducts = await apiOdoo('GET', `/api/v1/product_variant/${productIds}`);
+ dataProducts = dataProducts.map((product) => ({
+ ...product,
+ quantity: cart[product.id].quantity,
+ selected: cart[product.id].selected,
+ }));
+ setProducts(dataProducts);
+ }
+ };
+ getProducts();
+ }, [ router, auth ]);
+
+ useEffect(() => {
+ if (products) {
+ let calculateTotalAmount = 0;
+ let calculateTotalDiscountAmount = 0;
+ products.forEach(product => {
+ calculateTotalAmount += product.price.price * product.quantity;
+ calculateTotalDiscountAmount += (product.price.price - product.price.price_discount) * product.quantity;
+ });
+ setTotalAmount(calculateTotalAmount);
+ setTotalDiscountAmount(calculateTotalDiscountAmount);
+ }
+ }, [products]);
+
+ const submitQuotation = async () => {
+ let productOrder = products.map((product) => ({ 'product_id': product.id, 'quantity': product.quantity }));
+ let data = {
+ 'partner_shipping_id': auth.partner_id,
+ 'partner_invoice_id': auth.partner_id,
+ 'order_line': JSON.stringify(productOrder)
+ };
+ const quotation = await apiOdoo('POST', `/api/v1/partner/${auth.partner_id}/sale_order/checkout`, data);
+ for (const product of products) {
+ deleteItemCart(product.id);
+ }
+ if (quotation?.id) {
+ router.push(`/shop/quotation/finish?id=${quotation.id}`);
+ return;
+ };
+ toast.error('Terdapat kesalahan internal, hubungi kami');
+ }
+ return (
+ <WithAuth>
+ <Layout>
+ <AppBar title="Penawaran Harga" />
+
+ <div className="p-4 flex flex-col gap-y-4">
+ <p className="h2">Produk</p>
+ {products.map((product, index) => (
+ <VariantCard
+ data={product}
+ openOnClick={false}
+ key={index}
+ />
+ ))}
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4">
+ <div className="flex justify-between items-center">
+ <p className="h2">Ringkasan Penawaran</p>
+ <p className="text-gray_r-11 text-caption-1">{products.length} Barang</p>
+ </div>
+ <hr className="my-4 border-gray_r-6"/>
+ <div className="flex flex-col gap-y-4">
+ <div className="flex gap-x-2 justify-between">
+ <p>Total Belanja</p>
+ <p className="font-medium">{currencyFormat(totalAmount)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>Total Diskon</p>
+ <p className="font-medium text-red_r-11">{'- ' + currencyFormat(totalDiscountAmount)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>Subtotal</p>
+ <p className="font-medium">{currencyFormat(totalAmount - totalDiscountAmount)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>PPN 11% (Incl.)</p>
+ <p className="font-medium">{currencyFormat((totalAmount - totalDiscountAmount) * 0.11)}</p>
+ </div>
+ </div>
+ <hr className="my-4 border-gray_r-6"/>
+ <div className="flex gap-x-2 justify-between mb-4">
+ <p>Grand Total</p>
+ <p className="font-medium text-yellow_r-11">{currencyFormat(totalAmount - totalDiscountAmount)}</p>
+ </div>
+ <p className="text-caption-2 text-gray_r-10 mb-2">*) Belum termasuk biaya pengiriman</p>
+ <p className="text-caption-2 text-gray_r-10 leading-5">
+ Dengan melakukan pembelian melalui website Indoteknik, saya menyetujui <Link href="/">Syarat & Ketentuan</Link> yang berlaku
+ </p>
+ </div>
+
+ <LineDivider />
+
+ <div className="p-4">
+ <button
+ type="button"
+ className="btn-yellow w-full"
+ onClick={submitQuotation}
+ >
+ Kirim Penawaran
+ </button>
+ </div>
+ </Layout>
+ </WithAuth>
+ )
+} \ No newline at end of file
diff --git a/src2/pages/shop/search.js b/src2/pages/shop/search.js
new file mode 100644
index 00000000..4152bd43
--- /dev/null
+++ b/src2/pages/shop/search.js
@@ -0,0 +1,125 @@
+import axios from "axios";
+import Header from "@/components/layouts/Header";
+import Layout from "@/components/layouts/Layout";
+import Pagination from "@/components/elements/Pagination";
+import ProductCard from "@/components/products/ProductCard";
+import FilterIcon from "@/icons/filter.svg";
+import { useEffect, useState } from "react";
+import Filter from "@/components/elements/Filter";
+import Footer from "@/components/layouts/Footer";
+
+export async function getServerSideProps(context) {
+ const {
+ q = '*',
+ page = 1,
+ brand = '',
+ category = '',
+ price_from = '',
+ price_to = '',
+ order_by = '',
+ } = context.query;
+
+ let urlParameter = [
+ `page=${page}`,
+ `brand=${brand}`,
+ `category=${category}`,
+ `price_from=${price_from}`,
+ `price_to=${price_to}`,
+ `order_by=${order_by}`
+ ].join('&');
+ let searchResults = await axios(`${process.env.SELF_HOST}/api/shop/search?q=${q}&${urlParameter}`);
+ searchResults = searchResults.data;
+ return { props: { searchResults, q, page, brand, category, price_from, price_to, order_by } };
+}
+
+export default function ShopSearch({
+ searchResults,
+ q,
+ page,
+ brand,
+ category,
+ price_from,
+ price_to,
+ order_by
+}) {
+ const pageCount = Math.ceil(searchResults.response.numFound / searchResults.responseHeader.params.rows);
+ const productStart = searchResults.responseHeader.params.start;
+ const productRows = searchResults.responseHeader.params.rows;
+ const productFound = searchResults.response.numFound;
+
+ // Variable for <Filter/> props state
+ const [activeFilter, setActiveFilter] = useState(false);
+ const [filterCount, setFilterCount] = useState(0);
+
+ const route = () => {
+ let route = `/shop/search?q=${q}`;
+ if (brand) route += `&brand=${brand}`;
+ if (category) route += `&category=${category}`;
+ if (price_from) route += `&price_from=${price_from}`;
+ if (price_to) route += `&price_to=${price_to}`;
+ if (order_by) route += `&order_by=${order_by}`;
+ return route;
+ }
+
+ useEffect(() => {
+ let calculateFilterCount = 0;
+ if (brand) calculateFilterCount++;
+ if (category) calculateFilterCount++;
+ if (price_from || price_to) calculateFilterCount++;
+ if (order_by) calculateFilterCount++;
+ setFilterCount(calculateFilterCount);
+ }, [brand, category, price_from, price_to, order_by]);
+
+ return (
+ <>
+ <Header title={`Jual ${q} - Indoteknik`} />
+ <Filter
+ defaultRoute={`/shop/search?q=${q}`}
+ isActive={activeFilter}
+ closeFilter={() => setActiveFilter(false)}
+ defaultPriceFrom={price_from}
+ defaultPriceTo={price_to}
+ defaultBrand={brand}
+ defaultCategory={category}
+ defaultOrderBy={order_by}
+ searchResults={searchResults}
+ />
+ <Layout>
+ <div className="p-4">
+ <h1 className="mb-2">Produk</h1>
+ <div className="text-caption-1 mb-4">
+ {productFound > 0 ? (
+ <>
+ Menampilkan&nbsp;
+ {pageCount > 1 ? (
+ <>
+ {productStart + 1}-{
+ (productStart + productRows) > productFound ? productFound : productStart + productRows
+ }
+ &nbsp;dari&nbsp;
+ </>
+ ) : ''}
+ {searchResults.response.numFound}
+ &nbsp;produk { q != '*' && (<>untuk pencarian <span className="font-semibold">{q}</span></>) }
+ </>
+ ) : 'Mungkin yang anda cari'}
+ </div>
+ <button className="btn-light py-2 flex items-center gap-x-2 mb-4" onClick={() => setActiveFilter(true)}>
+ <FilterIcon className="w-4 h-4" /> <span>Filter {filterCount > 0 ? `(${filterCount})` : ''}</span>
+ </button>
+ <div className="grid grid-cols-2 gap-3">
+ {searchResults.response.products.map((product) => (
+ <ProductCard key={product.id} data={product} />
+ ))}
+ </div>
+
+ <div className="mt-4">
+ <Pagination pageCount={pageCount} currentPage={parseInt(page)} url={route()} />
+ </div>
+ </div>
+
+ <Footer />
+ </Layout>
+ </>
+ )
+} \ No newline at end of file