Profit Loss Report

This commit is contained in:
efrilm 2025-09-25 14:59:08 +07:00
parent cfa3686de3
commit 79cd4f9dcb
3 changed files with 298 additions and 165 deletions

View File

@ -1,22 +1,72 @@
'use client'
import ReportTitle from '@/components/report/ReportTitle' import ReportTitle from '@/components/report/ReportTitle'
import ReportProfitLossCard from '@/views/apps/report/profit-loss/ReportProfitLossCard' import ReportProfitLossCard from '@/views/apps/report/profit-loss/ReportProfitLossCard'
import ReportProfitLossContent from '@/views/apps/report/profit-loss/ReportProfitLossContent' import ReportProfitLossContent from '@/views/apps/report/profit-loss/ReportProfitLossContent'
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
import { CircularProgress, Box } from '@mui/material'
import { useState } from 'react'
import { useProfitLossAnalytics } from '@/services/queries/analytics'
import { formatDateDDMMYYYY } from '@/utils/transform'
import Loading from '@/components/layout/shared/Loading'
const ProfitLossPage = () => {
const today = new Date()
const monthAgo = new Date()
monthAgo.setDate(today.getDate() - 30)
const [startDate, setStartDate] = useState<Date | null>(monthAgo)
const [endDate, setEndDate] = useState<Date | null>(today)
// Single API call at parent level
const {
data: profitData,
isLoading,
error
} = useProfitLossAnalytics({
date_from: formatDateDDMMYYYY(startDate!),
date_to: formatDateDDMMYYYY(endDate!)
})
// Handle loading state
if (isLoading) {
return <Loading />
}
// Handle error state
if (error) {
return (
<Grid container spacing={6}>
<Grid size={{ xs: 12 }}>
<ReportTitle title='Laba Rugi' />
</Grid>
<Grid size={{ xs: 12 }}>
<Box display='flex' justifyContent='center' alignItems='center' minHeight={400}>
<span>Error loading data: {error.message}</span>
</Box>
</Grid>
</Grid>
)
}
const ProfiltLossPage = () => {
return ( return (
<Grid container spacing={6}> <Grid container spacing={6}>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<ReportTitle title='Laba Rugi' /> <ReportTitle title='Laba Rugi' />
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<ReportProfitLossCard /> <ReportProfitLossCard profitData={profitData} />
</Grid> </Grid>
<Grid size={{ xs: 12 }}> <Grid size={{ xs: 12 }}>
<ReportProfitLossContent /> <ReportProfitLossContent
profitData={profitData}
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
</Grid> </Grid>
</Grid> </Grid>
) )
} }
export default ProfiltLossPage export default ProfitLossPage

View File

@ -1,89 +1,105 @@
// MUI Imports
import Grid from '@mui/material/Grid2' import Grid from '@mui/material/Grid2'
// Type Imports
import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle' import type { UserDataType } from '@components/card-statistics/HorizontalWithSubtitle'
// Component Imports
import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle' import HorizontalWithSubtitle from '@components/card-statistics/HorizontalWithSubtitle'
import { ProfitLossReport } from '@/types/services/analytic'
// Vars // Utility functions
const data: UserDataType[] = [ const formatIDR = (amount: number) => {
{ return new Intl.NumberFormat('id-ID', {
title: 'Pendapatan', minimumFractionDigits: 0,
stats: '29.004.775', maximumFractionDigits: 0
avatarIcon: 'tabler-trending-down', }).format(amount)
avatarColor: 'error', }
trend: 'negative',
trendNumber: '48,8%', const formatPercentage = (value: number) => {
subtitle: 'vs Bulan Lalu' return `${value.toFixed(1)}%`
}, }
{
title: 'Margin Laba Bersih', interface ReportProfitLossCardProps {
stats: '38%', profitData: ProfitLossReport | undefined
avatarIcon: 'tabler-gauge', }
avatarColor: 'success',
trend: 'positive', const ReportProfitLossCard = ({ profitData }: ReportProfitLossCardProps) => {
trendNumber: 'Bulan Ini', if (!profitData) {
subtitle: 'Bulan Ini' return null // Will be handled by parent loading state
},
{
title: 'Laba Kotor',
stats: '21.076.389',
avatarIcon: 'tabler-trending-down',
avatarColor: 'error',
trend: 'negative',
trendNumber: '43,5%',
subtitle: 'vs bulan lalu'
},
{
title: 'Laba Bersih',
stats: '11.111.074',
avatarIcon: 'tabler-trending-down',
avatarColor: 'error',
trend: 'negative',
trendNumber: '36,8%',
subtitle: 'vs bulan lalu'
},
{
title: 'Margin Laba Kotor',
stats: '73%',
avatarIcon: 'tabler-gauge',
avatarColor: 'success',
trend: 'positive',
trendNumber: 'Bulan Ini',
subtitle: 'Bulan Ini'
},
{
title: 'Biaya Operasional',
stats: '9.965.315',
avatarIcon: 'tabler-trending-down',
avatarColor: 'error',
trend: 'negative',
trendNumber: '49,4%',
subtitle: 'vs Bulan Lalu'
},
{
title: 'Rasio Biaya Operasional',
stats: '61,7%',
avatarIcon: 'tabler-gauge',
avatarColor: 'success',
trend: 'positive',
trendNumber: 'Bulan Ini',
subtitle: 'Bulan Ini'
},
{
title: 'EBITDA',
stats: '11.032.696',
avatarIcon: 'tabler-trending-down',
avatarColor: 'error',
trend: 'negative',
trendNumber: '37,3%',
subtitle: 'vs bulan lalu'
} }
]
const ReportProfitLossCard = () => { // Using actual data from API response with correct field names
const data: UserDataType[] = [
{
title: 'Pendapatan',
stats: formatIDR(profitData.summary.total_revenue),
avatarIcon: 'tabler-trending-up',
avatarColor: 'success',
trend: 'positive',
trendNumber: 'Current Period',
subtitle: 'Total Revenue'
},
{
title: 'Margin Laba Bersih',
stats: formatPercentage(profitData.summary.net_profit_margin),
avatarIcon: 'tabler-gauge',
avatarColor: profitData.summary.net_profit_margin >= 0 ? 'success' : 'error',
trend: profitData.summary.net_profit_margin >= 0 ? 'positive' : 'negative',
trendNumber: 'Current Period',
subtitle: 'Net Profit Margin'
},
{
title: 'Laba Kotor',
stats: formatIDR(profitData.summary.gross_profit),
avatarIcon: 'tabler-trending-up',
avatarColor: profitData.summary.gross_profit >= 0 ? 'success' : 'error',
trend: profitData.summary.gross_profit >= 0 ? 'positive' : 'negative',
trendNumber: 'Current Period',
subtitle: 'Gross Profit'
},
{
title: 'Laba Bersih',
stats: formatIDR(profitData.summary.net_profit),
avatarIcon: profitData.summary.net_profit >= 0 ? 'tabler-trending-up' : 'tabler-trending-down',
avatarColor: profitData.summary.net_profit >= 0 ? 'success' : 'error',
trend: profitData.summary.net_profit >= 0 ? 'positive' : 'negative',
trendNumber: 'Current Period',
subtitle: 'Net Profit'
},
{
title: 'Margin Laba Kotor',
stats: formatPercentage(profitData.summary.gross_profit_margin),
avatarIcon: 'tabler-gauge',
avatarColor: profitData.summary.gross_profit_margin >= 0 ? 'success' : 'error',
trend: profitData.summary.gross_profit_margin >= 0 ? 'positive' : 'negative',
trendNumber: 'Current Period',
subtitle: 'Gross Profit Margin'
},
{
title: 'Total Cost',
stats: formatIDR(profitData.summary.total_cost),
avatarIcon: 'tabler-trending-down',
avatarColor: 'error',
trend: 'negative',
trendNumber: 'Current Period',
subtitle: 'Total Cost'
},
{
title: 'Tax',
stats: formatIDR(profitData.summary.total_tax),
avatarIcon: 'tabler-receipt-tax',
avatarColor: 'warning',
trend: 'neutral',
trendNumber: 'Current Period',
subtitle: 'Total Tax'
},
{
title: 'Total Orders',
stats: profitData.summary.total_orders.toString(),
avatarIcon: 'tabler-shopping-cart',
avatarColor: 'info',
trend: 'positive',
trendNumber: 'Current Period',
subtitle: 'Total Orders'
}
]
return ( return (
<Grid container spacing={6}> <Grid container spacing={6}>
{data.map((item, i) => ( {data.map((item, i) => (

View File

@ -2,12 +2,38 @@
import DateRangePicker from '@/components/RangeDatePicker' import DateRangePicker from '@/components/RangeDatePicker'
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
import { Button, Card, CardContent, Paper } from '@mui/material' import { ProfitLossReport } from '@/types/services/analytic'
import { useState } from 'react' import { Button, Card, CardContent, Box } from '@mui/material'
const ReportProfitLossContent = () => { interface ReportProfitLossContentProps {
const [startDate, setStartDate] = useState<Date | null>(new Date()) profitData: ProfitLossReport | undefined
const [endDate, setEndDate] = useState<Date | null>(new Date()) startDate: Date | null
endDate: Date | null
onStartDateChange: (date: Date | null) => void
onEndDateChange: (date: Date | null) => void
}
// Utility function to format date for display
const formatDisplayDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
const ReportProfitLossContent = ({
profitData,
startDate,
endDate,
onStartDateChange,
onEndDateChange
}: ReportProfitLossContentProps) => {
const handleExport = () => {
// TODO: Implement export functionality
console.log('Export data:', profitData)
}
return ( return (
<Card> <Card>
@ -18,97 +44,138 @@ const ReportProfitLossContent = () => {
variant='tonal' variant='tonal'
startIcon={<i className='tabler-upload' />} startIcon={<i className='tabler-upload' />}
className='max-sm:is-full' className='max-sm:is-full'
onClick={handleExport}
disabled={!profitData}
> >
Ekspor Ekspor
</Button> </Button>
<DateRangePicker <DateRangePicker
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onStartDateChange={setStartDate} onStartDateChange={onStartDateChange}
onEndDateChange={setEndDate} onEndDateChange={onEndDateChange}
/> />
</div> </div>
</div> </div>
<CardContent> <CardContent>
<ReportItemHeader title='Pendapatan' date='10/09/2025' /> {profitData ? (
<ReportItemSubheader title='Penjualan' /> <>
<ReportItem accountCode='4-40000' accountName='Pendapatan' amount={116791108} onClick={() => {}} /> {/* Summary Section */}
<ReportItemSubheader title='Penghasilan lain' /> <ReportItemHeader
<ReportItem title='Pendapatan'
accountCode='7-70001' date={`${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`}
accountName='Pendapatan Bunga - Deposito' />
amount={-86486} <ReportItemSubheader title='Penjualan' />
onClick={() => {}} <ReportItem
/> accountCode=''
<ReportItem accountCode='7-70099' accountName='Pendapatan Lain - lain' amount={54054} onClick={() => {}} /> accountName='Revenue'
<ReportItem amount={profitData.summary.total_revenue}
accountCode='7-70100' onClick={() => {}}
accountName='Pendapatan lainnya (Service Charge)' />
amount={-15315}
onClick={() => {}}
/>
<ReportItemFooter title='Total Pendapatan' amount={116743360} />
<ReportItemSubheader title='' />
<ReportItemHeader title='Beban Pokok Penjualan' date='10/09/2025' /> <ReportItemFooter title='Total Pendapatan' amount={profitData.summary.total_revenue} />
<ReportItem accountCode='5-50000' accountName='Beban Pokok Pendapatan' amount={35018079} onClick={() => {}} /> <ReportItemSubheader title='' />
<ReportItem accountCode='5-50300' accountName='Pengiriman & Pengangkutan' amount={-15315} onClick={() => {}} />
<ReportItemFooter title='Total Beban Pokok Penjualan' amount={35002764} />
<ReportItemSubheader title='' />
<ReportItemHeader title='Laba Kotor' amount={81740597} /> <ReportItemHeader
<ReportItemSubheader title='' /> title='Beban Pokok Penjualan'
date={`${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`}
/>
<ReportItem
accountCode=''
accountName='Cost of Goods Sold'
amount={profitData.summary.total_cost}
onClick={() => {}}
/>
<ReportItemFooter title='Total Beban Pokok Penjualan' amount={profitData.summary.total_cost} />
<ReportItemSubheader title='' />
<ReportItemHeader title='Biaya Operasional' date='10/09/2025' /> <ReportItemHeader title='Laba Kotor' amount={profitData.summary.gross_profit} />
<ReportItemSubheader title='Biaya Operasional' /> <ReportItemSubheader title='' />
<ReportItem accountCode='6-60218' accountName='Air' amount={15315} onClick={() => {}} />
<ReportItem
accountCode='6-60301'
accountName='Alat Tulis Kantor & Printing'
amount={-19820}
onClick={() => {}}
/>
<ReportItem accountCode='6-60302' accountName='Bea Materai' amount={-40541} onClick={() => {}} />
<ReportItem
accountCode='6-60003'
accountName='Bensin, Tol dan Parkir - Penjualan'
amount={6264865}
onClick={() => {}}
/>
<ReportItem accountCode='6-60401' accountName='Biaya Sewa - Kendaraan' amount={62162} onClick={() => {}} />
<ReportItem accountCode='6-60403' accountName='Biaya Sewa - Lain - lain' amount={63964} onClick={() => {}} />
<ReportItem accountCode='6-60402' accountName='Biaya Sewa - Operasional' amount={-2703} onClick={() => {}} />
<ReportItem accountCode='6-60101' accountName='Gaji' amount={6306} onClick={() => {}} />
<ReportItem accountCode='6-60001' accountName='Iklan & Promosi' amount={7851892} onClick={() => {}} />
<ReportItem accountCode='6-60002' accountName='Komisi & Fee' amount={6277748} onClick={() => {}} />
<ReportItem accountCode='6-60005' accountName='Komunikasi - Penjualan' amount={12058018} onClick={() => {}} />
<ReportItem accountCode='6-60206' accountName='Komunikasi - Umum' amount={85586} onClick={() => {}} />
<ReportItem accountCode='6-60500' accountName='Penyusutan - Bangunan' amount={73874} onClick={() => {}} />
<ReportItem accountCode='6-60502' accountName='Penyusutan - Kendaraan' amount={-78378} onClick={() => {}} />
<ReportItem
accountCode='6-60004'
accountName='Perjalanan Dinas - Penjualan'
amount={6745045}
onClick={() => {}}
/>
<ReportItem accountCode='6-60204' accountName='Perjalanan Dinas - Umum' amount={-48649} onClick={() => {}} />
<ReportItem accountCode='6-60304' accountName='Supplies dan Material' amount={58559} onClick={() => {}} />
<ReportItem accountCode='6-60106' accountName='THR & Bonus' amount={-59459} onClick={() => {}} />
<ReportItemSubheader title='Biaya Lain-Lain' /> {/* Daily Data Breakdown Section */}
<ReportItem {profitData.data && profitData.data.length > 0 && (
accountCode='8-80002' <>
accountName='(Laba)/Rugi Pelepasan Aset Tetap' <ReportItemHeader
amount={2703} title='Rincian Data Harian'
onClick={() => {}} date={`${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`}
/> />
<ReportItem accountCode='8-80999' accountName='Beban Lain - lain' amount={81982} onClick={() => {}} /> <ReportItemSubheader title='Breakdown per Hari' />
<ReportItem accountCode='8-80100' accountName='Penyesuaian Persediaan' amount={-1477900} onClick={() => {}} />
<ReportItem accountCode='8-80001' accountName='Provisi' amount={-12613} onClick={() => {}} />
<ReportItemFooter title='Total Biaya Operasional' amount={37907956} />
<ReportItemSubheader title='' />
<ReportItemHeader title='Laba Bersih' amount={43832641} /> {profitData.data.map((dailyData, index) => (
<div key={index} className='mb-4'>
<ReportItemSubheader title={`Data ${formatDisplayDate(dailyData.date)}`} />
<ReportItem
accountCode=''
accountName='Revenue Harian'
amount={dailyData.revenue}
onClick={() => {}}
/>
<ReportItem accountCode='' accountName='Cost Harian' amount={dailyData.cost} onClick={() => {}} />
<ReportItem
accountCode=''
accountName='Gross Profit Harian'
amount={dailyData.gross_profit}
onClick={() => {}}
/>
<ReportItem accountCode='' accountName='Tax Harian' amount={dailyData.tax} onClick={() => {}} />
<ReportItem
accountCode=''
accountName='Discount Harian'
amount={dailyData.discount}
onClick={() => {}}
/>
<ReportItem
accountCode=''
accountName='Orders Harian'
amount={dailyData.orders}
onClick={() => {}}
/>
<ReportItemFooter
title={`Net Profit ${formatDisplayDate(dailyData.date)}`}
amount={dailyData.net_profit}
/>
</div>
))}
<ReportItemSubheader title='' />
</>
)}
{/* Operational Costs Section */}
<ReportItemHeader
title='Biaya Operasional'
date={`${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`}
/>
<ReportItemSubheader title='Biaya Operasional' />
<ReportItem accountCode='' accountName='Tax' amount={profitData.summary.total_tax} onClick={() => {}} />
<ReportItem
accountCode=''
accountName='Discount'
amount={profitData.summary.total_discount}
onClick={() => {}}
/>
<ReportItemFooter
title='Total Biaya Operasional'
amount={profitData.summary.total_tax + profitData.summary.total_discount}
/>
<ReportItemSubheader title='' />
<ReportItemHeader title='Laba Bersih' amount={profitData.summary.net_profit} />
</>
) : (
<Box display='flex' justifyContent='center' alignItems='center' minHeight={200}>
<span>No data available</span>
</Box>
)}
</CardContent> </CardContent>
</Card> </Card>
) )