new functionality: Add project coding configuration feature for projects
This commit is contained in:
223
app/Http/Controllers/ProjectSettingsController.php
Normal file
223
app/Http/Controllers/ProjectSettingsController.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectCodingConfig;
|
||||
use App\Models\ProjectDocumentStatus;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ProjectSettingsController extends Controller
|
||||
{
|
||||
public function index(Project $project)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
107
app/Models/ProjectCodingConfig.php
Normal file
107
app/Models/ProjectCodingConfig.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectCodingConfig extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'format',
|
||||
'elements',
|
||||
'next_sequence',
|
||||
'year_format',
|
||||
'separator',
|
||||
'sequence_length',
|
||||
'auto_generate'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'elements' => '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]',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user