summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRafi Zadanly <zadanlyr@gmail.com>2023-01-31 14:32:38 +0700
committerRafi Zadanly <zadanlyr@gmail.com>2023-01-31 14:32:38 +0700
commit3496025d97140268dc2e899adca994b5b9f343c0 (patch)
tree3d8b51fc624b09a7ba46409e9e8a81fbd02da582 /src
parentd194dcc23c19a4cf31863b32770f8df77e1f675a (diff)
quotation and categories
Diffstat (limited to 'src')
-rw-r--r--src/components/layouts/AppBar.js2
-rw-r--r--src/components/layouts/Header.js89
-rw-r--r--src/components/transactions/TransactionStatusBadge.js41
-rw-r--r--src/pages/my/transaction/[id].js5
-rw-r--r--src/pages/my/transactions.js6
-rw-r--r--src/pages/my/wishlist.js20
-rw-r--r--src/pages/shop/cart.js1
-rw-r--r--src/pages/shop/checkout.js1
-rw-r--r--src/pages/shop/product/[slug].js2
-rw-r--r--src/pages/shop/quotation/finish.js39
-rw-r--r--src/pages/shop/quotation/index.js142
-rw-r--r--src/pages/shop/search.js4
-rw-r--r--src/styles/globals.css12
13 files changed, 349 insertions, 15 deletions
diff --git a/src/components/layouts/AppBar.js b/src/components/layouts/AppBar.js
index b4c7703c..c3d38707 100644
--- a/src/components/layouts/AppBar.js
+++ b/src/components/layouts/AppBar.js
@@ -43,7 +43,7 @@ const AppBar = ({ title }) => {
{/* --- Start Icons */}
<div className="flex gap-x-4 items-center">
- <Link href="/">
+ <Link href="/my/wishlist">
<HeartIcon className="w-6 stroke-2 text-gray_r-12"/>
</Link>
<Link href="/">
diff --git a/src/components/layouts/Header.js b/src/components/layouts/Header.js
index 4741905f..bc740810 100644
--- a/src/components/layouts/Header.js
+++ b/src/components/layouts/Header.js
@@ -1,5 +1,5 @@
import Image from "next/image";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import axios from "axios";
@@ -9,7 +9,9 @@ import {
ShoppingCartIcon,
ChevronRightIcon,
Cog6ToothIcon,
- HeartIcon
+ HeartIcon,
+ ChevronDownIcon,
+ ChevronUpIcon
} from "@heroicons/react/24/outline";
// Helpers
@@ -19,11 +21,11 @@ import Link from "../elements/Link";
// Images
import Logo from "@/images/logo.png";
import greeting from "@/core/utils/greeting";
+import apiOdoo from "@/core/utils/apiOdoo";
const menus = [
{ name: 'Semua Brand', href: '/shop/brands' },
{ name: 'Blog Indoteknik', href: '/' },
- { name: 'Kategori', href: '/' },
];
export default function Header({ title }) {
@@ -71,6 +73,47 @@ export default function Header({ title }) {
}
}
+ const [ isOpenCategory, setOpenCategory ] = useState(false);
+ const [ categories, setCategories ] = useState([]);
+
+ useEffect(() => {
+ const loadCategories = async () => {
+ if (isOpenCategory && categories.length == 0) {
+ let dataCategories = await apiOdoo('GET', '/api/v1/category/tree');
+ dataCategories = dataCategories.map((category) => {
+ category.childs = category.childs.map((child1Category) => {
+ return {
+ ...child1Category,
+ isOpen: false
+ }
+ })
+ return {
+ ...category,
+ isOpen: false
+ }
+ });
+ setCategories(dataCategories);
+ }
+ }
+ loadCategories();
+ }, [ isOpenCategory, categories ]);
+
+ const toggleCategories = (id = 0) => {
+ let newCategories = categories.map((category) => {
+ category.childs = category.childs.map((child1Category) => {
+ return {
+ ...child1Category,
+ isOpen: id == child1Category.id ? !child1Category.isOpen : child1Category.isOpen
+ }
+ })
+ return {
+ ...category,
+ isOpen: id == category.id ? !category.isOpen : category.isOpen
+ }
+ });
+ setCategories(newCategories);
+ }
+
return (
<>
<Head>
@@ -108,6 +151,46 @@ export default function Header({ title }) {
</div>
</Link>
)) }
+ <div className="flex w-full font-normal text-gray_r-11 border-b border-gray_r-6 p-4" onClick={() => setOpenCategory(!isOpenCategory)}>
+ <span>Kategori</span>
+ <div className="ml-auto">
+ { !isOpenCategory && <ChevronDownIcon className="text-gray_r-12 w-5" /> }
+ { isOpenCategory && <ChevronUpIcon className="text-gray_r-12 w-5" /> }
+ </div>
+ </div>
+ { isOpenCategory && categories.map((category) => (
+ <Fragment key={category.id}>
+ <div className="flex w-full text-gray_r-11 border-b border-gray_r-6 px-4 pl-8">
+ <Link href={`/shop/search?category=${category.name}`} className="flex-1 font-normal text-gray_r-11 py-4">
+ { category.name }
+ </Link>
+ <div className="ml-3 h-full py-4" onClick={() => toggleCategories(category.id)}>
+ { !category.isOpen && <ChevronDownIcon className="text-gray_r-12 w-5" /> }
+ { category.isOpen && <ChevronUpIcon className="text-gray_r-12 w-5" /> }
+ </div>
+ </div>
+ { category.isOpen && category.childs.map((child1Category) => (
+ <Fragment key={child1Category.id}>
+ <div className="flex w-full text-gray_r-11 border-b border-gray_r-6 p-4 pl-12">
+ <Link href={`/shop/search?category=${child1Category.name}`} className="flex-1 font-normal text-gray_r-11">
+ { child1Category.name }
+ </Link>
+ { child1Category.childs.length > 0 && (
+ <div className="ml-3 h-full" onClick={() => toggleCategories(child1Category.id)}>
+ { !child1Category.isOpen && <ChevronDownIcon className="text-gray_r-12 w-5" /> }
+ { child1Category.isOpen && <ChevronUpIcon className="text-gray_r-12 w-5" /> }
+ </div>
+ ) }
+ </div>
+ { child1Category.isOpen && child1Category.childs.map((child2Category) => (
+ <Link key={child2Category.id} href={`/shop/search?category=${child2Category.name}`} className="flex w-full font-normal text-gray_r-11 border-b border-gray_r-6 p-4 pl-16">
+ { child2Category.name }
+ </Link>
+ )) }
+ </Fragment>
+ )) }
+ </Fragment>
+ )) }
</div>
</div>
<div className={isMenuActive ? 'menu-overlay block opacity-100' : 'menu-overlay hidden opacity-0'} onClick={closeMenu}></div>
diff --git a/src/components/transactions/TransactionStatusBadge.js b/src/components/transactions/TransactionStatusBadge.js
new file mode 100644
index 00000000..588542fe
--- /dev/null
+++ b/src/components/transactions/TransactionStatusBadge.js
@@ -0,0 +1,41 @@
+const TransactionStatusBadge = ({ status }) => {
+ let badgeProps = {
+ className: ['h-fit'],
+ text: ''
+ };
+ switch (status) {
+ case 'cancel':
+ badgeProps.className.push('badge-red');
+ badgeProps.text = 'Batal'
+ break;
+ case 'draft':
+ badgeProps.className.push('badge-gray');
+ badgeProps.text = 'Pending Quotation'
+ break;
+ case 'waiting':
+ badgeProps.className.push('badge-yellow');
+ badgeProps.text = 'Menunggu Konfirmasi'
+ break;
+ case 'sale':
+ badgeProps.className.push('badge-blue');
+ badgeProps.text = 'Pesanan Diproses'
+ break;
+ case 'shipping':
+ badgeProps.className.push('badge-blue');
+ badgeProps.text = 'Pesanan Dikirim'
+ break;
+ case 'done':
+ badgeProps.className.push('badge-green');
+ badgeProps.text = 'Selesai'
+ break;
+ }
+ badgeProps.className = badgeProps.className.join(' ');
+
+ return (
+ <div className={badgeProps.className}>
+ { badgeProps.text }
+ </div>
+ )
+};
+
+export default TransactionStatusBadge; \ No newline at end of file
diff --git a/src/pages/my/transaction/[id].js b/src/pages/my/transaction/[id].js
index 4c33c97a..a508ef77 100644
--- a/src/pages/my/transaction/[id].js
+++ b/src/pages/my/transaction/[id].js
@@ -15,6 +15,7 @@ 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";
export default function DetailTransaction() {
const router = useRouter();
@@ -41,7 +42,9 @@ export default function DetailTransaction() {
<>
<div className="p-4 flex flex-col gap-y-4">
<DescriptionRow label="Status Transaksi">
- <span className="badge-green">Pending Quotation</span>
+ <div className="flex justify-end">
+ <TransactionStatusBadge status={transaction?.status} />
+ </div>
</DescriptionRow>
<DescriptionRow label="No Transaksi">
{ transaction?.name }
diff --git a/src/pages/my/transactions.js b/src/pages/my/transactions.js
index 85f0935f..221859f9 100644
--- a/src/pages/my/transactions.js
+++ b/src/pages/my/transactions.js
@@ -11,6 +11,7 @@ import { EllipsisVerticalIcon, MagnifyingGlassIcon } from "@heroicons/react/24/o
import Link from "@/components/elements/Link";
import Pagination from "@/components/elements/Pagination";
import Alert from "@/components/elements/Alert";
+import TransactionStatusBadge from "@/components/transactions/TransactionStatusBadge";
export default function Transactions() {
const [ auth ] = useAuth();
@@ -40,7 +41,8 @@ export default function Transactions() {
const dataTransactions = await apiOdoo('GET', `/api/v1/partner/${auth.partner_id}/sale_order${queryParams}`);
setTransactions(dataTransactions);
- setPageCount(Math.ceil(dataTransactions.sale_order_total / limit));
+ console.log(dataTransactions);
+ setPageCount(Math.ceil(dataTransactions?.sale_order_total / limit));
setIsLoading(false);
};
}
@@ -88,7 +90,7 @@ export default function Transactions() {
<h2 className="text-red_r-11 mt-1">{ transaction.name }</h2>
</Link>
<div className="flex gap-x-1 justify-end">
- <div className="badge-green h-fit">Pending</div>
+ <TransactionStatusBadge status={transaction.status} />
<EllipsisVerticalIcon className="w-5 h-5" onClick={() => setActivePopupId(transaction.id)} />
</div>
</div>
diff --git a/src/pages/my/wishlist.js b/src/pages/my/wishlist.js
index 175bfa08..9683c785 100644
--- a/src/pages/my/wishlist.js
+++ b/src/pages/my/wishlist.js
@@ -1,24 +1,33 @@
import WithAuth from "@/components/auth/WithAuth";
+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`);
+ 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 ]);
+ }, [ auth, page ]);
return (
<WithAuth>
@@ -26,11 +35,18 @@ export default function Wishlist() {
<AppBar title='Wishlist' />
<div className="px-4 py-6">
+ { !wishlists && (
+ <Spinner className="w-6 h-6 text-gray-600 fill-gray-900 mx-auto" />
+ ) }
<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>
diff --git a/src/pages/shop/cart.js b/src/pages/shop/cart.js
index 53d5e648..aaf67e1f 100644
--- a/src/pages/shop/cart.js
+++ b/src/pages/shop/cart.js
@@ -276,6 +276,7 @@ export default function Cart() {
<button
className="flex-1 btn-light"
disabled={getProductsSelected().length == 0}
+ onClick={() => router.push('/shop/quotation')}
>
Quotation {getProductsSelected().length > 0 && `(${getProductsSelected().length})`}
</button>
diff --git a/src/pages/shop/checkout.js b/src/pages/shop/checkout.js
index 49d1a848..f55b200f 100644
--- a/src/pages/shop/checkout.js
+++ b/src/pages/shop/checkout.js
@@ -2,7 +2,6 @@ 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 Image from "@/components/elements/Image";
import Layout from "@/components/layouts/Layout";
import LineDivider from "@/components/elements/LineDivider";
import Link from "@/components/elements/Link";
diff --git a/src/pages/shop/product/[slug].js b/src/pages/shop/product/[slug].js
index dd097bbe..bcfb12ba 100644
--- a/src/pages/shop/product/[slug].js
+++ b/src/pages/shop/product/[slug].js
@@ -149,7 +149,7 @@ export default function ProductDetail({ product }) {
/>
<div className="p-4">
- <div className="flex justify-between">
+ <div className="flex justify-between gap-x-3">
<div>
<Link href={'/shop/brands/' + createSlug(product.manufacture.name, product.manufacture.id)}>
{product.manufacture.name ?? '-'}
diff --git a/src/pages/shop/quotation/finish.js b/src/pages/shop/quotation/finish.js
new file mode 100644
index 00000000..b1f092d3
--- /dev/null
+++ b/src/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-4 w-full">Ke Halaman Utama</Link>
+ </div>
+ </Layout>
+ </WithAuth>
+ );
+} \ No newline at end of file
diff --git a/src/pages/shop/quotation/index.js b/src/pages/shop/quotation/index.js
new file mode 100644
index 00000000..cf2b956d
--- /dev/null
+++ b/src/pages/shop/quotation/index.js
@@ -0,0 +1,142 @@
+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 [ totalPriceBeforeTax, setTotalPriceBeforeTax ] = useState(0);
+ const [ totalTaxAmount, setTotalTaxAmount ] = 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) {
+ 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 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>Subtotal</p>
+ <p className="font-medium">{currencyFormat(totalPriceBeforeTax)}</p>
+ </div>
+ <div className="flex gap-x-2 justify-between">
+ <p>PPN 11%</p>
+ <p className="font-medium">{currencyFormat(totalTaxAmount)}</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>
+ <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(totalPriceBeforeTax + totalTaxAmount - 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/src/pages/shop/search.js b/src/pages/shop/search.js
index 2521c7a2..4152bd43 100644
--- a/src/pages/shop/search.js
+++ b/src/pages/shop/search.js
@@ -10,7 +10,7 @@ import Footer from "@/components/layouts/Footer";
export async function getServerSideProps(context) {
const {
- q,
+ q = '*',
page = 1,
brand = '',
category = '',
@@ -100,7 +100,7 @@ export default function ShopSearch({
</>
) : ''}
{searchResults.response.numFound}
- &nbsp;produk untuk pencarian <span className="font-semibold">{q}</span>
+ &nbsp;produk { q != '*' && (<>untuk pencarian <span className="font-semibold">{q}</span></>) }
</>
) : 'Mungkin yang anda cari'}
</div>
diff --git a/src/styles/globals.css b/src/styles/globals.css
index e43ad1a9..3a890a0e 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -56,6 +56,7 @@ html, body {
.badge-red,
.badge-gray,
.badge-yellow,
+ .badge-blue,
.badge-green {
@apply
text-caption-2
@@ -84,8 +85,15 @@ html, body {
.badge-yellow {
@apply
- bg-yellow_r-5
- text-yellow_r-10
+ bg-yellow_r-3
+ text-yellow_r-11
+ ;
+ }
+
+ .badge-blue {
+ @apply
+ bg-blue-200
+ text-blue-600
;
}