From d386ef690d814f556a33dbb699da0ad81e4e9bdf Mon Sep 17 00:00:00 2001 From: Aji Kamaludin Date: Wed, 18 Aug 2021 19:46:27 +0700 Subject: [PATCH] products done, selection input hook --- package-lock.json | 106 +++++++++++++++++++---- package.json | 1 + src/api/index.js | 9 +- src/components/Common/Card.js | 2 +- src/components/Common/Hooks.js | 6 +- src/components/Common/Input.js | 36 +++++++- src/components/Common/Pagination.js | 5 +- src/views/categories/List.js | 13 +-- src/views/categories/Modal.js | 72 ++++++++++++++++ src/views/products/Api.js | 59 +++++++++++++ src/views/products/Create.js | 84 +++++++++++++++++-- src/views/products/Detail.js | 7 -- src/views/products/Edit.js | 126 ++++++++++++++++++++++++++++ src/views/products/List.js | 102 ++++++++++++++++++++-- src/views/products/index.js | 4 +- 15 files changed, 572 insertions(+), 60 deletions(-) create mode 100644 src/views/categories/Modal.js create mode 100644 src/views/products/Api.js delete mode 100644 src/views/products/Detail.js create mode 100644 src/views/products/Edit.js diff --git a/package-lock.json b/package-lock.json index 4fb933f..f5db124 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "axios": "^0.21.1", "framer-motion": "^4.1.17", "is_js": "^0.9.0", + "query-string": "^7.0.1", "react": "^17.0.2", "react-datepicker": "^4.2.0", "react-dom": "^17.0.2", @@ -10264,6 +10265,14 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -15310,6 +15319,26 @@ "node": ">=4" } }, + "node_modules/normalize-url/node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url/node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -17609,15 +17638,20 @@ } }, "node_modules/query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", + "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", "dependencies": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/querystring": { @@ -20033,6 +20067,14 @@ "node": ">= 6" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -20221,11 +20263,11 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/string_decoder": { @@ -31187,6 +31229,11 @@ "to-regex-range": "^5.0.1" } }, + "filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -35013,6 +35060,22 @@ "prepend-http": "^1.0.0", "query-string": "^4.1.0", "sort-keys": "^1.0.0" + }, + "dependencies": { + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + } } }, "npm-run-path": { @@ -36841,12 +36904,14 @@ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz", + "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==", "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" } }, "querystring": { @@ -38765,6 +38830,11 @@ } } }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -38922,9 +38992,9 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" }, "string_decoder": { "version": "1.3.0", diff --git a/package.json b/package.json index b0c232a..31b27b7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "axios": "^0.21.1", "framer-motion": "^4.1.17", "is_js": "^0.9.0", + "query-string": "^7.0.1", "react": "^17.0.2", "react-datepicker": "^4.2.0", "react-dom": "^17.0.2", diff --git a/src/api/index.js b/src/api/index.js index a0439d5..3be4844 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,8 +1,9 @@ import useSWR from "swr" +import qs from "query-string" -export function useProducts(user, { page = 1, q = '' }) { +export function useProducts(user, params) { const { data, error } = useSWR([ - `/products?page=${page}&q=${q}`, user.accessToken + `/products?${qs.stringify(params)}`, user.accessToken ]) return [ @@ -11,9 +12,9 @@ export function useProducts(user, { page = 1, q = '' }) { ] } -export function useCategories(user, { page = 1, q }) { +export function useCategories(user, params) { const { data, error } = useSWR([ - `/categories?page=${page}&q=${q}`, user.accessToken + `/categories?${qs.stringify(params)}`, user.accessToken ]) return [ diff --git a/src/components/Common/Card.js b/src/components/Common/Card.js index 308c61d..d1f1e46 100644 --- a/src/components/Common/Card.js +++ b/src/components/Common/Card.js @@ -2,7 +2,7 @@ import { Box } from "@chakra-ui/react" export default function Card(props) { return ( - + {props.children} ) diff --git a/src/components/Common/Hooks.js b/src/components/Common/Hooks.js index ae8dfd0..9be55ec 100644 --- a/src/components/Common/Hooks.js +++ b/src/components/Common/Hooks.js @@ -9,9 +9,9 @@ export function useModalState(init = false) { setVal(data) } - return { + return [ isOpen, toggle, - selected: val - } + val + ] } \ No newline at end of file diff --git a/src/components/Common/Input.js b/src/components/Common/Input.js index 403159b..2f78d81 100644 --- a/src/components/Common/Input.js +++ b/src/components/Common/Input.js @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; -import { FormControl, FormLabel, Input, Box, InputGroup, InputRightElement } from "@chakra-ui/react"; +import { FormControl, FormLabel, Input, Box, InputGroup, InputRightElement, InputRightAddon } from "@chakra-ui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import NumberFormat from "react-number-format"; export function SearchInput({ setter: [search, setSearch] }) { return ( - + + + {name} + + + {onRemove && val ? + ( + } + onClick={onRemove} + {...props} + /> + ) : + ( } {...props}/> ) + } + + + + ) +} + export function FormInputNumber(props) { const { data:[name, val, setVal] } = props return ( diff --git a/src/components/Common/Pagination.js b/src/components/Common/Pagination.js index 41e38c0..4a39267 100644 --- a/src/components/Common/Pagination.js +++ b/src/components/Common/Pagination.js @@ -16,9 +16,10 @@ export function usePaginate() {} ) } -export function Pagination({ page = 1, setPage, totalPages = 1}) { +export function Pagination(props) { + const { page = 1, setPage, totalPages = 1, ...restProps} = props return ( - + setPage(+page - 1 <= 0 ? 1 : +page - 1)} diff --git a/src/views/categories/List.js b/src/views/categories/List.js index 148b888..a6f72de 100644 --- a/src/views/categories/List.js +++ b/src/views/categories/List.js @@ -18,6 +18,7 @@ import { import { Link } from "react-router-dom" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { mutate } from 'swr' +import qs from "query-string" import { Breadcrumb, Card, @@ -38,11 +39,11 @@ export default function List() { const [page, setPage] = useState(1) const [search, setSearch] = useState('') - const q = useDebounce(search, 300) + const q = useDebounce(search, 600) + const params = { page, q } + const [data, error] = useCategories(user, params) - const [data, error] = useCategories(user, { page, q }) - - const {isOpen, toggle, selected} = useModalState(false) + const [isOpen, toggle, selected] = useModalState(false) const handleDelete = async () => { await deleteCategory(selected.id, user.accessToken) @@ -55,7 +56,7 @@ export default function List() { duration: 4000, isClosable: true }) - mutate([`/categories?page=${page}&q=${q}`, user.accessToken]) + mutate([`/categories?${qs.stringify(params)}`, user.accessToken]) }) .catch((err) => { toast({ @@ -82,7 +83,7 @@ export default function List() { <> - diff --git a/src/views/categories/Modal.js b/src/views/categories/Modal.js new file mode 100644 index 0000000..9a18f83 --- /dev/null +++ b/src/views/categories/Modal.js @@ -0,0 +1,72 @@ +import { useState } from "react" +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + Table, + Thead, + Tr,Th,Td, + Tbody, + Alert, + AlertIcon +} from "@chakra-ui/react" +import { Loading, Pagination, SearchInput, useDebounce } from "../../components/Common" +import { useAuth } from "../../context/AppContext" +import { useCategories } from "../../api" + +export default function ModalCom(props) { + const { isOpen, toggle, onClose } = props + const { user } = useAuth() + + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const q = useDebounce(search, 600) + + const [data, error] = useCategories(user, { page, q }) + + return ( + + + + kategori + + + + {error && ( + + + {error.message} + + )} + {data ? ( + <> + + + + + + + + {data.categories.map((category) => ( + { + onClose(category) + toggle() + }} _hover={{bg: "gray.200"}}> + + + ))} + +
nama
{ category.name }
+ + + ) : ( + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/views/products/Api.js b/src/views/products/Api.js new file mode 100644 index 0000000..46fa9d8 --- /dev/null +++ b/src/views/products/Api.js @@ -0,0 +1,59 @@ +import axios from "axios"; + +export function createProduct(payload, token) { + return axios({ + method: 'POST', + url: '/products', + data: payload, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data) + .catch(err => { + throw err.response.data + }) +} + +export function getProduct(id, token){ + return axios({ + method: 'GET', + url: `/products/${id}`, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data.data) + .catch(err => { + throw err.response.data + }) +} + +export function updateProduct(id, payload, token) { + return axios({ + method: 'PUT', + url: `/products/${id}`, + data: payload, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data) + .catch(err => { + throw err.response.data + }) +} + +export function deleteProduct(id, token) { + return axios({ + method: 'DELETE', + url: `/products/${id}`, + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.data) + .catch(err => { + throw err.response.data + }) +} \ No newline at end of file diff --git a/src/views/products/Create.js b/src/views/products/Create.js index 6d3a236..2a82a01 100644 --- a/src/views/products/Create.js +++ b/src/views/products/Create.js @@ -1,29 +1,97 @@ -import { - Button, -} from "@chakra-ui/react" +import { Alert, AlertIcon, Button, useToast } from "@chakra-ui/react" import { useState } from "react" -import { Breadcrumb, Card, FormInput, FormInputNumber } from "../../components/Common" +import { + Breadcrumb, + Card, + FormInput, + FormInputNumber, + FormInputSelection, + useModalState +} from "../../components/Common" +import { createProduct } from "./Api" +import CategoryModalSelection from "../categories/Modal" +import { useAuth } from "../../context/AppContext" + +export default function Create(props) { + const { user } = useAuth() -export default function Create() { const [name, setName] = useState('') const [description, setDescription] = useState('') const [cost, setCost] = useState(0) const [price, setPrice] = useState(0) const [stock, setStock] = useState(0) - const [category, setCategory] = useState({}) + const [category, setCategory] = useState({name: ''}) + + const toast = useToast() + const [submit, setSubmit] = useState(false) + const [error, setError] = useState(null) + + const [ isOpen, toggle ] = useModalState() + + const handleSubmit = () => { + setSubmit(true) + createProduct({ + name, + description, + cost, + price, + stock, + category_id: category ? category.id : '' + }, user.accessToken) + .then(res => { + toast({ + title: res.status, + description: "item ditambahkan", + status: "success", + duration: 4000, + isClosable: true, + position: "top-right" + }) + props.history.push('/products') + }) + .catch((err) => { + setError(err.message) + }) + .finally(() => { + setSubmit(false) + }) + } return ( <> + {error !== null && ( + + + {error} + + )} setCost('')}/> setPrice('')}/> - - + toggle()} + /> + + { + setCategory(category) + }} + /> ) } \ No newline at end of file diff --git a/src/views/products/Detail.js b/src/views/products/Detail.js deleted file mode 100644 index d023daa..0000000 --- a/src/views/products/Detail.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Detail(props) { - const id = props.match.params.id - - return ( -
Product: { id }
- ) -} \ No newline at end of file diff --git a/src/views/products/Edit.js b/src/views/products/Edit.js new file mode 100644 index 0000000..fb07445 --- /dev/null +++ b/src/views/products/Edit.js @@ -0,0 +1,126 @@ +import { Alert, AlertIcon, Button, useToast } from "@chakra-ui/react" +import { useState, useEffect } from "react" +import { + Breadcrumb, + Card, + FormInput, + FormInputNumber, + FormInputSelection, + Loading, + useModalState +} from "../../components/Common" +import { getProduct, updateProduct } from "./Api" +import CategoryModalSelection from "../categories/Modal" +import { useAuth } from "../../context/AppContext" +import { formatIDR } from "../../utils" + +export default function Edit(props) { + const id = props.match.params.id + const { user } = useAuth() + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [cost, setCost] = useState(0) + const [price, setPrice] = useState(0) + const [stock, setStock] = useState(0) + const [category, setCategory] = useState({name: ''}) + + const toast = useToast() + const [loading, setLoading] = useState(false) + const [submit, setSubmit] = useState(false) + const [error, setError] = useState(null) + + const [isOpen, toggle] = useModalState() + + const handleSubmit = () => { + setSubmit(true) + updateProduct(id, { + name, + description, + cost, + price, + stock, + category_id: category ? category.id : '' + }, user.accessToken) + .then(res => { + toast({ + title: res.status, + description: "item diubah", + status: "success", + duration: 4000, + isClosable: true, + position: "top-right" + }) + props.history.push('/products') + }) + .catch((err) => { + setError(err.message) + }) + .finally(() => { + setSubmit(false) + }) + } + + useEffect(() => { + setLoading(true) + getProduct(id, user.accessToken) + .then(res => { + setName(res.product.name) + setDescription(res.product.description) + setPrice(res.product.price) + setCost(res.product.cost) + setStock(res.product.stock) + setCategory({ + id: res.product.category_id, + name: res.product.category_name + }) + }) + .catch(err => setError(err)) + .finally(() => setLoading(false)) + return () => {} + }, [id, user]) + + return ( + <> + + + {error !== null && ( + + + {error} + + )} + {loading ? ( + + ) : ( + <> + + + + + + toggle()} + /> + + + )} + + { + setCategory(category) + }} + /> + + ) +} \ No newline at end of file diff --git a/src/views/products/List.js b/src/views/products/List.js index 1fcf076..c17b4d5 100644 --- a/src/views/products/List.js +++ b/src/views/products/List.js @@ -9,29 +9,72 @@ import { Td, Th, Tbody, + Menu, + MenuButton, + MenuList, + MenuItem, + useToast, + Grid, } from "@chakra-ui/react" import { useProducts } from "../../api" +import { mutate } from "swr" +import qs from "query-string" import { Breadcrumb, Card, Loading, SearchInput, Pagination, - useDebounce + AlertDialog, + useDebounce, + useModalState, + FormInputSelection } from "../../components/Common" +import { deleteProduct } from "./Api" import { formatIDR } from "../../utils" import { Link } from "react-router-dom" import { useAuth } from "../../context/AppContext" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" + +import CategorySelectionModal from "../categories/Modal" export default function List({ history }) { const { user } = useAuth() + const toast = useToast() + const [page, setPage] = useState(1) const [search, setSearch] = useState('') const q = useDebounce(search, 600) - const [ data, error ] = useProducts(user, { page, q }) + const [category, setCategory] = useState({name: ''}) + const params = { page, q, withCategory: true, withStock: true, categoryId: category?.id } + const [ data, error ] = useProducts(user, params) + + const [isOpen, toggle, selected] = useModalState() + const [isCategoryOpen, toggleCategory] = useModalState() - const handleItemClick = (id) => { - history.push(`/products/${id}`) + const handleDelete = () => { + deleteProduct(selected.id, user.accessToken) + .then(res => { + toast({ + title: res.status, + description: "item dihapus", + status: "success", + position: "top-right", + duration: 4000, + isClosable: true + }) + mutate([`/products?${qs.stringify(params)}`, user.accessToken]) + }) + .catch(err => { + toast({ + title: err.status, + description: err.message, + status: "error", + position: "top-right", + duration: 4000, + isClosable: true + }) + }) } if(error) { @@ -47,28 +90,61 @@ export default function List({ history }) { <> - - + + + { + setCategory({name: ''}) + }} + /> + {data ? ( <> + + + {data.products.map((product) => ( - handleItemClick(product.id)} key={product.id}> + + + + ))} @@ -79,6 +155,18 @@ export default function List({ history }) { )} + toggle()} + /> + { + setCategory(category) + }} + toggle={toggleCategory} + /> ) } \ No newline at end of file diff --git a/src/views/products/index.js b/src/views/products/index.js index 089eda8..7a1177c 100644 --- a/src/views/products/index.js +++ b/src/views/products/index.js @@ -2,14 +2,14 @@ import { Switch, Route } from 'react-router-dom' import Create from './Create' import List from './List' -import Detail from './Detail' +import Edit from './Edit' function routes(props) { return ( + - ) }
namakategori harga beli harga jualstok deskripsi
{ product.name }{ product.category_name } { formatIDR(product.cost) } { formatIDR(product.price) }{ formatIDR(product.stock) } { product.description } + + + + + + ubah + { + toggle(product) + }} + > + hapus + + + +