updates to document handling and code editing features

This commit is contained in:
2025-12-03 23:27:08 +01:00
parent 88e526cf6c
commit 7b00887372
29 changed files with 20851 additions and 1114 deletions

View File

@@ -0,0 +1,134 @@
<?php
class DocumentIdentifier {
// Diccionarios para validar disciplinas y tipos
private $disciplinasValidas = [
'ENG' => 'Ingeniería',
'ARC' => 'Arquitectura',
'CIV' => 'Civil',
'MEC' => 'Mecánica',
'ELC' => 'Eléctrica',
'INS' => 'Instrumentación',
'PIP' => 'Piping',
'STR' => 'Estructural'
];
private $tiposDocumentoValidos = [
'DRW' => 'Dibujo',
'ESP' => 'Especificación',
'LST' => 'Lista de materiales',
'PRO' => 'Procedimiento',
'INF' => 'Informe',
'MAN' => 'Manual',
'CAL' => 'Cálculo',
'REP' => 'Reporte'
];
public function analizarDocumento($codigoCompleto) {
// Validar formato básico
if (strpos($codigoCompleto, ' - ') === false) {
return $this->crearResultadoError("Formato inválido: falta separador ' - '");
}
list($codigo, $nombre) = explode(' - ', $codigoCompleto, 2);
$segmentos = explode('-', $codigo);
// Validar número de segmentos
if (count($segmentos) != 5) {
return $this->crearResultadoError("Número incorrecto de segmentos");
}
// Extraer y validar cada parte
$codigoProyecto = $segmentos[0];
$codigoMaker = $segmentos[1];
$disciplina = $segmentos[2];
$tipoDocumento = $segmentos[3];
$revisionCompleta = $segmentos[4];
// Validar formato de revisión
if (strpos($revisionCompleta, 'REV.') !== 0) {
return $this->crearResultadoError("Formato de revisión inválido");
}
$numeroRevision = substr($revisionCompleta, 4); // Remover "REV."
// Validar número de revisión
if (!ctype_digit($numeroRevision) || strlen($numeroRevision) != 2) {
return $this->crearResultadoError("Número de revisión inválido");
}
// Validar disciplinas y tipos
$disciplinaValida = $this->validarDisciplina($disciplina);
$tipoValido = $this->validarTipoDocumento($tipoDocumento);
return [
'codigo_completo' => $codigoCompleto,
'codigo_proyecto' => $codigoProyecto,
'codigo_maker' => $codigoMaker,
'disciplina' => $disciplina,
'disciplina_desc' => $disciplinaValida,
'tipo_documento' => $tipoDocumento,
'tipo_documento_desc' => $tipoValido,
'revision' => $numeroRevision,
'nombre_documento' => $nombre,
'estructura_valida' => true,
'errores' => []
];
}
private function validarDisciplina($codigo) {
return isset($this->disciplinasValidas[$codigo])
? $this->disciplinasValidas[$codigo]
: "Desconocida";
}
private function validarTipoDocumento($codigo) {
return isset($this->tiposDocumentoValidos[$codigo])
? $this->tiposDocumentoValidos[$codigo]
: "Desconocido";
}
private function crearResultadoError($mensaje) {
return [
'estructura_valida' => false,
'errores' => [$mensaje]
];
}
// Método para generar un nuevo código
public function generarCodigo($proyecto, $maker, $disciplina, $tipo, $revision, $nombre = '') {
$revisionFormateada = str_pad($revision, 2, '0', STR_PAD_LEFT);
$codigo = "{$proyecto}-{$maker}-{$disciplina}-{$tipo}-REV.{$revisionFormateada}";
if (!empty($nombre)) {
$codigo .= " - {$nombre}";
}
return $codigo;
}
}
// EJEMPLOS DE USO
/*
$analizador = new DocumentIdentifier();
// Analizar un código existente
$codigo1 = "MP00002-SOGOS-ENG-DRW-REV.01 - Plano principal";
$resultado1 = $analizador->analizarDocumento($codigo1);
echo "Análisis del documento:\n";
print_r($resultado1);
// Generar un nuevo código
$nuevoCodigo = $analizador->generarCodigo(
'MP00002',
'SOGOS',
'CIV',
'ESP',
'03',
'Especificación técnica de cimientos'
);
echo "\nNuevo código generado: " . $nuevoCodigo . "\n";*/
?>

View File

@@ -20,4 +20,50 @@ class FileHelper
default => 'document'
};
}
public static function getFileIconClass($filename)
{
$fileType = self::getFileType($filename);
$classes = [
'pdf' => 'text-red-500',
'word' => 'text-blue-500',
'excel' => 'text-green-500',
'image' => 'text-yellow-500',
'archive' => 'text-purple-500',
'text' => 'text-gray-500',
'powerpoint' => 'text-orange-500',
'document' => 'text-gray-400'
];
return $classes[$fileType] ?? 'text-gray-400';
}
public static function getFileIconSvg($filename)
{
$fileType = self::getFileType($filename);
$colorClass = self::getFileIconClass($filename);
$icons = [
'pdf' => '<svg class="w-5 h-5 '.$colorClass.'" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
</svg>',
'word' => '<svg class="w-5 h-5 '.$colorClass.'" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
</svg>',
'excel' => '<svg class="w-5 h-5 '.$colorClass.'" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
</svg>',
'image' => '<svg class="w-5 h-5 '.$colorClass.'" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/>
</svg>',
'archive' => '<svg class="w-5 h-5 '.$colorClass.'" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/>
</svg>'
];
return $icons[$fileType] ?? '<svg class="w-5 h-5 '.$colorClass.'" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/>
</svg>';
}
}

View File

@@ -68,6 +68,8 @@ class DocumentController extends Controller
$document->url = Storage::url($document->file_path);
$document->load('user');
return view('documents.show', [
'document' => $document,
'versions' => $document->versions()->latest()->get(),
@@ -108,7 +110,7 @@ class DocumentController extends Controller
foreach ($request->file('files') as $file) {
$document = $project->documents()->create([
'name' => $file->getClientOriginalName(),
'status' => 'pending'
'status' => 0
]);
$this->createVersion($document, $file);
@@ -125,4 +127,331 @@ class DocumentController extends Controller
$document->update(['current_version_id' => $version->id]);
}
/**
* Actualizar PDF con anotaciones, firmas y sellos
*/
public function updatePdf(Request $request, Document $document)
{
$this->authorize('update', $document);
$request->validate([
'pdf_data' => 'required|string', // PDF modificado en base64
'annotations' => 'sometimes|array',
'signatures' => 'sometimes|array',
'stamps' => 'sometimes|array',
]);
try {
// Procesar el PDF modificado
$modifiedPdf = $this->processPdfData($request->pdf_data);
// Reemplazar el archivo original (sin crear nueva versión si no quieres)
$this->replaceOriginalPdf($document, $modifiedPdf);
// Opcional: también crear una nueva versión para historial
$newVersion = $this->createNewVersion($document, $modifiedPdf, $request->all());
return response()->json([
'success' => true,
'message' => 'PDF actualizado correctamente',
'version_id' => $newVersion->id ?? null,
]);
} catch (\Exception $e) {
\Log::error('Error updating PDF: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al actualizar el PDF: ' . $e->getMessage()
], 500);
}
}
/**
* Procesar datos del PDF en base64
*/
private function processPdfData($pdfData)
{
// Eliminar el prefijo data:application/pdf;base64, si existe
$pdfData = preg_replace('/^data:application\/pdf;base64,/', '', $pdfData);
// Decodificar base64
$pdfContent = base64_decode($pdfData);
if (!$pdfContent) {
throw new \Exception('Datos PDF inválidos');
}
return $pdfContent;
}
/**
* Procesar PDF con anotaciones (método alternativo)
*/
private function processPdfWithAnnotations($document, $data)
{
// Aquí integrarías una librería PHP para PDF como spatie/pdf-to-image o setasign/fpdi
// Por ahora, devolvemos el contenido del archivo original
// En producción, implementarías la lógica de modificación
if ($document->currentVersion) {
$filePath = $document->currentVersion->file_path;
} else {
$filePath = $document->getFirstMedia('documents')->getPath();
}
if (!Storage::exists($filePath)) {
throw new \Exception('Archivo PDF no encontrado');
}
return Storage::get($filePath);
}
/**
* Reemplazar el PDF original
*/
private function replaceOriginalPdf($document, $pdfContent)
{
// Obtener la ruta del archivo original
$filePath = $document->file_path;
// Si el documento usa media library
if ($document->getFirstMedia('documents')) {
$media = $document->getFirstMedia('documents');
$media->update([
'file_name' => $document->name . '.pdf',
'size' => strlen($pdfContent),
]);
// Reemplazar el archivo
Storage::put($media->getPath(), $pdfContent);
} else {
// Si usas file_path directo
Storage::put($filePath, $pdfContent);
// Actualizar metadata del documento
$document->update([
'file_size' => strlen($pdfContent),
'updated_at' => now(),
]);
}
}
/**
* Crear nueva versión del documento
*/
private function createNewVersion($document, $pdfContent, $data = [])
{
$versionNumber = $document->versions()->count() + 1;
$fileName = "documents/{$document->id}/v{$versionNumber}.pdf";
// Guardar el nuevo PDF
Storage::put($fileName, $pdfContent);
// Crear registro de versión
$version = $document->versions()->create([
'version_number' => $versionNumber,
'file_path' => $fileName,
'file_size' => strlen($pdfContent),
'hash' => hash('sha256', $pdfContent),
'created_by' => auth()->id(),
'metadata' => [
'annotations_count' => count($data['annotations'] ?? []),
'signatures_count' => count($data['signatures'] ?? []),
'stamps_count' => count($data['stamps'] ?? []),
'edited_at' => now()->toISOString(),
'edited_by' => auth()->user()->name
]
]);
return $version;
}
/**
* Guardar metadatos de anotaciones
*/
private function saveAnnotationsMetadata($version, $data)
{
// Guardar anotaciones en la base de datos si es necesario
if (!empty($data['annotations'])) {
foreach ($data['annotations'] as $annotation) {
$version->annotations()->create([
'type' => $annotation['type'] ?? 'text',
'content' => $annotation['content'] ?? '',
'position' => $annotation['position'] ?? [],
'page' => $annotation['page'] ?? 1,
'created_by' => auth()->id()
]);
}
}
}
/**
* Subir firma
*/
public function uploadSignature(Request $request)
{
$request->validate([
'signature' => 'required|image|max:2048|mimes:png,jpg,jpeg'
]);
try {
$user = auth()->user();
$path = $request->file('signature')->store("signatures/{$user->id}", 'public');
// Opcional: Guardar en base de datos
$user->signatures()->create([
'file_path' => $path,
'file_name' => $request->file('signature')->getClientOriginalName()
]);
return response()->json([
'success' => true,
'path' => Storage::url($path),
'filename' => basename($path)
]);
} catch (\Exception $e) {
\Log::error('Error uploading signature: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al subir la firma'
], 500);
}
}
/**
* Subir sello
*/
public function uploadStamp(Request $request)
{
$request->validate([
'stamp' => 'required|image|max:2048|mimes:png,jpg,jpeg'
]);
try {
$user = auth()->user();
$path = $request->file('stamp')->store("stamps/{$user->id}", 'public');
// Opcional: Guardar en base de datos
$user->stamps()->create([
'file_path' => $path,
'file_name' => $request->file('stamp')->getClientOriginalName(),
'type' => $request->type ?? 'custom'
]);
return response()->json([
'success' => true,
'path' => Storage::url($path),
'filename' => basename($path)
]);
} catch (\Exception $e) {
\Log::error('Error uploading stamp: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al subir el sello'
], 500);
}
}
/**
* Obtener firmas del usuario
*/
public function getSignatures()
{
$user = auth()->user();
$signatures = $user->signatures()->get()->map(function($signature) {
return [
'id' => $signature->id,
'url' => Storage::url($signature->file_path),
'name' => $signature->file_name
];
});
return response()->json([
'success' => true,
'signatures' => $signatures
]);
}
/**
* Obtener sellos del usuario
*/
public function getStamps()
{
$user = auth()->user();
$stamps = $user->stamps()->get()->map(function($stamp) {
return [
'id' => $stamp->id,
'url' => Storage::url($stamp->file_path),
'name' => $stamp->file_name,
'type' => $stamp->type
];
});
return response()->json([
'success' => true,
'stamps' => $stamps
]);
}
/**
* Descargar documento
*/
public function download(Document $document, $versionId = null)
{
$this->authorize('view', $document);
$version = $versionId ?
$document->versions()->findOrFail($versionId) :
$document->currentVersion;
if (!$version || !Storage::exists($version->file_path)) {
abort(404);
}
return Storage::download($version->file_path, $document->name . '.pdf');
}
/**
* Obtener el PDF actual para edición
*/
public function getPdfForEditing(Document $document)
{
$this->authorize('view', $document);
try {
if ($document->getFirstMedia('documents')) {
$filePath = $document->getFirstMedia('documents')->getPath();
} else {
$filePath = $document->file_path;
}
if (!Storage::exists($filePath)) {
abort(404);
}
$pdfContent = Storage::get($filePath);
$base64Pdf = base64_encode($pdfContent);
return response()->json([
'success' => true,
'pdf_data' => $base64Pdf,
'document_name' => $document->name
]);
} catch (\Exception $e) {
\Log::error('Error getting PDF for editing: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Error al cargar el PDF'
], 500);
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Folder;
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -11,7 +12,7 @@ use Illuminate\Support\Facades\Storage;
class ProjectController extends Controller
{
use AuthorizesRequests; // ← Añadir este trait
use AuthorizesRequests;
/**
* Display a listing of the resource.
@@ -43,7 +44,7 @@ class ProjectController extends Controller
'project' => $project,
'categories' => Category::orderBy('name')->get(),
'users' => User::where('id', '!=', auth()->id())->get(),
'companies' => \App\Models\Company::all(), // Pass companies to the view
'companies' => \App\Models\Company::all(), // Pass companies to the view,
]);
}
@@ -93,6 +94,12 @@ class ProjectController extends Controller
if($request->has('categories')) {
$project->categories()->sync($request->categories);
}
Folder::create([
'name' => 'Project',
'project_id' => $project->id,
'parent_id' => null,
]);
return redirect()->route('projects.show', $project)->with('success', 'Proyecto creado exitosamente');
@@ -172,6 +179,9 @@ class ProjectController extends Controller
//
}
/**
* Display the specified resource.
*/
public function __invoke(Project $project)
{
return view('projects.show', [

235
app/Livewire/CodeEdit.php Normal file
View File

@@ -0,0 +1,235 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class CodeEdit extends Component
{
public $componentId; // Nuevo: ID del componente padre
public $name = '';
public $codeInput = '';
public $labelInput = '';
public $maxLength = 3;
public $documentTypes = [];
public $sortBy = 'code';
public $sortDirection = 'asc';
protected $rules = [
'name' => 'required|string|min:2|max:50',
'codeInput' => 'required|string',
'labelInput' => 'required|string',
'maxLength' => 'required|integer|min:2|max:12',
];
public function mount($componentId, $initialName = '')
{
$this->componentId = $componentId;
$this->name = $initialName;
$this->maxLength = 3;
// Disparar evento inicial para establecer el nombre
$this->dispatch('nameUpdated',
componentId: $this->componentId,
data: [
'name' => $this->name
]
);
}
public function updateName()
{
$this->validate([
'name' => 'required|string|min:2|max:50',
]);
$this->dispatch('nameUpdated',
componentId: $this->componentId,
data: [
'name' => $this->name
]
);
}
public function updateMaxLength()
{
$this->validate([
'maxLength' => 'integer|min:2|max:12',
]);
if (strlen($this->codeInput) > $this->maxLength) {
$this->codeInput = substr($this->codeInput, 0, $this->maxLength);
}
}
public function addField()
{
$this->validate([
'codeInput' => "required|string|size:{$this->maxLength}",
'labelInput' => 'required|string|min:1',
], [
'codeInput.size' => "El código debe tener exactamente {$this->maxLength} caracteres",
]);
$this->documentTypes[] = [
'code' => $this->codeInput,
'label' => $this->labelInput,
'max_length' => $this->maxLength,
];
$this->sortList();
$this->reset(['codeInput', 'labelInput']);
$this->dispatch('componentUpdated',
componentId: $this->componentId, // Cambiado aquí
data: [
'documentTypes' => $this->documentTypes,
'maxLength' => $this->maxLength
]
);
}
public function addCode()
{
$this->validate([
'codeInput' => "required|string|size:{$this->maxLength}",
], [
'codeInput.size' => "El código debe tener exactamente {$this->maxLength} caracteres",
]);
$this->dispatch('focus-label-input');
}
public function addLabel()
{
$this->validate([
'labelInput' => 'required|string|min:1',
]);
if (!empty($this->codeInput) && !empty($this->labelInput)) {
$this->addField();
}
}
public function removeField($index)
{
if (isset($this->documentTypes[$index])) {
unset($this->documentTypes[$index]);
$this->documentTypes = array_values($this->documentTypes);
$this->dispatch('componentUpdated',
componentId: $this->componentId, // Cambiado aquí
data: [
'documentTypes' => $this->documentTypes,
'maxLength' => $this->maxLength
]
);
}
}
public function updatedMaxLength($value)
{
$this->validate([
'maxLength' => 'integer|min:2|max:12',
]);
if (strlen($this->codeInput) > $value) {
$this->codeInput = substr($this->codeInput, 0, $value);
}
}
public function updatedCodeInput($value)
{
if (strlen($value) > $this->maxLength) {
$this->codeInput = substr($value, 0, $this->maxLength);
}
$this->codeInput = strtoupper($value);
}
public function sortByCode()
{
if ($this->sortBy === 'code') {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = 'code';
$this->sortDirection = 'asc';
}
$this->sortList();
}
public function sortByLabel()
{
if ($this->sortBy === 'label') {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = 'label';
$this->sortDirection = 'asc';
}
$this->sortList();
}
private function sortList()
{
$direction = $this->sortDirection === 'asc' ? SORT_ASC : SORT_DESC;
if ($this->sortBy === 'code') {
array_multisort(
array_column($this->documentTypes, 'code'),
$direction,
SORT_STRING,
$this->documentTypes
);
} else {
array_multisort(
array_column($this->documentTypes, 'label'),
$direction,
SORT_STRING,
$this->documentTypes
);
}
}
public function getSortedDocumentTypesProperty()
{
$sorted = $this->documentTypes;
$direction = $this->sortDirection === 'asc' ? SORT_ASC : SORT_DESC;
if ($this->sortBy === 'code') {
array_multisort(
array_column($sorted, 'code'),
$direction,
SORT_STRING,
$sorted
);
} else {
array_multisort(
array_column($sorted, 'label'),
$direction,
SORT_STRING,
$sorted
);
}
return $sorted;
}
public function getTotalDocumentTypesProperty()
{
return count($this->documentTypes);
}
public function getSortIcon($column)
{
if ($this->sortBy !== $column) {
return 'sort';
}
return $this->sortDirection === 'asc' ? 'sort-up' : 'sort-down';
}
public function render()
{
return view('livewire.code-edit');
}
}

View File

@@ -0,0 +1,273 @@
<?php
namespace App\Livewire;
use App\Models\Document;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\DocumentsExport;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
use Rappasoft\LaravelLivewireTables\Views\Filters\SelectFilter;
use Rappasoft\LaravelLivewireTables\Views\Filters\TextFilter;
class ProjectDocumentList extends DataTableComponent
{
public $projectId;
public $folderId = null;
protected $model = Document::class;
public function configure(): void
{
$this->setPrimaryKey('id')
->setAdditionalSelects(['documents.id as id'])
/*->setConfigurableAreas([
'toolbar-left-start' => ['includes.areas.toolbar-left-start', ['param1' => 'Default', 'param2' => ['param2' => 2]]],
])*/
->setPaginationEnabled()
->setPaginationMethod('simple')
->setPaginationVisibilityEnabled()
//->setReorderEnabled()
->setHideReorderColumnUnlessReorderingEnabled()
->setSecondaryHeaderTrAttributes(function ($rows) {
return ['class' => 'bg-gray-100'];
})
->setSecondaryHeaderTdAttributes(function (Column $column, $rows) {
if ($column->isField('id')) {
return ['class' => 'text-red-100'];
}
return ['default' => true];
})
->setFooterTrAttributes(function ($rows) {
return ['class' => 'bg-gray-100'];
})
->setFooterTdAttributes(function (Column $column, $rows) {
if ($column->isField('name')) {
return ['class' => 'text-green-500'];
}
return ['default' => true];
})
->setHideBulkActionsWhenEmptyEnabled()
->setUseHeaderAsFooterEnabled()
->setPaginationEnabled()
->setPaginationVisibilityEnabled()
//->setToolsDisabled()
//->setToolBarDisabled()
// Configuración de paginación
->setPerPage(25) // Número de elementos por página
->setPerPageAccepted([10, 25, 50, 100]) // Opciones de elementos por página
->setPaginationEnabled() // Asegurar que la paginación esté habilitada
->setPaginationVisibilityStatus(true); // Hacer visible el paginador
;
}
public function mount($projectId = null, $folderId = null)
{
$this->projectId = $projectId;
$this->folderId = $folderId;
}
public function columns(): array
{
return [
Column::make("Código", "code")
->sortable()
->searchable()
->secondaryHeaderFilter('code') // Filtro para esta columna
->format(
fn($value, $row, Column $column) =>
'<a href="'.route('documents.show', $row->id).'" target="_blank" class=" target="_blank" class="flex items-center hover:text-blue-600 transition-colors"">'.$value.'</a>'
)->html(),
Column::make("Nombre", "name")
->sortable()
->searchable()
->secondaryHeaderFilter('name') // Filtro para esta columna
->format(
fn($value, $row, Column $column) =>
'<div class="flex items-center">
<span class="mr-2 text-lg">
'.\App\Helpers\FileHelper::getFileIconSvg($value).'
</span>
<a href="'.route('documents.show', $row->id).'"
target="_blank"
class=" target="_blank" class="flex items-center hover:text-blue-600 transition-colors"">
'.$value.'
</a>
</div>'
)->html(),
Column::make("Estado", "status")
->sortable()
->searchable()
->secondaryHeaderFilter('status') // Filtro para esta columna
->format(
fn($value, $row, Column $column) =>
'<span class="px-2 py-1 text-xs font-semibold rounded-full '.
($value === 'active' ? 'bg-green-100 text-green-800' :
($value === 'pending' ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800')).'">'.
$value.'</span>'
)->html(),
Column::make("Revisión", "revision")
->sortable()
->searchable(),
Column::make("Versión", "version")
->sortable()
->searchable(),
Column::make("Área", "area")
->sortable()
->searchable()
->secondaryHeaderFilter('area'), // Filtro para esta columna
Column::make("Disciplina", "discipline")
->sortable()
->searchable()
->secondaryHeaderFilter('discipline'), // Filtro para esta columna
Column::make("Tipo", "document_type")
->sortable()
->searchable()
->secondaryHeaderFilter('type'), // Filtro para esta columna
Column::make("Fecha Entrada", "entry_date")
->sortable()
->searchable()
->format(
fn($value, $row, Column $column) =>
$value ? \Carbon\Carbon::parse($value)->format('d/m/Y') : '-'
),
Column::make("Actualizado", "updated_at")
->sortable()
->searchable()
->format(
fn($value, $row, Column $column) =>
$value ? \Carbon\Carbon::parse($value)->diffForHumans() : '-'
),
];
}
public function filters(): array
{
return [
TextFilter::make('Código', 'code') // Agregar clave 'code'
->config([
'placeholder' => 'Buscar por código',
])
->filter(function (Builder $builder, string $value) {
$builder->where('documents.code', 'like', '%'.$value.'%');
}),
TextFilter::make('Nombre', 'name') // Agregar clave 'name'
->config([
'placeholder' => 'Buscar por nombre',
])
->filter(function (Builder $builder, string $value) {
$builder->where('documents.name', 'like', '%'.$value.'%');
}),
SelectFilter::make('Estado', 'status') // Agregar clave 'status'
->options([
'' => 'Todos',
'active' => 'Activo',
'pending' => 'Pendiente',
'inactive' => 'Inactivo',
])
->filter(function (Builder $builder, string $value) {
if ($value) {
$builder->where('documents.status', $value);
}
}),
SelectFilter::make('Disciplina', 'discipline') // Agregar clave 'discipline'
->options(
collect(['Estructural', 'Arquitectura', 'Eléctrica', 'Mecánica', 'Civil', 'Otros'])
->prepend('Todas', '')
->toArray()
)
->filter(function (Builder $builder, string $value) {
if ($value) {
$builder->where('documents.discipline', $value);
}
}),
SelectFilter::make('Area', 'area') // Agregar clave 'area'
->options(
collect(['Estructural', 'Arquitectura', 'Eléctrica', 'Mecánica', 'Civil', 'Otros'])
->prepend('Todas', '')
->toArray()
)
->filter(function (Builder $builder, string $value) {
if ($value) {
$builder->where('documents.area', $value);
}
}),
SelectFilter::make('Tipo', 'type') // Agregar clave 'document_type'
->options(
collect(['Estructural', 'Arquitectura', 'Eléctrica', 'Mecánica', 'Civil', 'Otros'])
->prepend('Todas', '')
->toArray()
)
->filter(function (Builder $builder, string $value) {
if ($value) {
$builder->where('documents.document_type', $value);
}
}),
];
}
public function builder(): Builder
{
$query = Document::query()->where('project_id', $this->projectId);
if ($this->folderId) {
$query->where('folder_id', $this->folderId);
} else {
$query->whereNull('folder_id');
}
return $query->with('user');
}
public function bulkActions(): array
{
return [
'activate' => 'Activar',
'deactivate' => 'Desactivar',
'export' => 'Exportar',
];
}
public function export()
{
$documents = $this->getSelected();
$this->clearSelected();
return Excel::download(new DocumentsExport($documents), 'documentos.xlsx');
}
public function activate()
{
Document::whereIn('id', $this->getSelected())->update(['status' => 'active']);
$this->clearSelected();
$this->dispatch('documents-updated');
}
public function deactivate()
{
Document::whereIn('id', $this->getSelected())->update(['status' => 'inactive']);
$this->clearSelected();
$this->dispatch('documents-updated');
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class ProjectNameCoder extends Component
{
public $components = [];
public $nextId = 1;
protected $listeners = [
'nameUpdated' => 'headerLabelUpdate',
'componentUpdated' => 'handleComponentUpdate',
'removeComponent' => 'removeComponent'
];
public function mount()
{
// Inicializar con un componente vacío
$this->addComponent();
}
public function addComponent()
{
$id = $this->nextId++;
$this->components[] = [
'id' => $id,
'data' => [],
'order' => count($this->components),
'headerLabel' => ''
];
}
public function removeComponent($componentId)
{
$this->components = array_filter($this->components, function($component) use ($componentId) {
return $component['id'] != $componentId;
});
// Reindexar el orden
$this->reorderComponents();
}
public function headerLabelUpdate($componentId, $data)
{
foreach ($this->components as &$component) {
if ($component['id'] == $componentId) {
$component['headerLabel'] = $data['name'];
break;
}
}
}
public function handleComponentUpdate($componentId, $data)
{
foreach ($this->components as &$component) {
if ($component['id'] == $componentId) {
$component['data'] = $data;
break;
}
}
}
public function updateComponentOrder($orderedIds)
{
foreach ($orderedIds as $index => $id) {
foreach ($this->components as &$component) {
if ($component['id'] == $id) {
$component['order'] = $index;
break;
}
}
}
// Ordenar el array por el campo 'order'
usort($this->components, function($a, $b) {
return $a['order'] - $b['order'];
});
}
private function reorderComponents()
{
foreach ($this->components as $index => &$component) {
$component['order'] = $index;
}
}
public function moveComponentUp($componentId)
{
$index = $this->findComponentIndex($componentId);
if ($index > 0) {
// Intercambiar con el componente anterior
$temp = $this->components[$index];
$this->components[$index] = $this->components[$index - 1];
$this->components[$index - 1] = $temp;
// Actualizar órdenes
$this->reorderComponents();
}
}
public function moveComponentDown($componentId)
{
$index = $this->findComponentIndex($componentId);
if ($index < count($this->components) - 1) {
// Intercambiar con el componente siguiente
$temp = $this->components[$index];
$this->components[$index] = $this->components[$index + 1];
$this->components[$index + 1] = $temp;
// Actualizar órdenes
$this->reorderComponents();
}
}
private function findComponentIndex($componentId)
{
foreach ($this->components as $index => $component) {
if ($component['id'] == $componentId) {
return $index;
}
}
return -1;
}
public function getComponentsCountProperty()
{
return count($this->components);
}
public function getTotalDocumentTypesProperty()
{
$total = 0;
foreach ($this->components as $component) {
if (isset($component['data']['documentTypes'])) {
$total += count($component['data']['documentTypes']);
}
}
return $total;
}
public function render()
{
return view('livewire.project-name-coder');
}
}

View File

@@ -11,6 +11,8 @@ use App\Models\Document;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use App\Helpers\DocumentIdentifier;
class ProjectShow extends Component
{
@@ -31,6 +33,8 @@ class ProjectShow extends Component
public $selectedFiles = []; // Archivos temporales en el modal
public $uploadProgress = [];
protected $listeners = ['documents-updated' => '$refresh'];
public function mount(Project $project)
{
@@ -80,28 +84,6 @@ class ProjectShow extends Component
$this->project->refresh();
}
/*public function uploadFiles(): void
{
$this->validate([
'files.*' => 'file|max:10240|mimes:pdf,docx,xlsx,jpg,png'
]);
dd($this->files);
foreach ($this->files as $file) {
Document::create([
'name' => $file->getClientOriginalName(),
'file_path' => $file->store("projects/{$this->project->id}/documents"),
'project_id' => $this->project->id,
'folder_id' => $this->currentFolder?->id
]);
}
$this->reset('files');
if ($this->currentFolder) {
$this->currentFolder->refresh(); // Recargar documentos
}
$this->reset('files');
}*/
public function getDocumentsProperty()
{
return $this->currentFolder
@@ -158,20 +140,41 @@ class ProjectShow extends Component
$this->selectedFiles = array_values($this->selectedFiles); // Reindexar array
}
// Método para confirmar y guardar
public function uploadFiles(): void
{
foreach ($this->selectedFiles as $file) {
Document::create([
'name' => $file->getClientOriginalName(),
'file_path' => $file->store("projects/{$this->project->id}/documents"),
'project_id' => $this->project->id, // Asegurar que se envía
'folder_id' => $this->currentFolder?->id,
'user_id' => Auth::id(),
//'status' => 'active' // Añadir si tu modelo lo requiere
]);
//$analizador = analizarDocumento();
//print_r($analizador);
//$resultado1 = $analizador->analizarDocumento($file->getClientOriginalName());
$code = $this->project->reference;
// Buscar si ya existe un documento con el mismo nombre en el mismo proyecto y carpeta
$existingDocument = Document::where('project_id', $this->project->id)
->where('folder_id', $this->currentFolder?->id)
->where('name', $file->getClientOriginalName())
->first();
if ($existingDocument) {
// Si existe, crear una nueva versión/revisión
$existingDocument->createVersion($file);
} else {
// Si no existe, crear el documento con revisión 0
$document = Document::create([
'name' => $file->getClientOriginalName(),
'file_path' => $file->store("projects/{$this->project->id}/documents"),
'project_id' => $this->project->id,
'folder_id' => $this->currentFolder?->id,
'issuer_id' => Auth::id(),
'code' => $code,
'entry_date' => now(),
'revision' => 0, // Revisión inicial
]);
$document->createVersion($file);
}
}
$this->resetUpload();
$this->project->refresh();
}
@@ -226,7 +229,8 @@ class ProjectShow extends Component
'file_path' => $path,
'project_id' => $this->project->id,
'folder_id' => $this->currentFolder?->id,
'user_id' => Auth::id()
'user_id' => Auth::id(),
'code' => $code,
]);
} catch (\Exception $e) {
@@ -247,4 +251,9 @@ class ProjectShow extends Component
{
return \App\Helpers\ProjectNamingSchema::generate($fields);
}
public function sellectAllDocuments()
{
$this->selectedFiles = $this->documents->pluck('id')->toArray();
}
}

View File

@@ -6,6 +6,8 @@ use App\Events\DocumentVersionUpdated;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Document extends Model
{
@@ -20,22 +22,63 @@ class Document extends Model
'file_path',
'project_id', // Asegurar que está en fillable
'folder_id',
'user_id',
'issuer',
'status',
'revision',
'version',
'discipline',
'document_type',
'issuer',
'entry_date',
'current_version_id',
'code',
];
public function versions() {
protected static function booted()
{
static::created(function ($document) {
if (request()->hasFile('file')) {
$file = request()->file('file');
$document->createVersion($file);
}
});
}
/**
* Get all versions of the document.
*/
public function versions(): HasMany
{
return $this->hasMany(DocumentVersion::class);
}
/**
* Get the current version of the document.
*/
public function currentVersion(): BelongsTo
{
return $this->belongsTo(DocumentVersion::class, 'current_version_id');
}
/**
* Get the latest version of the document.
*/
public function getLatestVersionAttribute()
{
return $this->versions()->latestFirst()->first();
}
/**
* Create a new version from file content.
*/
public function createVersion(string $content, array $changes = [], ?User $user = null): DocumentVersion
{
$version = DocumentVersion::createFromContent($this, $content, $changes, $user);
// Update current version pointer
$this->update(['current_version_id' => $version->id]);
return $version;
}
public function approvals() {
return $this->hasMany(Approval::class);
@@ -44,17 +87,6 @@ class Document extends Model
public function comments() {
return $this->hasMany(Comment::class)->whereNull('parent_id');
}
public function createVersion($file)
{
return $this->versions()->create([
'file_path' => $file->store("documents/{$this->id}/versions"),
'hash' => hash_file('sha256', $file),
'user_id' => auth()->id(),
'version_number' => $this->versions()->count() + 1
]);
}
public function getCurrentVersionAttribute()
{
@@ -63,7 +95,7 @@ class Document extends Model
public function uploadVersion($file)
{
$this->versions()->create([
$version = $this->versions()->create([
'file_path' => $file->store("projects/{$this->id}/versions"),
'hash' => hash_file('sha256', $file),
'version' => $this->versions()->count() + 1,
@@ -82,4 +114,9 @@ class Document extends Model
->logUnguarded();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -2,9 +2,320 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
class DocumentVersion extends Model
{
//
}
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'document_id',
'file_path',
'hash',
'version',
'user_id',
'changes',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'changes' => 'array',
'version' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* The attributes that should be appended to the model's array form.
*
* @var array<int, string>
*/
protected $appends = [
'file_url',
'file_size_formatted',
'created_at_formatted',
];
/**
* Boot function for model events
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
// Asegurar que la versión sea incremental para el mismo documento
if (empty($model->version)) {
$lastVersion = self::where('document_id', $model->document_id)
->max('version');
$model->version = $lastVersion ? $lastVersion + 1 : 1;
}
});
static::deleting(function ($model) {
// No eliminar el archivo físico si hay otras versiones que lo usan
$sameFileCount = self::where('file_path', $model->file_path)
->where('id', '!=', $model->id)
->count();
if ($sameFileCount === 0 && Storage::exists($model->file_path)) {
Storage::delete($model->file_path);
}
});
}
/**
* Get the document that owns the version.
*/
public function document(): BelongsTo
{
return $this->belongsTo(Document::class);
}
/**
* Get the user who created the version.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the file URL for the version.
*/
public function getFileUrlAttribute(): string
{
return Storage::url($this->file_path);
}
/**
* Get the file size in a human-readable format.
*/
public function getFileSizeFormattedAttribute(): string
{
if (!Storage::exists($this->file_path)) {
return '0 B';
}
$bytes = Storage::size($this->file_path);
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$index = 0;
while ($bytes >= 1024 && $index < count($units) - 1) {
$bytes /= 1024;
$index++;
}
return round($bytes, 2) . ' ' . $units[$index];
}
/**
* Get the actual file size in bytes.
*/
public function getFileSizeAttribute(): int
{
return Storage::exists($this->file_path) ? Storage::size($this->file_path) : 0;
}
/**
* Get formatted created_at date.
*/
public function getCreatedAtFormattedAttribute(): string
{
return $this->created_at->format('d/m/Y H:i');
}
/**
* Get the version label (v1, v2, etc.)
*/
public function getVersionLabelAttribute(): string
{
return 'v' . $this->version;
}
/**
* Check if this is the current version of the document.
*/
public function getIsCurrentAttribute(): bool
{
return $this->document->current_version_id === $this->id;
}
/**
* Get changes summary for display.
*/
public function getChangesSummaryAttribute(): string
{
if (empty($this->changes)) {
return 'Sin cambios registrados';
}
$changes = $this->changes;
if (is_array($changes)) {
$summary = [];
if (isset($changes['annotations_count']) && $changes['annotations_count'] > 0) {
$summary[] = $changes['annotations_count'] . ' anotación(es)';
}
if (isset($changes['signatures_count']) && $changes['signatures_count'] > 0) {
$summary[] = $changes['signatures_count'] . ' firma(s)';
}
if (isset($changes['stamps_count']) && $changes['stamps_count'] > 0) {
$summary[] = $changes['stamps_count'] . ' sello(s)';
}
if (isset($changes['edited_by'])) {
$summary[] = 'por ' . $changes['edited_by'];
}
return implode(', ', $summary);
}
return (string) $changes;
}
/**
* Scope a query to only include versions of a specific document.
*/
public function scopeOfDocument($query, $documentId)
{
return $query->where('document_id', $documentId);
}
/**
* Scope a query to order versions by latest first.
*/
public function scopeLatestFirst($query)
{
return $query->orderBy('version', 'desc');
}
/**
* Scope a query to order versions by oldest first.
*/
public function scopeOldestFirst($query)
{
return $query->orderBy('version', 'asc');
}
/**
* Get the previous version.
*/
public function getPreviousVersion(): ?self
{
return self::where('document_id', $this->document_id)
->where('version', '<', $this->version)
->orderBy('version', 'desc')
->first();
}
/**
* Get the next version.
*/
public function getNextVersion(): ?self
{
return self::where('document_id', $this->document_id)
->where('version', '>', $this->version)
->orderBy('version', 'asc')
->first();
}
/**
* Check if file exists in storage.
*/
public function fileExists(): bool
{
return Storage::exists($this->file_path);
}
/**
* Get file content.
*/
public function getFileContent(): ?string
{
return $this->fileExists() ? Storage::get($this->file_path) : null;
}
/**
* Verify file integrity using hash.
*/
public function verifyIntegrity(): bool
{
if (!$this->fileExists()) {
return false;
}
$currentHash = hash_file('sha256', Storage::path($this->file_path));
return $currentHash === $this->hash;
}
/**
* Create a new version from file content.
*/
public static function createFromContent(Document $document, string $content, array $changes = [], ?User $user = null): self
{
$user = $user ?: auth()->user();
// Calcular hash
$hash = hash('sha256', $content);
// Determinar siguiente versión
$lastVersion = self::where('document_id', $document->id)->max('version');
$version = $lastVersion ? $lastVersion + 1 : 1;
// Guardar archivo
$filePath = "documents/{$document->id}/versions/v{$version}.pdf";
Storage::put($filePath, $content);
// Crear registro
return self::create([
'document_id' => $document->id,
'file_path' => $filePath,
'hash' => $hash,
'version' => $version,
'user_id' => $user->id,
'changes' => $changes,
]);
}
/**
* Restore this version as the current version.
*/
public function restoreAsCurrent(): bool
{
if (!$this->fileExists()) {
return false;
}
// Crear una nueva versión idéntica a esta
$content = $this->getFileContent();
$newVersion = self::createFromContent(
$this->document,
$content,
['restored_from' => 'v' . $this->version]
);
// Actualizar documento para apuntar a la nueva versión
$this->document->update([
'current_version_id' => $newVersion->id
]);
return true;
}
}