implemented purchases
parent
0eb6437835
commit
c72c24d094
@ -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
|
Loading…
Reference in New Issue