519 lines
15 KiB
TypeScript
519 lines
15 KiB
TypeScript
import React, { ReactNode } from 'react'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
Button,
|
|
Typography,
|
|
FormControl,
|
|
FormControlLabel,
|
|
Radio,
|
|
RadioGroup,
|
|
TextField,
|
|
Box,
|
|
Chip,
|
|
Divider,
|
|
Grid,
|
|
useTheme
|
|
} from '@mui/material'
|
|
import { styled } from '@mui/material/styles'
|
|
|
|
// Type definitions
|
|
interface DateRange {
|
|
startDate: Date
|
|
endDate: Date
|
|
}
|
|
|
|
interface QuickAction {
|
|
label: string
|
|
handler: () => void
|
|
}
|
|
|
|
interface CustomQuickActions {
|
|
single?: QuickAction[]
|
|
range?: QuickAction[]
|
|
}
|
|
|
|
interface Labels {
|
|
filterLabel?: string
|
|
singleDateLabel?: string
|
|
rangeDateLabel?: string
|
|
selectDateLabel?: string
|
|
fromLabel?: string
|
|
toLabel?: string
|
|
todayLabel?: string
|
|
yesterdayLabel?: string
|
|
last7DaysLabel?: string
|
|
last30DaysLabel?: string
|
|
periodLabel?: string
|
|
exportHelpText?: string
|
|
}
|
|
|
|
interface ReportGeneratorProps {
|
|
// Required props
|
|
reportTitle: string
|
|
filterType: 'single' | 'range'
|
|
selectedDate: Date
|
|
dateRange: DateRange
|
|
onFilterTypeChange: (filterType: 'single' | 'range') => void
|
|
onSingleDateChange: (date: Date) => void
|
|
onDateRangeChange: (dateRange: DateRange) => void
|
|
onGeneratePDF: () => void
|
|
|
|
// Optional props dengan default values
|
|
maxWidth?: string
|
|
showQuickActions?: boolean
|
|
customQuickActions?: CustomQuickActions | null
|
|
periodFormat?: string
|
|
downloadButtonText?: string
|
|
cardShadow?: string
|
|
primaryColor?: string
|
|
|
|
// Optional helper functions
|
|
formatDateForInput?: ((date: Date) => string) | null
|
|
getReportPeriodText?: (() => string) | null
|
|
|
|
// Additional customization
|
|
className?: string
|
|
style?: React.CSSProperties
|
|
children?: ReactNode
|
|
|
|
// Loading state
|
|
isGenerating?: boolean
|
|
|
|
// Custom labels
|
|
labels?: Labels
|
|
}
|
|
|
|
// Custom styled components yang responsif terhadap theme
|
|
const StyledCard = styled(Card)(({ theme }) => ({
|
|
margin: '0 0 24px',
|
|
boxShadow: theme.palette.mode === 'dark' ? '0 2px 10px rgba(20, 21, 33, 0.3)' : '0 2px 10px rgba(58, 53, 65, 0.1)',
|
|
borderRadius: '8px',
|
|
backgroundColor: theme.palette.mode === 'dark' ? theme.palette.background.paper : '#ffffff'
|
|
}))
|
|
|
|
const PurpleButton = styled(Button)(({ theme }) => ({
|
|
backgroundColor: '#36175e',
|
|
color: 'white',
|
|
textTransform: 'none',
|
|
fontWeight: 500,
|
|
padding: '8px 24px',
|
|
'&:hover': {
|
|
backgroundColor: '#2d1350',
|
|
opacity: 0.9
|
|
},
|
|
'&:disabled': {
|
|
backgroundColor: theme.palette.mode === 'dark' ? '#444' : '#ccc',
|
|
color: theme.palette.mode === 'dark' ? '#888' : '#666'
|
|
}
|
|
}))
|
|
|
|
const QuickActionButton = styled(Button)(({ theme }) => ({
|
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.08)' : '#f8f7fa',
|
|
color: theme.palette.mode === 'dark' ? theme.palette.text.secondary : '#6f6b7d',
|
|
textTransform: 'none',
|
|
fontSize: '0.875rem',
|
|
padding: '6px 16px',
|
|
borderRadius: '6px',
|
|
'&:hover': {
|
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.16)' : '#ebe9f1'
|
|
}
|
|
}))
|
|
|
|
const InfoBox = styled(Box)(({ theme }) => ({
|
|
marginTop: theme.spacing(3),
|
|
padding: theme.spacing(2),
|
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.04)' : '#f8f7fa',
|
|
borderRadius: theme.shape.borderRadius,
|
|
border: theme.palette.mode === 'dark' ? '1px solid rgba(231, 227, 252, 0.12)' : 'none'
|
|
}))
|
|
|
|
const ReportGeneratorComponent: React.FC<ReportGeneratorProps> = ({
|
|
// Required props
|
|
reportTitle,
|
|
filterType,
|
|
selectedDate,
|
|
dateRange,
|
|
onFilterTypeChange,
|
|
onSingleDateChange,
|
|
onDateRangeChange,
|
|
onGeneratePDF,
|
|
|
|
// Optional props dengan default values
|
|
maxWidth = '1024px',
|
|
showQuickActions = true,
|
|
customQuickActions = null,
|
|
periodFormat = 'id-ID',
|
|
downloadButtonText = 'Download PDF',
|
|
cardShadow,
|
|
primaryColor = '#36175e',
|
|
|
|
// Optional helper functions
|
|
formatDateForInput = null,
|
|
getReportPeriodText = null,
|
|
|
|
// Additional customization
|
|
className = '',
|
|
style = {},
|
|
children = null,
|
|
|
|
// Loading state
|
|
isGenerating = false,
|
|
|
|
// Custom labels
|
|
labels = {
|
|
filterLabel: 'Filter Tanggal:',
|
|
singleDateLabel: 'Hari Tunggal',
|
|
rangeDateLabel: 'Rentang Tanggal',
|
|
selectDateLabel: 'Pilih Tanggal:',
|
|
fromLabel: 'Dari:',
|
|
toLabel: 'Sampai:',
|
|
todayLabel: 'Hari Ini',
|
|
yesterdayLabel: 'Kemarin',
|
|
last7DaysLabel: '7 Hari Terakhir',
|
|
last30DaysLabel: '30 Hari Terakhir',
|
|
periodLabel: 'Periode:',
|
|
exportHelpText: 'Klik tombol download untuk mengeksport laporan ke PDF'
|
|
}
|
|
}) => {
|
|
const theme = useTheme()
|
|
|
|
// Dynamic colors berdasarkan theme
|
|
const textPrimary = theme.palette.text.primary
|
|
const textSecondary = theme.palette.text.secondary
|
|
const dynamicCardShadow =
|
|
cardShadow ||
|
|
(theme.palette.mode === 'dark' ? '0 2px 10px rgba(20, 21, 33, 0.3)' : '0 2px 10px rgba(58, 53, 65, 0.1)')
|
|
|
|
// Default format date function
|
|
const defaultFormatDateForInput = (date: Date): string => {
|
|
return date.toISOString().split('T')[0]
|
|
}
|
|
|
|
// Default period text function
|
|
const defaultGetReportPeriodText = (): string => {
|
|
if (filterType === 'single') {
|
|
return selectedDate.toLocaleDateString(periodFormat, {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
} else {
|
|
return `${dateRange.startDate.toLocaleDateString(periodFormat)} - ${dateRange.endDate.toLocaleDateString(periodFormat)}`
|
|
}
|
|
}
|
|
|
|
// Use provided functions or defaults
|
|
const formatDate = formatDateForInput || defaultFormatDateForInput
|
|
const getPeriodText = getReportPeriodText || defaultGetReportPeriodText
|
|
|
|
// Quick action handlers
|
|
const handleToday = (): void => {
|
|
onSingleDateChange(new Date())
|
|
}
|
|
|
|
const handleYesterday = (): void => {
|
|
const yesterday = new Date()
|
|
yesterday.setDate(yesterday.getDate() - 1)
|
|
onSingleDateChange(yesterday)
|
|
}
|
|
|
|
const handleLast7Days = (): void => {
|
|
const today = new Date()
|
|
const weekAgo = new Date()
|
|
weekAgo.setDate(today.getDate() - 6)
|
|
onDateRangeChange({ startDate: weekAgo, endDate: today })
|
|
}
|
|
|
|
const handleLast30Days = (): void => {
|
|
const today = new Date()
|
|
const monthAgo = new Date()
|
|
monthAgo.setDate(today.getDate() - 29)
|
|
onDateRangeChange({ startDate: monthAgo, endDate: today })
|
|
}
|
|
|
|
// Default quick actions
|
|
const defaultQuickActions: CustomQuickActions = {
|
|
single: [
|
|
{ label: labels.todayLabel || 'Hari Ini', handler: handleToday },
|
|
{ label: labels.yesterdayLabel || 'Kemarin', handler: handleYesterday }
|
|
],
|
|
range: [
|
|
{ label: labels.last7DaysLabel || '7 Hari Terakhir', handler: handleLast7Days },
|
|
{ label: labels.last30DaysLabel || '30 Hari Terakhir', handler: handleLast30Days }
|
|
]
|
|
}
|
|
|
|
const quickActions = customQuickActions || defaultQuickActions
|
|
|
|
return (
|
|
<StyledCard
|
|
className={className}
|
|
style={{
|
|
...style,
|
|
maxWidth,
|
|
boxShadow: dynamicCardShadow
|
|
}}
|
|
>
|
|
<CardHeader
|
|
title={
|
|
<Typography
|
|
variant='h4'
|
|
sx={{
|
|
fontWeight: 600,
|
|
color: textPrimary
|
|
}}
|
|
>
|
|
Generator {reportTitle}
|
|
</Typography>
|
|
}
|
|
action={
|
|
<PurpleButton
|
|
onClick={onGeneratePDF}
|
|
variant='contained'
|
|
disabled={isGenerating}
|
|
sx={{ backgroundColor: primaryColor }}
|
|
>
|
|
{isGenerating ? 'Generating...' : downloadButtonText}
|
|
</PurpleButton>
|
|
}
|
|
sx={{ pb: 2 }}
|
|
/>
|
|
|
|
<CardContent>
|
|
<Divider sx={{ mb: 3 }} />
|
|
|
|
{/* Filter Controls */}
|
|
<Box sx={{ mb: 3 }}>
|
|
<Typography
|
|
variant='subtitle2'
|
|
sx={{
|
|
mb: 2,
|
|
fontWeight: 600,
|
|
color: textPrimary
|
|
}}
|
|
>
|
|
{labels.filterLabel || 'Filter Tanggal:'}
|
|
</Typography>
|
|
|
|
<FormControl component='fieldset' sx={{ mb: 3 }}>
|
|
<RadioGroup
|
|
row
|
|
value={filterType}
|
|
onChange={e => onFilterTypeChange(e.target.value as 'single' | 'range')}
|
|
sx={{ gap: 3 }}
|
|
>
|
|
<FormControlLabel
|
|
value='single'
|
|
control={
|
|
<Radio
|
|
sx={{
|
|
color: primaryColor,
|
|
'&.Mui-checked': { color: primaryColor }
|
|
}}
|
|
/>
|
|
}
|
|
label={
|
|
<Typography variant='body2' sx={{ color: textSecondary }}>
|
|
{labels.singleDateLabel || 'Hari Tunggal'}
|
|
</Typography>
|
|
}
|
|
/>
|
|
<FormControlLabel
|
|
value='range'
|
|
control={
|
|
<Radio
|
|
sx={{
|
|
color: primaryColor,
|
|
'&.Mui-checked': { color: primaryColor }
|
|
}}
|
|
/>
|
|
}
|
|
label={
|
|
<Typography variant='body2' sx={{ color: textSecondary }}>
|
|
{labels.rangeDateLabel || 'Rentang Tanggal'}
|
|
</Typography>
|
|
}
|
|
/>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
|
|
{/* Single Date Filter */}
|
|
{filterType === 'single' && (
|
|
<Grid container spacing={2} alignItems='center'>
|
|
<Grid item xs={12} sm='auto'>
|
|
<Typography
|
|
variant='subtitle2'
|
|
sx={{
|
|
fontWeight: 600,
|
|
color: textPrimary
|
|
}}
|
|
>
|
|
{labels.selectDateLabel || 'Pilih Tanggal:'}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid item xs={12} sm='auto'>
|
|
<TextField
|
|
type='date'
|
|
value={formatDate(selectedDate)}
|
|
onChange={e => onSingleDateChange(new Date(e.target.value))}
|
|
size='small'
|
|
sx={{
|
|
'& .MuiOutlinedInput-root': {
|
|
'&.Mui-focused fieldset': {
|
|
borderColor: primaryColor
|
|
},
|
|
'& fieldset': {
|
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider
|
|
}
|
|
},
|
|
'& .MuiInputBase-input': {
|
|
color: textPrimary
|
|
}
|
|
}}
|
|
/>
|
|
</Grid>
|
|
{showQuickActions && (
|
|
<Grid item xs={12} sm='auto'>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
{quickActions.single?.map((action, index) => (
|
|
<QuickActionButton key={index} onClick={action.handler}>
|
|
{action.label}
|
|
</QuickActionButton>
|
|
))}
|
|
</Box>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Date Range Filter */}
|
|
{filterType === 'range' && (
|
|
<Grid container spacing={2} alignItems='center'>
|
|
<Grid item xs={12} sm='auto'>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography
|
|
variant='subtitle2'
|
|
sx={{
|
|
fontWeight: 600,
|
|
color: textPrimary
|
|
}}
|
|
>
|
|
{labels.fromLabel || 'Dari:'}
|
|
</Typography>
|
|
<TextField
|
|
type='date'
|
|
value={formatDate(dateRange.startDate)}
|
|
onChange={e =>
|
|
onDateRangeChange({
|
|
...dateRange,
|
|
startDate: new Date(e.target.value)
|
|
})
|
|
}
|
|
size='small'
|
|
sx={{
|
|
'& .MuiOutlinedInput-root': {
|
|
'&.Mui-focused fieldset': {
|
|
borderColor: primaryColor
|
|
},
|
|
'& fieldset': {
|
|
borderColor:
|
|
theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider
|
|
}
|
|
},
|
|
'& .MuiInputBase-input': {
|
|
color: textPrimary
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item xs={12} sm='auto'>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography
|
|
variant='subtitle2'
|
|
sx={{
|
|
fontWeight: 600,
|
|
color: textPrimary
|
|
}}
|
|
>
|
|
{labels.toLabel || 'Sampai:'}
|
|
</Typography>
|
|
<TextField
|
|
type='date'
|
|
value={formatDate(dateRange.endDate)}
|
|
onChange={e =>
|
|
onDateRangeChange({
|
|
...dateRange,
|
|
endDate: new Date(e.target.value)
|
|
})
|
|
}
|
|
size='small'
|
|
sx={{
|
|
'& .MuiOutlinedInput-root': {
|
|
'&.Mui-focused fieldset': {
|
|
borderColor: primaryColor
|
|
},
|
|
'& fieldset': {
|
|
borderColor:
|
|
theme.palette.mode === 'dark' ? 'rgba(231, 227, 252, 0.22)' : theme.palette.divider
|
|
}
|
|
},
|
|
'& .MuiInputBase-input': {
|
|
color: textPrimary
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Grid>
|
|
{showQuickActions && (
|
|
<Grid item xs={12} sm='auto'>
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
{quickActions.range?.map((action, index) => (
|
|
<QuickActionButton key={index} onClick={action.handler}>
|
|
{action.label}
|
|
</QuickActionButton>
|
|
))}
|
|
</Box>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
)}
|
|
</Box>
|
|
|
|
<InfoBox>
|
|
<Typography variant='body2' component='div' sx={{ color: textSecondary }}>
|
|
{labels.periodLabel || 'Periode:'}{' '}
|
|
<Chip
|
|
label={getPeriodText()}
|
|
size='small'
|
|
sx={{
|
|
backgroundColor: primaryColor,
|
|
color: 'white',
|
|
fontWeight: 500,
|
|
ml: 1
|
|
}}
|
|
/>
|
|
</Typography>
|
|
<Typography
|
|
variant='body2'
|
|
sx={{
|
|
color: textSecondary,
|
|
mt: 1
|
|
}}
|
|
>
|
|
{labels.exportHelpText || 'Klik tombol download untuk mengeksport laporan ke PDF'}
|
|
</Typography>
|
|
</InfoBox>
|
|
|
|
{/* Custom content dapat ditambahkan di sini */}
|
|
{children}
|
|
</CardContent>
|
|
</StyledCard>
|
|
)
|
|
}
|
|
|
|
export default ReportGeneratorComponent
|