Developing Customer Account Dashboard for E-commerce
Account dashboard — customer retention point. Stores order history, manages addresses, accesses bonuses and returns. Weak dashboard forces support calls; strong dashboard reduces support load. Takes 7–12 business days.
Section Structure
Typical account sections:
| Section | URL | Functionality |
|---|---|---|
| Overview | /account |
Recent orders, bonuses, notifications |
| Orders | /account/orders |
History, filters, tracking |
| Returns | /account/returns |
RMA requests status |
| Addresses | /account/addresses |
CRUD delivery addresses |
| Profile | /account/profile |
Name, phone, email, password change |
| Subscriptions | /account/subscriptions |
Email lists, push notifications |
| Wishlist | /account/wishlist |
Saved products |
| Bonuses | /account/bonuses |
Balance, history |
Routing and Protection
Cabinet routes protected by auth middleware:
Route::middleware(['auth', 'verified'])->prefix('account')->group(function () {
Route::get('/', [AccountController::class, 'dashboard'])->name('dashboard');
Route::get('/orders', [OrderController::class, 'index'])->name('orders.index');
Route::get('/orders/{order}', [OrderController::class, 'show'])
->can('view', 'order')
->name('orders.show');
Route::resource('addresses', AddressController::class);
});
Policy OrderPolicy ensures user sees only their orders.
Dashboard Overview
Collects from multiple sources in single request:
public function dashboard(Request $request): Response {
$user = $request->user()->load([
'orders' => fn($q) => $q->latest()->limit(3)->with('items.product'),
'bonusAccount',
'activeReturns',
]);
return Inertia::render('Account/Dashboard', [
'recentOrders' => OrderResource::collection($user->orders),
'bonusBalance' => $user->bonusAccount?->balance ?? 0,
'pendingReturns' => $user->activeReturns->count(),
'notifications' => $user->unreadNotifications()->limit(5)->get(),
]);
}
Address Management
Users can have multiple addresses: home, office, pickup point. One marked default:
class Address extends Model {
protected $fillable = [
'user_id', 'label', 'full_name', 'phone',
'country', 'region', 'city', 'street', 'house',
'apartment', 'postal_code', 'is_default',
];
public function setAsDefault(): void {
DB::transaction(function () {
$this->user->addresses()->update(['is_default' => false]);
$this->update(['is_default' => true]);
});
}
}
Email Change and Two-Factor
Email change — sensitive operation:
- User enters new email
- Confirmation email sent to new address
- Only after link click — email updates in DB
- Notification sent to old address
public function updateEmail(Request $request): void {
$request->validate(['email' => 'required|email|unique:users,email']);
$token = Str::random(64);
Cache::put("email_change:{$token}", [
'user_id' => $request->user()->id,
'new_email' => $request->email,
], now()->addHours(2));
Mail::to($request->email)->send(new EmailChangeConfirmation($token));
}
Subscription Management
Central place for communication preferences:
const SubscriptionSettings = () => {
const { preferences, toggle } = useNotificationPreferences();
const options = [
{ key: 'order_updates', label: 'Order status' },
{ key: 'promotions', label: 'Sales and discounts' },
{ key: 'back_in_stock', label: 'Back in stock' },
{ key: 'price_drops', label: 'Favorite price drops' },
{ key: 'newsletter', label: 'Weekly newsletter' },
];
return (
<div className="space-y-3">
{options.map(({ key, label }) => (
<div key={key} className="flex items-center justify-between">
<span>{label}</span>
<Switch checked={preferences[key]} onCheckedChange={() => toggle(key)} />
</div>
))}
</div>
);
};
Password Change Security
Requires current password and confirmation:
public function updatePassword(Request $request): void {
$request->validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', 'min:8', 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($request->password),
]);
Auth::logoutOtherDevices($request->password);
event(new PasswordChanged($request->user()));
}
Account Deletion (GDPR)
Per GDPR, users can delete accounts. Soft delete with anonymization:
public function deleteAccount(Request $request): void {
$user = $request->user();
$user->update([
'email' => "deleted_{$user->id}@removed.invalid",
'name' => 'Deleted user',
'phone' => null,
'deleted_at' => now(),
]);
Auth::logout();
}
Orders preserved for accounting — only personal data anonymized.
Mobile Adaptation
Mobile — separate layout with bottom navigation (tab bar) instead of sidebar. Critical operations (order status, tracking) accessible in 2–3 clicks from main screen.







