Initial commit - construprogress app

This commit is contained in:
2026-05-07 23:31:33 +02:00
commit 156aa14bbb
157 changed files with 21654 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<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(),
'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,
]);
}
}
@@ -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');
}
};
@@ -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')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -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');
}
};
@@ -0,0 +1,29 @@
<?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('projects', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('address');
$table->decimal('lat', 10, 8);
$table->decimal('lng', 11, 8);
$table->date('start_date');
$table->date('end_date_estimated')->nullable();
$table->enum('status', ['planning', 'in_progress', 'paused', 'completed'])->default('planning');
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('projects');
}
};
@@ -0,0 +1,27 @@
<?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('phases', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->text('description')->nullable();
$table->integer('order')->default(0);
$table->string('color', 7)->default('#3b82f6'); // hex color
$table->integer('progress_percent')->default(0);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('phases');
}
};
@@ -0,0 +1,27 @@
<?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('layers', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->onDelete('cascade');
$table->foreignId('phase_id')->nullable()->constrained()->onDelete('set null');
$table->string('name');
$table->json('geojson_data'); // GeoJSON geometry collection
$table->string('original_file')->nullable(); // path to original uploaded file
$table->foreignId('uploaded_by')->constrained('users');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('layers');
}
};
@@ -0,0 +1,26 @@
<?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('progress_updates', function (Blueprint $table) {
$table->id();
$table->foreignId('phase_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained();
$table->integer('progress_percent');
$table->text('comment')->nullable();
$table->json('location')->nullable(); // GPS point from mobile
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('progress_updates');
}
};
@@ -0,0 +1,25 @@
<?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('pending_syncs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('action'); // 'progress_update', 'task_complete'
$table->json('payload');
$table->timestamp('synced_at')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('pending_syncs');
}
};
@@ -0,0 +1,24 @@
<?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('project_user', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('role_in_project'); // supervisor, consultant, client
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('project_user');
}
};
@@ -0,0 +1,134 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};
@@ -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::create('inspection_templates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->foreignId('project_id')->nullable()->constrained()->onDelete('cascade');
$table->json('fields'); // [{name, label, type, options, required}]
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('inspection_templates');
}
};
@@ -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('inspections', function (Blueprint $table) {
$table->id();
$table->foreignId('project_id')->constrained()->onDelete('cascade');
$table->foreignId('layer_id')->constrained()->onDelete('cascade');
$table->string('feature_id'); // ID del elemento GeoJSON
$table->foreignId('template_id')->nullable()->constrained('inspection_templates')->onDelete('set null');
$table->foreignId('user_id')->constrained();
$table->json('data'); // Valores de los campos del template
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('inspections');
}
};
@@ -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('features', function (Blueprint $table) {
$table->id();
$table->foreignId('layer_id')->constrained()->onDelete('cascade');
$table->string('name')->nullable();
$table->json('geometry'); // GeoJSON geometry object (Point, LineString, Polygon)
$table->json('properties')->nullable(); // propiedades extra (progreso, responsable, etc.)
$table->foreignId('template_id')->nullable()->constrained('inspection_templates');
$table->integer('progress')->default(0);
$table->string('responsible')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('features');
}
};
@@ -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
{
// Primero eliminar la columna antigua (si existe) y crear la nueva
Schema::table('inspections', function (Blueprint $table) {
// Si ya existe como string, la eliminamos
if (Schema::hasColumn('inspections', 'feature_id')) {
$table->dropColumn('feature_id');
}
$table->foreignId('feature_id')->constrained('features')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('inspections', function (Blueprint $table) {
$table->dropForeign(['feature_id']);
$table->dropColumn('feature_id');
$table->string('feature_id')->nullable(); // rollback opcional
});
}
};
@@ -0,0 +1,28 @@
<?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('layers', function (Blueprint $table) {
$table->dropColumn('geojson_data');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('layers', function (Blueprint $table) {
$table->json('geojson_data'); // GeoJSON geometry collection
});
}
};
@@ -0,0 +1,28 @@
<?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('layers', function (Blueprint $table) {
$table->string('color', 7)->default('#3b82f6')->after('name');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('layers', function (Blueprint $table) {
$table->dropColumn('color');
});
}
};
+31
View File
@@ -0,0 +1,31 @@
<?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();
/*User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);*/
$this->call([
RolesAndPermissionsSeeder::class,
ProjectExampleSeeder::class,
]);
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Project;
use App\Models\Phase;
use App\Models\Layer;
use App\Models\Feature;
use App\Models\InspectionTemplate;
use App\Models\User;
class ProjectExampleSeeder extends Seeder
{
public function run()
{
// Obtener usuario admin
$admin = User::where('email', 'admin@email.com')->first();
if (!$admin) {
$admin = User::first();
}
// Crear proyecto
$project = Project::create([
'name' => 'Edificio Central',
'address' => 'Av. Principal 123, Ciudad',
'lat' => 40.416775,
'lng' => -3.703790,
'start_date' => now(),
'end_date_estimated' => now()->addMonths(12),
'status' => 'in_progress',
'created_by' => $admin->id,
]);
$project->users()->attach($admin->id, ['role_in_project' => 'supervisor']);
// Crear fases
$phases = [
['name' => 'Cimientos', 'order' => 1, 'color' => '#ef4444'],
['name' => 'Estructura', 'order' => 2, 'color' => '#3b82f6'],
['name' => 'Instalaciones', 'order' => 3, 'color' => '#10b981'],
['name' => 'Acabados', 'order' => 4, 'color' => '#f59e0b'],
];
foreach ($phases as $phaseData) {
Phase::create(array_merge($phaseData, ['project_id' => $project->id, 'progress_percent' => 0]));
}
$phaseCimientos = Phase::where('project_id', $project->id)->where('name', 'Cimientos')->first();
// Crear templates de inspección
$templateHormigon = InspectionTemplate::create([
'project_id' => $project->id,
'name' => 'Control de hormigón',
'fields' => [
['name' => 'progress', 'label' => 'Progreso de vertido', 'type' => 'percentage', 'required' => true],
['name' => 'calidad', 'label' => 'Calidad del hormigón', 'type' => 'select', 'options' => 'Excelente,Bueno,Regular,Deficiente', 'required' => true],
['name' => 'observaciones', 'label' => 'Observaciones', 'type' => 'textarea', 'required' => false],
]
]);
$templateAcero = InspectionTemplate::create([
'project_id' => $project->id,
'name' => 'Control de acero',
'fields' => [
['name' => 'progress', 'label' => '% de acero colocado', 'type' => 'percentage', 'required' => true],
['name' => 'diametros', 'label' => 'Diámetros verificados', 'type' => 'text', 'required' => false],
]
]);
// Crear capa (sin geojson_data)
$layer = Layer::create([
'project_id' => $project->id,
'phase_id' => $phaseCimientos->id,
'name' => 'Plano de cimientos',
'original_file' => null,
'uploaded_by' => $admin->id,
]);
// Crear features (elementos geográficos)
$feature1 = Feature::create([
'layer_id' => $layer->id,
'name' => 'Zapata A1',
'geometry' => [
'type' => 'Polygon',
'coordinates' => [[
[-3.705, 40.415],
[-3.702, 40.415],
[-3.702, 40.418],
[-3.705, 40.418],
[-3.705, 40.415]
]]
],
'properties' => ['description' => 'Zapata esquina noroeste'],
'template_id' => $templateHormigon->id,
'progress' => 45,
'responsible' => 'Ing. Gómez',
]);
$feature2 = Feature::create([
'layer_id' => $layer->id,
'name' => 'Zapata B1',
'geometry' => [
'type' => 'Polygon',
'coordinates' => [[
[-3.700, 40.415],
[-3.697, 40.415],
[-3.697, 40.418],
[-3.700, 40.418],
[-3.700, 40.415]
]]
],
'properties' => ['description' => 'Zapata lado este'],
'template_id' => $templateAcero->id,
'progress' => 80,
'responsible' => 'Arq. Martínez',
]);
Feature::create([
'layer_id' => $layer->id,
'name' => 'Punto de control topográfico',
'geometry' => [
'type' => 'Point',
'coordinates' => [-3.703, 40.4165]
],
'properties' => ['tipo' => 'estación total'],
'template_id' => null,
'progress' => 100,
'responsible' => 'Topógrafo Pérez',
]);
// (Opcional) Crear una inspección de ejemplo
\App\Models\Inspection::create([
'project_id' => $project->id,
'layer_id' => $layer->id,
'feature_id' => $feature1->id,
'template_id' => $templateHormigon->id,
'user_id' => $admin->id,
'data' => [
'progress' => 45,
'calidad' => 'Bueno',
'observaciones' => 'Vertido completado en un 45%, sin fisuras aparentes.'
],
]);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use App\Models\User;
class RolesAndPermissionsSeeder extends Seeder
{
public function run()
{
// Reset cached roles and permissions
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Create permissions
$permissions = [
'view projects', 'create projects', 'edit projects', 'delete projects',
'assign users', 'upload layers', 'update progress', 'view reports', 'manage all'
];
foreach ($permissions as $perm) {
Permission::firstOrCreate(['name' => $perm]);
}
// Create roles and assign permissions
$admin = Role::firstOrCreate(['name' => 'Admin']);
$admin->givePermissionTo(Permission::all());
$supervisor = Role::firstOrCreate(['name' => 'Supervisor']);
$supervisor->givePermissionTo(['view projects', 'upload layers', 'update progress']);
$consultor = Role::firstOrCreate(['name' => 'Consultor']);
$consultor->givePermissionTo(['view projects', 'view reports']);
$cliente = Role::firstOrCreate(['name' => 'Cliente']);
$cliente->givePermissionTo(['view projects']);
// Create default admin user
$user = User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@email.com',
'password' => bcrypt('password'),
]);
$user->assignRole('Admin');
}
}