implemented purchases

dev
Aji Kamaludin 3 years ago
parent 0eb6437835
commit c72c24d094
No known key found for this signature in database
GPG Key ID: 670E1F26AD5A8099

@ -3,8 +3,10 @@ const Dashboard = React.lazy(() => import("../views/dashboard"))
const User = React.lazy(() => import("../views/users"))
const Product = React.lazy(() => import("../views/products"))
const Categories = React.lazy(() => import("../views/categories"))
const Sales = React.lazy(() => import("../views/sales"))
const Customers = React.lazy(() => import("../views/customers"))
const Sales = React.lazy(() => import("../views/sales"))
const Purchases = React.lazy(() => import("../views/purchases"))
const routes = [
{ path: '/', exact: true, name: 'Home' },
@ -14,6 +16,7 @@ const routes = [
{ path: "/categories", name: "kategori", component: Categories },
{ path: "/users", name: "pengguna", component: User },
{ path: "/sales", name: "penjualan", component: Sales },
{ path: "/purchases", name: "pembelian", component: Purchases },
]
export default routes;

@ -0,0 +1,43 @@
import axios from "axios"
import useSWR from "swr"
import qs from "query-string"
export function usePurchases(user, params) {
const { data, error } = useSWR([
`/purchases?${qs.stringify(params)}`, user.accessToken
])
return [
data,
error
]
}
export function createPurchase(payload, token) {
return axios({
method: 'POST',
url: '/purchases',
data: payload,
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(res => res.data)
.catch(err => {
throw err.response.data
})
}
export function getPurchase(id, token){
return axios({
method: 'GET',
url: `/purchases/${id}`,
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(res => res.data.data)
.catch(err => {
throw err.response.data
})
}

@ -0,0 +1,261 @@
import {
Flex,
Box,
Table,
Thead,
Tbody,
Th,
Td,
Tr,
Textarea,
Button,
Heading,
useToast
} from "@chakra-ui/react"
import { useState } from "react"
import {
Card,
FormDatePicker,
FormInput,
InputNumber,
Breadcrumb,
FormInputSelectionOpen,
useModalState,
} from "../../components/Common"
import { formatDate, formatIDR, genInvId } from "../../utils"
import { useAuth } from "../../context/AppContext"
import { searchProductByCode } from "../products/Api"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createPurchase } from "./Api"
import ProductSelectionModal from "../products/Modal"
import { mutate } from "swr"
export default function Create() {
const { user } = useAuth()
const currentDate = new Date()
const invoicePrefix = genInvId()
const [invoice, setInvoice] = useState(`${invoicePrefix}/${Date.parse(currentDate)/1000}`)
const [date, setDate] = useState(currentDate)
const [note, setNote] = useState('')
const [productCode, setProductCode] = useState('')
const [submit, setSummit] = useState(false)
const toast = useToast()
const [isProductOpen, toggleProduct] = useModalState()
const [items, setItems] = useState([])
const searchProductCode = (e) => {
if(e.code === "Enter") {
searchProductByCode(productCode, user.accessToken)
.then(product => {
if(product) {
addItems(product)
}
})
setProductCode('')
}
}
const addItems = (product) => {
const findId = items.find(item => item.id === product.id)
if(findId) {
setItems(items.map(item => {
return {
...item,
quantity: item.id === product.id ? +item.quantity + 1 : item.quantity
}
}))
} else {
setItems(items.concat({
...product,
quantity: 1
}))
}
}
const setItemQuantity = (itemId, value) => {
setItems(items.map(item => {
if (itemId === item.id) {
return {
...item,
quantity: value
}
} else {
return item
}
}))
}
const removeItem = (itemId) => {
setItems(items.filter(item => item.id !== itemId))
}
const totalAmount = items.reduce((mr, item) => {
return mr + +item.cost * +item.quantity
}, 0)
const resetForm = () => {
setItems([])
setNote('')
setInvoice(`${genInvId()}/${Date.parse(currentDate)/1000}`)
mutate(["/products?page=1&q=&withCategory=true&withStock=true", user.accessToken])
}
const handleCreatePurchase = () => {
if(items.length <= 0) {
return
}
setSummit(true)
createPurchase({
officeId: user.officeid,
date: formatDate(date),
invoice,
amount: totalAmount,
discount: 0,
description: note,
items: items.map(item => {
return {
productId: item.id,
quantity: item.quantity,
cost: item.cost,
}
})
}, user.accessToken)
.then((res) => {
toast({
title: "success",
description: res.message,
status: "success",
isClosable: true,
duration: 6000,
position: "top-right"
})
resetForm()
})
.catch(err => {
toast({
title: "error",
description: err.message,
status: "warning",
isClosable: true,
duration: 6000,
position: "top-right"
})
})
.finally(() => {
setSummit(false)
})
}
return (
<Flex direction="column">
<Breadcrumb main={["/purchases", "pembelian", "baru"]}/>
<Flex>
<Card flex="1">
<Flex direction="column">
<Box minH="32rem">
<FormInput data={["penerima", user.name]} readOnly={true} bg="gray.200"/>
<FormDatePicker data={["tangal", date, setDate]}/>
<FormInput data={["no. invoice", invoice, setInvoice]}/>
<Textarea
focusBorderColor="red.500"
placeholder="catatan"
value={note}
onChange={e => setNote(e.target.value)}
/>
</Box>
<Box>
<Button
isLoading={submit}
onClick={handleCreatePurchase}
>
Simpan
</Button>
</Box>
</Flex>
</Card>
<Card flex="3">
<Flex direction="row">
<FormInputSelectionOpen
data={["", productCode, setProductCode]}
onClick={toggleProduct}
placeholder="cari produk"
autoFocus
onKeyUp={searchProductCode}
flex="8"
/>
<Button
flex="1"
mt="2"
mx="2"
bg="gray.100"
color="black"
_hover={{ bg: "gray.200"}}
_active={{ bg: "gray.300"}}
onClick={toggleProduct}
>
produk
</Button>
</Flex>
<Table px="3" mt="3" minH="27rem">
<Thead style={{display: "table", width: "calc( 100% )", tableLayout: "fixed"}}>
<Tr>
<Th>kode</Th>
<Th>nama</Th>
<Th isNumeric>harga</Th>
<Th isNumeric>jumlah</Th>
<Th isNumeric>subtotal</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody style={{display: "block", maxHeight: "27rem", overflow: "auto", width: "100%"}}>
{items.map(item => (
<Tr key={item.id} style={{display: "table", width: "100%", tableLayout: "fixed"}}>
<Td>{item.code}</Td>
<Td>{item.name}</Td>
<Td isNumeric>{formatIDR(item.cost)}</Td>
<Td isNumeric>
<InputNumber
type="number"
value={item.quantity}
onValueChange={(e) => setItemQuantity(item.id, e.value)}
/>
</Td>
<Td isNumeric>
{formatIDR(item.cost * item.quantity)}
</Td>
<Td isNumeric>
<FontAwesomeIcon icon="times" onClick={() => removeItem(item.id)}/>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Flex direction="row" justifyContent="space-between">
<Box>
<Heading>Total</Heading>
</Box>
<Box>
<Heading>
{formatIDR(totalAmount)}
</Heading>
</Box>
</Flex>
</Card>
</Flex>
<ProductSelectionModal
isOpen={isProductOpen}
toggle={toggleProduct}
onClose={(product) => {
addItems(product)
}}
/>
</Flex>
)
}

@ -0,0 +1,129 @@
import {
Flex,
Box,
Table,
Thead,
Tbody,
Th,
Td,
Tr,
Textarea,
Heading,
AlertIcon,
Alert
} from "@chakra-ui/react"
import { useState, useEffect } from "react"
import {
Card,
FormInput,
Breadcrumb,
Loading
} from "../../components/Common"
import { formatDate, formatIDR } from "../../utils"
import { useAuth } from "../../context/AppContext"
import { getPurchase } from "./Api"
export default function Create(props) {
const id = props.match.params.id
const { user } = useAuth()
const [invoice, setInvoice] = useState('')
const [date, setDate] = useState(new Date())
const [note, setNote] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [items, setItems] = useState([])
const totalAmount = items.reduce((mr, item) => {
return mr + +item.cost * +item.quantity
}, 0)
useEffect(() => {
setLoading(true)
getPurchase(id, user.accessToken)
.then(res => {
setInvoice(res.purchase.invoice)
setDate(new Date(res.purchase.date))
setNote(res.purchase.description)
setItems(res.purchase.items)
})
.catch(err => {
setError(err.message)
})
.finally(() => setLoading(false))
return () => {}
}, [id, user])
if(loading) {
return <Loading/>
}
return (
<Flex direction="column">
<Breadcrumb main={["/purchases", "pembelian", "detail"]}/>
{error && (
<Alert status="error" mb="5" rounded="md">
<AlertIcon />
{error}
</Alert>
)}
<Flex>
<Card flex="1">
<Flex direction="column">
<Box minH="32rem">
<FormInput data={["penerima", user.name]} readOnly={true} bg="gray.200"/>
<FormInput data={["tangal", formatDate(date), setDate]} readOnly={true} bg="gray.200"/>
<FormInput data={["no. invoice", invoice, setInvoice]} readOnly={true} bg="gray.200"/>
<Textarea
focusBorderColor="red.500"
placeholder="catatan"
value={note}
readOnly={true} bg="gray.200"
/>
</Box>
</Flex>
</Card>
<Card flex="3">
<Table px="3" mt="3" minH="27rem">
<Thead style={{display: "table", width: "calc( 100% )", tableLayout: "fixed"}}>
<Tr>
<Th>kode</Th>
<Th>nama</Th>
<Th isNumeric>harga</Th>
<Th isNumeric>jumlah</Th>
<Th isNumeric>subtotal</Th>
</Tr>
</Thead>
<Tbody style={{display: "block", maxHeight: "27rem", overflow: "auto", width: "100%"}}>
{items.map(item => (
<Tr key={item.id} style={{display: "table", width: "100%", tableLayout: "fixed"}}>
<Td>{item.code}</Td>
<Td>{item.name}</Td>
<Td isNumeric>{formatIDR(item.cost)}</Td>
<Td isNumeric>
{formatIDR(item.quantity)}
</Td>
<Td isNumeric>
{formatIDR(item.cost * item.quantity)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<Flex direction="row" justifyContent="space-between">
<Box>
<Heading>Total</Heading>
</Box>
<Box>
<Heading>
{formatIDR(totalAmount)}
</Heading>
</Box>
</Flex>
</Card>
</Flex>
</Flex>
)
}

@ -0,0 +1,102 @@
import { useState } from "react"
import {
Button,
Alert,
AlertIcon,
Table,
Thead,
Tr,
Td,
Th,
Tbody,
} from "@chakra-ui/react"
import { Link } from "react-router-dom"
import {
Breadcrumb,
Card,
Loading,
Pagination,
DatePickerFilter,
SearchInput,
useDebounce,
useDatePickerFilter,
} from "../../components/Common"
import { usePurchases } from "./Api"
import { useAuth } from "../../context/AppContext"
import { formatDate, formatIDR } from "../../utils"
export default function List() {
const { user } = useAuth()
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const q = useDebounce(search, 600)
const [startDate, endDate, setter] = useDatePickerFilter()
const params = {
page,
q,
startDate: formatDate(startDate),
endDate: formatDate(endDate),
}
const [data, error] = usePurchases(user, params)
if(error) {
return (
<Alert status="error">
<AlertIcon />
{error.message}
</Alert>
)
}
return (
<>
<Breadcrumb main={["/purchases", "pembelian"]}/>
<Card>
<Button as={Link} to="/purchases/create" size="md" mb="3">
tambah
</Button>
<DatePickerFilter
mx="3"
mt="2"
startDate={startDate}
endDate={endDate}
setter={setter}
/>
<SearchInput setter={[search, setSearch]} px="3" mt="3"/>
{data ? (
<>
<Table variant="simple" mt="2" mb="4">
<Thead>
<Tr>
<Th>invoice</Th>
<Th isNumeric>tanggal</Th>
<Th isNumeric>total</Th>
<Th>penerima</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{data.purchases.map((purchase) => (
<Tr key={purchase.id}>
<Td>{purchase.invoice}</Td>
<Td isNumeric>{formatDate(new Date(purchase.date))}</Td>
<Td isNumeric>{formatIDR(purchase.amount)}</Td>
<Td>{purchase.creator}</Td>
<Td isNumeric>
<Button as={Link} to={`/purchases/${purchase.id}/detail`}>detail</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
<Pagination page={page} setPage={setPage} totalPages={data.meta.totalPages}/>
</>
) : (
<Loading/>
)}
</Card>
</>
)
}

@ -0,0 +1,17 @@
import { Switch, Route } from 'react-router-dom'
import Detail from "./Detail"
import Create from "./Create"
import List from "./List"
function routes(props) {
return (
<Switch>
<Route path="/purchases/:id/detail" exact component={Detail} />
<Route path="/purchases/create" exact component={Create} />
<Route path="/purchases" exact component={List} />
</Switch>
)
}
export default routes

@ -23,6 +23,7 @@ import {
FormInputSelectionOpen,
AlertChange,
ModalChange,
Breadcrumb,
useModalState,
} from "../../components/Common"
import { formatDate, formatIDR, genInvId } from "../../utils"
@ -63,6 +64,9 @@ export default function Create() {
const searchProductCode = (e) => {
if(e.code === "Enter") {
if(!productCode) {
return
}
searchProductByCode(productCode, user.accessToken)
.then(product => {
if(product) {
@ -190,7 +194,10 @@ export default function Create() {
}, [data])
return (
<Flex direction="column" mt="-7">
<Flex direction="column" mt={user.role === "admin" ? "" : "-7"}>
{user.role === "admin" && (
<Breadcrumb main={["/sales", "penjualan", "baru"]}/>
)}
<AlertChange isOpen={isAlert} toggle={toggleAlert} onClose={() => handleCreateSale()}/>
<ModalChange
isOpen={isModalChange}

@ -9,12 +9,15 @@ import {
Tr,
Textarea,
Heading,
Alert, AlertIcon,
} from "@chakra-ui/react"
import { useState, useEffect } from "react"
import {
Card,
FormInput,
FormInputNumber,
Loading,
Breadcrumb,
} from "../../components/Common"
import { formatDate, formatIDR } from "../../utils"
import { useAuth } from "../../context/AppContext"
@ -32,12 +35,15 @@ export default function Create(props) {
const [note, setNote] = useState('')
const [items, setItems] = useState([])
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const totalAmount = items.reduce((mr, item) => {
return mr + +item.price * +item.quantity
}, 0) - +discount
useEffect(() => {
setLoading(true)
getSale(id, user.accessToken)
.then(res => {
setCasier(res.sale.casier)
@ -49,13 +55,27 @@ export default function Create(props) {
setItems(res.sale.items)
})
.catch(err => {
console.log(err)
setError(err.message)
})
.finally(() => {
setLoading(false)
})
return () => {}
}, [id, user])
if(loading) {
return <Loading/>
}
return (
<Flex direction="column" mt="-7">
<Flex direction="column">
<Breadcrumb main={["/sales", "penjualan", "detail"]}/>
{error && (
<Alert status="error" mb="5" rounded="md">
<AlertIcon />
{error}
</Alert>
)}
<Flex>
<Card flex="1">
<FormInput data={["kasir", casier, setCasier]} readOnly={true} bg="gray.200"/>

Loading…
Cancel
Save