first commit

This commit is contained in:
furen81
2026-01-23 19:18:52 +07:00
commit 6e681c4ad3
80 changed files with 13874 additions and 0 deletions

1
database/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -0,0 +1,49 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'employee_id' => 'EMP' . fake()->unique()->numberBetween(100, 999),
'phone' => fake()->phoneNumber(),
'color' => fake()->hexColor(),
'role' => 'sales',
'is_active' => true,
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn(array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('employee_id', 20)->unique();
$table->string('phone', 20)->nullable();
$table->string('color', 7)->default('#3B82F6');
$table->enum('role', ['admin', 'sales', 'supervisor'])->default('sales');
$table->boolean('is_active')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['employee_id', 'phone', 'color', 'role', 'is_active']);
});
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sales_routes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->date('date');
$table->enum('status', ['active', 'completed'])->default('active');
$table->decimal('total_distance_km', 8, 2)->nullable();
$table->integer('total_duration_minutes')->nullable();
$table->timestamp('started_at')->nullable();
$table->timestamp('ended_at')->nullable();
$table->timestamps();
$table->unique(['user_id', 'date']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_routes');
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('waypoints', function (Blueprint $table) {
$table->id();
$table->foreignId('sales_route_id')->constrained()->cascadeOnDelete();
$table->enum('type', ['checkin', 'checkout', 'gps', 'lunch', 'visit']);
$table->decimal('latitude', 10, 8);
$table->decimal('longitude', 11, 8);
$table->timestamp('recorded_at');
$table->string('location_name', 200)->nullable();
$table->text('address')->nullable();
$table->text('notes')->nullable();
$table->string('photo_url', 500)->nullable();
$table->timestamps();
$table->index(['sales_route_id', 'recorded_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('waypoints');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sales_plans', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->date('date');
$table->enum('status', ['pending', 'optimized', 'assigned', 'in_progress', 'completed'])->default('pending');
$table->decimal('total_distance_km', 8, 2)->nullable();
$table->timestamp('optimized_at')->nullable();
$table->json('optimized_route')->nullable();
$table->text('notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['user_id', 'date']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sales_plans');
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->string('tokenable_type');
$table->string('tokenable_id');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('address')->nullable();
$table->string('owner_name')->nullable();
$table->string('phone')->nullable();
$table->decimal('latitude', 10, 8)->nullable();
$table->decimal('longitude', 11, 8)->nullable();
$table->string('city')->nullable();
$table->foreignId('pic_sales_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('customers');
}
};

View File

@ -0,0 +1,24 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
// Seed GeoPlan Data
$this->call(GeoPlanSeeder::class);
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\SalesPlan;
use App\Models\PlanTarget;
use Carbon\Carbon;
class GeoPlanSeeder extends Seeder
{
public function run(): void
{
// 1. Ensure Sales User Exists
$salesUser = User::where('employee_id', 'SLS001')->first();
if (!$salesUser) {
$salesUser = User::create([
'name' => 'Sales Person 1',
'email' => 'sales1@georeach.com',
'password' => bcrypt('password'),
'role' => 'sales',
'employee_id' => 'SLS001',
'color' => '#3B82F6', // Blue
'is_active' => true,
]);
$this->command->info('Created Sales User: SLS001');
} else {
$this->command->info('Using Existing Sales User: SLS001');
}
// 2. Create Sales Plan for Today
$today = Carbon::today();
// Check if plan already exists to avoid duplicates on re-run
$existingPlan = SalesPlan::where('user_id', $salesUser->id)
->whereDate('date', $today)
->first();
if ($existingPlan) {
$existingPlan->targets()->delete();
$existingPlan->delete();
$this->command->info('Cleaned up existing plan for today');
}
$plan = SalesPlan::create([
'user_id' => $salesUser->id,
'date' => $today,
'status' => 'pending',
'created_by' => $salesUser->id,
]);
$this->command->info('Created Sales Plan for Today: ' . $today->toDateString());
// 3. Create Plan Targets (The Dummy Data)
$targets = [
[
'name' => 'Toko Jaya Abadi',
'address' => 'Jl. Merdeka No. 123, Bandung',
'latitude' => -6.9175,
'longitude' => 107.6191,
'status' => true, // is_completed
'completed_at' => Carbon::parse('09:00'),
'notes' => 'Sales visit completed.',
],
[
'name' => 'Warung Makan Sederhana',
'address' => 'Jl. Sudirman No. 45, Bandung',
'latitude' => -6.9215,
'longitude' => 107.6098,
'status' => true,
'completed_at' => Carbon::parse('10:30'),
'notes' => 'Lunch and prospect.',
],
[
'name' => 'Minimarket Berkah',
'address' => 'Jl. Asia Afrika No. 78, Bandung',
'latitude' => -6.9218,
'longitude' => 107.6073,
'status' => false,
'completed_at' => null,
'notes' => null,
],
[
'name' => 'Apotek Sehat',
'address' => 'Jl. Braga No. 56, Bandung',
'latitude' => -6.9186,
'longitude' => 107.6097,
'status' => false,
'completed_at' => null,
'notes' => null,
],
[
'name' => 'Toko Elektronik Jaya',
'address' => 'Jl. Dago No. 89, Bandung',
'latitude' => -6.8853,
'longitude' => 107.6146,
'status' => false,
'completed_at' => null,
'notes' => null,
],
];
foreach ($targets as $index => $t) {
PlanTarget::create([
'sales_plan_id' => $plan->id,
'order' => $index + 1,
'name' => $t['name'],
'address' => $t['address'],
'latitude' => $t['latitude'],
'longitude' => $t['longitude'],
'is_completed' => $t['status'],
'completed_at' => $t['completed_at'] ? Carbon::today()->setTimeFrom(Carbon::parse($t['completed_at'])) : null,
'notes' => $t['notes'],
'source' => 'admin',
]);
}
$this->command->info('Seeded ' . count($targets) . ' targets.');
}
}