Store Management - luckydeva03/barbershop_app GitHub Wiki
Panduan lengkap untuk mengelola data toko dalam barbershop management system.
Store management dalam aplikasi ini tidak menggunakan interface admin khusus, melainkan dikelola melalui:
- Database Migrations: Struktur tabel
- Model Eloquent: Relasi dan business logic
- Seeders: Data dummy dan initial data
- Console Commands: Operasi bulk data
<?php
// database/migrations/xxxx_create_stores_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('stores', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->string('address');
$table->string('phone')->nullable();
$table->string('email')->nullable();
$table->decimal('latitude', 10, 8)->nullable();
$table->decimal('longitude', 11, 8)->nullable();
$table->string('image_url')->nullable();
$table->json('operating_hours')->nullable();
$table->json('services')->nullable();
$table->decimal('average_rating', 3, 2)->default(0);
$table->integer('reviews_count')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
}
public function down()
{
Schema::dropIfExists('stores');
}
};
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Store extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'description',
'address',
'phone',
'email',
'latitude',
'longitude',
'image_url',
'operating_hours',
'services',
'average_rating',
'reviews_count',
'is_active',
];
protected $casts = [
'operating_hours' => 'array',
'services' => 'array',
'latitude' => 'decimal:8',
'longitude' => 'decimal:8',
'average_rating' => 'decimal:2',
'is_active' => 'boolean',
];
// Relationships
public function reviews()
{
return $this->hasMany(Review::class);
}
public function historyPoints()
{
return $this->hasMany(HistoryPoint::class);
}
// Scopes
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeNearby($query, $latitude, $longitude, $radius = 10)
{
return $query->selectRaw("
*,
( 6371 * acos( cos( radians(?) ) *
cos( radians( latitude ) ) *
cos( radians( longitude ) - radians(?) ) +
sin( radians(?) ) *
sin( radians( latitude ) ) ) ) AS distance
", [$latitude, $longitude, $latitude])
->having('distance', '<', $radius)
->orderBy('distance');
}
// Accessors & Mutators
public function getFormattedPhoneAttribute()
{
if (!$this->phone) return null;
// Format Indonesian phone number
$phone = preg_replace('/[^0-9]/', '', $this->phone);
if (substr($phone, 0, 1) === '0') {
$phone = '62' . substr($phone, 1);
}
return $phone;
}
public function getOperatingHoursFormattedAttribute()
{
if (!$this->operating_hours) return 'Hours not specified';
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$formatted = [];
foreach ($days as $day) {
if (isset($this->operating_hours[$day])) {
$hours = $this->operating_hours[$day];
if ($hours['open'] && $hours['close']) {
$formatted[] = ucfirst($day) . ': ' . $hours['open'] . ' - ' . $hours['close'];
} else {
$formatted[] = ucfirst($day) . ': Closed';
}
}
}
return implode("\n", $formatted);
}
// Methods
public function updateAverageRating()
{
$avgRating = $this->reviews()->avg('rating') ?: 0;
$reviewsCount = $this->reviews()->count();
$this->update([
'average_rating' => round($avgRating, 2),
'reviews_count' => $reviewsCount,
]);
}
public function isOpenNow()
{
if (!$this->operating_hours) return false;
$now = now();
$dayOfWeek = strtolower($now->format('l'));
$currentTime = $now->format('H:i');
if (!isset($this->operating_hours[$dayOfWeek])) return false;
$hours = $this->operating_hours[$dayOfWeek];
return $currentTime >= $hours['open'] && $currentTime <= $hours['close'];
}
public function getDistanceFrom($latitude, $longitude)
{
if (!$this->latitude || !$this->longitude) return null;
$earthRadius = 6371; // km
$latFrom = deg2rad($latitude);
$lonFrom = deg2rad($longitude);
$latTo = deg2rad($this->latitude);
$lonTo = deg2rad($this->longitude);
$latDelta = $latTo - $latFrom;
$lonDelta = $lonTo - $lonFrom;
$a = sin($latDelta / 2) * sin($latDelta / 2) +
cos($latFrom) * cos($latTo) *
sin($lonDelta / 2) * sin($lonDelta / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
}
<?php
namespace Database\Seeders;
use App\Models\Store;
use Illuminate\Database\Seeder;
class StoreSeeder extends Seeder
{
public function run()
{
$stores = [
[
'name' => 'Barbershop Sentral',
'description' => 'Barbershop modern dengan pelayanan profesional dan suasana nyaman. Tersedia berbagai layanan potong rambut untuk pria dengan gaya terkini.',
'address' => 'Jl. Sudirman No. 123, Jakarta Pusat',
'phone' => '021-12345678',
'email' => '[email protected]',
'latitude' => -6.2088,
'longitude' => 106.8456,
'image_url' => '/images/stores/barbershop-sentral.jpg',
'operating_hours' => [
'monday' => ['open' => '09:00', 'close' => '21:00'],
'tuesday' => ['open' => '09:00', 'close' => '21:00'],
'wednesday' => ['open' => '09:00', 'close' => '21:00'],
'thursday' => ['open' => '09:00', 'close' => '21:00'],
'friday' => ['open' => '09:00', 'close' => '22:00'],
'saturday' => ['open' => '08:00', 'close' => '22:00'],
'sunday' => ['open' => '10:00', 'close' => '20:00'],
],
'services' => [
'Classic Haircut',
'Modern Style',
'Beard Trim',
'Hair Wash',
'Styling',
'Hair Treatment'
],
'is_active' => true,
],
[
'name' => 'The Gentleman Barber',
'description' => 'Barbershop premium dengan konsep gentleman klasik. Menyediakan layanan potong rambut, cukur jenggot, dan perawatan rambut pria.',
'address' => 'Jl. Kemang Raya No. 45, Jakarta Selatan',
'phone' => '021-87654321',
'email' => '[email protected]',
'latitude' => -6.2615,
'longitude' => 106.8106,
'image_url' => '/images/stores/gentleman-barber.jpg',
'operating_hours' => [
'monday' => ['open' => '10:00', 'close' => '20:00'],
'tuesday' => ['open' => '10:00', 'close' => '20:00'],
'wednesday' => ['open' => '10:00', 'close' => '20:00'],
'thursday' => ['open' => '10:00', 'close' => '20:00'],
'friday' => ['open' => '10:00', 'close' => '21:00'],
'saturday' => ['open' => '09:00', 'close' => '21:00'],
'sunday' => ['open' => '11:00', 'close' => '19:00'],
],
'services' => [
'Premium Haircut',
'Traditional Shave',
'Mustache Trim',
'Hair Styling',
'Scalp Treatment',
'Consultation'
],
'is_active' => true,
],
[
'name' => 'Cut & Style Barbershop',
'description' => 'Barbershop trendy dengan barber berpengalaman. Spesialis gaya rambut modern dan klasik dengan harga terjangkau.',
'address' => 'Jl. Gatot Subroto No. 78, Jakarta Selatan',
'phone' => '021-11223344',
'email' => '[email protected]',
'latitude' => -6.2297,
'longitude' => 106.8253,
'image_url' => '/images/stores/cut-style.jpg',
'operating_hours' => [
'monday' => ['open' => '08:30', 'close' => '21:30'],
'tuesday' => ['open' => '08:30', 'close' => '21:30'],
'wednesday' => ['open' => '08:30', 'close' => '21:30'],
'thursday' => ['open' => '08:30', 'close' => '21:30'],
'friday' => ['open' => '08:30', 'close' => '22:00'],
'saturday' => ['open' => '08:00', 'close' => '22:00'],
'sunday' => ['open' => '09:00', 'close' => '21:00'],
],
'services' => [
'Basic Haircut',
'Fade Cut',
'Undercut',
'Pompadour',
'Hair Gel',
'Quick Service'
],
'is_active' => true,
],
[
'name' => 'Royal Barbershop',
'description' => 'Barbershop mewah dengan interior klasik dan pelayanan berkelas. Melayani eksekutif dan profesional dengan standar tinggi.',
'address' => 'Jl. HR Rasuna Said No. 156, Jakarta Selatan',
'phone' => '021-55667788',
'email' => '[email protected]',
'latitude' => -6.2254,
'longitude' => 106.8317,
'image_url' => '/images/stores/royal-barbershop.jpg',
'operating_hours' => [
'monday' => ['open' => '09:30', 'close' => '20:30'],
'tuesday' => ['open' => '09:30', 'close' => '20:30'],
'wednesday' => ['open' => '09:30', 'close' => '20:30'],
'thursday' => ['open' => '09:30', 'close' => '20:30'],
'friday' => ['open' => '09:30', 'close' => '21:00'],
'saturday' => ['open' => '09:00', 'close' => '21:00'],
'sunday' => ['open' => '10:30', 'close' => '19:30'],
],
'services' => [
'Executive Haircut',
'Premium Shave',
'Hair Treatment',
'Scalp Massage',
'Grooming Package',
'VIP Service'
],
'is_active' => true,
],
[
'name' => 'Street Barber Co.',
'description' => 'Barbershop urban dengan vibe anak muda. Spesialis gaya street style dan fashion-forward haircuts dengan harga bersahabat.',
'address' => 'Jl. Senopati No. 89, Jakarta Selatan',
'phone' => '021-99887766',
'email' => '[email protected]',
'latitude' => -6.2388,
'longitude' => 106.8161,
'image_url' => '/images/stores/street-barber.jpg',
'operating_hours' => [
'monday' => ['open' => '11:00', 'close' => '22:00'],
'tuesday' => ['open' => '11:00', 'close' => '22:00'],
'wednesday' => ['open' => '11:00', 'close' => '22:00'],
'thursday' => ['open' => '11:00', 'close' => '22:00'],
'friday' => ['open' => '11:00', 'close' => '23:00'],
'saturday' => ['open' => '10:00', 'close' => '23:00'],
'sunday' => ['open' => '12:00', 'close' => '21:00'],
],
'services' => [
'Trendy Cuts',
'Color Treatment',
'Artistic Styling',
'Creative Designs',
'Youth Packages',
'Style Consultation'
],
'is_active' => true,
],
];
foreach ($stores as $storeData) {
Store::create($storeData);
}
}
}
<?php
namespace Database\Factories;
use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;
class StoreFactory extends Factory
{
protected $model = Store::class;
public function definition()
{
$services = [
'Classic Haircut', 'Modern Style', 'Fade Cut', 'Undercut',
'Pompadour', 'Beard Trim', 'Mustache Trim', 'Hair Wash',
'Styling', 'Hair Treatment', 'Scalp Massage', 'Traditional Shave'
];
$operatingHours = [
'monday' => ['open' => '09:00', 'close' => '21:00'],
'tuesday' => ['open' => '09:00', 'close' => '21:00'],
'wednesday' => ['open' => '09:00', 'close' => '21:00'],
'thursday' => ['open' => '09:00', 'close' => '21:00'],
'friday' => ['open' => '09:00', 'close' => '22:00'],
'saturday' => ['open' => '08:00', 'close' => '22:00'],
'sunday' => ['open' => '10:00', 'close' => '20:00'],
];
return [
'name' => $this->faker->company . ' Barbershop',
'description' => $this->faker->paragraph(3),
'address' => $this->faker->address,
'phone' => $this->faker->phoneNumber,
'email' => $this->faker->unique()->safeEmail,
'latitude' => $this->faker->latitude(-6.4, -6.1), // Jakarta area
'longitude' => $this->faker->longitude(106.7, 106.9),
'image_url' => '/images/stores/default-store.jpg',
'operating_hours' => $operatingHours,
'services' => $this->faker->randomElements($services, rand(4, 8)),
'average_rating' => $this->faker->randomFloat(2, 3.0, 5.0),
'reviews_count' => $this->faker->numberBetween(0, 100),
'is_active' => $this->faker->boolean(90), // 90% active
];
}
public function inactive()
{
return $this->state(function (array $attributes) {
return [
'is_active' => false,
];
});
}
public function withHighRating()
{
return $this->state(function (array $attributes) {
return [
'average_rating' => $this->faker->randomFloat(2, 4.0, 5.0),
'reviews_count' => $this->faker->numberBetween(50, 200),
];
});
}
}
<?php
namespace App\Console\Commands;
use App\Models\Store;
use Illuminate\Console\Command;
class CreateStore extends Command
{
protected $signature = 'store:create
{name : Store name}
{address : Store address}
{--phone= : Store phone number}
{--email= : Store email}
{--description= : Store description}';
protected $description = 'Create a new store';
public function handle()
{
$data = [
'name' => $this->argument('name'),
'address' => $this->argument('address'),
'phone' => $this->option('phone'),
'email' => $this->option('email'),
'description' => $this->option('description'),
'is_active' => true,
];
// Remove null values
$data = array_filter($data, function($value) {
return $value !== null;
});
$store = Store::create($data);
$this->info("Store created successfully!");
$this->table(
['Field', 'Value'],
[
['ID', $store->id],
['Name', $store->name],
['Address', $store->address],
['Phone', $store->phone ?: 'Not set'],
['Email', $store->email ?: 'Not set'],
['Status', $store->is_active ? 'Active' : 'Inactive'],
]
);
return 0;
}
}
<?php
namespace App\Console\Commands;
use App\Models\Store;
use Illuminate\Console\Command;
class ManageStores extends Command
{
protected $signature = 'store:manage';
protected $description = 'Interactive store management';
public function handle()
{
$this->info('๐ช Store Management System');
$this->line('');
$action = $this->choice('What would you like to do?', [
'List all stores',
'Create new store',
'Update store',
'Deactivate store',
'Activate store',
'Delete store',
'Store statistics',
]);
switch ($action) {
case 'List all stores':
$this->listStores();
break;
case 'Create new store':
$this->createStore();
break;
case 'Update store':
$this->updateStore();
break;
case 'Deactivate store':
$this->toggleStore(false);
break;
case 'Activate store':
$this->toggleStore(true);
break;
case 'Delete store':
$this->deleteStore();
break;
case 'Store statistics':
$this->showStatistics();
break;
}
return 0;
}
private function listStores()
{
$stores = Store::withTrashed()->get();
if ($stores->isEmpty()) {
$this->warn('No stores found.');
return;
}
$this->table(
['ID', 'Name', 'Address', 'Phone', 'Rating', 'Reviews', 'Status'],
$stores->map(function ($store) {
return [
$store->id,
$store->name,
Str::limit($store->address, 30),
$store->phone ?: '-',
number_format($store->average_rating, 1),
$store->reviews_count,
$store->deleted_at ? 'โ Deleted' : ($store->is_active ? 'โ
Active' : 'โธ๏ธ Inactive'),
];
})->toArray()
);
}
private function createStore()
{
$name = $this->ask('Store name');
$address = $this->ask('Store address');
$phone = $this->ask('Phone number (optional)');
$email = $this->ask('Email (optional)');
$description = $this->ask('Description (optional)');
$data = array_filter([
'name' => $name,
'address' => $address,
'phone' => $phone,
'email' => $email,
'description' => $description,
'is_active' => true,
]);
$store = Store::create($data);
$this->info("โ
Store '{$store->name}' created successfully!");
}
private function updateStore()
{
$stores = Store::active()->get();
if ($stores->isEmpty()) {
$this->warn('No active stores found.');
return;
}
$storeChoices = $stores->pluck('name', 'id')->toArray();
$storeId = $this->choice('Select store to update', $storeChoices);
$store = Store::find($storeId);
$this->info("Updating: {$store->name}");
$fields = ['name', 'address', 'phone', 'email', 'description'];
$updates = [];
foreach ($fields as $field) {
$current = $store->$field;
$new = $this->ask("$field (current: $current)", $current);
if ($new !== $current) {
$updates[$field] = $new;
}
}
if (!empty($updates)) {
$store->update($updates);
$this->info("โ
Store updated successfully!");
} else {
$this->info("No changes made.");
}
}
private function toggleStore($activate)
{
$status = $activate ? 'activate' : 'deactivate';
$stores = Store::where('is_active', !$activate)->get();
if ($stores->isEmpty()) {
$this->warn("No stores to $status.");
return;
}
$storeChoices = $stores->pluck('name', 'id')->toArray();
$storeId = $this->choice("Select store to $status", $storeChoices);
$store = Store::find($storeId);
$store->update(['is_active' => $activate]);
$status = $activate ? 'activated' : 'deactivated';
$this->info("โ
Store '{$store->name}' $status successfully!");
}
private function deleteStore()
{
$stores = Store::get();
if ($stores->isEmpty()) {
$this->warn('No stores found.');
return;
}
$storeChoices = $stores->pluck('name', 'id')->toArray();
$storeId = $this->choice('Select store to delete', $storeChoices);
$store = Store::find($storeId);
if ($this->confirm("Are you sure you want to delete '{$store->name}'? This action cannot be undone.")) {
$store->delete();
$this->info("โ
Store '{$store->name}' deleted successfully!");
} else {
$this->info("Deletion cancelled.");
}
}
private function showStatistics()
{
$total = Store::count();
$active = Store::active()->count();
$inactive = Store::where('is_active', false)->count();
$deleted = Store::onlyTrashed()->count();
$avgRating = Store::avg('average_rating');
$totalReviews = Store::sum('reviews_count');
$topRated = Store::orderBy('average_rating', 'desc')->first();
$this->info('๐ Store Statistics');
$this->line('');
$this->table(
['Metric', 'Value'],
[
['Total Stores', $total],
['Active Stores', $active],
['Inactive Stores', $inactive],
['Deleted Stores', $deleted],
['Average Rating', number_format($avgRating, 2)],
['Total Reviews', number_format($totalReviews)],
['Top Rated Store', $topRated ? "{$topRated->name} ({$topRated->average_rating}โญ)" : 'None'],
]
);
}
}
<?php
namespace App\Console\Commands;
use App\Models\Store;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
class ImportStores extends Command
{
protected $signature = 'store:import {file : JSON file path}';
protected $description = 'Import stores from JSON file';
public function handle()
{
$filePath = $this->argument('file');
if (!file_exists($filePath)) {
$this->error("File not found: $filePath");
return 1;
}
$jsonContent = file_get_contents($filePath);
$data = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->error('Invalid JSON file.');
return 1;
}
$this->info('Importing stores...');
$bar = $this->output->createProgressBar(count($data));
$bar->start();
$imported = 0;
$errors = [];
foreach ($data as $index => $storeData) {
$validator = Validator::make($storeData, [
'name' => 'required|string|max:255',
'address' => 'required|string',
'phone' => 'nullable|string',
'email' => 'nullable|email',
'description' => 'nullable|string',
'latitude' => 'nullable|numeric|between:-90,90',
'longitude' => 'nullable|numeric|between:-180,180',
'operating_hours' => 'nullable|array',
'services' => 'nullable|array',
]);
if ($validator->fails()) {
$errors[] = "Row $index: " . implode(', ', $validator->errors()->all());
$bar->advance();
continue;
}
try {
Store::create($storeData);
$imported++;
} catch (\Exception $e) {
$errors[] = "Row $index: " . $e->getMessage();
}
$bar->advance();
}
$bar->finish();
$this->line('');
$this->info("โ
Import completed!");
$this->info("Imported: $imported stores");
if (!empty($errors)) {
$this->warn("Errors: " . count($errors));
foreach ($errors as $error) {
$this->error($error);
}
}
return 0;
}
}
<?php
namespace App\Console\Commands;
use App\Models\Store;
use Illuminate\Console\Command;
class ExportStores extends Command
{
protected $signature = 'store:export {file : Output file path} {--format=json : Export format (json|csv)}';
protected $description = 'Export stores to file';
public function handle()
{
$filePath = $this->argument('file');
$format = $this->option('format');
$stores = Store::with('reviews')->get();
if ($stores->isEmpty()) {
$this->warn('No stores to export.');
return 1;
}
switch ($format) {
case 'json':
$this->exportJson($stores, $filePath);
break;
case 'csv':
$this->exportCsv($stores, $filePath);
break;
default:
$this->error("Unsupported format: $format");
return 1;
}
$this->info("โ
Exported {$stores->count()} stores to $filePath");
return 0;
}
private function exportJson($stores, $filePath)
{
$data = $stores->map(function ($store) {
return [
'name' => $store->name,
'description' => $store->description,
'address' => $store->address,
'phone' => $store->phone,
'email' => $store->email,
'latitude' => $store->latitude,
'longitude' => $store->longitude,
'operating_hours' => $store->operating_hours,
'services' => $store->services,
'average_rating' => $store->average_rating,
'reviews_count' => $store->reviews_count,
'is_active' => $store->is_active,
'created_at' => $store->created_at->toISOString(),
];
})->toArray();
file_put_contents($filePath, json_encode($data, JSON_PRETTY_PRINT));
}
private function exportCsv($stores, $filePath)
{
$fp = fopen($filePath, 'w');
// Header
fputcsv($fp, [
'ID', 'Name', 'Description', 'Address', 'Phone', 'Email',
'Latitude', 'Longitude', 'Average Rating', 'Reviews Count',
'Is Active', 'Created At'
]);
// Data
foreach ($stores as $store) {
fputcsv($fp, [
$store->id,
$store->name,
$store->description,
$store->address,
$store->phone,
$store->email,
$store->latitude,
$store->longitude,
$store->average_rating,
$store->reviews_count,
$store->is_active ? 'Yes' : 'No',
$store->created_at->format('Y-m-d H:i:s'),
]);
}
fclose($fp);
}
}
<?php
namespace App\Console\Commands;
use App\Models\Store;
use Illuminate\Console\Command;
class CleanupStores extends Command
{
protected $signature = 'store:cleanup
{--inactive-days=30 : Days to keep inactive stores}
{--no-reviews-days=90 : Days to keep stores with no reviews}
{--dry-run : Show what would be deleted without actually deleting}';
protected $description = 'Cleanup old/unused stores';
public function handle()
{
$inactiveDays = $this->option('inactive-days');
$noReviewsDays = $this->option('no-reviews-days');
$dryRun = $this->option('dry-run');
$this->info('๐งน Store Cleanup');
$this->line('');
// Find inactive stores
$inactiveStores = Store::where('is_active', false)
->where('updated_at', '<', now()->subDays($inactiveDays))
->get();
// Find stores with no reviews for long time
$noReviewStores = Store::where('reviews_count', 0)
->where('created_at', '<', now()->subDays($noReviewsDays))
->get();
$toDelete = $inactiveStores->merge($noReviewStores)->unique('id');
if ($toDelete->isEmpty()) {
$this->info('โ
No stores need cleanup.');
return 0;
}
$this->warn("Found {$toDelete->count()} stores for cleanup:");
$this->table(
['ID', 'Name', 'Reason', 'Last Updated'],
$toDelete->map(function ($store) use ($inactiveDays, $noReviewsDays) {
$reason = $store->is_active ? 'No reviews' : 'Inactive';
return [
$store->id,
$store->name,
$reason,
$store->updated_at->diffForHumans(),
];
})->toArray()
);
if ($dryRun) {
$this->info('๐ Dry run mode - no stores were deleted.');
return 0;
}
if ($this->confirm('Do you want to delete these stores?')) {
foreach ($toDelete as $store) {
$store->delete();
}
$this->info("โ
Deleted {$toDelete->count()} stores.");
} else {
$this->info('Cleanup cancelled.');
}
return 0;
}
}
<?php
// app/Helper/LocationHelper.php
namespace App\Helper;
use Illuminate\Support\Facades\Http;
class LocationHelper
{
public static function geocodeAddress($address)
{
$apiKey = config('services.google.maps_api_key');
if (!$apiKey) {
return null;
}
try {
$response = Http::get('https://maps.googleapis.com/maps/api/geocode/json', [
'address' => $address,
'key' => $apiKey,
]);
$data = $response->json();
if ($data['status'] === 'OK' && !empty($data['results'])) {
$location = $data['results'][0]['geometry']['location'];
return [
'latitude' => $location['lat'],
'longitude' => $location['lng'],
'formatted_address' => $data['results'][0]['formatted_address'],
];
}
} catch (\Exception $e) {
// Log error
\Log::error('Geocoding failed: ' . $e->getMessage());
}
return null;
}
public static function calculateDistance($lat1, $lon1, $lat2, $lon2, $unit = 'km')
{
$earthRadius = $unit === 'miles' ? 3958.756 : 6371;
$latFrom = deg2rad($lat1);
$lonFrom = deg2rad($lon1);
$latTo = deg2rad($lat2);
$lonTo = deg2rad($lon2);
$latDelta = $latTo - $latFrom;
$lonDelta = $lonTo - $lonFrom;
$a = sin($latDelta / 2) * sin($latDelta / 2) +
cos($latFrom) * cos($latTo) *
sin($lonDelta / 2) * sin($lonDelta / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return $earthRadius * $c;
}
}
<?php
namespace App\Console\Commands;
use App\Models\Store;
use App\Helper\LocationHelper;
use Illuminate\Console\Command;
class UpdateStoreLocations extends Command
{
protected $signature = 'store:update-locations';
protected $description = 'Update store coordinates using Google Geocoding API';
public function handle()
{
$stores = Store::whereNull('latitude')
->orWhereNull('longitude')
->get();
if ($stores->isEmpty()) {
$this->info('โ
All stores have coordinates.');
return 0;
}
$this->info("Updating coordinates for {$stores->count()} stores...");
$bar = $this->output->createProgressBar($stores->count());
$bar->start();
$updated = 0;
$failed = 0;
foreach ($stores as $store) {
$location = LocationHelper::geocodeAddress($store->address);
if ($location) {
$store->update([
'latitude' => $location['latitude'],
'longitude' => $location['longitude'],
]);
$updated++;
} else {
$failed++;
}
$bar->advance();
// Rate limiting
sleep(1);
}
$bar->finish();
$this->line('');
$this->info("โ
Updated: $updated stores");
if ($failed > 0) {
$this->warn("โ Failed: $failed stores");
}
return 0;
}
}
Daftarkan semua command di app/Console/Kernel.php
:
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
Commands\CreateStore::class,
Commands\ManageStores::class,
Commands\ImportStores::class,
Commands\ExportStores::class,
Commands\CleanupStores::class,
Commands\UpdateStoreLocations::class,
];
protected function schedule(Schedule $schedule)
{
// Update store ratings daily
$schedule->command('app:update-store-ratings')->daily();
// Cleanup inactive stores weekly
$schedule->command('store:cleanup')->weekly();
}
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
# Create new store
php artisan store:create "New Barbershop" "Jl. Example No. 123" --phone="021-12345678" --email="[email protected]"
# Interactive management
php artisan store:manage
# Import from JSON
php artisan store:import storage/app/stores.json
# Export to CSV
php artisan store:export storage/app/stores.csv --format=csv
# Update coordinates
php artisan store:update-locations
# Cleanup old stores
php artisan store:cleanup --dry-run
[
{
"name": "Example Barbershop",
"description": "Professional barbershop with modern style",
"address": "Jl. Example No. 123, Jakarta",
"phone": "021-12345678",
"email": "[email protected]",
"latitude": -6.2088,
"longitude": 106.8456,
"operating_hours": {
"monday": {"open": "09:00", "close": "21:00"},
"tuesday": {"open": "09:00", "close": "21:00"},
"wednesday": {"open": "09:00", "close": "21:00"},
"thursday": {"open": "09:00", "close": "21:00"},
"friday": {"open": "09:00", "close": "22:00"},
"saturday": {"open": "08:00", "close": "22:00"},
"sunday": {"open": "10:00", "close": "20:00"}
},
"services": [
"Classic Haircut",
"Modern Style",
"Beard Trim",
"Hair Wash"
],
"is_active": true
}
]
Next: Point System Guide untuk panduan sistem poin customer.