Update Report
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
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 }) => ({
|
||||
maxWidth: '1024px',
|
||||
margin: '0 auto 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' 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
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
|
||||
// React Imports
|
||||
import type { FC } from 'react'
|
||||
|
||||
// MUI Imports
|
||||
import { Box, Typography, Divider } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// Component Imports
|
||||
import Logo from '@core/svg/Logo'
|
||||
|
||||
// Type Imports
|
||||
import type { Outlet } from '@/types/services/outlet'
|
||||
|
||||
// Props Interface
|
||||
export interface ReportHeaderProps {
|
||||
outlet?: Outlet
|
||||
reportTitle: string
|
||||
reportSubtitle?: string
|
||||
brandName?: string
|
||||
brandColor?: string
|
||||
className?: string
|
||||
periode?: string
|
||||
}
|
||||
|
||||
const ReportHeader: FC<ReportHeaderProps> = ({
|
||||
outlet,
|
||||
reportTitle,
|
||||
reportSubtitle = 'Laporan Operasional',
|
||||
brandName = 'Apskel',
|
||||
brandColor,
|
||||
className,
|
||||
periode
|
||||
}) => {
|
||||
// Hooks
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box className={className}>
|
||||
<Box sx={{ p: theme.spacing(8, 8, 6) }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||
{/* Left Section - Brand & Outlet Info */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Logo />
|
||||
<Box>
|
||||
<Typography
|
||||
variant='h3'
|
||||
component='h1'
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: brandColor || theme.palette.primary.main
|
||||
}}
|
||||
>
|
||||
{brandName}
|
||||
</Typography>
|
||||
{outlet?.name && (
|
||||
<Typography
|
||||
variant='h5'
|
||||
component='h2'
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mt: 1,
|
||||
color: theme.palette.text.primary
|
||||
}}
|
||||
>
|
||||
{outlet.name}
|
||||
</Typography>
|
||||
)}
|
||||
{outlet?.address && (
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={{
|
||||
mt: 1,
|
||||
color: theme.palette.text.secondary
|
||||
}}
|
||||
>
|
||||
{outlet.address}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Right Section - Report Info */}
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
<Typography
|
||||
variant='h4'
|
||||
component='h3'
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: theme.palette.text.primary
|
||||
}}
|
||||
>
|
||||
{reportTitle}
|
||||
</Typography>
|
||||
{periode && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 2, mt: 2 }}>
|
||||
<Typography variant='body2' sx={{ color: theme.palette.text.secondary }}>
|
||||
{periode}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{reportSubtitle && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 2, mt: 2 }}>
|
||||
<Typography variant='body2' sx={{ color: theme.palette.text.secondary }}>
|
||||
{reportSubtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider
|
||||
sx={{
|
||||
borderWidth: 2,
|
||||
borderColor: brandColor || theme.palette.primary.main
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportHeader
|
||||
Reference in New Issue
Block a user