feat: restrict dashboard sensitive financial metrics for admin role
This commit is contained in:
parent
002b6fa468
commit
781962b44c
@ -9,6 +9,7 @@
|
|||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@ -127,6 +128,19 @@ public function __invoke()
|
|||||||
->limit(5)
|
->limit(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
if ($user->hasRole('Admin')) {
|
||||||
|
unset(
|
||||||
|
$stats['cogs'],
|
||||||
|
$stats['aov'],
|
||||||
|
$stats['gross_profit'],
|
||||||
|
$stats['net_profit'],
|
||||||
|
$stats['profit_margin'],
|
||||||
|
$stats['total_purchases']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Inertia::render('dashboard', [
|
return Inertia::render('dashboard', [
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
'salesByHour' => $salesByHour,
|
'salesByHour' => $salesByHour,
|
||||||
|
|||||||
@ -1,15 +1,33 @@
|
|||||||
import { Head, usePage } from '@inertiajs/react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { StatCard } from '@/components/cards/stat-card';
|
import { StatCard } from '@/components/cards/stat-card';
|
||||||
import { WelcomeCard } from '@/components/cards/welcome-card';
|
import { WelcomeCard } from '@/components/cards/welcome-card';
|
||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
import type { DashboardPageProps } from '@/types';
|
import type { DashboardPageProps } from '@/types';
|
||||||
|
import { Head, usePage } from '@inertiajs/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { CustomBarChart } from '../components/charts/bar-chart';
|
import { CustomBarChart } from '../components/charts/bar-chart';
|
||||||
import { CustomPieChart } from '../components/charts/pie-chart';
|
import { CustomPieChart } from '../components/charts/pie-chart';
|
||||||
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { auth, stats, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers } = usePage<DashboardPageProps & { salesByHour: any[], paymentMethods: any[], orderStatuses: any[], orderChannels: any[], topProducts: any[], topCustomers: any[] }>().props;
|
const {
|
||||||
|
auth,
|
||||||
|
stats,
|
||||||
|
salesByHour,
|
||||||
|
paymentMethods,
|
||||||
|
orderStatuses,
|
||||||
|
orderChannels,
|
||||||
|
topProducts,
|
||||||
|
topCustomers,
|
||||||
|
} = usePage<
|
||||||
|
DashboardPageProps & {
|
||||||
|
salesByHour: any[];
|
||||||
|
paymentMethods: any[];
|
||||||
|
orderStatuses: any[];
|
||||||
|
orderChannels: any[];
|
||||||
|
topProducts: any[];
|
||||||
|
topCustomers: any[];
|
||||||
|
}
|
||||||
|
>().props;
|
||||||
const [time, setTime] = useState(new Date());
|
const [time, setTime] = useState(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -22,7 +40,7 @@ export default function Dashboard() {
|
|||||||
const hour = time.getHours();
|
const hour = time.getHours();
|
||||||
|
|
||||||
if (hour >= 5 && hour < 11) {
|
if (hour >= 5 && hour < 11) {
|
||||||
return {
|
return {
|
||||||
greeting: 'Selamat Pagi',
|
greeting: 'Selamat Pagi',
|
||||||
class: 'bg-gradient-to-br from-amber-50 to-orange-100 dark:from-amber-950/40 dark:to-orange-900/40 border-amber-200 dark:border-amber-800/30 shadow-sm',
|
class: 'bg-gradient-to-br from-amber-50 to-orange-100 dark:from-amber-950/40 dark:to-orange-900/40 border-amber-200 dark:border-amber-800/30 shadow-sm',
|
||||||
text: 'text-amber-950 dark:text-amber-50',
|
text: 'text-amber-950 dark:text-amber-50',
|
||||||
@ -30,12 +48,12 @@ return {
|
|||||||
decoration: 'bg-amber-500/10 dark:bg-amber-500/20',
|
decoration: 'bg-amber-500/10 dark:bg-amber-500/20',
|
||||||
pageBackground: 'bg-amber-50/50 dark:bg-amber-950/10',
|
pageBackground: 'bg-amber-50/50 dark:bg-amber-950/10',
|
||||||
card: 'bg-white/80 dark:bg-amber-900/10 backdrop-blur-sm border-amber-200/50 dark:border-amber-800/20',
|
card: 'bg-white/80 dark:bg-amber-900/10 backdrop-blur-sm border-amber-200/50 dark:border-amber-800/20',
|
||||||
placeholder: 'stroke-amber-500/20 dark:stroke-amber-400/20'
|
placeholder: 'stroke-amber-500/20 dark:stroke-amber-400/20',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hour >= 11 && hour < 15) {
|
if (hour >= 11 && hour < 15) {
|
||||||
return {
|
return {
|
||||||
greeting: 'Selamat Siang',
|
greeting: 'Selamat Siang',
|
||||||
class: 'bg-gradient-to-br from-sky-50 to-blue-100 dark:from-sky-950/40 dark:to-blue-900/40 border-sky-200 dark:border-sky-800/30 shadow-sm',
|
class: 'bg-gradient-to-br from-sky-50 to-blue-100 dark:from-sky-950/40 dark:to-blue-900/40 border-sky-200 dark:border-sky-800/30 shadow-sm',
|
||||||
text: 'text-sky-950 dark:text-sky-50',
|
text: 'text-sky-950 dark:text-sky-50',
|
||||||
@ -43,12 +61,12 @@ return {
|
|||||||
decoration: 'bg-sky-500/10 dark:bg-sky-500/20',
|
decoration: 'bg-sky-500/10 dark:bg-sky-500/20',
|
||||||
pageBackground: 'bg-sky-50/50 dark:bg-sky-950/10',
|
pageBackground: 'bg-sky-50/50 dark:bg-sky-950/10',
|
||||||
card: 'bg-white/80 dark:bg-sky-900/10 backdrop-blur-sm border-sky-200/50 dark:border-sky-800/20',
|
card: 'bg-white/80 dark:bg-sky-900/10 backdrop-blur-sm border-sky-200/50 dark:border-sky-800/20',
|
||||||
placeholder: 'stroke-sky-500/20 dark:stroke-sky-400/20'
|
placeholder: 'stroke-sky-500/20 dark:stroke-sky-400/20',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hour >= 15 && hour < 19) {
|
if (hour >= 15 && hour < 19) {
|
||||||
return {
|
return {
|
||||||
greeting: 'Selamat Sore',
|
greeting: 'Selamat Sore',
|
||||||
class: 'bg-gradient-to-br from-orange-50 to-purple-100 dark:from-orange-950/40 dark:to-purple-900/40 border-orange-200 dark:border-orange-800/30 shadow-sm',
|
class: 'bg-gradient-to-br from-orange-50 to-purple-100 dark:from-orange-950/40 dark:to-purple-900/40 border-orange-200 dark:border-orange-800/30 shadow-sm',
|
||||||
text: 'text-orange-950 dark:text-orange-50',
|
text: 'text-orange-950 dark:text-orange-50',
|
||||||
@ -56,9 +74,9 @@ return {
|
|||||||
decoration: 'bg-orange-500/10 dark:bg-orange-500/20',
|
decoration: 'bg-orange-500/10 dark:bg-orange-500/20',
|
||||||
pageBackground: 'bg-orange-50/50 dark:bg-orange-950/10',
|
pageBackground: 'bg-orange-50/50 dark:bg-orange-950/10',
|
||||||
card: 'bg-white/80 dark:bg-orange-900/10 backdrop-blur-sm border-orange-200/50 dark:border-orange-800/20',
|
card: 'bg-white/80 dark:bg-orange-900/10 backdrop-blur-sm border-orange-200/50 dark:border-orange-800/20',
|
||||||
placeholder: 'stroke-orange-500/20 dark:stroke-orange-400/20'
|
placeholder: 'stroke-orange-500/20 dark:stroke-orange-400/20',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
greeting: 'Selamat Malam',
|
greeting: 'Selamat Malam',
|
||||||
@ -68,43 +86,94 @@ return {
|
|||||||
decoration: 'bg-indigo-500/20',
|
decoration: 'bg-indigo-500/20',
|
||||||
pageBackground: 'bg-slate-100 dark:bg-slate-950',
|
pageBackground: 'bg-slate-100 dark:bg-slate-950',
|
||||||
card: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800',
|
card: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800',
|
||||||
placeholder: 'stroke-slate-300 dark:stroke-slate-700'
|
placeholder: 'stroke-slate-300 dark:stroke-slate-700',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const theme = getTimeTheme();
|
const theme = getTimeTheme();
|
||||||
|
const isAdmin = auth.roles.includes('Admin');
|
||||||
|
|
||||||
|
const statItems = [
|
||||||
|
{
|
||||||
|
title: 'Total Penjualan',
|
||||||
|
stat: stats.total_sales,
|
||||||
|
isCurrency: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Pendapatan',
|
||||||
|
stat: stats.total_revenue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Pengeluaran',
|
||||||
|
stat: stats.total_expenses,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HPP (Modal)',
|
||||||
|
stat: stats.cogs,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'AOV (Avg Order Value)',
|
||||||
|
stat: stats.aov,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Profit Margin',
|
||||||
|
stat: stats.profit_margin,
|
||||||
|
isPercentage: true,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Diskon',
|
||||||
|
stat: stats.total_discount,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Laba Kotor',
|
||||||
|
stat: stats.gross_profit,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Laba Bersih',
|
||||||
|
stat: stats.net_profit,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Pembelian',
|
||||||
|
stat: stats.total_purchases,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Produk Terjual',
|
||||||
|
stat: stats.products_sold,
|
||||||
|
isCurrency: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Total Pelanggan',
|
||||||
|
stat: stats.total_customers,
|
||||||
|
isCurrency: false,
|
||||||
|
},
|
||||||
|
].filter((item) => !isAdmin || !item.sensitive);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Dasbor" />
|
<Head title="Dasbor" />
|
||||||
<div className={`flex h-full min-h-screen flex-1 flex-col gap-2 overflow-x-auto rounded-xl p-3 transition-colors duration-1000 @container/main`}>
|
<div
|
||||||
|
className={`@container/main flex h-full min-h-screen flex-1 flex-col gap-2 overflow-x-auto rounded-xl p-3 transition-colors duration-1000`}
|
||||||
|
>
|
||||||
{/* Welcome Section */}
|
{/* Welcome Section */}
|
||||||
<WelcomeCard user={auth.user} time={time} theme={theme} />
|
<WelcomeCard user={auth.user} time={time} theme={theme} />
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid grid-cols-1 gap-2 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs @sm/main:grid-cols-2 @3xl/main:grid-cols-3 @5xl/main:grid-cols-4 @7xl/main:grid-cols-6 dark:*:data-[slot=card]:bg-card">
|
<div className="grid grid-cols-1 gap-2 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs @sm/main:grid-cols-2 @3xl/main:grid-cols-3 @5xl/main:grid-cols-4 @7xl/main:grid-cols-6 dark:*:data-[slot=card]:bg-card">
|
||||||
{[
|
{statItems.map((item, index) => (
|
||||||
{ title: "Total Penjualan", stat: stats.total_sales, isCurrency: false },
|
|
||||||
{ title: "Total Pendapatan", stat: stats.total_revenue },
|
|
||||||
{ title: "Total Pengeluaran", stat: stats.total_expenses },
|
|
||||||
{ title: "HPP (Modal)", stat: stats.cogs },
|
|
||||||
{ title: "AOV (Avg Order Value)", stat: stats.aov },
|
|
||||||
{ title: "Profit Margin", stat: stats.profit_margin, isPercentage: true },
|
|
||||||
{ title: "Total Diskon", stat: stats.total_discount },
|
|
||||||
{ title: "Laba Kotor", stat: stats.gross_profit },
|
|
||||||
{ title: "Laba Bersih", stat: stats.net_profit },
|
|
||||||
{ title: "Total Pembelian", stat: stats.total_purchases },
|
|
||||||
{ title: "Produk Terjual", stat: stats.products_sold, isCurrency: false },
|
|
||||||
{ title: "Total Pelanggan", stat: stats.total_customers, isCurrency: false },
|
|
||||||
].map((item, index) => (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="animate-in fade-in slide-in-from-bottom-4 fill-mode-both"
|
className="animate-in fill-mode-both fade-in slide-in-from-bottom-4"
|
||||||
style={{ animationDelay: `${index * 100}ms` }}
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
>
|
>
|
||||||
<StatCard
|
<StatCard
|
||||||
{...item}
|
{...item}
|
||||||
className="!gap-3 !py-4 [&_[data-slot=card-header]]:!px-4 [&_[data-slot=card-footer]]:!px-4 transition-all duration-300 hover:scale-[1.03] hover:shadow-xl hover:-translate-y-1"
|
className="!gap-3 !py-4 transition-all duration-300 hover:-translate-y-1 hover:scale-[1.03] hover:shadow-xl [&_[data-slot=card-footer]]:!px-4 [&_[data-slot=card-header]]:!px-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
use function Pest\Laravel\actingAs;
|
use function Pest\Laravel\actingAs;
|
||||||
use function Pest\Laravel\get;
|
use function Pest\Laravel\get;
|
||||||
@ -70,4 +71,28 @@
|
|||||||
->has('topCustomers')
|
->has('topCustomers')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters sensitive data for users with Admin role', function () {
|
||||||
|
Role::findOrCreate('Admin');
|
||||||
|
$user = createAuthorizedUser(['View:Dashboard']);
|
||||||
|
$user->assignRole('Admin');
|
||||||
|
|
||||||
|
actingAs($user);
|
||||||
|
|
||||||
|
get(route('dashboard'))
|
||||||
|
->assertOk()
|
||||||
|
->assertInertia(fn ($page) => $page
|
||||||
|
->component('dashboard')
|
||||||
|
->has('stats')
|
||||||
|
->has('stats.total_sales')
|
||||||
|
->has('stats.total_revenue')
|
||||||
|
->missing('stats.cogs')
|
||||||
|
->missing('stats.aov')
|
||||||
|
->missing('stats.gross_profit')
|
||||||
|
->missing('stats.net_profit')
|
||||||
|
->missing('stats.profit_margin')
|
||||||
|
->missing('stats.total_purchases')
|
||||||
|
->has('topCustomers')
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user