Compare commits

...

8 Commits

Author SHA1 Message Date
047e155238 new functions
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-12-14 23:59:32 +01:00
e42ce8b092 new functionality: Add project coding configuration feature for projects
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-12-09 23:02:35 +01:00
7b00887372 updates to document handling and code editing features 2025-12-03 23:27:08 +01:00
88e526cf6c mejoras en la gestión de nombres y códigos de proyectos y documentos según la norma ISO 19650
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-10-25 11:30:59 +02:00
d8ae8c8894 mejoras en la gestión de proyectos y documentos: se añaden nuevos campos y validaciones para optimizar la organización y el seguimiento de los mismos.
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-10-25 11:29:20 +02:00
28c225687a mejoras
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-05-23 00:26:53 +02:00
19fa52ca65 Merge branch 'main' of https://homehud.duckdns.org/javier/Nexora
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-05-07 00:21:14 +02:00
655ea60d6b añadir nuevas funcionalidades 2025-05-07 00:07:40 +02:00
504 changed files with 187217 additions and 1996 deletions

View File

@@ -0,0 +1,69 @@
<?php
// En App\Helpers\FileHelper.php
namespace App\Helpers;
class FileHelper
{
public static function getFileType($filename)
{
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return match($extension) {
'pdf' => 'pdf',
'doc', 'docx' => 'word',
'xls', 'xlsx' => 'excel',
'ppt', 'pptx' => 'powerpoint',
'jpg', 'jpeg', 'png', 'gif' => 'image',
'zip', 'rar' => 'archive',
'txt' => 'text',
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

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Company;
use App\Models\User;
class CompanyContactController extends Controller
{
public function store(Request $request, Company $company)
{
$request->validate([
'user_id' => 'required|exists:users,id',
'position' => 'nullable|string|max:100'
]);
// Evitar duplicados
if (!$company->contacts()->where('user_id', $request->user_id)->exists()) {
$company->contacts()->attach($request->user_id, [
'position' => $request->position
]);
return redirect()->back()
->with('success', 'Contacto agregado exitosamente');
}
return redirect()->back()
->with('error', 'Este contacto ya está asociado a la empresa');
}
public function update(Request $request, Company $company, User $contact)
{
$request->validate([
'position' => 'nullable|string|max:100'
]);
$company->contacts()->updateExistingPivot($contact->id, [
'position' => $request->position
]);
return redirect()->back()
->with('success', 'Cargo del contacto actualizado');
}
public function destroy(Company $company, User $contact)
{
$company->contacts()->detach($contact->id);
return redirect()->back()
->with('success', 'Contacto eliminado de la empresa');
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers;
use App\Models\Company;
use App\Models\User;
use Illuminate\Http\Request;
class CompanyController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$companies = Company::orderBy('name')->paginate(10);
return view('companies.index', compact('companies'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('companies.form');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'commercial_name' => 'required|string|max:255',
'status' => 'required|in:active,closed',
'address' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:10',
'city' => 'nullable|string|max:100',
'country' => 'nullable|string|max:100',
'phone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:255',
'cif' => 'nullable|string|max:20',
'logo' => 'nullable|image|max:2048|mimes:jpg,jpeg,png,gif',
]);
// Manejar la carga del logo
if ($request->hasFile('logo')) {
$validated['logo'] = $request->file('logo')->store('companies/logos', 'public');
}
Company::create($validated);
return redirect()->route('companies.index')
->with('success', 'Empresa creada exitosamente.');
}
/**
* Display the specified resource.
*/
public function show(Company $company)
{
$company->load(['contacts' => function($query) {
$query->withPivot('position');
}]);
$contacts = $company->contacts()->paginate(5);
$projects = $company->contacts()->paginate(5);//$company->projects()->paginate(5);
$availableUsers = User::whereDoesntHave('companies', function($query) use ($company) {
$query->where('company_id', $company->id);
})->get();
return view('companies.show', compact('company', 'contacts', 'projects', 'availableUsers'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Company $company)
{
return view('companies.form', compact('company'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Company $company)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'commercial_name' => 'required|string|max:255',
'status' => 'required|in:active,closed',
'address' => 'nullable|string|max:255',
'postal_code' => 'nullable|string|max:10',
'city' => 'nullable|string|max:100',
'country' => 'nullable|string|max:100',
'phone' => 'nullable|string|max:20',
'email' => 'nullable|email|max:255',
'cif' => 'nullable|string|max:20',
'logo' => 'nullable|image|max:2048|mimes:jpg,jpeg,png,gif',
]);
// Manejar la actualización del logo
if ($request->hasFile('logo')) {
// Eliminar el logo anterior si existe
if ($company->logo && Storage::disk('public')->exists($company->logo)) {
Storage::disk('public')->delete($company->logo);
}
$validated['logo'] = $request->file('logo')->store('companies/logos', 'public');
} elseif ($request->has('remove_logo')) {
// Eliminar el logo si se seleccionó la opción de eliminar
if ($company->logo && Storage::disk('public')->exists($company->logo)) {
Storage::disk('public')->delete($company->logo);
}
$validated['logo'] = null;
} else {
// Mantener el logo existente
$validated['logo'] = $company->logo;
}
$company->update($validated);
return redirect()->route('companies.index')
->with('success', 'Empresa actualizada exitosamente.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Company $company)
{
// Eliminar el logo si existe
if ($company->logo && Storage::disk('public')->exists($company->logo)) {
Storage::disk('public')->delete($company->logo);
}
$company->delete();
return redirect()->route('companies.index')
->with('success', 'Empresa eliminada exitosamente.');
}
}

View File

@@ -24,10 +24,8 @@ class DashboardController extends Controller
// Documentos recientes (últimos 7 días)
$recentDocuments = Document::with(['project', 'currentVersion'])
->where('created_at', '>=', now()->subDays(7))
->orderBy('created_at', 'desc')
->limit(5)
->get();
->limit(5);
// Actividad reciente
$recentActivities = DB::table('activity_log')
@@ -35,7 +33,9 @@ class DashboardController extends Controller
->limit(10)
->get();
return view('dashboard', compact('stats', 'recentDocuments', 'recentActivities'));
$showSidebar = true; // Variable para mostrar el sidebar
return view('dashboard', compact('stats', 'recentDocuments', 'recentActivities', 'showSidebar'));
}
private function calculateStorageUsed()

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use App\Models\DocumentComment;
use Illuminate\Http\Request;
class DocumentCommentController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, Document $document)
{
$request->validate([
'content' => 'required|string|max:1000',
'page' => 'required|integer|min:1',
'x' => 'required|numeric|between:0,1',
'y' => 'required|numeric|between:0,1'
]);
$document->comments()->create([
'user_id' => auth()->id(),
'content' => $request->content,
'page' => $request->page,
'x' => $request->x,
'y' => $request->y,
'parent_id' => $request->parent_id
]);
return back();
}
/**
* Display the specified resource.
*/
public function show(DocumentComment $documentComment)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(DocumentComment $documentComment)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, DocumentComment $documentComment)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(DocumentComment $documentComment)
{
//
}
}

View File

@@ -6,9 +6,13 @@ use App\Jobs\ProcessDocumentOCR;
use App\Models\Document;
use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class DocumentController extends Controller
{
public $comments=[];
/**
* Display a listing of the resource.
*/
@@ -56,7 +60,21 @@ class DocumentController extends Controller
*/
public function show(Document $document)
{
//
$this->authorize('view', $document); // Si usas políticas
if (!Storage::exists($document->file_path)) {
abort(404);
}
$document->url = Storage::url($document->file_path);
$document->load('user');
return view('documents.show', [
'document' => $document,
'versions' => $document->versions()->latest()->get(),
'comments' => $this->comments,
]);
}
/**
@@ -92,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);
@@ -109,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,26 +3,32 @@
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;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class ProjectController extends Controller
{
use AuthorizesRequests; // ← Añadir este trait
use AuthorizesRequests;
/**
* Display a listing of the resource.
*/
public function index()
{
$projects = Project::withCount('documents')
->whereHas('users', function($query) {
$projects = auth()->user()->hasRole('admin')
? Project::get() // Todos los proyectos para admin
: auth()->user()->projects()->latest()->get(); // Solo proyectos asignados
/*
$projects = Project::whereHas('users', function($query) {
$query->where('user_id', auth()->id());
})
->filter(['search' => request('search')])
->paginate(9);
->paginate(9);*/
return view('projects.index', compact('projects'));
}
@@ -33,10 +39,12 @@ class ProjectController extends Controller
public function create()
{
$this->authorize('create projects');
$project = new Project();
return view('projects.create', [
'project' => $project,
'categories' => Category::orderBy('name')->get(),
'users' => User::where('id', '!=', auth()->id())->get(),
'companies' => \App\Models\Company::all(), // Pass companies to the view,
]);
}
@@ -46,25 +54,21 @@ class ProjectController extends Controller
public function store(Request $request)
{
$validated = $request->validate([
'reference' => 'required|string|max:12',
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:Activo,Inactivo',
//'team' => 'sometimes|array',
//'team.*' => 'exists:users,id',
'project_image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'address' => 'nullable|string|max:255',
'province' => 'nullable|string|max:100',
//'country' => 'nullable|string|size:2',
'country' => 'nullable|string|size:2',
'postal_code' => 'nullable|string|max:10',
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
//'icon' => 'nullable|in:'.implode(',', config('project.icons')),
'start_date' => 'nullable|date',
'deadline' => 'nullable|date|after:start_date',
'categories' => 'nullable|array|exists:categories,id',
//'categories' => 'required|array',
//'categories.*' => 'exists:categories,id',
//'documents.*' => 'file|max:5120|mimes:pdf,docx,xlsx,jpg,png'
'project_image_path' => 'nullable|string',
'company_id' => 'required|exists:companies,id', // Validate company_id
]);
@@ -75,11 +79,14 @@ class ProjectController extends Controller
]);
// Manejar la imagen
if ($request->hasFile('project_image')) {
$path = $request->file('project_image')->store('project-images', 'public');
$validated['project_image_path'] = $path; // Usar el nombre correcto de columna
if ($request->has('project_image_path') && $request->project_image_path) {
$tempPath = $request->project_image_path;
$newPath = 'images/projects/' . basename($tempPath);
Storage::move($tempPath, $newPath); // Mover el archivo
$validated['project_image_path'] = $newPath; // Actualizar path
}
// Crear el proyecto con todos los datos validados
$project = Project::create($validated);
@@ -87,16 +94,12 @@ class ProjectController extends Controller
if($request->has('categories')) {
$project->categories()->sync($request->categories);
}
// Manejar documentos adjuntos
if($request->hasFile('documents')) {
foreach ($request->file('documents') as $file) {
$project->documents()->create([
'file_path' => $file->store('project-documents', 'public'),
'original_name' => $file->getClientOriginalName()
]);
}
}
Folder::create([
'name' => 'Project',
'project_id' => $project->id,
'parent_id' => null,
]);
return redirect()->route('projects.show', $project)->with('success', 'Proyecto creado exitosamente');
@@ -105,12 +108,26 @@ class ProjectController extends Controller
}
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Project $project)
{
$this->authorize('update', $project);
return view('projects.create', [
'project' => $project,
'categories' => Category::orderBy('name')->get(),
'users' => User::where('id', '!=', auth()->id())->get(),
'companies' => \App\Models\Company::all(), // Pass companies to the view
]);
}
/**
* Display the specified resource.
*/
public function show(Project $project)
{
$this->authorize('view', $project); // Si usas políticas
//$this->authorize('view', $project); // Si usas políticas
$project->load(['categories', 'documents']);
return view('projects.show', [
@@ -120,22 +137,37 @@ class ProjectController extends Controller
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Project $project)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Project $project)
{
$this->authorize('update', $project);
// Lógica de actualización
$project->update($request->all());
$validated = $request->validate([
'reference' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:Activo,Inactivo',
'address' => 'nullable|string|max:255',
'province' => 'nullable|string|max:100',
'country' => 'nullable|string|size:2',
'postal_code' => 'nullable|string|max:10',
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
'start_date' => 'nullable|date',
'deadline' => 'nullable|date|after:start_date',
'categories' => 'nullable|array|exists:categories,id',
'project_image_path' => 'nullable|string',
'company_id' => 'required|exists:companies,id', // Validate company_id
]);
$project->update($validated);
if ($request->has('categories')) {
$project->categories()->sync($request->categories);
}
return redirect()->route('projects.show', $project)->with('success', 'Project updated successfully.');
}
@@ -147,6 +179,9 @@ class ProjectController extends Controller
//
}
/**
* Display the specified resource.
*/
public function __invoke(Project $project)
{
return view('projects.show', [

View 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);
}
}
}

View File

@@ -17,18 +17,25 @@ use Spatie\Permission\Models\Role;
class UserController extends Controller
{
public $collapsedGroups = [];
public function index()
{
$this->authorize('viewAny', User::class);
$users = User::paginate(10);
return view('users.index', compact('users'));
return view('users.index', ['users' => $users,
'showSidebar' => 'true',]);
}
public function create()
{
$this->authorize('create', User::class);
$roles = Role::all();
return view('users.create', compact('roles'));
$user = new User();
return view('users.create', ['user' => $user,
'roles' => $roles,
'showSidebar' => 'true',
]);
}
public function store(Request $request)
@@ -57,7 +64,10 @@ class UserController extends Controller
'end_date' => 'nullable|date|after_or_equal:start_date',
'email' => 'required|email|unique:users',
'phone' => 'nullable|string|max:20',
'address' => 'nullable|string|max:255'
'address' => 'nullable|string|max:255',
'user_type' => 'required|integer|in:0,1,2',
'company_id' => 'nullable|exists:companies,id', // Si se usa una relación con empresas
'profile_photo_path' => 'nullable|string' // Ruta de la imagen subida por Livewire
]);
// Creación del usuario
@@ -72,11 +82,14 @@ class UserController extends Controller
'address' => $validated['address'],
'access_start' => $validated['start_date'],
'access_end' => $validated['end_date'],
'is_active' => true
'is_active' => $validated['is_active'] ?? false, // Por defecto, inactivo
'user_type' => $validated['user_type'] ?? 0, // 0: Usuario, 1: Administrador, 2: Super Admin
'company_id' => $validated['company_id'] ?? null, // Si se usa una relación con empresas
'profile_photo_path' => $validated['profile_photo_path'] ?? null
]);
if ($request->hasFile('photo')) {
$path = $request->file('photo')->store('public/photos');
if ($request->hasFile('image_path')) {
$path = $request->file('image_path')->store('public/photos');
$user->profile_photo_path = basename($path);
$user->save();
}
@@ -128,9 +141,11 @@ class UserController extends Controller
Rule::unique('users')->ignore($user->id)
],
'phone' => 'nullable|string|max:20',
'address' => 'nullable|string|max:255'
'address' => 'nullable|string|max:255',
'profile_photo_path' => 'nullable|string', // Añadido para la ruta de la imagen
//'is_active' => 'nullable|boolean' // Añadido para el estado activo
]);
// Preparar datos para actualización
$updateData = [
'title' => $validated['title'],
@@ -142,37 +157,31 @@ class UserController extends Controller
'address' => $validated['address'],
'access_start' => $validated['start_date'],
'access_end' => $validated['end_date'],
'is_active' => $request->has('is_active') // Si usas un checkbox
'is_active' => $validated['is_active'] ?? false,
'profile_photo_path' => $validated['profile_photo_path'] ?? $user->profile_photo_path
];
// Actualizar contraseña solo si se proporciona
if (!empty($validated['password'])) {
$updateData['password'] = Hash::make($validated['password']);
}
if ($request->hasFile('photo')) {
// Eliminar foto anterior si existe
if ($user->prfile_photo_path) {
Storage::delete('public/photos/'.$user->profile_photo_path);
}
$path = $request->file('photo')->store('public/photos');
$user->update(['profile_photo_path' => basename($path)]);
// Eliminar imagen anterior si se está actualizando
if (isset($validated['profile_photo_path']) && $user->profile_photo_path) {
Storage::disk('public')->delete($user->profile_photo_path);
}
// Actualizar el usuario
$user->update($updateData);
// Redireccionar con mensaje de éxito
return redirect()->route('users.show', $user)
->with('success', 'Usuario actualizado exitosamente');
} catch (ValidationException $e) {
// Redireccionar con errores de validación
return redirect()->back()->withErrors($e->validator)->withInput();
} catch (QueryException $e) {
// Manejar errores de base de datos
$errorCode = $e->errorInfo[1];
$errorMessage = 'Error al actualizar el usuario: ';
@@ -181,12 +190,11 @@ class UserController extends Controller
} else {
$errorMessage .= 'Error en la base de datos';
}
Log::error("Error actualizando usuario ID {$user->id}: " . $e->getMessage());
return redirect()->back()->with('error', $errorMessage)->withInput();
} catch (\Exception $e) {
// Manejar otros errores
Log::error("Error general actualizando usuario ID {$user->id}: " . $e->getMessage());
return redirect()->back()->with('error', 'Ocurrió un error inesperado al actualizar el usuario')->withInput();
}
@@ -196,12 +204,15 @@ class UserController extends Controller
{
$previousUser = User::where('id', '<', $user->id)->latest('id')->first();
$nextUser = User::where('id', '>', $user->id)->oldest('id')->first();
$permissionGroups = $this->getPermissionGroups($user);
return view('users.show', [
'user' => $user,
'previousUser' => $previousUser,
'nextUser' => $nextUser,
'permissionGroups' => Permission::all()->groupBy('group')
'permissionGroups' => $permissionGroups,
'showSidebar' => true,
'collapsedGroups' => $this->collapsedGroups,
]);
}
@@ -234,4 +245,55 @@ class UserController extends Controller
return redirect()->route('users.index')
->with('success', 'Usuario eliminado correctamente');
}
private function getPermissionGroups(User $user)
{
// Obtener todos los permisos disponibles
$allPermissions = Permission::all();
// Agrupar permisos por tipo (asumiendo que los nombres siguen el formato "tipo.acción")
$grouped = $allPermissions->groupBy(function ($permission) {
return explode('.', $permission->name)[0]; // Extrae "user" de "user.create"
});
// Formatear para la vista
$groups = [];
foreach ($grouped as $groupName => $permissions) {
$groups[$groupName] = [
'name' => ucfirst($groupName),
'permissions' => $permissions->map(function ($permission) use ($user) {
return [
'id' => $permission->id,
'name' => $permission->name,
'description' => $this->getPermissionDescription($permission->name),
'enabled' => $user->hasPermissionTo($permission)
];
})
];
}
return $groups;
}
private function getPermissionDescription($permissionName)
{
$descriptions = [
'user.create' => 'Crear nuevos usuarios',
'user.edit' => 'Editar usuarios existentes',
'document.view' => 'Ver documentos',
// Agrega más descripciones según necesites
];
return $descriptions[$permissionName] ?? str_replace('.', ' ', $permissionName);
}
public function toggleGroupCollapse($groupName)
{
if (in_array($groupName, $this->collapsedGroups)) {
$this->collapsedGroups = array_diff($this->collapsedGroups, [$groupName]);
} else {
$this->collapsedGroups[] = $groupName;
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Services;
use Spatie\PdfToText\Pdf;
use PDFLib;
class PdfProcessor
{
public function extractAnnotations($path)
{
$text = (new Pdf())->setPdf($path)->text();
// Analizar texto para detectar anotaciones
return $this->parseAnnotations($text);
}
public function embedAnnotations($originalPdf, $annotations)
{
$pdf = new PDFLib();
$pdf->begin_document('', '');
foreach($annotations as $annotation) {
$this->addAnnotationToPdf($pdf, $annotation);
}
return $pdf->get_buffer();
}
}

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

@@ -0,0 +1,251 @@
<?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 = '', $initialMaxLength = 3, $initialDocumentTypes = [])
{
$this->componentId = $componentId;
$this->name = $initialName;
$this->maxLength = $initialMaxLength;
$this->documentTypes = $initialDocumentTypes;
// Guardar datos iniciales
$this->initialData = [
'name' => $initialName,
'maxLength' => $initialMaxLength,
'documentTypes' => $initialDocumentTypes
];
// Disparar eventos iniciales
$this->dispatch('nameUpdated',
componentId: $this->componentId,
data: [
'name' => $this->name,
]
);
$this->dispatch('componentUpdated',
componentId: $this->componentId,
data: [
'documentTypes' => $this->documentTypes,
'maxLength' => $this->maxLength
]
);
}
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

@@ -10,68 +10,75 @@ class ImageUploader extends Component
{
use WithFileUploads;
public $photo;
public $currentImage;
public $fieldName;
public $placeholder;
public $storagePath = 'tmp/uploads';
public $image;
public $imagePath;
public $fieldName; // Nombre del campo para el formulario
public $label = 'Subir imagen'; // Etiqueta personalizable
public $hover = false;
protected $rules = [
'photo' => 'nullable|image|max:2048', // 2MB Max
];
public function mount($fieldName = 'photo', $currentImage = null, $placeholder = null)
// Recibir parámetros si es necesario
public function mount($fieldName = 'image_path', $label = 'Subir imagen')
{
$this->fieldName = $fieldName;
$this->currentImage = $currentImage;
$this->placeholder = $placeholder ?? asset('images/default-avatar.png');
$this->label = $label;
}
public function updatedPhoto()
public function updatedImage()
{
$this->validate([
'photo' => 'image|max:2048', // 2MB Max
'image' => 'image|max:2048', // 2MB Max
]);
// Subir automáticamente al seleccionar
$this->uploadImage();
}
public function removePhoto()
public function uploadImage()
{
$this->photo = null;
$this->currentImage = null;
$this->validate([
'image' => 'required|image|max:2048',
]);
// Guardar la imagen
$this->imagePath = $this->image->store('uploads', 'public');
// Emitir evento con el nombre del campo y la ruta
$this->dispatch('imageUploaded',
field: $this->fieldName,
path: $this->imagePath
);
}
public function save()
{
$this->validate();
$this->validate([
'image' => 'required|image|max:2048',
]);
// Guardar la imagen
$this->imagePath = $this->image->store('images', 'public');
if ($this->photo) {
$path = $this->photo->store($this->storagePath);
if ($this->model) {
// Eliminar imagen anterior si existe
if ($this->model->{$this->fieldName}) {
Storage::delete($this->model->{$this->fieldName});
}
$this->model->{$this->fieldName} = $path;
$this->model->save();
}
$this->currentUrl = Storage::url($path);
$this->showSavedMessage = true;
$this->photo = null; // Limpiar el input de subida
}
// Emitir evento con el nombre del campo y la ruta
$this->emit('imageUploaded', [
'field' => $this->fieldName,
'path' => $this->imagePath
]);
}
protected function getCurrentImageUrl()
public function removeImage()
{
if ($this->model && $this->model->{$this->fieldName}) {
return Storage::url($this->model->{$this->fieldName});
}
$this->reset(['image', 'imagePath']);
return $this->placeholder;
// Cambiar emit por dispatch en Livewire v3+
$this->dispatch('imageRemoved',
field: $this->fieldName
);
}
public function toggleHover($status)
{
$this->hover = $status;
}
public function render()
{
return view('livewire.image-uploader');

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Livewire;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class PdfViewer extends Component
{
public $pdfUrl;
public $currentPage = 1;
public $totalPages = 1;
public $zoomLevel = 1;
protected $listeners = ['pageChanged', 'annotationSaved'];
public function mount($pdfId)
{
$this->pdfUrl = Storage::disk('pdfs')->temporaryUrl($pdfId, now()->addMinutes(30));
}
public function pageChanged($page)
{
$this->currentPage = $page;
}
public function annotationSaved($data)
{
// Procesar y guardar anotaciones
$this->emit('refreshAnnotations');
}
public function render()
{
return view('livewire.pdf-viewer');
}
}

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,317 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Project;
use App\Models\ProjectDocumentStatus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class ProjectDocumentStatusManager extends Component
{
public $project;
public $statuses = [];
public $showForm = false;
public $formData = [
'id' => null,
'name' => '',
'color' => '#6b7280',
'text_color' => '#ffffff',
'description' => '',
'allow_upload' => true,
'allow_edit' => true,
'allow_delete' => false,
'requires_approval' => false,
'is_default' => false,
];
public $showDeleteModal = false;
public $statusToDelete = null;
public $orderedStatusIds = [];
protected $rules = [
'formData.name' => 'required|string|min:2|max:100',
'formData.color' => 'required|string|max:7',
'formData.text_color' => 'nullable|string|max:7',
'formData.description' => 'nullable|string|max:500',
'formData.allow_upload' => 'boolean',
'formData.allow_edit' => 'boolean',
'formData.allow_delete' => 'boolean',
'formData.requires_approval' => 'boolean',
'formData.is_default' => 'boolean',
];
public function mount(Project $project)
{
$this->project = $project;
$this->loadStatuses();
}
public function loadStatuses()
{
$this->statuses = $this->project->documentStatuses()
->orderBy('order')
->get()
->toArray();
$this->statuses = [];
$this->orderedStatusIds = collect($this->statuses)->pluck('id')->toArray();
}
public function openForm($statusId = null)
{
$this->resetForm();
if ($statusId) {
$status = ProjectDocumentStatus::find($statusId);
if ($status && $status->project_id === $this->project->id) {
$this->formData = [
'id' => $status->id,
'name' => $status->name,
'color' => $status->color,
'text_color' => $status->text_color,
'description' => $status->description,
'allow_upload' => $status->allow_upload,
'allow_edit' => $status->allow_edit,
'allow_delete' => $status->allow_delete,
'requires_approval' => $status->requires_approval,
'is_default' => $status->is_default,
];
}
}
$this->showForm = true;
}
public function closeForm()
{
$this->showForm = false;
$this->resetForm();
}
public function resetForm()
{
$this->formData = [
'id' => null,
'name' => '',
'color' => '#6b7280',
'text_color' => '#ffffff',
'description' => '',
'allow_upload' => true,
'allow_edit' => true,
'allow_delete' => false,
'requires_approval' => false,
'is_default' => false,
];
$this->resetErrorBag();
}
public function saveStatus()
{
$this->validate();
try {
DB::beginTransaction();
// Si se marca como default, quitar el default de otros estados
if ($this->formData['is_default']) {
$this->project->documentStatuses()->update(['is_default' => false]);
}
$data = [
'name' => $this->formData['name'],
'color' => $this->formData['color'],
'text_color' => $this->formData['text_color'] ?: $this->calculateTextColor($this->formData['color']),
'description' => $this->formData['description'],
'allow_upload' => $this->formData['allow_upload'],
'allow_edit' => $this->formData['allow_edit'],
'allow_delete' => $this->formData['allow_delete'],
'requires_approval' => $this->formData['requires_approval'],
'is_default' => $this->formData['is_default'],
];
if ($this->formData['id']) {
// Editar estado existente
$status = ProjectDocumentStatus::find($this->formData['id']);
if ($status && $status->project_id === $this->project->id) {
$status->update($data);
}
} else {
// Crear nuevo estado
$data['project_id'] = $this->project->id;
$data['slug'] = $this->generateUniqueSlug($this->formData['name']);
$data['order'] = $this->project->documentStatuses()->count();
ProjectDocumentStatus::create($data);
}
DB::commit();
$this->loadStatuses();
$this->closeForm();
$this->dispatch('show-message', [
'type' => 'success',
'message' => $this->formData['id'] ? 'Estado actualizado correctamente.' : 'Estado creado correctamente.'
]);
} catch (\Exception $e) {
DB::rollBack();
$this->dispatch('show-message', [
'type' => 'error',
'message' => 'Error al guardar: ' . $e->getMessage()
]);
}
}
private function generateUniqueSlug($name)
{
$slug = Str::slug($name);
$originalSlug = $slug;
$counter = 1;
while ($this->project->documentStatuses()->where('slug', $slug)->exists()) {
$slug = $originalSlug . '-' . $counter;
$counter++;
}
return $slug;
}
private function calculateTextColor($backgroundColor)
{
// Convertir hex a RGB
$hex = str_replace('#', '', $backgroundColor);
if (strlen($hex) == 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
// Calcular luminosidad
$luminosity = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
return $luminosity > 0.5 ? '#000000' : '#ffffff';
}
public function confirmDelete($statusId)
{
$this->statusToDelete = ProjectDocumentStatus::find($statusId);
if ($this->statusToDelete && $this->statusToDelete->project_id === $this->project->id) {
$this->showDeleteModal = true;
}
}
public function closeDeleteModal()
{
$this->showDeleteModal = false;
$this->statusToDelete = null;
}
public function deleteStatus()
{
if (!$this->statusToDelete) {
return;
}
try {
// Verificar que no haya documentos con este estado
if ($this->statusToDelete->documents()->exists()) {
$this->dispatch('show-message', [
'type' => 'error',
'message' => 'No se puede eliminar el estado porque hay documentos asociados.'
]);
$this->closeDeleteModal();
return;
}
// Si es el estado por defecto, establecer otro como default
if ($this->statusToDelete->is_default) {
$newDefault = $this->project->documentStatuses()
->where('id', '!=', $this->statusToDelete->id)
->first();
if ($newDefault) {
$newDefault->update(['is_default' => true]);
}
}
$this->statusToDelete->delete();
$this->loadStatuses();
$this->dispatch('show-message', [
'type' => 'success',
'message' => 'Estado eliminado correctamente.'
]);
} catch (\Exception $e) {
$this->dispatch('show-message', [
'type' => 'error',
'message' => 'Error al eliminar: ' . $e->getMessage()
]);
}
$this->closeDeleteModal();
}
public function updateOrder()
{
try {
foreach ($this->orderedStatusIds as $index => $statusId) {
$status = ProjectDocumentStatus::find($statusId);
if ($status && $status->project_id === $this->project->id) {
$status->update(['order' => $index]);
}
}
$this->loadStatuses();
$this->dispatch('show-message', [
'type' => 'success',
'message' => 'Orden actualizado correctamente.'
]);
} catch (\Exception $e) {
$this->dispatch('show-message', [
'type' => 'error',
'message' => 'Error al actualizar el orden: ' . $e->getMessage()
]);
}
}
public function moveUp($statusId)
{
$index = array_search($statusId, $this->orderedStatusIds);
if ($index > 0) {
$temp = $this->orderedStatusIds[$index];
$this->orderedStatusIds[$index] = $this->orderedStatusIds[$index - 1];
$this->orderedStatusIds[$index - 1] = $temp;
$this->updateOrder();
}
}
public function moveDown($statusId)
{
$index = array_search($statusId, $this->orderedStatusIds);
if ($index < count($this->orderedStatusIds) - 1) {
$temp = $this->orderedStatusIds[$index];
$this->orderedStatusIds[$index] = $this->orderedStatusIds[$index + 1];
$this->orderedStatusIds[$index + 1] = $temp;
$this->updateOrder();
}
}
public function render()
{
return view('livewire.project-document-status-manager');
}
}

View File

@@ -0,0 +1,274 @@
<?php
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',
'componentUpdated' => 'handleComponentUpdate',
'removeComponent' => 'removeComponent'
];
public function mount(Project $project)
{
// Inicializar con un componente vacío
$this->project = $project;
if ($project->codingConfig) {
$this->loadDatabaseConfiguration();
} else {
$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()
{
$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 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');
}
}

View File

@@ -9,12 +9,17 @@ use App\Models\Project;
use App\Models\Folder;
use App\Models\Document;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use App\Services\ProjectCodeService;
class ProjectShow extends Component
{
use WithFileUploads;
protected $middleware = ['auth']; // Añade esto
public Project $project;
public ?Folder $currentFolder = null;
public $expandedFolders = [];
@@ -22,11 +27,20 @@ class ProjectShow extends Component
public $folderName = '';
public $selectedFolderId = null;
public $showFolderModal = false;
public $showUploadModal = false;
public $tempFiles = [];
public $selectedFiles = []; // Archivos temporales en el modal
public $uploadProgress = [];
protected $listeners = ['documents-updated' => '$refresh'];
protected ProjectCodeService $codeService;
public function mount(Project $project)
{
$this->project = $project->load('rootFolders');
$this->project = $project;
$this->project['javi'] = 'braña';
$this->currentFolder = $this->project->rootFolders->first() ?? null;
}
@@ -57,13 +71,13 @@ class ProjectShow extends Component
})
]
]);
Folder::create([
'name' => $this->folderName,
'project_id' => $this->project->id,
'parent_id' => $this->currentFolder?->id
'parent_id' => $this->currentFolder?->id,
]);
$this->hideCreateFolderModal();
$this->reset('folderName');
$this->project->load('rootFolders'); // Recargar carpetas raíz
if ($this->currentFolder) {
@@ -72,28 +86,6 @@ class ProjectShow extends Component
$this->project->refresh();
}
public function uploadFiles(): void
{
$this->validate([
'files.*' => 'file|max:10240|mimes:pdf,docx,xlsx,jpg,png'
]);
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
@@ -116,8 +108,167 @@ class ProjectShow extends Component
return $breadcrumbs;
}
public function showCreateFolderModal()
{
$this->folderName = '';
$this->showFolderModal = true;
}
public function hideCreateFolderModal()
{
$this->showFolderModal = false;
}
// Método para abrir el modal
public function openUploadModal(): void
{
$this->showUploadModal = true;
}
// Método para manejar archivos seleccionados
public function selectFiles($files): void
{
$this->validate([
'selectedFiles.*' => 'file|max:10240|mimes:pdf,docx,xlsx,jpg,png'
]);
$isValid = app(ProjectCodeService::class)->validate($this->project, $files[0]->getClientOriginalName());
if ($isValid) {
echo "✅ Código válido\n";
} else {
echo "❌ Código inválido\n";
}
$this->selectedFiles = array_merge($this->selectedFiles, $files);
}
// Método para eliminar un archivo de la lista
public function removeFile($index): void
{
unset($this->selectedFiles[$index]);
$this->selectedFiles = array_values($this->selectedFiles); // Reindexar array
}
public function uploadFiles(): void
{
$validator = new ProjectCodeValidator($project->codingConfig);
foreach ($this->selectedFiles as $file) {
//print_r($analizador);
//$resultado1 = $analizador->analizarDocumento($file->getClientOriginalName());
if ($validator->validate($file->getClientOriginalName())) {
echo "✅ Código válido\n";
} else {
echo "❌ Código inválido\n";
return;
}
$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();
}
// Método para procesar los archivos
protected function processFiles(): void
{
foreach ($this->files 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
]);
}
}
// Método para resetear
public function resetUpload(): void
{
$this->reset(['selectedFiles', 'showUploadModal', 'uploadProgress']);
}
#[On('upload-progress')]
public function updateProgress($name, $progress)
{
$this->uploadProgress[$name] = $progress;
}
public function addFiles($files)
{
$this->validate([
'selectedFiles.*' => 'file|max:10240|mimes:pdf,docx,xlsx,jpg,png'
]);
$this->selectedFiles = array_merge($this->selectedFiles, $files);
}
public function startUpload()
{
foreach ($this->selectedFiles as $file) {
try {
$path = $file->store(
"projects/{$this->project->id}/".($this->currentFolder ? "folders/{$this->currentFolder->id}" : ""),
'public'
);
Document::create([
'name' => $file->getClientOriginalName(),
'file_path' => $path,
'project_id' => $this->project->id,
'folder_id' => $this->currentFolder?->id,
'user_id' => Auth::id(),
'code' => $code,
]);
} catch (\Exception $e) {
$this->addError('upload', "Error subiendo {$file->getClientOriginalName()}: {$e->getMessage()}");
}
}
$this->resetUpload();
$this->project->refresh();
}
public function render()
{
return view('livewire.project-show');
return view('livewire.project.show');
}
public function generateProjectCode(array $fields): string
{
return \App\Helpers\ProjectNamingSchema::generate($fields);
}
public function sellectAllDocuments()
{
$this->selectedFiles = $this->documents->pluck('id')->toArray();
}
}

36
app/Models/Comment.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Comment extends Model
{
use HasFactory;
protected $fillable = [
'content',
'user_id',
'document_id',
'parent_id',
'page',
'x',
'y'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function document()
{
return $this->belongsTo(Document::class);
}
public function replies()
{
return $this->hasMany(Comment::class, 'parent_id');
}
}

65
app/Models/Company.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Company extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'commercial_name',
'status',
'address',
'postal_code',
'city',
'country',
'phone',
'email',
'cif',
'logo'
];
protected $casts = [
'created_at' => 'datetime:d/m/Y H:i',
'updated_at' => 'datetime:d/m/Y H:i',
];
public function getStatusColorAttribute()
{
return [
'active' => 'bg-green-100 text-green-800',
'closed' => 'bg-red-100 text-red-800',
][$this->status] ?? 'bg-gray-100 text-gray-800';
}
public function getStatusTextAttribute()
{
return [
'active' => 'Activo',
'closed' => 'Cerrado',
][$this->status] ?? 'Desconocido';
}
public function contacts(): BelongsToMany
{
return $this->belongsToMany(User::class, 'company_contacts')
->withPivot('position')
->withTimestamps();
}
public function users()
{
return $this->hasMany(User::class);
}
public function projects()
{
return $this->hasMany(Project::class);
}
}

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
{
@@ -17,16 +19,66 @@ class Document extends Model
protected $fillable = [
'name',
'status',
'project_id',
'file_path',
'project_id', // Asegurar que está en fillable
'folder_id',
'current_version_id'
'issuer',
'status',
'revision',
'version',
'discipline',
'document_type',
'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);
@@ -35,16 +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()
{
@@ -53,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,
@@ -72,4 +114,9 @@ class Document extends Model
->logUnguarded();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DocumentComment extends Model
{
protected $fillable = ['content', 'page', 'x', 'y', 'parent_id'];
public function user() {
return $this->belongsTo(User::class);
}
public function children() {
return $this->hasMany(DocumentComment::class, 'parent_id');
}
public function parent() {
return $this->belongsTo(DocumentComment::class, 'parent_id');
}
}

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;
}
}

View File

@@ -10,9 +10,9 @@ class Folder extends Model
'name',
'parent_id',
'project_id',
'icon',
'color',
'description',
//'icon',
//'color',
//'description',
];
public function descendants()

View File

@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
protected $fillable = [
'reference',
'name',
'description',
'creator_id',
@@ -22,6 +23,7 @@ class Project extends Model
'icon',
'start_date',
'deadline',
'company_id',
// Agrega cualquier otro campo nuevo aquí
];
@@ -81,5 +83,113 @@ class Project extends Model
return $this->belongsToMany(Category::class);
}
public function company()
{
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;
}
}

View 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]',
],
];
}
}

View File

@@ -10,6 +10,7 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Permission\Traits\HasRoles;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Authenticatable
{
@@ -109,5 +110,17 @@ class User extends Authenticatable
{
return $this->profile_photo ? Storage::url($this->profile_photo) : asset('images/default-user.png');
}
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'company_contacts')
->withPivot('position')
->withTimestamps();
}
public function company()
{
return $this->belongsTo(Company::class);
}
}

View File

@@ -16,6 +16,6 @@ class DashboardPolicy
public function view(User $user)
{
return $user->hasPermissionTo('view dashboard');
return true; //$user->hasPermissionTo('view.dashboard');
}
}

View File

@@ -16,7 +16,7 @@ class DocumentPolicy
*/
public function viewAny(User $user): bool
{
return false;
return $user->hasPermissionTo('document.view');
}
/**
@@ -24,7 +24,7 @@ class DocumentPolicy
*/
public function view(User $user, Document $document)
{
return $user->hasPermissionTo('view documents')
return $user->hasPermissionTo('document.view')
&& $user->hasProjectAccess($document->project_id)
&& $user->hasPermissionToResource($document->resource(), 'view');
}
@@ -42,7 +42,7 @@ class DocumentPolicy
*/
public function update(User $user, Document $document): bool
{
return $user->hasPermissionToResource($document->resource(), 'edit');
return $user->hasPermissionToResource($document->resource(), 'document.edit');
}
/**
@@ -50,7 +50,7 @@ class DocumentPolicy
*/
public function delete(User $user, Document $document): bool
{
return $user->hasPermissionTo('delete documents');
return $user->hasPermissionTo('document.delete');
}
/**

View File

@@ -22,18 +22,18 @@ class FolderPolicy
$user->projects->contains($folder->project_id);
}
return $user->can('manage-projects');
return $user->can('project.create');
}
public function move(User $user, Folder $folder)
{
return $user->can('manage-projects') &&
return $user->can('project.create') &&
$user->projects->contains($folder->project_id);
}
public function delete(User $user, Folder $folder)
{
return $user->can('delete-projects') &&
return $user->can('project.delete') &&
$user->projects->contains($folder->project_id);
}
}

View File

@@ -14,7 +14,7 @@ class PermissionPolicy
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('view permissions');
return $user->hasPermissionTo('permission.view');
}
/**
@@ -22,7 +22,7 @@ class PermissionPolicy
*/
public function view(User $user, Permission $permission): bool
{
return $user->hasPermissionTo('view permissions');
return $user->hasPermissionTo('permission.view');
}
/**
@@ -30,7 +30,7 @@ class PermissionPolicy
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('create permissions');
return $user->hasPermissionTo('permission.create');
}
/**
@@ -40,7 +40,7 @@ class PermissionPolicy
{
if($permission->is_system) return false;
return $user->hasPermissionTo('edit permissions');
return $user->hasPermissionTo('permission.edit');
}
/**
@@ -52,7 +52,7 @@ class PermissionPolicy
return false;
}
return $user->hasPermissionTo('delete permissions');
return $user->hasPermissionTo('permission.delete');
}
/**
@@ -60,7 +60,7 @@ class PermissionPolicy
*/
public function restore(User $user, Permission $permission): bool
{
return $user->hasPermissionTo('manage permissions');
return $user->hasPermissionTo('permission.create');
}
/**
@@ -68,6 +68,6 @@ class PermissionPolicy
*/
public function forceDelete(User $user, Permission $permission): bool
{
return $user->hasPermissionTo('manage permissions');
return $user->hasPermissionTo('permission.delete');
}
}

View File

@@ -13,7 +13,7 @@ class ProjectPolicy
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('view projects');
return $user->hasPermissionTo('project.view');
}
/**
@@ -21,7 +21,13 @@ class ProjectPolicy
*/
public function view(User $user, Project $project): bool
{
return $user->hasPermissionTo('view projects') &&
// Admin ve todo, otros usuarios solo proyectos asignados
/*
return $user->hasRole('admin') ||
$project->users->contains($user->id) ||
$project->manager_id === $user->id;*/
return $user->hasPermissionTo('project.view') &&
$this->hasProjectAccess($user, $project);
}
@@ -30,7 +36,7 @@ class ProjectPolicy
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('create projects');
return $user->hasPermissionTo('project.create');
}
/**
@@ -38,7 +44,7 @@ class ProjectPolicy
*/
public function update(User $user, Project $project): bool
{
return $user->hasPermissionTo('edit projects') &&
return $user->hasPermissionTo('project.edit') &&
$this->hasProjectAccess($user, $project);
}
@@ -47,7 +53,7 @@ class ProjectPolicy
*/
public function delete(User $user, Project $project): bool
{
return $user->hasPermissionTo('delete projects') &&
return $user->hasPermissionTo('project.delete') &&
$this->hasProjectAccess($user, $project);
}

View File

@@ -13,7 +13,7 @@ class RolePolicy
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('view roles');
return $user->hasPermissionTo('role.view');
}
/**
@@ -21,7 +21,7 @@ class RolePolicy
*/
public function view(User $user, Role $role): bool
{
return false;
return $user->hasPermissionTo('role.view');
}
/**
@@ -29,7 +29,7 @@ class RolePolicy
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('create roles');
return $user->hasPermissionTo('role.create');
}
/**
@@ -37,7 +37,7 @@ class RolePolicy
*/
public function update(User $user, Role $role): bool
{
return $user->hasPermissionTo('edit roles') && !$role->is_protected;
return $user->hasPermissionTo('role.edit') && !$role->is_protected;
}
/**
@@ -45,7 +45,7 @@ class RolePolicy
*/
public function delete(User $user, Role $role): bool
{
return $user->hasPermissionTo('delete roles') && !$role->is_protected;
return $user->hasPermissionTo('role.delete') && !$role->is_protected;
}
/**

View File

@@ -12,7 +12,7 @@ class UserPolicy
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('manage users');
return $user->hasPermissionTo('user.view');
}
/**
@@ -20,7 +20,7 @@ class UserPolicy
*/
public function view(User $user, User $model): bool
{
return false;
return $user->hasPermissionTo('user.view');
}
/**
@@ -28,7 +28,7 @@ class UserPolicy
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('manage users');
return $user->hasPermissionTo('user.create');
}
/**
@@ -36,7 +36,7 @@ class UserPolicy
*/
public function update(User $user, User $model): bool
{
return $user->hasPermissionTo('manage users') && !$model->is_protected;
return $user->hasPermissionTo('user.create') && !$model->is_protected;
}
/**
@@ -44,7 +44,7 @@ class UserPolicy
*/
public function delete(User $user, User $model): bool
{
return $user->hasPermissionTo('manage users')
return $user->hasPermissionTo('user.delete')
&& !$model->is_protected
&& $user->id !== $model->id;
}

View File

@@ -2,6 +2,15 @@
namespace App\Providers;
use App\Livewire\FileUpload;
use App\Livewire\ImageUploader;
use App\Livewire\PdfViewer;
use App\Livewire\ProjectShow;
use App\Livewire\Toolbar;
use App\View\Components\Multiselect;
use App\Services\ProjectCodeService;
use App\Services\ProjectCodeValidator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
@@ -15,7 +24,16 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
// Registrar el Service
$this->app->singleton(ProjectCodeService::class, function ($app) {
return new ProjectCodeService(new ProjectCodeValidator([]));
});
// O si prefieres, registrar el validador por separado
$this->app->bind(ProjectCodeValidator::class, function ($app, $params) {
// $params[0] contendría los datos del proyecto
return new ProjectCodeValidator($params[0] ?? []);
});
}
/**
@@ -25,12 +43,14 @@ class AppServiceProvider extends ServiceProvider
{
// Configuración de componentes Blade
Blade::componentNamespace('App\\View\\Components', 'icons');
Blade::component('multiselect', \App\View\Components\Multiselect::class);
Blade::component('multiselect', Multiselect::class);
// Registro de componentes Livewire
Livewire::component('project-show', \App\Http\Livewire\ProjectShow::class);
Livewire::component('file-upload', \App\Http\Livewire\FileUpload::class);
Livewire::component('toolbar', \App\Http\Livewire\Toolbar::class);
Livewire::component('project-show', ProjectShow::class);
Livewire::component('file-upload', FileUpload::class);
Livewire::component('toolbar', Toolbar::class);
Livewire::component('image-uploader', ImageUploader::class);
Livewire::component('pdf-viewer', PdfViewer::class);
// Validación personalizada
Validator::extend('max_upload_size', function ($attribute, $value, $parameters, $validator) {
@@ -41,5 +61,6 @@ class AppServiceProvider extends ServiceProvider
return $totalSize <= ($maxSize * 1024);
});
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace App\Providers;
use App\Models\Document;
use App\Models\Folder;
use App\Models\Project;
use App\Models\User;
use App\Policies\DocumentPolicy;
use App\Policies\FolderPolicy;
use App\Policies\PermissionPolicy;
use App\Policies\ProfilePolicy;
use App\Policies\ProjectPolicy;
use App\Policies\RolePolicy;
use App\Policies\UserPolicy;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class AppServiceProvider extends ServiceProvider
{
protected $policies = [
User::class => UserPolicy::class,
User::class => ProfilePolicy::class,
Role::class => RolePolicy::class,
Permission::class => PermissionPolicy::class,
Document::class => DocumentPolicy::class,
Project::class => ProjectPolicy::class,
Folder::class => FolderPolicy::class,
];
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
Blade::componentNamespace('App\\View\\Components', 'icons');
Blade::component('multiselect', \App\View\Components\Multiselect::class);
Livewire::component('project-show', \App\Http\Livewire\ProjectShow::class);
Livewire::component('project-show', \App\Http\Livewire\FileUpload::class);
Livewire::component('toolbar', \App\Http\Livewire\Toolbar::class);
Validator::extend('max_upload_size', function ($attribute, $value, $parameters, $validator) {
$maxSize = env('MAX_UPLOAD_SIZE', 51200); // Default 50MB
$totalSize = array_reduce($value, function($sum, $file) {
return $sum + $file->getSize();
}, 0);
return $totalSize <= ($maxSize * 1024);
});
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Services;
use App\Models\Project;
use App\Services\ProjectCodeValidator;
use Illuminate\Support\Arr;
class ProjectCodeService
{
protected ProjectCodeValidator $validator;
public function __construct(ProjectCodeValidator $validator)
{
$this->validator = $validator;
}
/**
* Crea un validador para un proyecto específico
*/
public function forProject(Project $project): ProjectCodeValidator
{
// Si es un modelo Eloquent, convertir a array
//$projectData = $project instanceof Project ? $project->toArray() : $project;
return new ProjectCodeValidator($project);
}
/**
* Valida un código rápidamente
*/
public function validate(Project $project, string $code): bool
{
$validator = $this->forProject($project);
return $validator->validate($code);
}
/**
* Analiza un código y devuelve detalles
*/
public function analyze(Project $project, string $code): array
{
$validator = $this->forProject($project);
return $validator->analyze($code);
}
/**
* Genera un nuevo código para el proyecto
*/
public function generate(
Project $project,
array $components,
string $documentName
): string
{
$validator = $this->forProject($project);
return $validator->generateCode($components, $documentName);
}
/**
* Obtiene la configuración de codificación
*/
public function getConfiguration(Project $project): array
{
$validator = $this->forProject($project);
return [
'format' => $validator->getFormat(),
'reference' => $validator->getReference(),
'separator' => $validator->getSeparator(),
'sequence_length' => $validator->getSequenceLength(),
'components' => [
'maker' => $validator->getAllowedCodesForComponent('maker'),
'system' => $validator->getAllowedCodesForComponent('system'),
'discipline' => $validator->getAllowedCodesForComponent('discipline'),
'document_type' => $validator->getAllowedCodesForComponent('document_type'),
]
];
}
}

View File

@@ -0,0 +1,543 @@
<?php
namespace App\Services;
use InvalidArgumentException;
class ProjectCodeValidator
{
private array $projectData;
private array $components;
private string $separator;
private string $reference;
private int $sequenceLength;
/**
* Constructor de la clase
*
* @param array|string $project Los datos del proyecto (array o JSON string)
* @throws InvalidArgumentException Si los datos no son válidos
*/
public function __construct($project)
{
//print_r($project);
// Convertir string JSON a array si es necesario
if (is_string($project)) {
$project = json_decode($project, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InvalidArgumentException('JSON inválido: ' . json_last_error_msg());
}
}
if (!is_array($project)) {
throw new InvalidArgumentException('Los datos del proyecto deben ser un array o JSON string');
}
$this->projectData = $project;
// Validar que exista la configuración de codificación
/*if (!isset($this->projectData['codingConfig'])) {
throw new InvalidArgumentException('El proyecto no tiene configuración de codificación');
}*/
$this->initializeConfiguration();
}
/**
* Inicializa la configuración a partir de los datos del proyecto
*/
private function initializeConfiguration(): void
{
$codingConfig = $this->projectData['codingConfig'];
// Obtener componentes ordenados por 'order'
$this->components = $codingConfig['elements']['components'] ?? [];
usort($this->components, fn($a, $b) => $a['order'] <=> $b['order']);
$this->separator = $codingConfig['separator'] ?? '-';
$this->reference = $this->projectData['reference'] ?? '';
$this->sequenceLength = $codingConfig['sequence_length'] ?? 4;
}
/**
* Valida si un código cumple con la configuración de codificación
*
* @param string $code El código a validar
* @return bool True si el código es válido
*/
public function validate(string $code): bool
{
// Validación básica: número de partes
$parts = explode($this->separator, $code);
if (count($parts) !== 7) {
return false;
}
// Validar referencia (primera parte)
if ($parts[0] !== $this->reference) {
return false;
}
// Validar cada componente (partes 2-6)
for ($i = 1; $i <= 5; $i++) {
if (!$this->validateComponent($i - 1, $parts[$i])) {
return false;
}
}
// La parte 7 (DOCUMENT_NAME) no tiene validación específica
return true;
}
/**
* Valida un componente específico del código
*
* @param int $componentIndex Índice del componente (0-4)
* @param string $value Valor a validar
* @return bool True si el valor es válido para el componente
*/
private function validateComponent(int $componentIndex, string $value): bool
{
if (!isset($this->components[$componentIndex])) {
return false;
}
$component = $this->components[$componentIndex];
$headerLabel = $component['headerLabel'] ?? '';
// Validar longitud máxima
$maxLength = $component['data']['maxLength'] ?? null;
if ($maxLength && strlen($value) > $maxLength) {
return false;
}
// Validar según el tipo de componente
if ($headerLabel === 'Version') {
return $this->validateVersionComponent($value);
}
// Validar contra códigos permitidos
return $this->validateAgainstAllowedCodes($component, $value);
}
/**
* Valida el componente de versión
*
* @param string $value Valor de la versión
* @return bool True si es válido
*/
private function validateVersionComponent(string $value): bool
{
// La versión debe ser numérica
if (!is_numeric($value)) {
return false;
}
// La versión debe tener la longitud especificada
if (strlen($value) !== $this->sequenceLength) {
return false;
}
return true;
}
/**
* Valida un valor contra los códigos permitidos de un componente
*
* @param array $component Configuración del componente
* @param string $value Valor a validar
* @return bool True si el valor está en los códigos permitidos
*/
private function validateAgainstAllowedCodes(array $component, string $value): bool
{
$documentTypes = $component['data']['documentTypes'] ?? [];
if (empty($documentTypes)) {
return true; // No hay restricciones de códigos
}
$allowedCodes = array_column($documentTypes, 'code');
return in_array($value, $allowedCodes, true);
}
/**
* Realiza un análisis detallado de un código
*
* @param string $code El código a analizar
* @return array Array con información detallada de la validación
*/
public function analyze(string $code): array
{
$result = [
'is_valid' => false,
'code' => $code,
'parts' => [],
'errors' => [],
'warnings' => []
];
// Dividir en partes
$parts = explode($this->separator, $code);
// Verificar número de partes
$expectedParts = 7;
if (count($parts) !== $expectedParts) {
$result['errors'][] = sprintf(
'El código debe tener %d partes separadas por "%s", tiene %d',
$expectedParts,
$this->separator,
count($parts)
);
return $result;
}
// Nombres de los componentes según el orden esperado
$componentNames = [
'reference',
'maker',
'system',
'discipline',
'document_type',
'version',
'document_name'
];
// Analizar cada parte
foreach ($parts as $index => $value) {
$analysis = $this->analyzePart($index, $value);
$analysis['name'] = $componentNames[$index] ?? 'unknown';
$result['parts'][] = $analysis;
if (!$analysis['is_valid']) {
$result['errors'][] = sprintf(
'Parte %d (%s): %s',
$index + 1,
$componentNames[$index],
$analysis['error'] ?? 'Error desconocido'
);
}
}
$result['is_valid'] = empty($result['errors']);
return $result;
}
/**
* Analiza una parte específica del código
*
* @param int $partIndex Índice de la parte (0-6)
* @param string $value Valor de la parte
* @return array Análisis detallado de la parte
*/
private function analyzePart(int $partIndex, string $value): array
{
$analysis = [
'index' => $partIndex,
'value' => $value,
'is_valid' => true,
'expected' => '',
'error' => '',
'notes' => []
];
switch ($partIndex) {
case 0: // Referencia
$analysis['expected'] = $this->reference;
if ($value !== $this->reference) {
$analysis['is_valid'] = false;
$analysis['error'] = sprintf(
'Referencia incorrecta. Esperado: %s',
$this->reference
);
}
break;
case 1: // Maker (componente 0)
case 2: // System (componente 1)
case 3: // Discipline (componente 2)
case 4: // Document Type (componente 3)
$componentIndex = $partIndex - 1;
if (isset($this->components[$componentIndex])) {
$component = $this->components[$componentIndex];
$analysis = array_merge($analysis, $this->analyzeComponent($component, $value));
}
break;
case 5: // Version (componente 4)
if (isset($this->components[4])) {
$component = $this->components[4];
$analysis = array_merge($analysis, $this->analyzeComponent($component, $value));
// Información adicional para la versión
$analysis['notes'][] = sprintf(
'Longitud de secuencia: %d',
$this->sequenceLength
);
}
break;
case 6: // Document Name
$analysis['notes'][] = 'Nombre del documento (sin restricciones específicas)';
break;
default:
$analysis['is_valid'] = false;
$analysis['error'] = 'Índice de parte no válido';
break;
}
return $analysis;
}
/**
* Analiza un componente específico
*
* @param array $component Configuración del componente
* @param string $value Valor a analizar
* @return array Análisis del componente
*/
private function analyzeComponent(array $component, string $value): array
{
$result = [
'is_valid' => true,
'expected' => '',
'error' => '',
'notes' => []
];
$headerLabel = $component['headerLabel'] ?? '';
$maxLength = $component['data']['maxLength'] ?? null;
$documentTypes = $component['data']['documentTypes'] ?? [];
// Validar longitud
if ($maxLength && strlen($value) > $maxLength) {
$result['is_valid'] = false;
$result['error'] = sprintf(
'Longitud máxima excedida: %d (actual: %d)',
$maxLength,
strlen($value)
);
}
// Validar tipo de componente
if ($headerLabel === 'Version') {
if (!is_numeric($value)) {
$result['is_valid'] = false;
$result['error'] = 'Debe ser numérico';
} elseif (strlen($value) !== $this->sequenceLength) {
$result['is_valid'] = false;
$result['error'] = sprintf(
'Longitud incorrecta. Esperado: %d, Actual: %d',
$this->sequenceLength,
strlen($value)
);
}
$result['expected'] = sprintf('Número de %d dígitos', $this->sequenceLength);
} else {
// Obtener códigos permitidos
$allowedCodes = array_column($documentTypes, 'code');
if (!empty($allowedCodes) && !in_array($value, $allowedCodes, true)) {
$result['is_valid'] = false;
$result['error'] = sprintf(
'Código no permitido. Permitidos: %s',
implode(', ', $allowedCodes)
);
}
$result['expected'] = $allowedCodes ? implode('|', $allowedCodes) : 'Sin restricciones';
$result['notes'][] = sprintf('Componente: %s', $headerLabel);
// Agregar etiquetas de los códigos permitidos
$labels = [];
foreach ($documentTypes as $type) {
if (isset($type['label'])) {
$labels[] = sprintf('%s = %s', $type['code'], $type['label']);
}
}
if (!empty($labels)) {
$result['notes'][] = 'Significados: ' . implode(', ', $labels);
}
}
return $result;
}
/**
* Genera un código válido basado en la configuración
*
* @param array $componentsValues Valores para cada componente
* @param string $documentName Nombre del documento
* @return string Código generado
* @throws InvalidArgumentException Si los valores no son válidos
*/
public function generateCode(array $componentsValues, string $documentName): string
{
// Validar que tengamos valores para todos los componentes
$requiredComponents = 5; // Maker, System, Discipline, Document Type, Version
if (count($componentsValues) < $requiredComponents) {
throw new InvalidArgumentException(
sprintf('Se requieren valores para %d componentes', $requiredComponents)
);
}
// Construir las partes del código
$parts = [$this->reference];
// Agregar componentes validados
for ($i = 0; $i < $requiredComponents; $i++) {
$value = $componentsValues[$i];
if (!$this->validateComponent($i, $value)) {
throw new InvalidArgumentException(
sprintf('Valor inválido para componente %d: %s', $i, $value)
);
}
$parts[] = $value;
}
// Agregar nombre del documento
$parts[] = $this->sanitizeDocumentName($documentName);
return implode($this->separator, $parts);
}
/**
* Sanitiza el nombre del documento para usarlo en el código
*
* @param string $documentName Nombre del documento
* @return string Nombre sanitizado
*/
private function sanitizeDocumentName(string $documentName): string
{
// Reemplazar caracteres no deseados
$sanitized = preg_replace('/[^a-zA-Z0-9_-]/', '_', $documentName);
// Limitar longitud si es necesario
return substr($sanitized, 0, 50);
}
/**
* Obtiene los códigos permitidos para un componente específico
*
* @param string $componentName Nombre del componente (maker, system, discipline, document_type)
* @return array Array de códigos permitidos
*/
public function getAllowedCodesForComponent(string $componentName): array
{
$componentMap = [
'maker' => 0,
'system' => 1,
'discipline' => 2,
'document_type' => 3
];
if (!isset($componentMap[$componentName])) {
return [];
}
$index = $componentMap[$componentName];
if (!isset($this->components[$index])) {
return [];
}
$component = $this->components[$index];
$documentTypes = $component['data']['documentTypes'] ?? [];
$result = [];
foreach ($documentTypes as $type) {
$result[] = [
'code' => $type['code'],
'label' => $type['label'] ?? $type['code'],
'max_length' => $type['max_length'] ?? $component['data']['maxLength'] ?? null
];
}
return $result;
}
/**
* Obtiene el formato de codificación
*
* @return string Formato de codificación
*/
public function getFormat(): string
{
return $this->projectData['coding_config']['format'] ?? '';
}
/**
* Obtiene la referencia del proyecto
*
* @return string Referencia del proyecto
*/
public function getReference(): string
{
return $this->reference;
}
/**
* Obtiene el separador usado en la codificación
*
* @return string Separador
*/
public function getSeparator(): string
{
return $this->separator;
}
/**
* Obtiene la longitud de secuencia para la versión
*
* @return int Longitud de secuencia
*/
public function getSequenceLength(): int
{
return $this->sequenceLength;
}
}
// Ejemplo de uso:
// Suponiendo que $projectDataVariable es la variable que contiene tus datos del proyecto
/*
$validator = new ProjectCodeValidator($projectDataVariable);
// Validar un código
$codigo = "SOGOS0001-SOGOS-PV0-CV-DWG-0001-InformeTecnico";
if ($validator->validate($codigo)) {
echo "✅ Código válido\n";
} else {
echo "❌ Código inválido\n";
}
// Analizar un código detalladamente
$analisis = $validator->analyze($codigo);
echo "Código analizado: " . $analisis['code'] . "\n";
echo "Válido: " . ($analisis['is_valid'] ? 'Sí' : 'No') . "\n";
if (!$analisis['is_valid']) {
echo "Errores:\n";
foreach ($analisis['errors'] as $error) {
echo "- $error\n";
}
}
// Generar un código nuevo
try {
$componentes = ['SOGOS', 'PV0', 'CV', 'DWG', '0014'];
$nuevoCodigo = $validator->generateCode($componentes, 'PlanosGenerales');
echo "Código generado: $nuevoCodigo\n";
} catch (InvalidArgumentException $e) {
echo "Error al generar código: " . $e->getMessage() . "\n";
}
// Obtener códigos permitidos para un componente
$systemCodes = $validator->getAllowedCodesForComponent('system');
echo "Códigos permitidos para System:\n";
foreach ($systemCodes as $code) {
echo "- {$code['code']}: {$code['label']}\n";
}
*/

View File

@@ -0,0 +1,30 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Accordion extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return <<<'blade'
<div>
<!-- Simplicity is the essence of happiness. - Cedric Bledsoe -->
</div>
blade;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class AccordionItem extends Component
{
/**
* Create a new component instance.
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.accordion-item');
}
}

View File

@@ -15,6 +15,8 @@
"laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1.1",
"livewire/volt": "^1.7.0",
"luvi-ui/laravel-luvi": "^0.6.0",
"rappasoft/laravel-livewire-tables": "^3.7",
"spatie/laravel-activitylog": "^4.10",
"spatie/laravel-medialibrary": "^11.12",
"spatie/laravel-permission": "^6.17",

831
composer.lock generated

File diff suppressed because it is too large Load Diff

214
config/livewire-pdf.php Normal file
View File

@@ -0,0 +1,214 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| PDF Storage Path
|--------------------------------------------------------------------------
|
| This value determines the path where uploaded PDFs will be stored.
| The path is relative to the storage directory.
|
*/
'storage_path' => 'pdfs',
/*
|--------------------------------------------------------------------------
| Temporary Storage Path
|--------------------------------------------------------------------------
|
| This value determines the path where temporary PDFs will be stored
| during processing. The path is relative to the storage directory.
|
*/
'temp_path' => 'pdfs/temp',
/*
|--------------------------------------------------------------------------
| PDF Processing Options
|--------------------------------------------------------------------------
|
| These options control how PDFs are processed and rendered.
|
*/
'processing' => [
'max_file_size' => 10 * 1024 * 1024, // 10MB
'allowed_extensions' => ['pdf'],
'use_web_workers' => true,
'cache_rendered_pages' => true,
'cache_ttl' => 60 * 24, // 24 hours
],
/*
|--------------------------------------------------------------------------
| Form Field Options
|--------------------------------------------------------------------------
|
| These options control the default properties for form fields.
|
*/
'form_fields' => [
'text' => [
'font_size' => 12,
'font_family' => 'Helvetica',
'color' => '#000000',
'multiline' => false,
],
'checkbox' => [
'size' => 12,
'checked_by_default' => false,
],
'radio' => [
'size' => 12,
'selected_by_default' => false,
],
'dropdown' => [
'font_size' => 12,
'font_family' => 'Helvetica',
],
'signature' => [
'width' => 200,
'height' => 50,
],
'date' => [
'format' => 'YYYY-MM-DD',
'font_size' => 12,
'font_family' => 'Helvetica',
],
],
/*
|--------------------------------------------------------------------------
| eSignature Platform Compatibility
|--------------------------------------------------------------------------
|
| These options control compatibility with various eSignature platforms.
|
*/
'esignature_compatibility' => [
'docusign' => [
'enabled' => true,
'field_naming_convention' => 'docusign',
],
'adobe_sign' => [
'enabled' => true,
'field_naming_convention' => 'adobe',
],
'pandadoc' => [
'enabled' => true,
'field_naming_convention' => 'pandadoc',
],
],
/*
|--------------------------------------------------------------------------
| UI Configuration
|--------------------------------------------------------------------------
|
| Configure the UI elements of the PDF viewer and editor.
|
*/
'ui' => [
// Available zoom levels for the PDF viewer
'zoom_levels' => [0.5, 0.75, 1, 1.25, 1.5, 2],
// Default zoom level
'default_zoom' => 1,
// Show page thumbnails
'show_thumbnails' => true,
// Show toolbar
'show_toolbar' => true,
// Show sidebar
'show_sidebar' => true,
],
/*
|--------------------------------------------------------------------------
| Form Field Types
|--------------------------------------------------------------------------
|
| Configure the available form field types.
|
*/
'field_types' => [
'text' => [
'label' => 'Text Field',
'icon' => 'text-field',
'properties' => [
'font_size' => 12,
'font_family' => 'Helvetica',
'text_color' => '#000000',
'background_color' => '#FFFFFF',
'border_color' => '#000000',
'border_width' => 1,
'border_style' => 'solid',
'placeholder' => '',
'max_length' => null,
],
],
'checkbox' => [
'label' => 'Checkbox',
'icon' => 'checkbox',
'properties' => [
'checked' => false,
'background_color' => '#FFFFFF',
'border_color' => '#000000',
'border_width' => 1,
'border_style' => 'solid',
],
],
'radio' => [
'label' => 'Radio Button',
'icon' => 'radio',
'properties' => [
'checked' => false,
'background_color' => '#FFFFFF',
'border_color' => '#000000',
'border_width' => 1,
'border_style' => 'solid',
],
],
'dropdown' => [
'label' => 'Dropdown',
'icon' => 'dropdown',
'properties' => [
'options' => [],
'selected' => null,
'font_size' => 12,
'font_family' => 'Helvetica',
'text_color' => '#000000',
'background_color' => '#FFFFFF',
'border_color' => '#000000',
'border_width' => 1,
'border_style' => 'solid',
],
],
'signature' => [
'label' => 'Signature',
'icon' => 'signature',
'properties' => [
'background_color' => '#FFFFFF',
'border_color' => '#000000',
'border_width' => 1,
'border_style' => 'solid',
],
],
'date' => [
'label' => 'Date',
'icon' => 'date',
'properties' => [
'format' => 'YYYY-MM-DD',
'font_size' => 12,
'font_family' => 'Helvetica',
'text_color' => '#000000',
'background_color' => '#FFFFFF',
'border_color' => '#000000',
'border_width' => 1,
'border_style' => 'solid',
],
],
],
];

View File

@@ -15,7 +15,7 @@ return new class extends Migration
$table->id();
$table->string('name');
$table->foreignId('project_id')->constrained();
$table->foreignId('creator_id')->constrained('users');
//$table->foreignId('creator_id')->constrained('users');
$table->timestamps();
});
}

View File

@@ -13,10 +13,23 @@ return new class extends Migration
{
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('code');
$table->string('name');
$table->enum('status', ['pending', 'in_review', 'approved', 'rejected'])->default('pending');
$table->foreignId('project_id')->constrained();
$table->foreignId('folder_id')->nullable()->constrained();
$table->string('area', 2)->nullable();
$table->string('discipline', 2)->nullable();
$table->string('document_type', 3)->nullable();
$table->string('version')->nullable()->default("0");
$table->string('revision')->nullable()->default("0");
$table->enum('status', [0, 1, 2, 3, 4])->default(0); //['pending', 'in_review', 'approved', 'rejected']
$table->string('issuer_id')->nullable();
$table->date('entry_date')->nullable()->default(now());
//$table->foreignId('current_version_id')->nullable()->constrained('document_versions');
$table->timestamps();
});

View File

@@ -15,11 +15,19 @@ return new class extends Migration
$table->id();
$table->foreignId('document_id')->constrained();
$table->string('file_path');
$table->string('hash');
$table->string('hash'); // SHA256 hash for integrity verification
$table->unsignedInteger('version')->default(1);
$table->unsignedInteger('review')->default(0);
$table->foreignId('user_id')->constrained();
$table->text('changes')->nullable();
$table->timestamps();
// Unique constraint to ensure one version per document
$table->unique(['document_id', 'version', 'review']);
// Index for faster queries
$table->index(['document_id', 'version']);
$table->index('hash');
});
}

View File

@@ -12,6 +12,7 @@ return new class extends Migration
public function up(): void
{
Schema::table('projects', function (Blueprint $table) {
$table->string('reference', 12)->nullable()->after('id')->uniqidue();
$table->string('status')->nullable();
$table->string('project_image_path')->nullable();
$table->string('address')->nullable();
@@ -33,6 +34,7 @@ return new class extends Migration
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn('reference');
$table->dropColumn('status');
$table->dropColumn('project_image_path');
$table->dropColumn('address');

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->string('file_path')->require();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn('file_path');
});
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('document_comments', function (Blueprint $table) {
$table->id();
$table->foreignId('document_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->foreignId('parent_id')->nullable()->constrained('document_comments');
$table->text('content');
$table->unsignedInteger('page');
$table->decimal('x', 5, 3); // Posición X normalizada (0-1)
$table->decimal('y', 5, 3); // Posición Y normalizada (0-1)
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('document_comments');
}
};

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->foreignId('user_id')->constrained();
$table->foreignId('document_id')->constrained();
$table->foreignId('parent_id')->nullable()->constrained('comments');
$table->unsignedInteger('page');
$table->decimal('x', 5, 3); // Posición X (0-1)
$table->decimal('y', 5, 3); // Posición Y (0-1)
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('comments');
}
};

View File

@@ -0,0 +1,39 @@
<?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('companies', function (Blueprint $table) {
$table->id();
$table->string('name'); // Nombre legal
$table->string('commercial_name'); // Apodo comercial
$table->enum('status', ['active', 'closed'])->default('active');
$table->string('address')->nullable();
$table->string('postal_code')->nullable();
$table->string('city')->nullable();
$table->string('country')->nullable();
$table->string('phone')->nullable();
$table->string('email')->nullable();
$table->string('cif')->nullable(); // Código de identificación fiscal
$table->string('logo')->nullable(); // Ruta del logo
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('companies');
}
};

View File

@@ -0,0 +1,32 @@
<?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('company_contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('company_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('position')->nullable();
$table->timestamps();
$table->unique(['company_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('company_contacts');
}
};

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// 1. Corregir tipo de dato para 'type' (boolean en lugar de boolval)
$table->unsignedSmallInteger('user_type')->default(false)->after('remember_token');
// 2. Agregar company_id como nullable con constrained correcto
$table->foreignId('company_id')
->nullable()
->after('user_type')
->constrained('companies'); // Especificar tabla explícitamente
});
Schema::table('projects', function (Blueprint $table) {
$table->foreignId('company_id')
->constrained('companies');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
// 5. Eliminar restricción de clave foránea primero
$table->dropForeign(['company_id']);
// 6. Eliminar columnas en orden inverso
$table->dropColumn('company_id');
$table->dropColumn('user_type');
});
Schema::table('projects', function (Blueprint $table) {
// 7. Eliminar restricción antes de la columna
$table->dropForeign(['company_id']);
$table->dropColumn('company_id');
});
}
};

View File

@@ -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');
}
};

View File

@@ -13,6 +13,7 @@ class PermissionSeeder extends Seeder
*/
public function run()
{
/*
$permissions = [
// Permissions for Projects
'create projects',
@@ -49,6 +50,22 @@ class PermissionSeeder extends Seeder
'name' => $permission,
'guard_name' => 'web'
]);
}*/
$permissions = [
'user' => ['view', 'create', 'edit', 'delete'],
'document' => ['view', 'upload', 'edit', 'delete', 'approve'],
'project' => ['view', 'create', 'edit', 'delete'],
'comment' => ['create', 'edit', 'delete'],
'folder' => ['view', 'create', 'edit', 'delete'],
'role' => ['view', 'create', 'edit', 'delete'],
'permission' => ['view', 'create', 'edit', 'delete'],
];
foreach ($permissions as $type => $actions) {
foreach ($actions as $action) {
Permission::create(['name' => "{$type}.{$action}"]);
}
}
}
}

View File

@@ -23,38 +23,9 @@ class RolePermissionSeeder extends Seeder
//['description' => 'Administrador del sistema']
);
// Obtener o crear todos los permisos existentes
$permissions = Permission::all();
if ($permissions->isEmpty()) {
// Crear permisos básicos si no existen
$permissions = collect([
'view projects',
'edit projects',
'delete projects',
'view roles',
'create roles',
'edit roles',
'delete roles',
'view permissions',
'create permissions',
'edit permissions',
'delete permissions',
'assign permissions',
'revoke permissions',
])->map(function ($permission) {
return Permission::updateOrCreate(
['name' => $permission],
['guard_name' => 'web']
);
});
}
// Sincronizar todos los permisos con el rol admin
$allPermissions = Permission::all();
$adminRole->syncPermissions($allPermissions);
$adminRole->syncPermissions($permissions);
$adminEmail = env('ADMIN_EMAIL', 'admin@example.com');
$user = User::where('email', $adminEmail)->first();

View File

@@ -0,0 +1,93 @@
# ISO 19650 Project Naming Convention
## Overview
This document outlines the naming convention for projects based on the ISO 19650 standard. The naming convention ensures consistency, clarity, and compliance with industry standards.
## Naming Fields
The naming convention consists of the following fields:
| Field | Definition | Requirement | Length |
|--------------------|---------------------------------------------------------------------------|-------------------|--------------|
| **Proyecto** | Identificador del expediente, contrato o proyecto | Requerido | 2-12 |
| **Creador** | Organización creadora del documento | Requerido | 3-6 |
| **Volumen o Sistema** | Agrupaciones, áreas o tramos representativos en los que se fragmenta el proyecto | Requerido | 2-3 |
| **Nivel o Localización** | Localización dentro de un Volumen o Sistema | Requerido | 3 |
| **Tipo de Documento** | Tipología de documento, entregable o auxiliar | Requerido | 3 |
| **Disciplina** | Ámbito al que se corresponde el documento | Requerido | 3 |
| **Número** | Enumerador de partes | Requerido | 3 |
| **Descripción** | Texto que describe el documento y su contenido | Opcional | Sin límite |
| **Estado** | Situación, temporal o definitiva, del documento | Opcional/Metadato | 2 |
| **Revisión** | Versión del documento | Opcional/Metadato | 4 |
## Field Details
### Proyecto
- **Definition**: Identifies the project, contract, or file.
- **Requirement**: Required
- **Length**: 2-12 characters
### Creador
- **Definition**: Organization responsible for creating the document.
- **Requirement**: Required
- **Length**: 3-6 characters
### Volumen o Sistema
- **Definition**: Representative groupings, areas, or sections of the project.
- **Requirement**: Required
- **Length**: 2-3 characters
### Nivel o Localización
- **Definition**: Location within a Volume or System.
- **Requirement**: Required
- **Length**: 3 characters
### Tipo de Documento
- **Definition**: Document type, deliverable, or auxiliary.
- **Requirement**: Required
- **Length**: 3 characters
### Disciplina
- **Definition**: Scope to which the document corresponds.
- **Requirement**: Required
- **Length**: 3 characters
### Número
- **Definition**: Part enumerator.
- **Requirement**: Required
- **Length**: 3 characters
### Descripción
- **Definition**: Text describing the document and its content.
- **Requirement**: Optional
- **Length**: Unlimited
### Estado
- **Definition**: Temporary or definitive status of the document.
- **Requirement**: Optional/Metadata
- **Length**: 2 characters
### Revisión
- **Definition**: Document version.
- **Requirement**: Optional/Metadata
- **Length**: 4 characters
## Example
A sample project name following this convention:
```
PRJ001-ORG01-V01-L01-DOC-ARC-001-Description-ST-0001
```
Where:
- **PRJ001**: Project identifier
- **ORG01**: Creator organization
- **V01**: Volume
- **L01**: Level
- **DOC**: Document type
- **ARC**: Discipline
- **001**: Part number
- **Description**: Description of the document
- **ST**: Status
- **0001**: Revision
## Notes
- All fields marked as "Required" must be included.
- Optional fields can be omitted if not applicable.

View File

@@ -0,0 +1,80 @@
# Project Naming Code Generator
## Overview
The `ProjectNamingSchema` class provides a utility to generate project names based on the ISO 19650 naming convention. This ensures consistency and compliance with industry standards.
## Installation
Ensure the `ProjectNamingSchema` class is located in the `App\Helpers` namespace. No additional installation steps are required.
## Usage
To generate a project name, use the `generate` method of the `ProjectNamingSchema` class. This method accepts an associative array of fields and returns the generated project name.
### Example
```php
use App\Helpers\ProjectNamingSchema;
$fields = [
'project' => 'PRJ001',
'creator' => 'ORG01',
'volume' => 'V01',
'level' => 'L01',
'documentType' => 'DOC',
'discipline' => 'ARC',
'number' => '1',
'description' => 'Description',
'status' => 'ST',
'revision' => '1',
];
$projectName = ProjectNamingSchema::generate($fields);
// Output: PRJ001-ORG01-V01-L01-DOC-ARC-001-Description-ST-0001
```
## Fields
The following fields are supported:
| Field | Definition | Requirement | Length |
|--------------------|---------------------------------------------------------------------------|-------------------|--------------|
| **project** | Identifies the project, contract, or file. | Required | 2-12 |
| **creator** | Organization responsible for creating the document. | Required | 3-6 |
| **volume** | Representative groupings, areas, or sections of the project. | Required | 2-3 |
| **level** | Location within a Volume or System. | Required | 3 |
| **documentType** | Document type, deliverable, or auxiliary. | Required | 3 |
| **discipline** | Scope to which the document corresponds. | Required | 3 |
| **number** | Part enumerator. | Required | 3 |
| **description** | Text describing the document and its content. | Optional | Unlimited |
| **status** | Temporary or definitive status of the document. | Optional | 2 |
| **revision** | Document version. | Optional | 4 |
## Validation
The `generate` method validates that all required fields are provided. If any required field is missing, an `InvalidArgumentException` is thrown.
### Example
```php
$fields = [
'creator' => 'ORG01',
'volume' => 'V01',
'level' => 'L01',
'documentType' => 'DOC',
'discipline' => 'ARC',
'number' => '1',
];
try {
$projectName = ProjectNamingSchema::generate($fields);
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // Output: The field 'project' is required.
}
```
## Testing
Unit tests for the `ProjectNamingSchema` class are located in the `tests/Unit/ProjectNamingSchemaTest.php` file. Run the tests using the following command:
```bash
vendor\bin\pest --filter ProjectNamingSchemaTest
```
## Notes
- All fields marked as "Required" must be included.
- Optional fields can be omitted if not applicable.

1626
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,10 @@
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"concurrently": "^9.0.1",
"fabric": "^6.7.1",
"laravel-vite-plugin": "^1.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.2.133",
"quill": "^2.0.3",
"tailwindcss": "^4.0.7",
"vite": "^6.0"

View File

@@ -83,3 +83,20 @@ select:focus[data-flux-control] {
.btn-primary {
@apply inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 active:bg-blue-900 focus:outline-none focus:border-blue-900 focus:ring focus:ring-blue-300 disabled:opacity-25 transition;
}
.modal-enter {
opacity: 0;
}
.modal-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.modal-exit {
opacity: 1;
}
.modal-exit-active {
opacity: 0;
transition: opacity 200ms;
}

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More