Customer Portal Guide - luckydeva03/barbershop_app GitHub Wiki
Panduan lengkap untuk customer dalam menggunakan portal barbershop management system.
URL: /register
Registration Methods:
- Email Registration: Traditional email + password
- Google OAuth: Quick signup with Google account
Required Information:
- Full Name
- Email Address
- Phone Number (optional)
- Password (if not using Google)
Registration Flow:
1. Visit /register
2. Choose registration method:
• Fill registration form OR
• Click "Continue with Google"
3. Verify email (if email registration)
4. Automatic login and redirect to dashboard
URL: /login
Login Options:
- Email + Password
- Google OAuth (if account linked)
Login Flow:
1. Visit /login
2. Enter credentials OR click Google login
3. Successful authentication
4. Redirect to /dashboard
URL: /forgot-password
Reset Process:
1. Enter email address
2. Check email for reset link
3. Click link and set new password
4. Login with new credentials
- Personalized greeting with user name
- Profile photo (Google synced or default avatar)
- Quick account stats overview
<div class="points-card">
<h3>Your Points Balance</h3>
<div class="points-display">
<span class="points-value">1,250</span>
<span class="points-label">points</span>
</div>
<small class="text-muted">Earned this month: +150 pts</small>
</div>
- Redeem Code: Quick access to redeem form
- Browse Stores: Link to store directory
- View History: Point transaction history
- Update Profile: Account settings
- Last 5 point transactions
- Recent store visits
- Latest reviews written
- Welcome Bonus: 50 points upon registration
- Redeem Codes: Use promo codes from barbershops
- Write Reviews: 10 points per review
- Referral Bonus: 25 points for each friend referred
- Birthday Bonus: 100 points annually
- Minimum Redeem: 100 points
- Maximum Daily Redeem: 5 codes
- Point Expiry: 365 days from last activity
Step-by-Step Process:
1. Get code from barbershop (physical/digital)
2. Login to customer portal
3. Navigate to dashboard
4. Find "Redeem Code" section
5. Enter code in input field
6. Click "Redeem" button
7. Confirmation message + points added
Redeem Form:
<form id="redeem-form" method="POST" action="{{ route('points.redeem') }}">
@csrf
<div class="redeem-card">
<h4>Redeem Your Code</h4>
<div class="input-group mb-3">
<input type="text"
class="form-control"
name="code"
placeholder="Enter your code (e.g., WELCOME50)"
style="text-transform: uppercase"
maxlength="20"
required>
<button type="submit" class="btn btn-primary">
Redeem
</button>
</div>
<small class="form-text text-muted">
Codes are not case-sensitive and expire after use
</small>
</div>
</form>
- Format: Alphanumeric only (A-Z, 0-9)
- Length: 4-20 characters
- Case: Not case-sensitive
- Expiry: Checked on redemption
- Usage Limit: Enforced per code
// Common error messages
const errorMessages = {
'invalid_code': 'Invalid code. Please check and try again.',
'expired_code': 'This code has expired.',
'used_code': 'This code has already been used.',
'limit_reached': 'Maximum usage limit reached for this code.',
'rate_limit': 'Too many attempts. Please wait before trying again.',
'user_limit': 'You have reached your daily redeem limit.'
};
- Date & Time: When transaction occurred
- Type: Earned, Redeemed, Expired, Bonus
- Points: Amount (+/-)
- Description: What the transaction was for
- Balance: Running total after transaction
<div class="history-section">
<h4>Transaction History</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Points</th>
<th>Description</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
@foreach($history as $transaction)
<tr>
<td>{{ $transaction->created_at->format('M d, Y H:i') }}</td>
<td>
<span class="badge bg-{{ $transaction->type == 'earned' ? 'success' : 'info' }}">
{{ ucfirst($transaction->type) }}
</span>
</td>
<td class="fw-bold {{ $transaction->points > 0 ? 'text-success' : 'text-danger' }}">
{{ $transaction->points > 0 ? '+' : '' }}{{ $transaction->points }}
</td>
<td>{{ $transaction->description }}</td>
<td>{{ number_format($transaction->running_balance) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
- Date Range: Last 30 days, 3 months, 6 months, All time
- Transaction Type: All, Earned, Redeemed, Expired, Bonus
- Search: Search by description
- Grid Layout: Responsive card-based design
- Store Information: Name, description, address, phone
- Rating Display: Average rating with stars
- Review Count: Number of customer reviews
- Location Map: Google Maps integration
- WhatsApp Booking: Direct booking via WhatsApp
<div class="store-card">
<div class="store-image">
<img src="{{ $store->image_url }}" alt="{{ $store->name }}">
</div>
<div class="store-info">
<h5 class="store-name">{{ $store->name }}</h5>
<!-- Rating -->
<div class="rating-display">
@for($i = 1; $i <= 5; $i++)
<i class="fas fa-star {{ $i <= $store->average_rating ? 'text-warning' : 'text-muted' }}"></i>
@endfor
<span class="rating-text">{{ $store->average_rating }}/5 ({{ $store->reviews_count }} reviews)</span>
</div>
<!-- Address -->
<p class="store-address">
<i class="fas fa-map-marker-alt"></i>
{{ $store->address }}
</p>
<!-- Description -->
<p class="store-description">
{{ Str::limit($store->description, 120) }}
</p>
</div>
<div class="store-actions">
<button class="btn btn-outline-primary btn-sm" onclick="viewStoreDetails({{ $store->id }})">
View Details
</button>
@if($store->phone)
<a href="https://wa.me/{{ formatWhatsAppNumber($store->phone) }}?text={{ urlencode('Hi! I would like to book an appointment at ' . $store->name) }}"
class="btn btn-success btn-sm" target="_blank">
<i class="fab fa-whatsapp"></i> Book Now
</a>
@endif
</div>
</div>
- Complete Description: Full store details
- Photo Gallery: Multiple store images
- Location Map: Interactive Google Maps
- Contact Information: Phone, address, hours
- Services Offered: List of available services
- Reviews Section: All customer reviews
<div class="store-location">
<h4>Location</h4>
<div id="store-map" style="height: 300px; width: 100%;"></div>
<script>
function initStoreMap() {
const storeLocation = { lat: {{ $store->latitude }}, lng: {{ $store->longitude }} };
const map = new google.maps.Map(document.getElementById('store-map'), {
zoom: 15,
center: storeLocation,
});
const marker = new google.maps.Marker({
position: storeLocation,
map: map,
title: '{{ $store->name }}',
});
const infoWindow = new google.maps.InfoWindow({
content: `
<div>
<h6>{{ $store->name }}</h6>
<p>{{ $store->address }}</p>
<p>Phone: {{ $store->phone }}</p>
</div>
`
});
marker.addListener('click', () => {
infoWindow.open(map, marker);
});
}
</script>
</div>
<form method="POST" action="{{ route('reviews.store', $store) }}">
@csrf
<div class="review-form">
<h4>Write a Review</h4>
<!-- Rating Selection -->
<div class="rating-input mb-3">
<label class="form-label">Your Rating</label>
<div class="star-rating">
@for($i = 1; $i <= 5; $i++)
<input type="radio" name="rating" value="{{ $i }}" id="star{{ $i }}" required>
<label for="star{{ $i }}" class="star">★</label>
@endfor
</div>
</div>
<!-- Comment -->
<div class="mb-3">
<label for="comment" class="form-label">Your Review</label>
<textarea class="form-control"
name="comment"
id="comment"
rows="4"
placeholder="Share your experience..."
maxlength="1000"></textarea>
<div class="form-text">Maximum 1000 characters</div>
</div>
<button type="submit" class="btn btn-primary">Submit Review</button>
</div>
</form>
- One Review Per Store: Users can only review each store once
- Rating Scale: 1-5 stars
- Comment Length: Maximum 1000 characters
- Content Policy: No offensive language or spam
- Edit Permission: Users can edit their own reviews
<div class="reviews-section">
<h4>Customer Reviews ({{ $store->reviews_count }})</h4>
@foreach($reviews as $review)
<div class="review-item">
<div class="review-header">
<div class="reviewer-info">
<img src="{{ $review->user->profile_photo }}" alt="{{ $review->user->name }}" class="reviewer-avatar">
<div>
<h6 class="reviewer-name">{{ $review->user->name }}</h6>
<small class="review-date">{{ $review->created_at->diffForHumans() }}</small>
</div>
</div>
<div class="review-rating">
@for($i = 1; $i <= 5; $i++)
<i class="fas fa-star {{ $i <= $review->rating ? 'text-warning' : 'text-muted' }}"></i>
@endfor
</div>
</div>
<div class="review-content">
<p>{{ $review->comment }}</p>
</div>
@if(auth()->id() === $review->user_id)
<div class="review-actions">
<button class="btn btn-sm btn-outline-primary" onclick="editReview({{ $review->id }})">
Edit
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteReview({{ $review->id }})">
Delete
</button>
</div>
@endif
</div>
@endforeach
</div>
<div class="profile-section">
<h4>Personal Information</h4>
<form method="POST" action="{{ route('profile.update') }}">
@csrf
@method('PUT')
<!-- Profile Photo -->
<div class="photo-section mb-4">
<div class="current-photo">
<img src="{{ auth()->user()->profile_photo }}"
alt="Profile Photo"
class="profile-photo-large">
</div>
@if(auth()->user()->google_id)
<small class="text-muted">Profile photo synced from Google</small>
@else
<input type="file" name="profile_photo" class="form-control mt-2" accept="image/*">
@endif
</div>
<!-- Name -->
<div class="mb-3">
<label for="name" class="form-label">Full Name</label>
<input type="text" class="form-control" name="name" value="{{ auth()->user()->name }}" required>
</div>
<!-- Email -->
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" name="email" value="{{ auth()->user()->email }}" required>
</div>
<!-- Phone -->
<div class="mb-3">
<label for="phone" class="form-label">Phone Number</label>
<input type="tel" class="form-control" name="phone" value="{{ auth()->user()->phone }}">
</div>
<button type="submit" class="btn btn-primary">Update Profile</button>
</form>
</div>
<div class="security-section">
<h4>Account Security</h4>
<!-- Password Change -->
@if(!auth()->user()->google_id)
<form method="POST" action="{{ route('password.update') }}">
@csrf
@method('PUT')
<div class="mb-3">
<label for="current_password" class="form-label">Current Password</label>
<input type="password" class="form-control" name="current_password" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">New Password</label>
<input type="password" class="form-control" name="password" required>
</div>
<div class="mb-3">
<label for="password_confirmation" class="form-label">Confirm New Password</label>
<input type="password" class="form-control" name="password_confirmation" required>
</div>
<button type="submit" class="btn btn-warning">Change Password</button>
</form>
@endif
<!-- Google Account Linking -->
<div class="google-account mt-4">
@if(auth()->user()->google_id)
<div class="alert alert-success">
<i class="fab fa-google"></i>
Your Google account is linked
</div>
<form method="POST" action="{{ route('google.unlink') }}">
@csrf
<button type="submit" class="btn btn-outline-danger">
Unlink Google Account
</button>
</form>
@else
<div class="alert alert-info">
Link your Google account for easier login
</div>
<a href="{{ route('google.login') }}" class="btn btn-outline-primary">
<i class="fab fa-google"></i>
Link Google Account
</a>
@endif
</div>
</div>
- Mobile-First: Optimized for smartphone usage
- Touch-Friendly: Large buttons and touch targets
- Fast Loading: Optimized images and assets
- Offline Capability: Service worker for basic offline functionality
<!-- Mobile hamburger menu -->
<nav class="mobile-nav d-md-none">
<div class="mobile-nav-header">
<div class="nav-brand">
<img src="{{ asset('images/logo.png') }}" alt="Logo">
{{ config('app.name') }}
</div>
<button class="nav-toggle" id="mobile-nav-toggle">
<span></span>
<span></span>
<span></span>
</button>
</div>
<div class="mobile-nav-menu" id="mobile-nav-menu">
<a href="{{ route('dashboard') }}" class="nav-item">
<i class="fas fa-home"></i>
Dashboard
</a>
<a href="{{ route('stores.index') }}" class="nav-item">
<i class="fas fa-store"></i>
Stores
</a>
<a href="{{ route('profile.edit') }}" class="nav-item">
<i class="fas fa-user"></i>
Profile
</a>
<a href="{{ route('logout') }}" class="nav-item">
<i class="fas fa-sign-out-alt"></i>
Logout
</a>
</div>
</nav>
// Service Worker for offline capability
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
// Add to homescreen prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Show install button
const installButton = document.getElementById('install-app');
installButton.style.display = 'block';
installButton.addEventListener('click', () => {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
deferredPrompt = null;
});
});
});
- Point Earnings: When points are added
- Code Expiry: When codes are about to expire
- New Features: Updates and announcements
- Promotional: Special offers and events
<div class="notification-center">
<div class="notification-header">
<h4>Notifications</h4>
<button class="btn btn-sm btn-outline-secondary" onclick="markAllRead()">
Mark All Read
</button>
</div>
<div class="notification-list">
@foreach($notifications as $notification)
<div class="notification-item {{ $notification->read_at ? '' : 'unread' }}">
<div class="notification-icon">
<i class="fas fa-{{ $notification->icon }}"></i>
</div>
<div class="notification-content">
<h6>{{ $notification->title }}</h6>
<p>{{ $notification->message }}</p>
<small class="text-muted">{{ $notification->created_at->diffForHumans() }}</small>
</div>
</div>
@endforeach
</div>
</div>
- FAQ Section: Common questions and answers
- Contact Form: Direct support ticket submission
- Live Chat: Real-time support (if available)
- Email Support: Support email contact
- Account & Registration
- Points & Rewards
- Store Booking
- Technical Issues
- Privacy & Security
Next: Store Management untuk panduan pengelolaan data toko.