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]',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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('project_coding_configs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -134,6 +134,45 @@
|
||||
{{ __('Create User') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:separator />
|
||||
<!-- Sección de Empresas -->
|
||||
<flux:navlist.group :heading="__('Empresas')" expandable>
|
||||
<flux:navlist.item
|
||||
icon="building-office-2"
|
||||
:href="route('companies.index')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('List companies') }}
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item
|
||||
icon="building-office"
|
||||
:href="route('companies.create')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Create new company') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:separator />
|
||||
<flux:navlist.group :heading="__('Projects')" expandable>
|
||||
<flux:navlist.item
|
||||
icon="folder"
|
||||
:href="route('projects.index')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('List Projects') }}
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item
|
||||
icon="plus"
|
||||
:href="route('projects.create')"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Create Project') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
|
||||
@endpush
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
<div class="max-w-full mx-auto p-6">
|
||||
<div class="max-w-full">
|
||||
<!-- Header con contador y botón de agregar -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-800">
|
||||
Codificación de los documentos del proyecto
|
||||
</h2>
|
||||
<div class="flex content-start justify-between mb-6">
|
||||
<div class="flex space-x-6 content-start">
|
||||
<h2 class="text-2xl font-bold text-gray-800">
|
||||
Codificación de los documentos del proyecto
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="addComponent"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>Agregar Componente</span>
|
||||
</button>
|
||||
<div class="flex flex-col items-end space-y-4">
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="addComponent"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>Agregar Componente</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="saveConfiguration"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<svg class="w-4 h-4" wire:loading.remove wire:target="saveConfiguration" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
<svg class="w-4 h-4 animate-spin" wire:loading wire:target="saveConfiguration" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<span wire:loading.remove wire:target="saveConfiguration">Guardar</span>
|
||||
<span wire:loading wire:target="saveConfiguration">Guardando...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Label con nombres de componentes -->
|
||||
@@ -22,9 +44,9 @@
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium text-blue-800">Código:</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span ><flux:badge color="green">SOGOS0001</flux:badge>-</span>
|
||||
<span ><flux:badge color="green">{{ $project->reference}}</flux:badge>-</span>
|
||||
@foreach($components as $index => $component)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white text-blue-700 border border-blue-200">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-md text-xs font-medium bg-white text-blue-700 border border-blue-200">
|
||||
{{ $component['headerLabel'] }}
|
||||
@if(isset($component['data']['documentTypes']) && count($component['data']['documentTypes']) > 0)
|
||||
<span class="ml-1 bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded-full">
|
||||
@@ -36,6 +58,7 @@
|
||||
<span class="text-blue-400">-</span>
|
||||
@endif
|
||||
@endforeach
|
||||
<span>- <flux:badge color="green">Document Name</flux:badge></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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'] ?? []"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
<div class="p-6">
|
||||
<!-- Info Tab -->
|
||||
<div x-show="activeTab === 'info'">
|
||||
|
||||
<div class="flex flex-wrap gap-6">
|
||||
<!-- Columna Izquierda - Información -->
|
||||
<div class="w-full md:w-[calc(50%-12px)]">
|
||||
@@ -141,6 +140,12 @@
|
||||
Editar
|
||||
</a>
|
||||
|
||||
@can('update', $project)
|
||||
<a href="{{ route('project-settings.index', $project) }}" class="btn btn-primary">
|
||||
<x-icons icon="cog-6-tooth" class="w-4 h-4 mr-1" /> Configurar
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
{{-- Formulario de Edición --}}
|
||||
<form method="POST" action="{{ route('projects.update', $project) }}">
|
||||
@csrf
|
||||
@@ -150,7 +155,7 @@
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
||||
</svg>
|
||||
{{ $project->is_active ? 'Desactivar' : 'Activar' }}
|
||||
{{ $project->status == "Activo" ? 'Desactivar' : 'Activar' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
377
resources/views/project-settings/index.blade.php
Normal file
377
resources/views/project-settings/index.blade.php
Normal file
@@ -0,0 +1,377 @@
|
||||
|
||||
<x-layouts.app title="{{ 'Configuración del Proyecto - ' . $project->name }}"
|
||||
:showSidebar={{ $showSidebar }}>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900">Configuración del Proyecto</h2>
|
||||
<p class="text-gray-600">{{ $project->reference }} - {{ $project->name }}</p>
|
||||
</div>
|
||||
|
||||
<flux:button
|
||||
href="{{ route('projects.show', $project) }}"
|
||||
icon:trailing="arrow-uturn-left"
|
||||
variant="ghost"
|
||||
>
|
||||
Volver al Proyecto
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-100 text-green-700 rounded-lg">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-100 text-red-700 rounded-lg">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Tabs de navegación -->
|
||||
<div x-data="{ activeTab: 'coding' }" class="bg-white rounded-lg shadow-md border-1">
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<button @click="activeTab = 'coding'"
|
||||
:class="activeTab === 'coding' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
||||
class="py-4 px-1 border-b-2 font-medium">
|
||||
Codificación de Documentos
|
||||
</button>
|
||||
|
||||
<button @click="activeTab = 'statuses'"
|
||||
:class="activeTab === 'statuses' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'"
|
||||
class="py-4 px-1 border-b-2 font-medium">
|
||||
Estados de Documentos
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Sección de Codificación -->
|
||||
<div x-show="activeTab === 'coding'">
|
||||
<div id="coding" class="mb-12">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">Codificación de Documentos</h3>
|
||||
<div class="text-sm text-gray-500">
|
||||
Última actualización: {{ $project->codingConfig->updated_at->format('d/m/Y H:i') ?? 'No configurado' }}
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project-name-coder
|
||||
:project="$project"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección de Estados -->
|
||||
<div x-show="activeTab === 'statuses'">
|
||||
<div id="statuses">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">Estados de Documentos</h3>
|
||||
<button type="button"
|
||||
onclick="openStatusModal()"
|
||||
class="btn btn-primary">
|
||||
<x-icons icon="plus" class="w-4 h-4 mr-1" /> Nuevo Estado
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<!-- Lista de estados existentes -->
|
||||
<div id="statuses-list" class="divide-y divide-gray-200">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal para crear/editar estado -->
|
||||
<div id="status-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" style="display: none;">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
|
||||
<div class="border-b px-6 py-4">
|
||||
<h3 class="text-lg font-medium text-gray-900" id="modal-title">Nuevo Estado</h3>
|
||||
</div>
|
||||
|
||||
<form id="status-form" method="POST" class="p-6">
|
||||
@csrf
|
||||
<div id="method-field"></div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Nombre -->
|
||||
<div>
|
||||
<label for="modal-name" class="block text-sm font-medium text-gray-700">
|
||||
Nombre del Estado *
|
||||
</label>
|
||||
<input type="text"
|
||||
id="modal-name"
|
||||
name="name"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="modal-color" class="block text-sm font-medium text-gray-700">
|
||||
Color de fondo
|
||||
</label>
|
||||
<div class="mt-1 flex items-center">
|
||||
<input type="color"
|
||||
id="modal-color"
|
||||
name="color"
|
||||
value="#6b7280"
|
||||
class="h-10 w-16 rounded border">
|
||||
<input type="text"
|
||||
id="modal-color-text"
|
||||
name="color_text"
|
||||
value="#6b7280"
|
||||
class="ml-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="modal-text-color" class="block text-sm font-medium text-gray-700">
|
||||
Color del texto
|
||||
</label>
|
||||
<div class="mt-1 flex items-center">
|
||||
<input type="color"
|
||||
id="modal-text-color"
|
||||
name="text_color"
|
||||
value="#ffffff"
|
||||
class="h-10 w-16 rounded border">
|
||||
<input type="text"
|
||||
id="modal-text-color-text"
|
||||
name="text_color_text"
|
||||
value="#ffffff"
|
||||
class="ml-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Descripción -->
|
||||
<div>
|
||||
<label for="modal-description" class="block text-sm font-medium text-gray-700">
|
||||
Descripción
|
||||
</label>
|
||||
<textarea id="modal-description"
|
||||
name="description"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Permisos -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="allow_upload"
|
||||
value="1"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Permitir subida</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="allow_edit"
|
||||
value="1"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Permitir edición</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="allow_delete"
|
||||
value="1"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Permitir eliminación</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="requires_approval"
|
||||
value="1"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Requiere aprobación</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Estado por defecto -->
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
name="is_default"
|
||||
value="1"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<span class="ml-2 text-sm text-gray-700">Estado por defecto para nuevos documentos</span>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Solo puede haber un estado por defecto
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button type="button" onclick="closeStatusModal()" class="btn btn-secondary">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Guardar Estado
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de confirmación para eliminar -->
|
||||
<div id="delete-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" style="display: none;">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
|
||||
<div class="border-b px-6 py-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">Confirmar eliminación</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<p class="text-gray-700" id="delete-message">¿Estás seguro de que deseas eliminar este estado?</p>
|
||||
|
||||
<form id="delete-form" method="POST" class="mt-6">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="closeDeleteModal()" class="btn btn-secondary">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
Eliminar Estado
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Variables globales
|
||||
let currentStatusId = null;
|
||||
const statusModal = document.getElementById('status-modal');
|
||||
const deleteModal = document.getElementById('delete-modal');
|
||||
|
||||
// Funciones para el modal de estados
|
||||
function openStatusModal(status = null) {
|
||||
const form = document.getElementById('status-form');
|
||||
const title = document.getElementById('modal-title');
|
||||
const methodField = document.getElementById('method-field');
|
||||
|
||||
if (status) {
|
||||
// Modo edición
|
||||
title.textContent = 'Editar Estado';
|
||||
form.action = `/projects/{{ $project->id }}/settings/statuses/${status.id}`;
|
||||
methodField.innerHTML = '<input type="hidden" name="_method" value="PUT">';
|
||||
|
||||
// Llenar los campos con los datos del estado
|
||||
document.getElementById('modal-name').value = status.name;
|
||||
document.getElementById('modal-color').value = status.color;
|
||||
document.getElementById('modal-color-text').value = status.color;
|
||||
document.getElementById('modal-text-color').value = status.text_color;
|
||||
document.getElementById('modal-text-color-text').value = status.text_color;
|
||||
document.getElementById('modal-description').value = status.description || '';
|
||||
|
||||
// Checkboxes
|
||||
document.querySelector('input[name="allow_upload"]').checked = status.allow_upload;
|
||||
document.querySelector('input[name="allow_edit"]').checked = status.allow_edit;
|
||||
document.querySelector('input[name="allow_delete"]').checked = status.allow_delete;
|
||||
document.querySelector('input[name="requires_approval"]').checked = status.requires_approval;
|
||||
document.querySelector('input[name="is_default"]').checked = status.is_default;
|
||||
|
||||
currentStatusId = status.id;
|
||||
} else {
|
||||
// Modo creación
|
||||
title.textContent = 'Nuevo Estado';
|
||||
form.action = `/projects/{{ $project->id }}/settings/statuses`;
|
||||
methodField.innerHTML = '';
|
||||
|
||||
// Resetear campos
|
||||
form.reset();
|
||||
document.getElementById('modal-color').value = '#6b7280';
|
||||
document.getElementById('modal-color-text').value = '#6b7280';
|
||||
document.getElementById('modal-text-color').value = '#ffffff';
|
||||
document.getElementById('modal-text-color-text').value = '#ffffff';
|
||||
|
||||
currentStatusId = null;
|
||||
}
|
||||
|
||||
statusModal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeStatusModal() {
|
||||
statusModal.style.display = 'none';
|
||||
}
|
||||
|
||||
// Funciones para el modal de eliminación
|
||||
function openDeleteModal(status) {
|
||||
const message = document.getElementById('delete-message');
|
||||
const form = document.getElementById('delete-form');
|
||||
|
||||
message.textContent = `¿Estás seguro de que deseas eliminar el estado "${status.name}"?`;
|
||||
form.action = `/projects/{{ $project->id }}/settings/statuses/${status.id}`;
|
||||
|
||||
currentStatusId = status.id;
|
||||
deleteModal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
deleteModal.style.display = 'none';
|
||||
}
|
||||
|
||||
// Sincronizar inputs de color
|
||||
document.getElementById('modal-color').addEventListener('input', function(e) {
|
||||
document.getElementById('modal-color-text').value = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('modal-color-text').addEventListener('input', function(e) {
|
||||
document.getElementById('modal-color').value = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('modal-text-color').addEventListener('input', function(e) {
|
||||
document.getElementById('modal-text-color-text').value = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('modal-text-color-text').addEventListener('input', function(e) {
|
||||
document.getElementById('modal-text-color').value = e.target.value;
|
||||
});
|
||||
|
||||
// Cerrar modales al hacer clic fuera
|
||||
window.addEventListener('click', function(e) {
|
||||
if (e.target === statusModal) {
|
||||
closeStatusModal();
|
||||
}
|
||||
if (e.target === deleteModal) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Funcionalidad de drag & drop para reordenar estados
|
||||
new Sortable(document.getElementById('statuses-list'), {
|
||||
animation: 150,
|
||||
handle: '.drag-handle',
|
||||
onEnd: function() {
|
||||
// Obtener el nuevo orden
|
||||
const order = Array.from(document.querySelectorAll('.status-item')).map(item => {
|
||||
return item.dataset.statusId;
|
||||
});
|
||||
|
||||
// Enviar el nuevo orden al servidor
|
||||
fetch(`/projects/{{ $project->id }}/settings/statuses/reorder`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({ order: order })
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
</x-layouts.app>
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="status-item p-4 flex items-center hover:bg-gray-50 transition-colors"
|
||||
data-status-id="{{ $status->id }}">
|
||||
<!-- Handle para drag & drop -->
|
||||
<div class="drag-handle cursor-move mr-3 text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Badge de color -->
|
||||
<div class="w-8 h-8 rounded-full mr-3 border"
|
||||
style="background-color: {{ $status->color }}; color: {{ $status->text_color }}"
|
||||
title="{{ $status->name }}">
|
||||
<div class="flex items-center justify-center h-full text-xs font-bold">
|
||||
{{ substr($status->name, 0, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información del estado -->
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center">
|
||||
<span class="font-medium text-gray-900">{{ $status->name }}</span>
|
||||
@if($status->is_default)
|
||||
<span class="ml-2 px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Por defecto
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($status->description)
|
||||
<p class="text-sm text-gray-600 mt-1">{{ $status->description }}</p>
|
||||
@endif
|
||||
|
||||
<!-- Permisos -->
|
||||
<div class="flex items-center space-x-3 mt-2">
|
||||
@if($status->allow_upload)
|
||||
<span class="text-xs text-green-600">✓ Subir</span>
|
||||
@endif
|
||||
@if($status->allow_edit)
|
||||
<span class="text-xs text-blue-600">✓ Editar</span>
|
||||
@endif
|
||||
@if($status->allow_delete)
|
||||
<span class="text-xs text-red-600">✓ Eliminar</span>
|
||||
@endif
|
||||
@if($status->requires_approval)
|
||||
<span class="text-xs text-yellow-600">⚠ Aprobación</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="button"
|
||||
onclick="openStatusModal({{ json_encode($status) }})"
|
||||
class="text-yellow-600 hover:text-yellow-900"
|
||||
title="Editar">
|
||||
<x-icons.pencil class="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
@if(!$status->is_default && $status->documents_count == 0)
|
||||
<button type="button"
|
||||
onclick="openDeleteModal({{ json_encode($status) }})"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
title="Eliminar">
|
||||
<x-icons.trash class="w-5 h-5" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +189,6 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@livewire('project-name-coder')
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user