initial commit

This commit is contained in:
ferdiansyah783
2025-08-05 12:35:40 +07:00
commit fffa2ead5c
1069 changed files with 118056 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
'use client'
// React Imports
import { useState } from 'react'
// Next Imports
import Link from 'next/link'
import { useParams } from 'next/navigation'
// MUI Imports
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Button from '@mui/material/Button'
import Grid from '@mui/material/Grid2'
import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Switch from '@mui/material/Switch'
// Type Imports
import type { Locale } from '@configs/i18n'
// Component Imports
import SendInvoiceDrawer from '@views/apps/invoice/shared/SendInvoiceDrawer'
import CustomTextField from '@core/components/mui/TextField'
// Util Imports
import { getLocalizedUrl } from '@/utils/i18n'
const AddActions = () => {
// States
const [sendDrawerOpen, setSendDrawerOpen] = useState(false)
// Hooks
const { lang: locale } = useParams()
return (
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<Card>
<CardContent className='flex flex-col gap-4'>
<Button
fullWidth
variant='contained'
className='capitalize'
startIcon={<i className='tabler-send' />}
onClick={() => setSendDrawerOpen(true)}
>
Send Invoice
</Button>
<Button
fullWidth
component={Link}
color='secondary'
variant='tonal'
className='capitalize'
href={getLocalizedUrl('/apps/invoice/preview/4987', locale as Locale)}
>
Preview
</Button>
<Button fullWidth color='secondary' variant='tonal' className='capitalize'>
Save
</Button>
</CardContent>
</Card>
<SendInvoiceDrawer open={sendDrawerOpen} handleClose={() => setSendDrawerOpen(false)} />
</Grid>
<Grid size={{ xs: 12 }}>
<CustomTextField select fullWidth defaultValue='Internet Banking' label='Accept payments via'>
<MenuItem value='Internet Banking'>Internet Banking</MenuItem>
<MenuItem value='Debit Card'>Debit Card</MenuItem>
<MenuItem value='Credit Card'>Credit Card</MenuItem>
<MenuItem value='Paypal'>Paypal</MenuItem>
<MenuItem value='UPI Transfer'>UPI Transfer</MenuItem>
</CustomTextField>
<div className='flex items-center justify-between mbs-3'>
<InputLabel htmlFor='invoice-edit-payment-terms' className='cursor-pointer'>
Payment Terms
</InputLabel>
<Switch defaultChecked id='invoice-edit-payment-terms' />
</div>
<div className='flex items-center justify-between'>
<InputLabel htmlFor='invoice-edit-client-notes' className='cursor-pointer'>
Client Notes
</InputLabel>
<Switch id='invoice-edit-client-notes' />
</div>
<div className='flex items-center justify-between'>
<InputLabel htmlFor='invoice-edit-payment-stub' className='cursor-pointer'>
Payment Stub
</InputLabel>
<Switch id='invoice-edit-payment-stub' />
</div>
</Grid>
</Grid>
)
}
export default AddActions
+372
View File
@@ -0,0 +1,372 @@
'use client'
// React Imports
import { useState } from 'react'
import type { SyntheticEvent } from 'react'
// MUI Imports
import Grid from '@mui/material/Grid2'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import InputAdornment from '@mui/material/InputAdornment'
import Divider from '@mui/material/Divider'
import Button from '@mui/material/Button'
import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem'
import Tooltip from '@mui/material/Tooltip'
import InputLabel from '@mui/material/InputLabel'
import useMediaQuery from '@mui/material/useMediaQuery'
import type { Theme } from '@mui/material/styles'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { InvoiceType } from '@/types/apps/invoiceTypes'
import type { FormDataType } from './AddCustomerDrawer'
// Component Imports
import AddCustomerDrawer, { initialFormData } from './AddCustomerDrawer'
import Logo from '@components/layout/shared/Logo'
import CustomTextField from '@core/components/mui/TextField'
// Styled Component Imports
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
const AddAction = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
// States
const [open, setOpen] = useState(false)
const [count, setCount] = useState(1)
const [selectData, setSelectData] = useState<InvoiceType | null>(null)
const [issuedDate, setIssuedDate] = useState<Date | null | undefined>(null)
const [dueDate, setDueDate] = useState<Date | null | undefined>(null)
const [formData, setFormData] = useState<FormDataType>(initialFormData)
// Hooks
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
const onFormSubmit = (data: FormDataType) => {
setFormData(data)
}
const deleteForm = (e: SyntheticEvent) => {
e.preventDefault()
// @ts-ignore
e.target.closest('.repeater-item').remove()
}
return (
<>
<Card>
<CardContent className='sm:!p-12'>
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<div className='p-6 bg-actionHover rounded'>
<div className='flex justify-between gap-4 flex-col sm:flex-row'>
<div className='flex flex-col gap-6'>
<div className='flex items-center gap-2.5'>
<Logo />
</div>
<div>
<Typography color='text.primary'>Office 149, 450 South Brand Brooklyn</Typography>
<Typography color='text.primary'>San Diego County, CA 91905, USA</Typography>
<Typography color='text.primary'>+1 (123) 456 7891, +44 (876) 543 2198</Typography>
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='flex items-center gap-4'>
<Typography variant='h5' className='min-is-[95px]'>
Invoice
</Typography>
<CustomTextField
fullWidth
value={invoiceData?.[0].id}
slotProps={{
input: {
disabled: true,
startAdornment: <InputAdornment position='start'>#</InputAdornment>
}
}}
/>
</div>
<div className='flex items-center'>
<Typography className='min-is-[95px] mie-4' color='text.primary'>
Date Issued:
</Typography>
<AppReactDatepicker
boxProps={{ className: 'is-full' }}
selected={issuedDate}
placeholderText='YYYY-MM-DD'
dateFormat={'yyyy-MM-dd'}
onChange={(date: Date | null) => setIssuedDate(date)}
customInput={<CustomTextField fullWidth />}
/>
</div>
<div className='flex items-center'>
<Typography className='min-is-[95px] mie-4' color='text.primary'>
Date Due:
</Typography>
<AppReactDatepicker
boxProps={{ className: 'is-full' }}
selected={dueDate}
placeholderText='YYYY-MM-DD'
dateFormat={'yyyy-MM-dd'}
onChange={(date: Date | null) => setDueDate(date)}
customInput={<CustomTextField fullWidth />}
/>
</div>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<div className='flex justify-between flex-col gap-4 flex-wrap sm:flex-row'>
<div className='flex flex-col gap-4'>
<Typography className='font-medium' color='text.primary'>
Invoice To:
</Typography>
<CustomTextField
select
className={classnames('min-is-[220px]', { 'is-1/2': isBelowSmScreen })}
value={selectData?.id || ''}
onChange={e => {
setFormData({} as FormDataType)
setSelectData(invoiceData?.slice(0, 5).filter(item => item.id === e.target.value)[0] || null)
}}
>
<MenuItem
className='flex items-center gap-2 !text-success !bg-transparent hover:text-success hover:!bg-[var(--mui-palette-success-lightOpacity)]'
value=''
onClick={() => {
setSelectData(null)
setOpen(true)
}}
>
<i className='tabler-plus text-base' />
Add New Customer
</MenuItem>
{invoiceData?.slice(0, 5).map((invoice: InvoiceType, index) => (
<MenuItem key={index} value={invoice.id}>
{invoice.name}
</MenuItem>
))}
</CustomTextField>
{selectData?.id ? (
<div>
<Typography>{selectData?.name}</Typography>
<Typography>{selectData?.company}</Typography>
<Typography>{selectData?.address}</Typography>
<Typography>{selectData?.contact}</Typography>
<Typography>{selectData?.companyEmail}</Typography>
</div>
) : (
<div>
<Typography>{formData?.name}</Typography>
<Typography>{formData?.company}</Typography>
<Typography>{formData?.address}</Typography>
<Typography>{formData?.contactNumber}</Typography>
<Typography>{formData?.email}</Typography>
</div>
)}
</div>
<div className='flex flex-col gap-4'>
<Typography className='font-medium' color='text.primary'>
Bill To:
</Typography>
<div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Total Due:</Typography>
<Typography>$12,110.55</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Bank name:</Typography>
<Typography>American Bank</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Country:</Typography>
<Typography>United States</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>IBAN:</Typography>
<Typography>ETD95476213874685</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>SWIFT code:</Typography>
<Typography>BR91905</Typography>
</div>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
{Array.from(Array(count).keys()).map((item, index) => (
<div
key={index}
className={classnames('repeater-item flex relative mbe-4 border rounded', {
'mbs-8': !isBelowMdScreen,
'!mbs-14': index !== 0 && !isBelowMdScreen,
'gap-5': isBelowMdScreen
})}
>
<Grid container spacing={5} className='m-0 p-5'>
<Grid size={{ xs: 12, md: 5, lg: 6 }}>
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
Item
</Typography>
<CustomTextField select fullWidth defaultValue='App Design' className='mbe-5'>
<MenuItem value='App Design'>App Design</MenuItem>
<MenuItem value='App Customization'>App Customization</MenuItem>
<MenuItem value='ABC Template'>ABC Template</MenuItem>
<MenuItem value='App Development'>App Development</MenuItem>
</CustomTextField>
<CustomTextField rows={2} fullWidth multiline defaultValue='Customization & Bug Fixes' />
</Grid>
<Grid size={{ xs: 12, md: 3, lg: 2 }}>
<Typography className='font-medium md:absolute md:-top-8'>Cost</Typography>
<CustomTextField
{...(isBelowMdScreen && { fullWidth: true })}
type='number'
placeholder='24'
defaultValue='24'
className='mbe-5'
slotProps={{
input: {
inputProps: { min: 0 }
}
}}
/>
<div className='flex flex-col'>
<Typography component='span' color='text.primary'>
Discount:
</Typography>
<div className='flex gap-2'>
<Typography component='span' color='text.primary'>
0%
</Typography>
<Tooltip title='Tax 1' placement='top'>
<Typography component='span' color='text.primary'>
0%
</Typography>
</Tooltip>
<Tooltip title='Tax 2' placement='top'>
<Typography component='span' color='text.primary'>
0%
</Typography>
</Tooltip>
</div>
</div>
</Grid>
<Grid size={{ xs: 12, md: 2 }}>
<Typography className='font-medium md:absolute md:-top-8'>Hours</Typography>
<CustomTextField
{...(isBelowMdScreen && { fullWidth: true })}
type='number'
placeholder='1'
defaultValue='1'
slotProps={{
input: {
inputProps: { min: 0 }
}
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 2 }}>
<Typography className='font-medium md:absolute md:-top-8'>Price</Typography>
<Typography>$24.00</Typography>
</Grid>
</Grid>
<div className='flex flex-col justify-start border-is'>
<IconButton size='small' onClick={deleteForm}>
<i className='tabler-x text-2xl text-actionActive' />
</IconButton>
</div>
</div>
))}
<Grid size={{ xs: 12 }}>
<Button
size='small'
variant='contained'
onClick={() => setCount(count + 1)}
startIcon={<i className='tabler-plus' />}
>
Add Item
</Button>
</Grid>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
<div className='flex justify-between flex-col gap-4 sm:flex-row'>
<div className='flex flex-col gap-4 order-2 sm:order-[unset]'>
<div className='flex items-center gap-2'>
<Typography className='font-medium' color='text.primary'>
Salesperson:
</Typography>
<CustomTextField defaultValue='Tommy Shelby' />
</div>
<CustomTextField placeholder='Thanks for your business' />
</div>
<div className='min-is-[200px]'>
<div className='flex items-center justify-between'>
<Typography>Subtotal:</Typography>
<Typography className='font-medium' color='text.primary'>
$1800
</Typography>
</div>
<div className='flex items-center justify-between'>
<Typography>Discount:</Typography>
<Typography className='font-medium' color='text.primary'>
$28
</Typography>
</div>
<div className='flex items-center justify-between'>
<Typography>Tax:</Typography>
<Typography className='font-medium' color='text.primary'>
21%
</Typography>
</div>
<Divider className='mlb-2' />
<div className='flex items-center justify-between'>
<Typography>Total:</Typography>
<Typography className='font-medium' color='text.primary'>
$1690
</Typography>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
<InputLabel htmlFor='invoice-note' className='inline-flex mbe-1 text-textPrimary'>
Note:
</InputLabel>
<CustomTextField
id='invoice-note'
rows={2}
fullWidth
multiline
className='border rounded'
defaultValue='It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance
projects. Thank You!'
/>
</Grid>
</Grid>
</CardContent>
</Card>
<AddCustomerDrawer open={open} setOpen={setOpen} onFormSubmit={onFormSubmit} />
</>
)
}
export default AddAction
@@ -0,0 +1,144 @@
// React Imports
import { useState } from 'react'
import type { FormEvent } from 'react'
// MUI Imports
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import Typography from '@mui/material/Typography'
import MenuItem from '@mui/material/MenuItem'
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
type Props = {
open: boolean
setOpen: (open: boolean) => void
onFormSubmit: (formData: FormDataType) => void
}
export type FormDataType = {
name: string
company: string
email: string
address: string
country: string
contactNumber: string
}
// Vars
export const initialFormData: FormDataType = {
name: '',
company: '',
email: '',
address: '',
country: 'USA',
contactNumber: ''
}
const countries = ['USA', 'UK', 'Russia', 'Australia', 'Canada']
const AddCustomerDrawer = ({ open, setOpen, onFormSubmit }: Props) => {
// States
const [data, setData] = useState<FormDataType>(initialFormData)
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
setOpen(false)
onFormSubmit(data)
handleReset()
}
const handleReset = () => {
setOpen(false)
setData(initialFormData)
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between plb-5 pli-6'>
<Typography variant='h5'>Add New Customer</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-2xl text-textPrimary' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={e => handleSubmit(e)} className='flex flex-col gap-5'>
<CustomTextField
fullWidth
id='name'
label='Name'
value={data.name}
onChange={e => setData({ ...data, name: e.target.value })}
/>
<CustomTextField
fullWidth
id='company'
label='Company'
value={data.company}
onChange={e => setData({ ...data, company: e.target.value })}
/>
<CustomTextField
fullWidth
id='email'
label='Email'
value={data.email}
onChange={e => setData({ ...data, email: e.target.value })}
/>
<CustomTextField
rows={6}
multiline
fullWidth
id='address'
label='Address'
value={data.address}
onChange={e => setData({ ...data, address: e.target.value })}
/>
<CustomTextField
select
id='country'
label='Country'
name='country'
variant='outlined'
value={data?.country?.toLowerCase().replace(/\s+/g, '-') || ''}
onChange={e => setData({ ...data, country: e.target.value })}
>
{countries.map((item, index) => (
<MenuItem key={index} value={item.toLowerCase().replace(/\s+/g, '-')}>
{item}
</MenuItem>
))}
</CustomTextField>
<CustomTextField
fullWidth
id='contact'
type='number'
label='Contact Number'
value={data.contactNumber}
onChange={e => setData({ ...data, contactNumber: e.target.value })}
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit'>
Add
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Cancel
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default AddCustomerDrawer
+114
View File
@@ -0,0 +1,114 @@
'use client'
// React Imports
import { useState } from 'react'
// Next Imports
import Link from 'next/link'
import { useParams } from 'next/navigation'
// MUI Imports
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Button from '@mui/material/Button'
import Grid from '@mui/material/Grid2'
import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Switch from '@mui/material/Switch'
// Type Imports
import type { Locale } from '@configs/i18n'
// Component Imports
import AddPaymentDrawer from '@views/apps/invoice/shared/AddPaymentDrawer'
import SendInvoiceDrawer from '@views/apps/invoice/shared/SendInvoiceDrawer'
import CustomTextField from '@core/components/mui/TextField'
// Util Imports
import { getLocalizedUrl } from '@/utils/i18n'
const EditActions = ({ id }: { id: string }) => {
// States
const [paymentDrawerOpen, setPaymentDrawerOpen] = useState(false)
const [sendDrawerOpen, setSendDrawerOpen] = useState(false)
// Hooks
const { lang: locale } = useParams()
return (
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<Card>
<CardContent className='flex flex-col gap-4'>
<Button
fullWidth
variant='contained'
className='capitalize'
startIcon={<i className='tabler-send' />}
onClick={() => setSendDrawerOpen(true)}
>
Send Invoice
</Button>
<div className='flex items-center gap-4'>
<Button
fullWidth
component={Link}
color='secondary'
variant='tonal'
className='capitalize'
href={getLocalizedUrl(`/apps/invoice/preview/${id}`, locale as Locale)}
>
Preview
</Button>
<Button fullWidth color='secondary' variant='tonal' className='capitalize'>
Save
</Button>
</div>
<Button
fullWidth
color='success'
variant='contained'
className='capitalize'
onClick={() => setPaymentDrawerOpen(true)}
startIcon={<i className='tabler-currency-dollar' />}
>
Add Payment
</Button>
</CardContent>
</Card>
<AddPaymentDrawer open={paymentDrawerOpen} handleClose={() => setPaymentDrawerOpen(false)} />
<SendInvoiceDrawer open={sendDrawerOpen} handleClose={() => setSendDrawerOpen(false)} />
</Grid>
<Grid size={{ xs: 12 }}>
<CustomTextField select fullWidth defaultValue='Internet Banking' label='Accept payments via'>
<MenuItem value='Internet Banking'>Internet Banking</MenuItem>
<MenuItem value='Debit Card'>Debit Card</MenuItem>
<MenuItem value='Credit Card'>Credit Card</MenuItem>
<MenuItem value='Paypal'>Paypal</MenuItem>
<MenuItem value='UPI Transfer'>UPI Transfer</MenuItem>
</CustomTextField>
<div className='flex items-center justify-between gap-6 mbs-3'>
<InputLabel htmlFor='invoice-edit-payment-terms' className='cursor-pointer'>
Payment Terms
</InputLabel>
<Switch defaultChecked id='invoice-edit-payment-terms' />
</div>
<div className='flex items-center justify-between gap-6'>
<InputLabel htmlFor='invoice-edit-client-notes' className='cursor-pointer'>
Client Notes
</InputLabel>
<Switch id='invoice-edit-client-notes' />
</div>
<div className='flex items-center justify-between gap-6'>
<InputLabel htmlFor='invoice-edit-payment-stub' className='cursor-pointer'>
Payment Stub
</InputLabel>
<Switch id='invoice-edit-payment-stub' />
</div>
</Grid>
</Grid>
)
}
export default EditActions
+342
View File
@@ -0,0 +1,342 @@
'use client'
// React Imports
import { useState } from 'react'
import type { SyntheticEvent } from 'react'
// MUI Imports
import Grid from '@mui/material/Grid2'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import InputAdornment from '@mui/material/InputAdornment'
import MenuItem from '@mui/material/MenuItem'
import IconButton from '@mui/material/IconButton'
import Button from '@mui/material/Button'
import Tooltip from '@mui/material/Tooltip'
import Divider from '@mui/material/Divider'
import InputLabel from '@mui/material/InputLabel'
import useMediaQuery from '@mui/material/useMediaQuery'
import type { Theme } from '@mui/material/styles'
// Third-party Imports
import classnames from 'classnames'
// Type Imports
import type { InvoiceType } from '@/types/apps/invoiceTypes'
// Component Imports
import Logo from '@components/layout/shared/Logo'
import CustomTextField from '@core/components/mui/TextField'
// Styled Component Imports
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
const EditCard = ({ invoiceData, id, data }: { invoiceData?: InvoiceType; id: string; data?: InvoiceType[] }) => {
// States
const [selectData, setSelectData] = useState<InvoiceType | null>(data?.[0] || null)
const [count, setCount] = useState(1)
const [issueDate, setIssueDate] = useState(new Date(invoiceData?.issuedDate ?? ''))
const [dueDate, setDueDate] = useState(new Date(invoiceData?.dueDate ?? ''))
// Hooks
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
const deleteForm = (e: SyntheticEvent) => {
e.preventDefault()
// @ts-ignore
e.target.closest('.repeater-item').remove()
}
return (
<>
<Card>
<CardContent className='sm:!p-12'>
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<div className='p-6 rounded bg-actionHover'>
<div className='flex justify-between gap-4 flex-col sm:flex-row'>
<div className='flex flex-col gap-6'>
<div className='flex items-center gap-2.5'>
<Logo />
</div>
<div>
<Typography color='text.primary'>Office 149, 450 South Brand Brooklyn</Typography>
<Typography color='text.primary'>San Diego County, CA 91905, USA</Typography>
<Typography color='text.primary'>+1 (123) 456 7891, +44 (876) 543 2198</Typography>
</div>
</div>
<div className='flex flex-col gap-2'>
<div className='flex items-center gap-4'>
<Typography variant='h5' className='min-is-[95px]'>
Invoice
</Typography>
<CustomTextField
fullWidth
value={id}
slotProps={{
input: {
disabled: true,
startAdornment: <InputAdornment position='start'>#</InputAdornment>
}
}}
/>
</div>
<div className='flex items-center'>
<Typography className='min-is-[95px] mie-4' color='text.primary'>
Date Issued:
</Typography>
<AppReactDatepicker
boxProps={{ className: 'is-full' }}
selected={issueDate}
id='payment-date'
onChange={(date: Date | null) => date !== null && setIssueDate(date)}
customInput={<CustomTextField fullWidth />}
/>
</div>
<div className='flex items-center'>
<Typography className='min-is-[95px] mie-4' color='text.primary'>
Date Due:
</Typography>
<AppReactDatepicker
boxProps={{ className: 'is-full' }}
selected={dueDate}
id='payment-date'
onChange={(date: Date | null) => date !== null && setDueDate(date)}
customInput={<CustomTextField fullWidth />}
/>
</div>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<div className='flex justify-between flex-col gap-4 flex-wrap sm:flex-row'>
<div className='flex flex-col gap-4'>
<Typography className='font-medium' color='text.primary'>
Invoice To:
</Typography>
<CustomTextField
select
className='is-1/2 min-is-[220px] sm:is-auto'
value={selectData?.id}
onChange={e => {
setSelectData(data?.slice(0, 5).filter(item => item.id === e.target.value)[0] || null)
}}
>
{data?.slice(0, 5).map((invoice: InvoiceType, index) => (
<MenuItem key={index} value={invoice.id}>
{invoice.name}
</MenuItem>
))}
</CustomTextField>
<div>
<Typography>{selectData?.name}</Typography>
<Typography>{selectData?.company}</Typography>
<Typography>{selectData?.address}</Typography>
<Typography>{selectData?.contact}</Typography>
<Typography>{selectData?.companyEmail}</Typography>
</div>
</div>
<div className='flex flex-col gap-4'>
<Typography className='font-medium' color='text.primary'>
Bill To:
</Typography>
<div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Total Due:</Typography>
<Typography>$12,110.55</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Bank name:</Typography>
<Typography>American Bank</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Country:</Typography>
<Typography>United States</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>IBAN:</Typography>
<Typography>ETD95476213874685</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>SWIFT code:</Typography>
<Typography>BR91905</Typography>
</div>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
{Array.from(Array(count).keys()).map((item, index) => (
<div
key={index}
className={classnames('repeater-item flex relative mbe-4 border rounded', {
'mbs-8': !isBelowMdScreen,
'!mbs-14': index !== 0 && !isBelowMdScreen,
'gap-5': isBelowMdScreen
})}
>
<Grid container spacing={5} className='m-0 p-5'>
<Grid size={{ xs: 12, md: 5, lg: 6 }}>
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
Item
</Typography>
<CustomTextField select fullWidth defaultValue='App Design' className='mbe-5'>
<MenuItem value='App Design'>App Design</MenuItem>
<MenuItem value='App Customization'>App Customization</MenuItem>
<MenuItem value='ABC Template'>ABC Template</MenuItem>
<MenuItem value='App Development'>App Development</MenuItem>
</CustomTextField>
<CustomTextField rows={2} fullWidth multiline defaultValue='Customization & Bug Fixes' />
</Grid>
<Grid size={{ xs: 12, md: 3, lg: 2 }}>
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
Cost
</Typography>
<CustomTextField
{...(isBelowMdScreen && { fullWidth: true })}
type='number'
placeholder='24'
defaultValue='24'
className='mbe-5'
slotProps={{
input: {
inputProps: { min: 0 }
}
}}
/>
<div className='flex flex-col'>
<Typography component='span' color='text.primary'>
Discount:
</Typography>
<div className='flex gap-2'>
<Typography component='span' color='text.primary'>
0%
</Typography>
<Tooltip title='Tax 1' placement='top'>
<Typography component='span' color='text.primary'>
0%
</Typography>
</Tooltip>
<Tooltip title='Tax 2' placement='top'>
<Typography component='span' color='text.primary'>
0%
</Typography>
</Tooltip>
</div>
</div>
</Grid>
<Grid size={{ xs: 12, md: 2 }}>
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
Hours
</Typography>
<CustomTextField
{...(isBelowMdScreen && { fullWidth: true })}
type='number'
placeholder='1'
defaultValue='1'
slotProps={{
input: {
inputProps: { min: 0 }
}
}}
/>
</Grid>
<Grid size={{ xs: 12, md: 2 }}>
<Typography className='font-medium md:absolute md:-top-8' color='text.primary'>
Price
</Typography>
<Typography>$24.00</Typography>
</Grid>
</Grid>
<div className='flex flex-col justify-start border-is'>
<IconButton size='small' onClick={deleteForm}>
<i className='tabler-x text-2xl text-actionActive' />
</IconButton>
</div>
</div>
))}
<Grid size={{ xs: 12 }}>
<Button
size='small'
variant='contained'
onClick={() => setCount(count + 1)}
startIcon={<i className='tabler-plus' />}
>
Add Item
</Button>
</Grid>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
<div className='flex justify-between flex-col gap-4 sm:flex-row'>
<div className='flex flex-col gap-4 order-2 sm:order-[unset]'>
<div className='flex items-center gap-2'>
<Typography className='font-medium' color='text.primary'>
Salesperson:
</Typography>
<CustomTextField defaultValue='Tommy Shelby' />
</div>
<CustomTextField defaultValue='Thanks for your business' />
</div>
<div className='min-is-[200px]'>
<div className='flex items-center justify-between'>
<Typography>Subtotal:</Typography>
<Typography className='font-medium' color='text.primary'>
$1800
</Typography>
</div>
<div className='flex items-center justify-between'>
<Typography>Discount:</Typography>
<Typography className='font-medium' color='text.primary'>
$28
</Typography>
</div>
<div className='flex items-center justify-between'>
<Typography>Tax:</Typography>
<Typography className='font-medium' color='text.primary'>
21%
</Typography>
</div>
<Divider className='mlb-2' />
<div className='flex items-center justify-between'>
<Typography>Total:</Typography>
<Typography className='font-medium' color='text.primary'>
$1690
</Typography>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
<InputLabel htmlFor='invoice-note' className='inline-flex mbe-1 text-textPrimary'>
Note:
</InputLabel>
<CustomTextField
id='invoice-note'
rows={2}
fullWidth
multiline
className='border rounded'
defaultValue='It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance
projects. Thank You!'
/>
</Grid>
</Grid>
</CardContent>
</Card>
</>
)
}
export default EditCard
@@ -0,0 +1,84 @@
'use client'
// MUI Imports
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Grid from '@mui/material/Grid2'
import Divider from '@mui/material/Divider'
import Avatar from '@mui/material/Avatar'
import Typography from '@mui/material/Typography'
import useMediaQuery from '@mui/material/useMediaQuery'
import type { Theme } from '@mui/material/styles'
// Third-party Imports
import classnames from 'classnames'
// Vars
const data = [
{
title: 24,
subtitle: 'Clients',
icon: 'tabler-user'
},
{
title: 165,
subtitle: 'Invoices',
icon: 'tabler-file-invoice'
},
{
title: '$2.46k',
subtitle: 'Paid',
icon: 'tabler-checks'
},
{
title: '$876',
subtitle: 'Unpaid',
icon: 'tabler-circle-off'
}
]
const InvoiceCard = () => {
// Hooks
const isBelowMdScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
const isBelowSmScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
return (
<Card>
<CardContent>
<Grid container spacing={6}>
{data.map((item, index) => (
<Grid
size={{ xs: 12, sm: 6, md: 3 }}
key={index}
className={classnames({
'[&:nth-of-type(odd)>div]:pie-6 [&:nth-of-type(odd)>div]:border-ie':
isBelowMdScreen && !isBelowSmScreen,
'[&:not(:last-child)>div]:pie-6 [&:not(:last-child)>div]:border-ie': !isBelowMdScreen
})}
>
<div className='flex justify-between items-center'>
<div className='flex flex-col'>
<Typography variant='h4'>{item.title}</Typography>
<Typography>{item.subtitle}</Typography>
</div>
<Avatar variant='rounded' className='is-[42px] bs-[42px]'>
<i className={classnames(item.icon, 'text-[26px]')} />
</Avatar>
</div>
{isBelowMdScreen && !isBelowSmScreen && index < data.length - 2 && (
<Divider
className={classnames('mbs-6', {
'mie-6': index % 2 === 0
})}
/>
)}
{isBelowSmScreen && index < data.length - 1 && <Divider className='mbs-6' />}
</Grid>
))}
</Grid>
</CardContent>
</Card>
)
}
export default InvoiceCard
@@ -0,0 +1,463 @@
'use client'
// React Imports
import { useState, useEffect, useMemo } from 'react'
// Next Imports
import Link from 'next/link'
import { useParams } from 'next/navigation'
// MUI Imports
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import Checkbox from '@mui/material/Checkbox'
import Chip from '@mui/material/Chip'
import IconButton from '@mui/material/IconButton'
import MenuItem from '@mui/material/MenuItem'
import Tooltip from '@mui/material/Tooltip'
import TablePagination from '@mui/material/TablePagination'
import type { TextFieldProps } from '@mui/material/TextField'
// Third-party Imports
import classnames from 'classnames'
import { rankItem } from '@tanstack/match-sorter-utils'
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
getFilteredRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFacetedMinMaxValues,
getPaginationRowModel,
getSortedRowModel
} from '@tanstack/react-table'
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
import type { RankingInfo } from '@tanstack/match-sorter-utils'
// Type Imports
import type { ThemeColor } from '@core/types'
import type { InvoiceType } from '@/types/apps/invoiceTypes'
import type { Locale } from '@configs/i18n'
// Component Imports
import OptionMenu from '@core/components/option-menu'
import CustomAvatar from '@core/components/mui/Avatar'
import TablePaginationComponent from '@components/TablePaginationComponent'
import CustomTextField from '@core/components/mui/TextField'
// Util Imports
import { getInitials } from '@/utils/getInitials'
import { getLocalizedUrl } from '@/utils/i18n'
// Style Imports
import tableStyles from '@core/styles/table.module.css'
declare module '@tanstack/table-core' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
type InvoiceTypeWithAction = InvoiceType & {
action?: string
}
type InvoiceStatusObj = {
[key: string]: {
icon: string
color: ThemeColor
}
}
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value)
// Store the itemRank info
addMeta({
itemRank
})
// Return if the item should be filtered in/out
return itemRank.passed
}
const DebouncedInput = ({
value: initialValue,
onChange,
debounce = 500,
...props
}: {
value: string | number
onChange: (value: string | number) => void
debounce?: number
} & Omit<TextFieldProps, 'onChange'>) => {
// States
const [value, setValue] = useState(initialValue)
useEffect(() => {
setValue(initialValue)
}, [initialValue])
useEffect(() => {
const timeout = setTimeout(() => {
onChange(value)
}, debounce)
return () => clearTimeout(timeout)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value])
return <CustomTextField {...props} value={value} onChange={e => setValue(e.target.value)} />
}
// Vars
const invoiceStatusObj: InvoiceStatusObj = {
Sent: { color: 'secondary', icon: 'tabler-send-2' },
Paid: { color: 'success', icon: 'tabler-check' },
Draft: { color: 'primary', icon: 'tabler-mail' },
'Partial Payment': { color: 'warning', icon: 'tabler-chart-pie-2' },
'Past Due': { color: 'error', icon: 'tabler-alert-circle' },
Downloaded: { color: 'info', icon: 'tabler-arrow-down' }
}
// Column Definitions
const columnHelper = createColumnHelper<InvoiceTypeWithAction>()
const InvoiceListTable = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
// States
const [status, setStatus] = useState<InvoiceType['invoiceStatus']>('')
const [rowSelection, setRowSelection] = useState({})
const [data, setData] = useState(...[invoiceData])
const [filteredData, setFilteredData] = useState(data)
const [globalFilter, setGlobalFilter] = useState('')
// Hooks
const { lang: locale } = useParams()
const columns = useMemo<ColumnDef<InvoiceTypeWithAction, any>[]>(
() => [
{
id: 'select',
header: ({ table }) => (
<Checkbox
{...{
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onChange: table.getToggleAllRowsSelectedHandler()
}}
/>
),
cell: ({ row }) => (
<Checkbox
{...{
checked: row.getIsSelected(),
disabled: !row.getCanSelect(),
indeterminate: row.getIsSomeSelected(),
onChange: row.getToggleSelectedHandler()
}}
/>
)
},
columnHelper.accessor('id', {
header: '#',
cell: ({ row }) => (
<Typography
component={Link}
href={getLocalizedUrl(`/apps/invoice/preview/${row.original.id}`, locale as Locale)}
color='primary.main'
>{`#${row.original.id}`}</Typography>
)
}),
columnHelper.accessor('invoiceStatus', {
header: 'Status',
cell: ({ row }) => (
<Tooltip
title={
<div>
<Typography variant='body2' component='span' className='text-inherit'>
{row.original.invoiceStatus}
</Typography>
<br />
<Typography variant='body2' component='span' className='text-inherit'>
Balance:
</Typography>{' '}
{row.original.balance}
<br />
<Typography variant='body2' component='span' className='text-inherit'>
Due Date:
</Typography>{' '}
{row.original.dueDate}
</div>
}
>
<CustomAvatar skin='light' color={invoiceStatusObj[row.original.invoiceStatus].color} size={28}>
<i className={classnames('bs-4 is-4', invoiceStatusObj[row.original.invoiceStatus].icon)} />
</CustomAvatar>
</Tooltip>
)
}),
columnHelper.accessor('name', {
header: 'Client',
cell: ({ row }) => (
<div className='flex items-center gap-3'>
{getAvatar({ avatar: row.original.avatar, name: row.original.name })}
<div className='flex flex-col'>
<Typography className='font-medium' color='text.primary'>
{row.original.name}
</Typography>
<Typography variant='body2'>{row.original.companyEmail}</Typography>
</div>
</div>
)
}),
columnHelper.accessor('total', {
header: 'Total',
cell: ({ row }) => <Typography>{`$${row.original.total}`}</Typography>
}),
columnHelper.accessor('issuedDate', {
header: 'Issued Date',
cell: ({ row }) => <Typography>{row.original.issuedDate}</Typography>
}),
columnHelper.accessor('balance', {
header: 'Balance',
cell: ({ row }) => {
return row.original.balance === 0 ? (
<Chip label='Paid' color='success' size='small' variant='tonal' />
) : (
<Typography color='text.primary'>{row.original.balance}</Typography>
)
}
}),
columnHelper.accessor('action', {
header: 'Action',
cell: ({ row }) => (
<div className='flex items-center'>
<IconButton onClick={() => setData(data?.filter(invoice => invoice.id !== row.original.id))}>
<i className='tabler-trash text-textSecondary' />
</IconButton>
<IconButton>
<Link
href={getLocalizedUrl(`/apps/invoice/preview/${row.original.id}`, locale as Locale)}
className='flex'
>
<i className='tabler-eye text-textSecondary' />
</Link>
</IconButton>
<OptionMenu
iconButtonProps={{ size: 'medium' }}
iconClassName='text-textSecondary'
options={[
{
text: 'Download',
icon: 'tabler-download',
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
},
{
text: 'Edit',
icon: 'tabler-pencil',
href: getLocalizedUrl(`/apps/invoice/edit/${row.original.id}`, locale as Locale),
linkProps: {
className: 'flex items-center is-full plb-2 pli-4 gap-2 text-textSecondary'
}
},
{
text: 'Duplicate',
icon: 'tabler-copy',
menuItemProps: { className: 'flex items-center gap-2 text-textSecondary' }
}
]}
/>
</div>
),
enableSorting: false
})
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[data, filteredData]
)
const table = useReactTable({
data: filteredData as InvoiceType[],
columns,
filterFns: {
fuzzy: fuzzyFilter
},
state: {
rowSelection,
globalFilter
},
initialState: {
pagination: {
pageSize: 10
}
},
enableRowSelection: true, //enable row selection for all rows
// enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row
globalFilterFn: fuzzyFilter,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues()
})
const getAvatar = (params: Pick<InvoiceType, 'avatar' | 'name'>) => {
const { avatar, name } = params
if (avatar) {
return <CustomAvatar src={avatar} skin='light' size={34} />
} else {
return (
<CustomAvatar skin='light' size={34}>
{getInitials(name as string)}
</CustomAvatar>
)
}
}
useEffect(() => {
const filteredData = data?.filter(invoice => {
if (status && invoice.invoiceStatus.toLowerCase().replace(/\s+/g, '-') !== status) return false
return true
})
setFilteredData(filteredData)
}, [status, data, setFilteredData])
return (
<Card>
<CardContent className='flex justify-between flex-col items-start md:items-center md:flex-row gap-4'>
<div className='flex flex-col sm:flex-row items-center justify-between gap-4 is-full sm:is-auto'>
<div className='flex items-center gap-2 is-full sm:is-auto'>
<Typography className='hidden sm:block'>Show</Typography>
<CustomTextField
select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
className='is-[70px] max-sm:is-full'
>
<MenuItem value='10'>10</MenuItem>
<MenuItem value='25'>25</MenuItem>
<MenuItem value='50'>50</MenuItem>
</CustomTextField>
</div>
<Button
variant='contained'
component={Link}
startIcon={<i className='tabler-plus' />}
href={getLocalizedUrl('apps/invoice/add', locale as Locale)}
className='max-sm:is-full'
>
Create Invoice
</Button>
</div>
<div className='flex max-sm:flex-col max-sm:is-full sm:items-center gap-4'>
<DebouncedInput
value={globalFilter ?? ''}
onChange={value => setGlobalFilter(String(value))}
placeholder='Search Invoice'
className='max-sm:is-full sm:is-[250px]'
/>
<CustomTextField
select
id='select-status'
value={status}
onChange={e => setStatus(e.target.value)}
className='max-sm:is-full sm:is-[160px]'
slotProps={{
select: { displayEmpty: true }
}}
>
<MenuItem value=''>Invoice Status</MenuItem>
<MenuItem value='downloaded'>Downloaded</MenuItem>
<MenuItem value='draft'>Draft</MenuItem>
<MenuItem value='paid'>Paid</MenuItem>
<MenuItem value='partial-payment'>Partial Payment</MenuItem>
<MenuItem value='past-due'>Past Due</MenuItem>
<MenuItem value='sent'>Sent</MenuItem>
</CustomTextField>
</div>
</CardContent>
<div className='overflow-x-auto'>
<table className={tableStyles.table}>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<>
<div
className={classnames({
'flex items-center': header.column.getIsSorted(),
'cursor-pointer select-none': header.column.getCanSort()
})}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{
asc: <i className='tabler-chevron-up text-xl' />,
desc: <i className='tabler-chevron-down text-xl' />
}[header.column.getIsSorted() as 'asc' | 'desc'] ?? null}
</div>
</>
)}
</th>
))}
</tr>
))}
</thead>
{table.getFilteredRowModel().rows.length === 0 ? (
<tbody>
<tr>
<td colSpan={table.getVisibleFlatColumns().length} className='text-center'>
No data available
</td>
</tr>
</tbody>
) : (
<tbody>
{table
.getRowModel()
.rows.slice(0, table.getState().pagination.pageSize)
.map(row => {
return (
<tr key={row.id} className={classnames({ selected: row.getIsSelected() })}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
)
})}
</tbody>
)}
</table>
</div>
<TablePagination
component={() => <TablePaginationComponent table={table} />}
count={table.getFilteredRowModel().rows.length}
rowsPerPage={table.getState().pagination.pageSize}
page={table.getState().pagination.pageIndex}
onPageChange={(_, page) => {
table.setPageIndex(page)
}}
onRowsPerPageChange={e => table.setPageSize(Number(e.target.value))}
/>
</Card>
)
}
export default InvoiceListTable
+24
View File
@@ -0,0 +1,24 @@
// MUI Imports
import Grid from '@mui/material/Grid2'
// Type Imports
import type { InvoiceType } from '@/types/apps/invoiceTypes'
// Component Imports
import InvoiceListTable from './InvoiceListTable'
import InvoiceCard from './InvoiceCard'
const InvoiceList = ({ invoiceData }: { invoiceData?: InvoiceType[] }) => {
return (
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<InvoiceCard />
</Grid>
<Grid size={{ xs: 12 }}>
<InvoiceListTable invoiceData={invoiceData} />
</Grid>
</Grid>
)
}
export default InvoiceList
@@ -0,0 +1,80 @@
// React Imports
import { useState } from 'react'
// Next Imports
import Link from 'next/link'
import { useParams } from 'next/navigation'
// MUI Imports
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Button from '@mui/material/Button'
// Type Imports
import type { Locale } from '@configs/i18n'
// Component Imports
import AddPaymentDrawer from '@views/apps/invoice/shared/AddPaymentDrawer'
import SendInvoiceDrawer from '@views/apps/invoice/shared/SendInvoiceDrawer'
// Util Imports
import { getLocalizedUrl } from '@/utils/i18n'
const PreviewActions = ({ id, onButtonClick }: { id: string; onButtonClick: () => void }) => {
// States
const [paymentDrawerOpen, setPaymentDrawerOpen] = useState(false)
const [sendDrawerOpen, setSendDrawerOpen] = useState(false)
// Hooks
const { lang: locale } = useParams()
return (
<>
<Card>
<CardContent className='flex flex-col gap-4'>
<Button
fullWidth
variant='contained'
className='capitalize'
startIcon={<i className='tabler-send' />}
onClick={() => setSendDrawerOpen(true)}
>
Send Invoice
</Button>
<Button fullWidth color='secondary' variant='tonal' className='capitalize'>
Download
</Button>
<div className='flex items-center gap-4'>
<Button fullWidth color='secondary' variant='tonal' className='capitalize' onClick={onButtonClick}>
Print
</Button>
<Button
fullWidth
component={Link}
color='secondary'
variant='tonal'
className='capitalize'
href={getLocalizedUrl(`/apps/invoice/edit/${id}`, locale as Locale)}
>
Edit
</Button>
</div>
<Button
fullWidth
color='success'
variant='contained'
className='capitalize'
onClick={() => setPaymentDrawerOpen(true)}
startIcon={<i className='tabler-currency-dollar' />}
>
Add Payment
</Button>
</CardContent>
</Card>
<AddPaymentDrawer open={paymentDrawerOpen} handleClose={() => setPaymentDrawerOpen(false)} />
<SendInvoiceDrawer open={sendDrawerOpen} handleClose={() => setSendDrawerOpen(false)} />
</>
)
}
export default PreviewActions
@@ -0,0 +1,219 @@
// MUI Imports
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import Grid from '@mui/material/Grid2'
import Divider from '@mui/material/Divider'
// Type Imports
import type { InvoiceType } from '@/types/apps/invoiceTypes'
// Component Imports
import Logo from '@components/layout/shared/Logo'
// Style Imports
import tableStyles from '@core/styles/table.module.css'
import './print.css'
// Vars
const data = [
{
Item: 'Premium Branding Package',
Description: 'Branding & Promotion',
Hours: 48,
Qty: 1,
Total: '$32'
},
{
Item: 'Social Media',
Description: 'Social media templates',
Hours: 42,
Qty: 1,
Total: '$28'
},
{
Item: 'Web Design',
Description: 'Web designing package',
Hours: 46,
Qty: 1,
Total: '$24'
},
{
Item: 'SEO',
Description: 'Search engine optimization',
Hours: 40,
Qty: 1,
Total: '$22'
}
]
const PreviewCard = ({ invoiceData, id }: { invoiceData?: InvoiceType; id: string }) => {
return (
<Card className='previewCard'>
<CardContent className='sm:!p-12'>
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<div className='p-6 bg-actionHover rounded'>
<div className='flex justify-between gap-y-4 flex-col sm:flex-row'>
<div className='flex flex-col gap-6'>
<div className='flex items-center gap-2.5'>
<Logo />
</div>
<div>
<Typography color='text.primary'>Office 149, 450 South Brand Brooklyn</Typography>
<Typography color='text.primary'>San Diego County, CA 91905, USA</Typography>
<Typography color='text.primary'>+1 (123) 456 7891, +44 (876) 543 2198</Typography>
</div>
</div>
<div className='flex flex-col gap-6'>
<Typography variant='h5'>{`Invoice #${id}`}</Typography>
<div className='flex flex-col gap-1'>
<Typography color='text.primary'>{`Date Issued: ${invoiceData?.issuedDate}`}</Typography>
<Typography color='text.primary'>{`Date Due: ${invoiceData?.dueDate}`}</Typography>
</div>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<Grid container spacing={6}>
<Grid size={{ xs: 12, sm: 6 }}>
<div className='flex flex-col gap-4'>
<Typography className='font-medium' color='text.primary'>
Invoice To:
</Typography>
<div>
<Typography>{invoiceData?.name}</Typography>
<Typography>{invoiceData?.company}</Typography>
<Typography>{invoiceData?.address}</Typography>
<Typography>{invoiceData?.contact}</Typography>
<Typography>{invoiceData?.companyEmail}</Typography>
</div>
</div>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
<div className='flex flex-col gap-4'>
<Typography className='font-medium' color='text.primary'>
Bill To:
</Typography>
<div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Total Due:</Typography>
<Typography>$12,110.55</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Bank name:</Typography>
<Typography>American Bank</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>Country:</Typography>
<Typography>United States</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>IBAN:</Typography>
<Typography>ETD95476213874685</Typography>
</div>
<div className='flex items-center gap-4'>
<Typography className='min-is-[100px]'>SWIFT code:</Typography>
<Typography>BR91905</Typography>
</div>
</div>
</div>
</Grid>
</Grid>
</Grid>
<Grid size={{ xs: 12 }}>
<div className='overflow-x-auto border rounded'>
<table className={tableStyles.table}>
<thead className='border-bs-0'>
<tr>
<th className='!bg-transparent'>Item</th>
<th className='!bg-transparent'>Description</th>
<th className='!bg-transparent'>Hours</th>
<th className='!bg-transparent'>Qty</th>
<th className='!bg-transparent'>Total</th>
</tr>
</thead>
<tbody>
{data.map((item, index) => (
<tr key={index}>
<td>
<Typography color='text.primary'>{item.Item}</Typography>
</td>
<td>
<Typography color='text.primary'>{item.Description}</Typography>
</td>
<td>
<Typography color='text.primary'>{item.Hours}</Typography>
</td>
<td>
<Typography color='text.primary'>{item.Qty}</Typography>
</td>
<td>
<Typography color='text.primary'>{item.Total}</Typography>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<div className='flex justify-between flex-col gap-y-4 sm:flex-row'>
<div className='flex flex-col gap-1 order-2 sm:order-[unset]'>
<div className='flex items-center gap-2'>
<Typography className='font-medium' color='text.primary'>
Salesperson:
</Typography>
<Typography>Tommy Shelby</Typography>
</div>
<Typography>Thanks for your business</Typography>
</div>
<div className='min-is-[200px]'>
<div className='flex items-center justify-between'>
<Typography>Subtotal:</Typography>
<Typography className='font-medium' color='text.primary'>
$1800
</Typography>
</div>
<div className='flex items-center justify-between'>
<Typography>Discount:</Typography>
<Typography className='font-medium' color='text.primary'>
$28
</Typography>
</div>
<div className='flex items-center justify-between'>
<Typography>Tax:</Typography>
<Typography className='font-medium' color='text.primary'>
21%
</Typography>
</div>
<Divider className='mlb-2' />
<div className='flex items-center justify-between'>
<Typography>Total:</Typography>
<Typography className='font-medium' color='text.primary'>
$1690
</Typography>
</div>
</div>
</div>
</Grid>
<Grid size={{ xs: 12 }}>
<Divider className='border-dashed' />
</Grid>
<Grid size={{ xs: 12 }}>
<Typography>
<Typography component='span' className='font-medium' color='text.primary'>
Note:
</Typography>{' '}
It was a pleasure working with you and your team. We hope you will keep us in mind for future freelance
projects. Thank You!
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
)
}
export default PreviewCard
+31
View File
@@ -0,0 +1,31 @@
'use client'
// MUI Imports
import Grid from '@mui/material/Grid2'
// Type Imports
import type { InvoiceType } from '@/types/apps/invoiceTypes'
// Component Imports
import PreviewActions from './PreviewActions'
import PreviewCard from './PreviewCard'
const Preview = ({ invoiceData, id }: { invoiceData?: InvoiceType; id: string }) => {
// Handle Print Button Click
const handleButtonClick = () => {
window.print()
}
return (
<Grid container spacing={6}>
<Grid size={{ xs: 12, md: 9 }}>
<PreviewCard invoiceData={invoiceData} id={id} />
</Grid>
<Grid size={{ xs: 12, md: 3 }}>
<PreviewActions id={id} onButtonClick={handleButtonClick} />
</Grid>
</Grid>
)
}
export default Preview
+31
View File
@@ -0,0 +1,31 @@
@media print {
body * {
visibility: hidden;
}
#__next :is(.sm\:\!p-12) {
padding: 0rem !important;
}
[data-dark] {
--mui-palette-text-primary: rgba(47, 43, 61, 0.9);
--mui-palette-action-hover: rgba(47, 43, 61, 0.06);
--mui-palette-text-secondary: rgba(47, 43, 61, 0.7);
--mui-palette-divider: rgba(47, 43, 61, 0.12);
}
/* Only show the .preview-card element when printing */
.previewCard * {
visibility: visible;
}
.previewCard {
inline-size: 100%;
block-size: 100%;
position: fixed;
inset: 0;
box-sizing: border-box;
}
@media (max-width: 600px) {
.previewCard {
position: relative !important;
}
}
}
@@ -0,0 +1,142 @@
// React Imports
import { useState } from 'react'
import type { FormEvent } from 'react'
// MUI Import
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import Typography from '@mui/material/Typography'
import InputAdornment from '@mui/material/InputAdornment'
import MenuItem from '@mui/material/MenuItem'
import Button from '@mui/material/Button'
import Divider from '@mui/material/Divider'
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
// Styled Component Imports
import AppReactDatepicker from '@/libs/styles/AppReactDatepicker'
type Props = {
open: boolean
handleClose: () => void
}
type FormDataType = {
paymentDate: Date
paymentMethod: string
paymentAmount: number
paymentNote: string
}
// Vars
const initialData: FormDataType = {
paymentDate: new Date(),
paymentMethod: 'select-method',
paymentAmount: 500,
paymentNote: ''
}
const AddPaymentDrawer = ({ open, handleClose }: Props) => {
// States
const [formData, setFormData] = useState<FormDataType>(initialData)
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
handleClose()
setFormData(initialData)
}
const handleReset = () => {
handleClose()
setFormData(initialData)
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between plb-5 pli-6'>
<Typography variant='h5'>Add New User</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-2xl text-textPrimary' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleSubmit} className='flex flex-col gap-6'>
<CustomTextField
fullWidth
id='invoice-balance'
label='Invoice Balance'
slotProps={{
input: {
disabled: true
}
}}
defaultValue='5000.00'
/>
<CustomTextField
fullWidth
id='payment-amount'
label='Payment Amount'
type='number'
slotProps={{
input: {
startAdornment: <InputAdornment position='start'>$</InputAdornment>
}
}}
value={formData.paymentAmount}
onChange={e => setFormData({ ...formData, paymentAmount: +e.target.value })}
/>
<AppReactDatepicker
selected={formData.paymentDate}
id='payment-date'
onChange={(date: Date | null) => date !== null && setFormData({ ...formData, paymentDate: date })}
customInput={<CustomTextField fullWidth label='Payment Date' />}
/>
<CustomTextField
select
label='Payment Method'
id='payment-method-select'
value={formData.paymentMethod}
onChange={e => setFormData({ ...formData, paymentMethod: e.target.value as string })}
>
<MenuItem value='select-method' disabled>
Select Payment Method
</MenuItem>
<MenuItem value='cash'>Cash</MenuItem>
<MenuItem value='bank-transfer'>Bank Transfer</MenuItem>
<MenuItem value='credit'>Credit</MenuItem>
<MenuItem value='debit'>Debit</MenuItem>
<MenuItem value='paypal'>Paypal</MenuItem>
</CustomTextField>
<CustomTextField
rows={6}
multiline
fullWidth
label='Internal Payment Note'
placeholder='Internal Payment Note'
value={formData.paymentNote}
onChange={e => setFormData({ ...formData, paymentNote: e.target.value })}
/>
<div className='flex items-center gap-4'>
<Button variant='contained' type='submit'>
Send
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Cancel
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default AddPaymentDrawer
@@ -0,0 +1,127 @@
// React Imports
import { useState } from 'react'
import type { FormEvent } from 'react'
// MUI Imports
import Drawer from '@mui/material/Drawer'
import IconButton from '@mui/material/IconButton'
import Typography from '@mui/material/Typography'
import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider'
// Component Imports
import CustomTextField from '@core/components/mui/TextField'
type Props = {
open: boolean
handleClose: () => void
}
type FormDataType = {
from: string
to: string
subject: string
message: string
}
// Vars
const initialData: FormDataType = {
from: 'shelbyComapny@email.com',
to: 'qConsolidated@email.com',
subject: 'Invoice of purchased Admin Templates',
message: `Dear Queen Consolidated,
Thank you for your business, always a pleasure to work with you!
We have generated a new invoice in the amount of $95.59
We would appreciate payment of this invoice by 05/11/2019`
}
const SendInvoiceDrawer = ({ open, handleClose }: Props) => {
// States
const [formData, setFormData] = useState<FormDataType>(initialData)
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
handleClose()
setFormData(initialData)
}
const handleReset = () => {
handleClose()
setFormData(initialData)
}
return (
<Drawer
open={open}
anchor='right'
variant='temporary'
onClose={handleReset}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: { xs: 300, sm: 400 } } }}
>
<div className='flex items-center justify-between plb-5 pli-6'>
<Typography variant='h5'>Send Invoice</Typography>
<IconButton size='small' onClick={handleReset}>
<i className='tabler-x text-2xl text-textPrimary' />
</IconButton>
</div>
<Divider />
<div className='p-6'>
<form onSubmit={handleSubmit} className='flex flex-col items-start gap-6'>
<CustomTextField
fullWidth
label='From'
variant='outlined'
value={formData.from}
onChange={e => setFormData({ ...formData, from: e.target.value })}
/>
<CustomTextField
fullWidth
label='To'
variant='outlined'
value={formData.to}
onChange={e => setFormData({ ...formData, to: e.target.value })}
/>
<CustomTextField
fullWidth
label='Subject'
variant='outlined'
value={formData.subject}
onChange={e => setFormData({ ...formData, subject: e.target.value })}
/>
<CustomTextField
fullWidth
label='Message'
variant='outlined'
multiline
rows={10}
value={formData.message}
onChange={e => setFormData({ ...formData, message: e.target.value })}
/>
<Chip
size='small'
color='primary'
variant='tonal'
className='rounded'
label='Invoice Attached'
icon={<i className='tabler-link' />}
/>
<div className='flex items-center gap-4'>
<Button variant='contained' color='primary' type='submit'>
Send
</Button>
<Button variant='tonal' color='error' type='reset' onClick={handleReset}>
Cancel
</Button>
</div>
</form>
</div>
</Drawer>
)
}
export default SendInvoiceDrawer