Store Management - luckydeva03/barbershop_app GitHub Wiki

๐Ÿช Store Management

Panduan lengkap untuk mengelola data toko dalam barbershop management system.

๐Ÿ“‹ Overview

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

๐Ÿ—„๏ธ Database Schema

Store Model Structure

Migration File: stores table

<?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');
    }
};

Store Model (app/Models/Store.php)

<?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;
    }
}

๐ŸŒฑ Database Seeders

Store Seeder (database/seeders/StoreSeeder.php)

<?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);
        }
    }
}

Factory untuk Store (database/factories/StoreFactory.php)

<?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),
            ];
        });
    }
}

๐Ÿ”ง Management Commands

Create Store Command (app/Console/Commands/CreateStore.php)

<?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;
    }
}

Store Management Command (app/Console/Commands/ManageStores.php)

<?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'],
            ]
        );
    }
}

๐Ÿ› ๏ธ Store Operations

Bulk Import Command (app/Console/Commands/ImportStores.php)

<?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;
    }
}

Export Stores Command (app/Console/Commands/ExportStores.php)

<?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);
    }
}

๐Ÿ”„ Data Maintenance

Store Cleanup Command (app/Console/Commands/CleanupStores.php)

<?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;
    }
}

๐Ÿ“ Location Management

Geocoding Helper

<?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;
    }
}

Update Store Location Command

<?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;
    }
}

๐Ÿ“‹ Command Registration

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');
    }
}

๐Ÿ”จ Usage Examples

Menjalankan Commands

# 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

Sample JSON Import Format

[
    {
        "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.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ