diff --git a/app/Http/Controllers/ProjectSettingsController.php b/app/Http/Controllers/ProjectSettingsController.php new file mode 100644 index 0000000..3448fa8 --- /dev/null +++ b/app/Http/Controllers/ProjectSettingsController.php @@ -0,0 +1,223 @@ +authorize('update', $project); + + //$project->load(['codingConfig', 'documentStatuses']); + + return view('project-settings.index', compact('project')); + } + + public function updateCoding(Request $request, Project $project) + { + $this->authorize('update', $project); + + $validated = $request->validate([ + 'format' => 'required|string|max:255', + 'year_format' => 'required|in:Y,y,Yy,yy,Y-m,Y/m,y-m,y/m', + 'separator' => 'required|string|max:5', + 'sequence_length' => 'required|integer|min:1|max:10', + 'auto_generate' => 'boolean', + 'elements' => 'nullable|array', + 'reset_sequence' => 'boolean', + ]); + + try { + DB::beginTransaction(); + + $codingConfig = $project->codingConfig ?: new ProjectCodingConfig(); + $codingConfig->project_id = $project->id; + + $codingConfig->fill([ + 'format' => $validated['format'], + 'year_format' => $validated['year_format'], + 'separator' => $validated['separator'], + 'sequence_length' => $validated['sequence_length'], + 'auto_generate' => $validated['auto_generate'] ?? false, + 'elements' => $validated['elements'] ?? [], + ]); + + if ($request->boolean('reset_sequence')) { + $codingConfig->next_sequence = 1; + } + + $codingConfig->save(); + + DB::commit(); + + return redirect()->route('project-settings.index', $project) + ->with('success', 'Configuración de codificación actualizada correctamente.'); + + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back() + ->with('error', 'Error al actualizar la configuración: ' . $e->getMessage()); + } + } + + public function storeStatus(Request $request, Project $project) + { + $this->authorize('update', $project); + + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'color' => 'required|string|max:7', + 'text_color' => 'nullable|string|max:7', + 'description' => 'nullable|string|max:500', + 'allow_upload' => 'boolean', + 'allow_edit' => 'boolean', + 'allow_delete' => 'boolean', + 'requires_approval' => 'boolean', + 'is_default' => 'boolean', + ]); + + try { + DB::beginTransaction(); + + // Si se marca como default, quitar el default de otros estados + if ($validated['is_default'] ?? false) { + $project->documentStatuses()->update(['is_default' => false]); + } + + $status = new ProjectDocumentStatus($validated); + $status->project_id = $project->id; + $status->slug = Str::slug($validated['name']); + + // Verificar que el slug sea único + $counter = 1; + $originalSlug = $status->slug; + while ($project->documentStatuses()->where('slug', $status->slug)->exists()) { + $status->slug = $originalSlug . '-' . $counter; + $counter++; + } + + $status->save(); + + DB::commit(); + + return redirect()->route('project-settings.index', $project) + ->with('success', 'Estado creado correctamente.'); + + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back() + ->with('error', 'Error al crear el estado: ' . $e->getMessage()); + } + } + + public function updateStatus(Request $request, Project $project, ProjectDocumentStatus $status) + { + $this->authorize('update', $project); + + if ($status->project_id !== $project->id) { + abort(403); + } + + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'color' => 'required|string|max:7', + 'text_color' => 'nullable|string|max:7', + 'description' => 'nullable|string|max:500', + 'allow_upload' => 'boolean', + 'allow_edit' => 'boolean', + 'allow_delete' => 'boolean', + 'requires_approval' => 'boolean', + 'is_default' => 'boolean', + ]); + + try { + DB::beginTransaction(); + + // Si se marca como default, quitar el default de otros estados + if ($validated['is_default'] ?? false) { + $project->documentStatuses() + ->where('id', '!=', $status->id) + ->update(['is_default' => false]); + } + + $status->update($validated); + + DB::commit(); + + return redirect()->route('project-settings.index', $project) + ->with('success', 'Estado actualizado correctamente.'); + + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back() + ->with('error', 'Error al actualizar el estado: ' . $e->getMessage()); + } + } + + public function destroyStatus(Project $project, ProjectDocumentStatus $status) + { + $this->authorize('update', $project); + + if ($status->project_id !== $project->id) { + abort(403); + } + + // Verificar que no haya documentos con este estado + if ($status->documents()->exists()) { + return redirect()->back() + ->with('error', 'No se puede eliminar el estado porque hay documentos asociados.'); + } + + // Si es el estado por defecto, establecer otro como default + if ($status->is_default) { + $newDefault = $project->documentStatuses() + ->where('id', '!=', $status->id) + ->first(); + + if ($newDefault) { + $newDefault->update(['is_default' => true]); + } + } + + $status->delete(); + + return redirect()->route('project-settings.index', $project) + ->with('success', 'Estado eliminado correctamente.'); + } + + public function reorderStatuses(Request $request, Project $project) + { + $this->authorize('update', $project); + + $request->validate([ + 'order' => 'required|array', + 'order.*' => 'exists:project_document_statuses,id', + ]); + + try { + DB::beginTransaction(); + + foreach ($request->order as $index => $statusId) { + $status = ProjectDocumentStatus::find($statusId); + if ($status && $status->project_id === $project->id) { + $status->update(['order' => $index + 1]); + } + } + + DB::commit(); + + return response()->json(['success' => true]); + + } catch (\Exception $e) { + DB::rollBack(); + return response()->json(['error' => $e->getMessage()], 500); + } + } +} \ No newline at end of file diff --git a/app/Livewire/CodeEdit.php b/app/Livewire/CodeEdit.php index 93d2fa4..b4f1741 100644 --- a/app/Livewire/CodeEdit.php +++ b/app/Livewire/CodeEdit.php @@ -22,17 +22,33 @@ class CodeEdit extends Component 'maxLength' => 'required|integer|min:2|max:12', ]; - public function mount($componentId, $initialName = '') + public function mount($componentId, $initialName = '', $initialMaxLength = 3, $initialDocumentTypes = []) { $this->componentId = $componentId; $this->name = $initialName; - $this->maxLength = 3; + $this->maxLength = $initialMaxLength; + $this->documentTypes = $initialDocumentTypes; - // Disparar evento inicial para establecer el nombre + // Guardar datos iniciales + $this->initialData = [ + 'name' => $initialName, + 'maxLength' => $initialMaxLength, + 'documentTypes' => $initialDocumentTypes + ]; + + // Disparar eventos iniciales $this->dispatch('nameUpdated', componentId: $this->componentId, data: [ - 'name' => $this->name + 'name' => $this->name, + ] + ); + + $this->dispatch('componentUpdated', + componentId: $this->componentId, + data: [ + 'documentTypes' => $this->documentTypes, + 'maxLength' => $this->maxLength ] ); } @@ -74,7 +90,7 @@ class CodeEdit extends Component $this->documentTypes[] = [ 'code' => $this->codeInput, 'label' => $this->labelInput, - 'max_length' => $this->maxLength, + //'max_length' => $this->maxLength, ]; $this->sortList(); diff --git a/app/Livewire/ProjectNameCoder.php b/app/Livewire/ProjectNameCoder.php index 5c43255..4f8610a 100644 --- a/app/Livewire/ProjectNameCoder.php +++ b/app/Livewire/ProjectNameCoder.php @@ -3,11 +3,14 @@ namespace App\Livewire; use Livewire\Component; +use App\Models\Project; +use App\Models\ProjectCodingConfig; class ProjectNameCoder extends Component { public $components = []; public $nextId = 1; + public $project; protected $listeners = [ 'nameUpdated' => 'headerLabelUpdate', @@ -15,10 +18,40 @@ class ProjectNameCoder extends Component 'removeComponent' => 'removeComponent' ]; - public function mount() + public function mount(Project $project) { // Inicializar con un componente vacío - $this->addComponent(); + $this->project = $project; + + // Si hay configuración inicial, cargarla + if ($project->codingConfig) { + $this->loadDatabaseConfiguration(); + } else { + // Inicializar con un componente vacío + $this->addComponent(); + } + } + + private function loadDatabaseConfiguration() + { + // Buscar la configuración de codificación del proyecto + $config = $this->project->codingConfig; + + if ($config && isset($config->elements['components'])) { + $this->components = $config->elements['components']; + $this->nextId = count($this->components) + 1; + + // Asegurar que cada componente tenga los campos necesarios + foreach ($this->components as &$component) { + $component['data'] = $component['data'] ?? []; + $component['order'] = $component['order'] ?? 0; + $component['headerLabel'] = $component['headerLabel'] ?? ''; + $component['documentTypes'] = $component['documentTypes'] ?? []; + } + } else { + // Si no hay configuración, inicializar con un componente vacío + $this->addComponent(); + } } public function addComponent() @@ -72,7 +105,6 @@ class ProjectNameCoder extends Component } } } - // Ordenar el array por el campo 'order' usort($this->components, function($a, $b) { return $a['order'] - $b['order']; @@ -142,6 +174,101 @@ class ProjectNameCoder extends Component return $total; } + public function saveConfiguration() + { + try { + // Preparar la configuración completa + $configData = [ + 'components' => $this->components, + //'total_components' => $this->componentsCount, + //'total_document_types' => $this->totalDocumentTypes, + //'generated_format' => $this->generateFormatString(), + //'last_updated' => now()->toDateTimeString(), + ]; + + // Buscar o crear la configuración de codificación + $codingConfig = ProjectCodingConfig::firstOrNew(['project_id' => $this->project->id]); + + // Actualizar los campos + $codingConfig->fill([ + 'elements' => $configData, + 'format' => $this->generateFormatString(), + 'auto_generate' => true, + ]); + + $codingConfig->save(); + + // Emitir evento de éxito + $this->dispatch('configurationSaved', [ + 'message' => 'Configuración guardada exitosamente', + 'format' => $this->generateFormatString() + ]); + + } catch (\Exception $e) { + $this->dispatch('configurationError', [ + 'message' => 'Error al guardar la configuración: ' . $e->getMessage() + ]); + } + } + + private function autoSave() + { + // Auto-guardar cada 30 segundos de inactividad o cuando haya cambios importantes + // Esto es opcional pero mejora la experiencia de usuario + $this->dispatch('configurationAutoSaved'); + } + + private function generateFormatString() + { + $formatParts = []; + + // Agregar el código del proyecto + $formatParts[] = $this->project->code ?? $this->project->reference ?? 'PROJ'; + + // Agregar cada componente + foreach ($this->components as $component) { + if (!empty($component['headerLabel'])) { + $formatParts[] = '[' . strtoupper($component['headerLabel']) . ']'; + } + } + + // Agregar el nombre del documento + $formatParts[] = '[DOCUMENT_NAME]'; + + return implode('-', $formatParts); + } + + public function getExampleCodeAttribute() + { + $exampleParts = []; + + // Agregar el código del proyecto + $exampleParts[] = $this->project->code ?? $this->project->reference ?? 'PROJ'; + + // Agregar cada componente con un valor de ejemplo + foreach ($this->components as $component) { + if (!empty($component['headerLabel'])) { + $exampleParts[] = $this->getExampleForComponent($component); + } + } + + // Agregar nombre de documento de ejemplo + $exampleParts[] = 'Documento-Ejemplo'; + + return implode('-', $exampleParts); + } + + private function getExampleForComponent($component) + { + if (isset($component['data']['documentTypes']) && count($component['data']['documentTypes']) > 0) { + // Tomar el primer tipo de documento como ejemplo + $firstType = $component['data']['documentTypes'][0]; + return $firstType['code'] ?? $firstType['name'] ?? 'TIPO'; + } + + return 'VALOR'; + } + public function render() { return view('livewire.project-name-coder'); diff --git a/app/Models/Project.php b/app/Models/Project.php index b3b3258..84826f5 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -87,4 +87,109 @@ class Project extends Model { return $this->belongsTo(Company::class); } + + // Agregar estas relaciones al modelo Project + public function codingConfig() + { + return $this->hasOne(ProjectCodingConfig::class); + } + + public function documentStatuses() + { + //return $this->hasMany(ProjectDocumentStatus::class); + } + + public function defaultStatus() + { + //return $this->hasOne(ProjectDocumentStatus::class)->where('is_default', true); + } + + // Método para inicializar la configuración + public function initializeSettings() + { + // Crear configuración de codificación si no existe + if (!$this->codingConfig) { + $this->codingConfig()->create([ + 'format' => '[PROJECT]-[TYPE]-[YEAR]-[SEQUENCE]', + 'next_sequence' => 1, + 'year_format' => 'Y', + 'separator' => '-', + 'sequence_length' => 4, + 'auto_generate' => true, + ]); + } + + // Crear estados predeterminados si no existen + /* + if ($this->documentStatuses()->count() === 0) { + $defaultStatuses = [ + [ + 'name' => 'Borrador', + 'slug' => 'draft', + 'color' => '#6b7280', // Gris + 'order' => 1, + 'is_default' => true, + 'allow_upload' => true, + 'allow_edit' => true, + 'allow_delete' => true, + 'requires_approval' => false, + 'description' => 'Documento en proceso de creación', + ], + [ + 'name' => 'En Revisión', + 'slug' => 'in_review', + 'color' => '#f59e0b', // Ámbar + 'order' => 2, + 'is_default' => false, + 'allow_upload' => false, + 'allow_edit' => false, + 'allow_delete' => false, + 'requires_approval' => true, + 'description' => 'Documento en proceso de revisión', + ], + [ + 'name' => 'Aprobado', + 'slug' => 'approved', + 'color' => '#10b981', // Verde + 'order' => 3, + 'is_default' => false, + 'allow_upload' => false, + 'allow_edit' => false, + 'allow_delete' => false, + 'requires_approval' => false, + 'description' => 'Documento aprobado', + ], + [ + 'name' => 'Rechazado', + 'slug' => 'rejected', + 'color' => '#ef4444', // Rojo + 'order' => 4, + 'is_default' => false, + 'allow_upload' => true, + 'allow_edit' => true, + 'allow_delete' => false, + 'requires_approval' => false, + 'description' => 'Documento rechazado', + ], + [ + 'name' => 'Archivado', + 'slug' => 'archived', + 'color' => '#8b5cf6', // Violeta + 'order' => 5, + 'is_default' => false, + 'allow_upload' => false, + 'allow_edit' => false, + 'allow_delete' => false, + 'requires_approval' => false, + 'description' => 'Documento archivado', + ], + ]; + + foreach ($defaultStatuses as $status) { + //$this->documentStatuses()->create($status); + } + }*/ + + return $this; + } } diff --git a/app/Models/ProjectCodingConfig.php b/app/Models/ProjectCodingConfig.php new file mode 100644 index 0000000..9490d55 --- /dev/null +++ b/app/Models/ProjectCodingConfig.php @@ -0,0 +1,107 @@ + 'array', + 'auto_generate' => 'boolean', + ]; + + public function project() + { + return $this->belongsTo(Project::class); + } + + // Método para generar un código de documento + public function generateCode($type = 'DOC', $year = null) + { + $code = $this->format; + + // Reemplazar variables + $replacements = [ + '[PROJECT]' => $this->project->code ?? 'PROJ', + '[TYPE]' => $type, + '[YEAR]' => $year ?? date($this->year_format), + '[MONTH]' => date('m'), + '[DAY]' => date('d'), + '[SEQUENCE]' => str_pad($this->next_sequence, $this->sequence_length, '0', STR_PAD_LEFT), + '[RANDOM]' => strtoupper(substr(md5(uniqid()), 0, 6)), + ]; + + // Si hay elementos personalizados, agregarlos + if (is_array($this->elements)) { + foreach ($this->elements as $key => $value) { + $replacements["[{$key}]"] = $value; + } + } + + $code = str_replace(array_keys($replacements), array_values($replacements), $code); + + // Incrementar secuencia + if (strpos($this->format, '[SEQUENCE]') !== false && $this->auto_generate) { + $this->increment('next_sequence'); + } + + return $code; + } + + // Método para obtener elementos disponibles + public static function getAvailableElements() + { + return [ + 'project' => [ + 'label' => 'Código del Proyecto', + 'description' => 'Código único del proyecto', + 'variable' => '[PROJECT]', + ], + 'type' => [ + 'label' => 'Tipo de Documento', + 'description' => 'Tipo de documento (DOC, IMG, PDF, etc.)', + 'variable' => '[TYPE]', + ], + 'year' => [ + 'label' => 'Año', + 'description' => 'Año actual en formato configurable', + 'variable' => '[YEAR]', + ], + 'month' => [ + 'label' => 'Mes', + 'description' => 'Mes actual (01-12)', + 'variable' => '[MONTH]', + ], + 'day' => [ + 'label' => 'Día', + 'description' => 'Día actual (01-31)', + 'variable' => '[DAY]', + ], + 'sequence' => [ + 'label' => 'Secuencia', + 'description' => 'Número secuencial autoincremental', + 'variable' => '[SEQUENCE]', + ], + 'random' => [ + 'label' => 'Aleatorio', + 'description' => 'Cadena aleatoria de 6 caracteres', + 'variable' => '[RANDOM]', + ], + ]; + } +} \ No newline at end of file diff --git a/database/migrations/2025_12_07_141030_create_project_coding_configs_table.php b/database/migrations/2025_12_07_141030_create_project_coding_configs_table.php new file mode 100644 index 0000000..5aef554 --- /dev/null +++ b/database/migrations/2025_12_07_141030_create_project_coding_configs_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('project_id')->constrained()->onDelete('cascade')->unique(); + $table->string('format')->default('[PROJECT]-[TYPE]-[YEAR]-[SEQUENCE]'); + $table->json('elements')->nullable(); // Elementos configurados del código + $table->integer('next_sequence')->default(1); + $table->string('year_format')->default('Y'); // Y, y, YY, yyyy + $table->string('separator')->default('-'); + $table->integer('sequence_length')->default(4); + $table->boolean('auto_generate')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('project_coding_configs'); + } +}; diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index d6be9f7..5bed522 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -134,6 +134,45 @@ {{ __('Create User') }} + + + + + + {{ __('List companies') }} + + + + {{ __('Create new company') }} + + + + + + + {{ __('List Projects') }} + + + + {{ __('Create Project') }} + + @endpush diff --git a/resources/views/livewire/project-name-coder.blade.php b/resources/views/livewire/project-name-coder.blade.php index 296726b..197c4a8 100644 --- a/resources/views/livewire/project-name-coder.blade.php +++ b/resources/views/livewire/project-name-coder.blade.php @@ -1,20 +1,42 @@ -
+
-
-

- Codificación de los documentos del proyecto -

+
+
+

+ Codificación de los documentos del proyecto +

+
- +
+
+ + + +
+
@@ -22,9 +44,9 @@
Código:
- SOGOS0001- + {{ $project->reference}}- @foreach($components as $index => $component) - + {{ $component['headerLabel'] }} @if(isset($component['data']['documentTypes']) && count($component['data']['documentTypes']) > 0) @@ -36,6 +58,7 @@ - @endif @endforeach + - Document Name
@@ -157,6 +180,8 @@ :key="'document-manager-' . $component['id']" :component-id="$component['id']" :initial-name="$component['headerLabel']" + :initial-max-length="$component['data']['maxLength'] ?? 3" + :initial-document-types="$component['data']['documentTypes'] ?? []" />
diff --git a/resources/views/livewire/project/show.blade.php b/resources/views/livewire/project/show.blade.php index 12c80af..1e4275e 100644 --- a/resources/views/livewire/project/show.blade.php +++ b/resources/views/livewire/project/show.blade.php @@ -101,7 +101,6 @@
-
@@ -141,6 +140,12 @@ Editar + @can('update', $project) + + Configurar + + @endcan + {{-- Formulario de Edición --}}
@csrf @@ -150,7 +155,7 @@ - {{ $project->is_active ? 'Desactivar' : 'Activar' }} + {{ $project->status == "Activo" ? 'Desactivar' : 'Activar' }}
diff --git a/resources/views/project-settings/index.blade.php b/resources/views/project-settings/index.blade.php new file mode 100644 index 0000000..2460ec0 --- /dev/null +++ b/resources/views/project-settings/index.blade.php @@ -0,0 +1,377 @@ + + + + +
+
+

Configuración del Proyecto

+

{{ $project->reference }} - {{ $project->name }}

+
+ + + Volver al Proyecto + +
+ + @if(session('success')) +
+ {{ session('success') }} +
+ @endif + + @if(session('error')) +
+ {{ session('error') }} +
+ @endif + + +
+
+ +
+ +
+ +
+
+
+

Codificación de Documentos

+
+ Última actualización: {{ $project->codingConfig->updated_at->format('d/m/Y H:i') ?? 'No configurado' }} +
+
+ +
+
+ + +
+
+
+

Estados de Documentos

+ +
+ +
+ +
+ +
+
+
+
+
+
+ + + + + + + + @push('scripts') + + @endpush +
\ No newline at end of file diff --git a/resources/views/project-settings/partials/status-item.blade.php b/resources/views/project-settings/partials/status-item.blade.php new file mode 100644 index 0000000..52f8184 --- /dev/null +++ b/resources/views/project-settings/partials/status-item.blade.php @@ -0,0 +1,68 @@ +
+ +
+ + + +
+ + +
+
+ {{ substr($status->name, 0, 2) }} +
+
+ + +
+
+ {{ $status->name }} + @if($status->is_default) + + Por defecto + + @endif +
+ @if($status->description) +

{{ $status->description }}

+ @endif + + +
+ @if($status->allow_upload) + ✓ Subir + @endif + @if($status->allow_edit) + ✓ Editar + @endif + @if($status->allow_delete) + ✓ Eliminar + @endif + @if($status->requires_approval) + ⚠ Aprobación + @endif +
+
+ + +
+ + + @if(!$status->is_default && $status->documents_count == 0) + + @endif +
+
\ No newline at end of file diff --git a/resources/views/projects/create.blade.php b/resources/views/projects/create.blade.php index ce9c432..0fec4bb 100644 --- a/resources/views/projects/create.blade.php +++ b/resources/views/projects/create.blade.php @@ -189,7 +189,6 @@ - @livewire('project-name-coder')
diff --git a/routes/web.php b/routes/web.php index 4c9bc80..052f289 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,6 +9,7 @@ use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProjectController; use App\Http\Controllers\RoleController; use App\Http\Controllers\UserController; +use App\Http\Controllers\ProjectSettingsController; use App\Livewire\ProjectShow; use Illuminate\Support\Facades\Route; use Livewire\Volt\Volt; @@ -56,6 +57,20 @@ Route::middleware(['auth', 'verified'])->group(function () { //Route::get('/projects/{project}', ProjectController::class)->name('projects.show'); //Route::get('/projects/{project}', ProjectController::class)->name('projects.show')->middleware('can:view,project'); // Opcional: política de acceso Route::get('/projects/{project}', ProjectShow::class)->name('projects.show'); + + // Configuración de proyectos + Route::prefix('projects/{project}/settings')->name('project-settings.')->group(function () { + Route::get('/', [ProjectSettingsController::class, 'index'])->name('index'); + + // Codificación + Route::put('/coding', [ProjectSettingsController::class, 'updateCoding'])->name('coding.update'); + + // Estados + Route::post('/statuses', [ProjectSettingsController::class, 'storeStatus'])->name('statuses.store'); + Route::put('/statuses/{status}', [ProjectSettingsController::class, 'updateStatus'])->name('statuses.update'); + Route::delete('/statuses/{status}', [ProjectSettingsController::class, 'destroyStatus'])->name('statuses.destroy'); + Route::post('/statuses/reorder', [ProjectSettingsController::class, 'reorderStatuses'])->name('statuses.reorder'); + }); // Documentos