Expense List Table
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
// React Imports
|
||||
import React from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import type { TextFieldProps } from '@mui/material/TextField'
|
||||
|
||||
interface DateRangePickerProps {
|
||||
/**
|
||||
* Start date value (Date object or date string)
|
||||
*/
|
||||
startDate: Date | string | null
|
||||
/**
|
||||
* End date value (Date object or date string)
|
||||
*/
|
||||
endDate: Date | string | null
|
||||
/**
|
||||
* Callback when start date changes
|
||||
*/
|
||||
onStartDateChange: (date: Date | null) => void
|
||||
/**
|
||||
* Callback when end date changes
|
||||
*/
|
||||
onEndDateChange: (date: Date | null) => void
|
||||
/**
|
||||
* Label for start date field
|
||||
*/
|
||||
startLabel?: string
|
||||
/**
|
||||
* Label for end date field
|
||||
*/
|
||||
endLabel?: string
|
||||
/**
|
||||
* Placeholder for start date field
|
||||
*/
|
||||
startPlaceholder?: string
|
||||
/**
|
||||
* Placeholder for end date field
|
||||
*/
|
||||
endPlaceholder?: string
|
||||
/**
|
||||
* Size of the text fields
|
||||
*/
|
||||
size?: 'small' | 'medium'
|
||||
/**
|
||||
* Whether the fields are disabled
|
||||
*/
|
||||
disabled?: boolean
|
||||
/**
|
||||
* Whether the fields are required
|
||||
*/
|
||||
required?: boolean
|
||||
/**
|
||||
* Custom className for the container
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Custom styles for the container
|
||||
*/
|
||||
containerStyle?: React.CSSProperties
|
||||
/**
|
||||
* Separator between date fields
|
||||
*/
|
||||
separator?: string
|
||||
/**
|
||||
* Custom props for start date TextField
|
||||
*/
|
||||
startTextFieldProps?: Omit<TextFieldProps, 'type' | 'value' | 'onChange'>
|
||||
/**
|
||||
* Custom props for end date TextField
|
||||
*/
|
||||
endTextFieldProps?: Omit<TextFieldProps, 'type' | 'value' | 'onChange'>
|
||||
/**
|
||||
* Error state for start date
|
||||
*/
|
||||
startError?: boolean
|
||||
/**
|
||||
* Error state for end date
|
||||
*/
|
||||
endError?: boolean
|
||||
/**
|
||||
* Helper text for start date
|
||||
*/
|
||||
startHelperText?: string
|
||||
/**
|
||||
* Helper text for end date
|
||||
*/
|
||||
endHelperText?: string
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
const formatDateForInput = (date: Date | string | null): string => {
|
||||
if (!date) return ''
|
||||
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date
|
||||
|
||||
if (isNaN(dateObj.getTime())) return ''
|
||||
|
||||
return dateObj.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
const parseDateFromInput = (dateString: string): Date | null => {
|
||||
if (!dateString) return null
|
||||
|
||||
const date = new Date(dateString)
|
||||
return isNaN(date.getTime()) ? null : date
|
||||
}
|
||||
|
||||
const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange,
|
||||
onEndDateChange,
|
||||
startLabel,
|
||||
endLabel,
|
||||
startPlaceholder,
|
||||
endPlaceholder,
|
||||
size = 'small',
|
||||
disabled = false,
|
||||
required = false,
|
||||
className = '',
|
||||
containerStyle = {},
|
||||
separator = '-',
|
||||
startTextFieldProps = {},
|
||||
endTextFieldProps = {},
|
||||
startError = false,
|
||||
endError = false,
|
||||
startHelperText,
|
||||
endHelperText
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
const defaultTextFieldSx = {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: 'primary.main'
|
||||
},
|
||||
'& fieldset': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const date = parseDateFromInput(event.target.value)
|
||||
onStartDateChange(date)
|
||||
}
|
||||
|
||||
const handleEndDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const date = parseDateFromInput(event.target.value)
|
||||
onEndDateChange(date)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-4 ${className}`} style={containerStyle}>
|
||||
<TextField
|
||||
type='date'
|
||||
label={startLabel}
|
||||
placeholder={startPlaceholder}
|
||||
value={formatDateForInput(startDate)}
|
||||
onChange={handleStartDateChange}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
error={startError}
|
||||
helperText={startHelperText}
|
||||
sx={{
|
||||
...defaultTextFieldSx,
|
||||
...startTextFieldProps.sx
|
||||
}}
|
||||
{...startTextFieldProps}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
color='text.secondary'
|
||||
sx={{
|
||||
userSelect: 'none',
|
||||
fontSize: size === 'small' ? '14px' : '16px'
|
||||
}}
|
||||
>
|
||||
{separator}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
type='date'
|
||||
label={endLabel}
|
||||
placeholder={endPlaceholder}
|
||||
value={formatDateForInput(endDate)}
|
||||
onChange={handleEndDateChange}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
error={endError}
|
||||
helperText={endHelperText}
|
||||
sx={{
|
||||
...defaultTextFieldSx,
|
||||
...endTextFieldProps.sx
|
||||
}}
|
||||
{...endTextFieldProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateRangePicker
|
||||
|
||||
// Export utility functions for external use
|
||||
export { formatDateForInput, parseDateFromInput }
|
||||
|
||||
// Example usage:
|
||||
/*
|
||||
import DateRangePicker from '@/components/DateRangePicker'
|
||||
|
||||
// In your component:
|
||||
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
||||
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
||||
|
||||
// Basic usage
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
|
||||
// With labels and validation
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
startLabel="Tanggal Mulai"
|
||||
endLabel="Tanggal Selesai"
|
||||
required
|
||||
startError={startDate && endDate && startDate > endDate}
|
||||
endError={startDate && endDate && startDate > endDate}
|
||||
startHelperText={startDate && endDate && startDate > endDate ? "Tanggal mulai tidak boleh lebih besar dari tanggal selesai" : ""}
|
||||
/>
|
||||
|
||||
// Custom styling
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
separator="sampai"
|
||||
size="medium"
|
||||
className="my-custom-class"
|
||||
startTextFieldProps={{
|
||||
variant: "filled",
|
||||
sx: { minWidth: '150px' }
|
||||
}}
|
||||
endTextFieldProps={{
|
||||
variant: "filled",
|
||||
sx: { minWidth: '150px' }
|
||||
}}
|
||||
/>
|
||||
|
||||
// Integration with your existing filter logic
|
||||
const handleDateRangeChange = (start: Date | null, end: Date | null) => {
|
||||
setFilter({
|
||||
...filter,
|
||||
date_from: start ? formatDateDDMMYYYY(start) : null,
|
||||
date_to: end ? formatDateDDMMYYYY(end) : null
|
||||
})
|
||||
}
|
||||
|
||||
<DateRangePicker
|
||||
startDate={filter.date_from ? new Date(filter.date_from) : null}
|
||||
endDate={filter.date_to ? new Date(filter.date_to) : null}
|
||||
onStartDateChange={(date) => handleDateRangeChange(date, filter.date_to ? new Date(filter.date_to) : null)}
|
||||
onEndDateChange={(date) => handleDateRangeChange(filter.date_from ? new Date(filter.date_from) : null, date)}
|
||||
/>
|
||||
*/
|
||||
@@ -0,0 +1,249 @@
|
||||
// React Imports
|
||||
import React, { useState } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import Button from '@mui/material/Button'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
const DropdownButton = styled(Button)(({ theme }) => ({
|
||||
textTransform: 'none',
|
||||
fontWeight: 400,
|
||||
borderRadius: '8px',
|
||||
borderColor: '#e0e0e0',
|
||||
color: '#666',
|
||||
'&:hover': {
|
||||
borderColor: '#ccc',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)'
|
||||
}
|
||||
}))
|
||||
|
||||
interface StatusFilterTabsProps {
|
||||
/**
|
||||
* Array of status options to display as filter tabs
|
||||
*/
|
||||
statusOptions: string[]
|
||||
/**
|
||||
* Currently selected status filter
|
||||
*/
|
||||
selectedStatus: string
|
||||
/**
|
||||
* Callback function when a status is selected
|
||||
*/
|
||||
onStatusChange: (status: string) => void
|
||||
/**
|
||||
* Custom className for the container
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* Custom styles for the container
|
||||
*/
|
||||
containerStyle?: React.CSSProperties
|
||||
/**
|
||||
* Size of the buttons
|
||||
*/
|
||||
buttonSize?: 'small' | 'medium' | 'large'
|
||||
/**
|
||||
* Maximum number of status options to show as buttons before switching to dropdown
|
||||
*/
|
||||
maxButtonsBeforeDropdown?: number
|
||||
/**
|
||||
* Label for the dropdown when there are many options
|
||||
*/
|
||||
dropdownLabel?: string
|
||||
}
|
||||
|
||||
const StatusFilterTabs: React.FC<StatusFilterTabsProps> = ({
|
||||
statusOptions,
|
||||
selectedStatus,
|
||||
onStatusChange,
|
||||
className = '',
|
||||
containerStyle = {},
|
||||
buttonSize = 'small',
|
||||
maxButtonsBeforeDropdown = 5,
|
||||
dropdownLabel = 'Lainnya'
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
const handleDropdownClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleDropdownClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleDropdownItemClick = (status: string) => {
|
||||
onStatusChange(status)
|
||||
handleDropdownClose()
|
||||
}
|
||||
|
||||
// If status options are <= maxButtonsBeforeDropdown, show all as buttons
|
||||
if (statusOptions.length <= maxButtonsBeforeDropdown) {
|
||||
return (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{statusOptions.map(status => (
|
||||
<Button
|
||||
key={status}
|
||||
variant={selectedStatus === status ? 'contained' : 'outlined'}
|
||||
color={selectedStatus === status ? 'primary' : 'inherit'}
|
||||
onClick={() => onStatusChange(status)}
|
||||
size={buttonSize}
|
||||
className='rounded-lg'
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: selectedStatus === status ? 600 : 400,
|
||||
borderRadius: '8px',
|
||||
...(selectedStatus !== status && {
|
||||
borderColor: '#e0e0e0',
|
||||
color: '#666'
|
||||
})
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If more than maxButtonsBeforeDropdown, show first few as buttons and rest in dropdown
|
||||
const buttonStatuses = statusOptions.slice(0, maxButtonsBeforeDropdown - 1)
|
||||
const dropdownStatuses = statusOptions.slice(maxButtonsBeforeDropdown - 1)
|
||||
const isDropdownItemSelected = dropdownStatuses.includes(selectedStatus)
|
||||
|
||||
return (
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{/* Regular buttons for first few statuses */}
|
||||
{buttonStatuses.map(status => (
|
||||
<Button
|
||||
key={status}
|
||||
variant={selectedStatus === status ? 'contained' : 'outlined'}
|
||||
color={selectedStatus === status ? 'primary' : 'inherit'}
|
||||
onClick={() => onStatusChange(status)}
|
||||
size={buttonSize}
|
||||
className='rounded-lg'
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
fontWeight: selectedStatus === status ? 600 : 400,
|
||||
borderRadius: '8px',
|
||||
...(selectedStatus !== status && {
|
||||
borderColor: '#e0e0e0',
|
||||
color: '#666'
|
||||
})
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* Dropdown button for remaining statuses */}
|
||||
<DropdownButton
|
||||
variant='outlined'
|
||||
onClick={handleDropdownClick}
|
||||
size={buttonSize}
|
||||
endIcon={<i className='tabler-chevron-down' />}
|
||||
sx={{
|
||||
...(isDropdownItemSelected && {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
borderColor: 'primary.main',
|
||||
fontWeight: 600,
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.dark',
|
||||
borderColor: 'primary.dark'
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
{isDropdownItemSelected ? selectedStatus : dropdownLabel}
|
||||
</DropdownButton>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleDropdownClose}
|
||||
PaperProps={{
|
||||
elevation: 3,
|
||||
sx: {
|
||||
mt: 1,
|
||||
borderRadius: '8px',
|
||||
minWidth: '160px'
|
||||
}
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'left', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'left', vertical: 'bottom' }}
|
||||
>
|
||||
{dropdownStatuses.map(status => (
|
||||
<MenuItem
|
||||
key={status}
|
||||
onClick={() => handleDropdownItemClick(status)}
|
||||
selected={selectedStatus === status}
|
||||
sx={{
|
||||
fontSize: '14px',
|
||||
fontWeight: selectedStatus === status ? 600 : 400,
|
||||
color: selectedStatus === status ? 'primary.main' : 'text.primary'
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusFilterTabs
|
||||
|
||||
// Example usage:
|
||||
/*
|
||||
import StatusFilterTabs from '@/components/StatusFilterTabs'
|
||||
|
||||
// In your component:
|
||||
const [statusFilter, setStatusFilter] = useState('Semua')
|
||||
|
||||
// For few statuses (will show all as buttons)
|
||||
const expenseStatusOptions = ['Semua', 'Belum Dibayar', 'Dibayar Sebagian', 'Lunas']
|
||||
|
||||
// For many statuses (will show some buttons + dropdown)
|
||||
const manyStatusOptions = [
|
||||
'Semua',
|
||||
'Belum Dibayar',
|
||||
'Dibayar Sebagian',
|
||||
'Lunas',
|
||||
'Void',
|
||||
'Retur',
|
||||
'Jatuh Tempo',
|
||||
'Transaksi Berulang',
|
||||
'Ditangguhkan',
|
||||
'Dibatalkan'
|
||||
]
|
||||
|
||||
<StatusFilterTabs
|
||||
statusOptions={expenseStatusOptions}
|
||||
selectedStatus={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
/>
|
||||
|
||||
// With many options (will automatically use dropdown)
|
||||
<StatusFilterTabs
|
||||
statusOptions={manyStatusOptions}
|
||||
selectedStatus={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
maxButtonsBeforeDropdown={4} // Show 3 buttons + dropdown
|
||||
dropdownLabel="Status Lain"
|
||||
/>
|
||||
|
||||
// Custom configuration
|
||||
<StatusFilterTabs
|
||||
statusOptions={manyStatusOptions}
|
||||
selectedStatus={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
maxButtonsBeforeDropdown={3}
|
||||
dropdownLabel="Lainnya"
|
||||
buttonSize="medium"
|
||||
showBorder={false}
|
||||
/>
|
||||
*/
|
||||
@@ -91,14 +91,14 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
<MenuItem href={`/${locale}/dashboards/daily-report`}>{dictionary['navigation'].dailyReport}</MenuItem>
|
||||
</SubMenu>
|
||||
<MenuSection label={dictionary['navigation'].appsPages}>
|
||||
<SubMenu label={dictionary['navigation'].sales} icon={<i className='tabler-user' />}>
|
||||
<SubMenu label={dictionary['navigation'].sales} icon={<i className='tabler-receipt-2' />}>
|
||||
<MenuItem href={`/${locale}/apps/sales/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/sales/sales-bills`}>{dictionary['navigation'].invoices}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/sales/sales-deliveries`}>{dictionary['navigation'].deliveries}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/sales/sales-orders`}>{dictionary['navigation'].sales_orders}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/sales/sales-quotes`}>{dictionary['navigation'].quotes}</MenuItem>
|
||||
</SubMenu>
|
||||
<SubMenu label={dictionary['navigation'].purchase_text} icon={<i className='tabler-user' />}>
|
||||
<SubMenu label={dictionary['navigation'].purchase_text} icon={<i className='tabler-shopping-cart' />}>
|
||||
<MenuItem href={`/${locale}/apps/purchase/overview`}>{dictionary['navigation'].overview}</MenuItem>
|
||||
<MenuItem href={`/${locale}/apps/purchase/purchase-bills`}>
|
||||
{dictionary['navigation'].purchase_bills}
|
||||
@@ -113,6 +113,9 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
{dictionary['navigation'].purchase_quotes}
|
||||
</MenuItem>
|
||||
</SubMenu>
|
||||
<SubMenu label={dictionary['navigation'].expenses} icon={<i className='tabler-cash' />}>
|
||||
<MenuItem href={`/${locale}/apps/expense`}>{dictionary['navigation'].list}</MenuItem>
|
||||
</SubMenu>
|
||||
<SubMenu label={dictionary['navigation'].inventory} icon={<i className='tabler-salad' />}>
|
||||
<SubMenu label={dictionary['navigation'].products}>
|
||||
<MenuItem href={`/${locale}/apps/inventory/products/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||
@@ -160,7 +163,7 @@ const VerticalMenu = ({ dictionary, scrollMenu }: Props) => {
|
||||
<MenuItem href={`/${locale}/apps/user/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||
{/* <MenuItem href={`/${locale}/apps/user/view`}>{dictionary['navigation'].view}</MenuItem> */}
|
||||
</SubMenu>
|
||||
<SubMenu label={dictionary['navigation'].vendor} icon={<i className='tabler-user' />}>
|
||||
<SubMenu label={dictionary['navigation'].vendor} icon={<i className='tabler-building' />}>
|
||||
<MenuItem href={`/${locale}/apps/vendor/list`}>{dictionary['navigation'].list}</MenuItem>
|
||||
{/* <MenuItem href={`/${locale}/apps/user/view`}>{dictionary['navigation'].view}</MenuItem> */}
|
||||
</SubMenu>
|
||||
|
||||
Reference in New Issue
Block a user