Compare commits
8 Commits
f97a7a8498
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 047e155238 | |||
| e42ce8b092 | |||
| 7b00887372 | |||
| 88e526cf6c | |||
| d8ae8c8894 | |||
| 28c225687a | |||
| 19fa52ca65 | |||
| 655ea60d6b |
69
app/Helpers/FileHelper.php
Normal file
69
app/Helpers/FileHelper.php
Normal 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>';
|
||||
}
|
||||
}
|
||||
53
app/Http/Controllers/CompanyContactController.php
Normal file
53
app/Http/Controllers/CompanyContactController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
143
app/Http/Controllers/CompanyController.php
Normal file
143
app/Http/Controllers/CompanyController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
82
app/Http/Controllers/DocumentCommentController.php
Normal file
82
app/Http/Controllers/DocumentCommentController.php
Normal 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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', [
|
||||
|
||||
223
app/Http/Controllers/ProjectSettingsController.php
Normal file
223
app/Http/Controllers/ProjectSettingsController.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectCodingConfig;
|
||||
use App\Models\ProjectDocumentStatus;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ProjectSettingsController extends Controller
|
||||
{
|
||||
public function index(Project $project)
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
//$project->load(['codingConfig', 'documentStatuses']);
|
||||
|
||||
return view('project-settings.index', compact('project'));
|
||||
}
|
||||
|
||||
public function updateCoding(Request $request, Project $project)
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
$validated = $request->validate([
|
||||
'format' => 'required|string|max:255',
|
||||
'year_format' => 'required|in:Y,y,Yy,yy,Y-m,Y/m,y-m,y/m',
|
||||
'separator' => 'required|string|max:5',
|
||||
'sequence_length' => 'required|integer|min:1|max:10',
|
||||
'auto_generate' => 'boolean',
|
||||
'elements' => 'nullable|array',
|
||||
'reset_sequence' => 'boolean',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$codingConfig = $project->codingConfig ?: new ProjectCodingConfig();
|
||||
$codingConfig->project_id = $project->id;
|
||||
|
||||
$codingConfig->fill([
|
||||
'format' => $validated['format'],
|
||||
'year_format' => $validated['year_format'],
|
||||
'separator' => $validated['separator'],
|
||||
'sequence_length' => $validated['sequence_length'],
|
||||
'auto_generate' => $validated['auto_generate'] ?? false,
|
||||
'elements' => $validated['elements'] ?? [],
|
||||
]);
|
||||
|
||||
if ($request->boolean('reset_sequence')) {
|
||||
$codingConfig->next_sequence = 1;
|
||||
}
|
||||
|
||||
$codingConfig->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('project-settings.index', $project)
|
||||
->with('success', 'Configuración de codificación actualizada correctamente.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return redirect()->back()
|
||||
->with('error', 'Error al actualizar la configuración: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function storeStatus(Request $request, Project $project)
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'color' => 'required|string|max:7',
|
||||
'text_color' => 'nullable|string|max:7',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'allow_upload' => 'boolean',
|
||||
'allow_edit' => 'boolean',
|
||||
'allow_delete' => 'boolean',
|
||||
'requires_approval' => 'boolean',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Si se marca como default, quitar el default de otros estados
|
||||
if ($validated['is_default'] ?? false) {
|
||||
$project->documentStatuses()->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
$status = new ProjectDocumentStatus($validated);
|
||||
$status->project_id = $project->id;
|
||||
$status->slug = Str::slug($validated['name']);
|
||||
|
||||
// Verificar que el slug sea único
|
||||
$counter = 1;
|
||||
$originalSlug = $status->slug;
|
||||
while ($project->documentStatuses()->where('slug', $status->slug)->exists()) {
|
||||
$status->slug = $originalSlug . '-' . $counter;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
$status->save();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('project-settings.index', $project)
|
||||
->with('success', 'Estado creado correctamente.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return redirect()->back()
|
||||
->with('error', 'Error al crear el estado: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request, Project $project, ProjectDocumentStatus $status)
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
if ($status->project_id !== $project->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'color' => 'required|string|max:7',
|
||||
'text_color' => 'nullable|string|max:7',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'allow_upload' => 'boolean',
|
||||
'allow_edit' => 'boolean',
|
||||
'allow_delete' => 'boolean',
|
||||
'requires_approval' => 'boolean',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Si se marca como default, quitar el default de otros estados
|
||||
if ($validated['is_default'] ?? false) {
|
||||
$project->documentStatuses()
|
||||
->where('id', '!=', $status->id)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
$status->update($validated);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return redirect()->route('project-settings.index', $project)
|
||||
->with('success', 'Estado actualizado correctamente.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return redirect()->back()
|
||||
->with('error', 'Error al actualizar el estado: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyStatus(Project $project, ProjectDocumentStatus $status)
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
if ($status->project_id !== $project->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
// Verificar que no haya documentos con este estado
|
||||
if ($status->documents()->exists()) {
|
||||
return redirect()->back()
|
||||
->with('error', 'No se puede eliminar el estado porque hay documentos asociados.');
|
||||
}
|
||||
|
||||
// Si es el estado por defecto, establecer otro como default
|
||||
if ($status->is_default) {
|
||||
$newDefault = $project->documentStatuses()
|
||||
->where('id', '!=', $status->id)
|
||||
->first();
|
||||
|
||||
if ($newDefault) {
|
||||
$newDefault->update(['is_default' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
$status->delete();
|
||||
|
||||
return redirect()->route('project-settings.index', $project)
|
||||
->with('success', 'Estado eliminado correctamente.');
|
||||
}
|
||||
|
||||
public function reorderStatuses(Request $request, Project $project)
|
||||
{
|
||||
$this->authorize('update', $project);
|
||||
|
||||
$request->validate([
|
||||
'order' => 'required|array',
|
||||
'order.*' => 'exists:project_document_statuses,id',
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
foreach ($request->order as $index => $statusId) {
|
||||
$status = ProjectDocumentStatus::find($statusId);
|
||||
if ($status && $status->project_id === $project->id) {
|
||||
$status->update(['order' => $index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json(['success' => true]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
app/Http/Services/PdfProcessor.php
Normal file
28
app/Http/Services/PdfProcessor.php
Normal 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
251
app/Livewire/CodeEdit.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
37
app/Livewire/PdfViewer.php
Normal file
37
app/Livewire/PdfViewer.php
Normal 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');
|
||||
}
|
||||
}
|
||||
273
app/Livewire/ProjectDocumentList.php
Normal file
273
app/Livewire/ProjectDocumentList.php
Normal 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');
|
||||
}
|
||||
}
|
||||
317
app/Livewire/ProjectDocumentStatusManager.php
Normal file
317
app/Livewire/ProjectDocumentStatusManager.php
Normal 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');
|
||||
}
|
||||
}
|
||||
274
app/Livewire/ProjectNameCoder.php
Normal file
274
app/Livewire/ProjectNameCoder.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
36
app/Models/Comment.php
Normal 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
65
app/Models/Company.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
22
app/Models/DocumentComment.php
Normal file
22
app/Models/DocumentComment.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ class Folder extends Model
|
||||
'name',
|
||||
'parent_id',
|
||||
'project_id',
|
||||
'icon',
|
||||
'color',
|
||||
'description',
|
||||
//'icon',
|
||||
//'color',
|
||||
//'description',
|
||||
];
|
||||
|
||||
public function descendants()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
107
app/Models/ProjectCodingConfig.php
Normal file
107
app/Models/ProjectCodingConfig.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ProjectCodingConfig extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'format',
|
||||
'elements',
|
||||
'next_sequence',
|
||||
'year_format',
|
||||
'separator',
|
||||
'sequence_length',
|
||||
'auto_generate'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'elements' => 'array',
|
||||
'auto_generate' => 'boolean',
|
||||
];
|
||||
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
// Método para generar un código de documento
|
||||
public function generateCode($type = 'DOC', $year = null)
|
||||
{
|
||||
$code = $this->format;
|
||||
|
||||
// Reemplazar variables
|
||||
$replacements = [
|
||||
'[PROJECT]' => $this->project->code ?? 'PROJ',
|
||||
'[TYPE]' => $type,
|
||||
'[YEAR]' => $year ?? date($this->year_format),
|
||||
'[MONTH]' => date('m'),
|
||||
'[DAY]' => date('d'),
|
||||
'[SEQUENCE]' => str_pad($this->next_sequence, $this->sequence_length, '0', STR_PAD_LEFT),
|
||||
'[RANDOM]' => strtoupper(substr(md5(uniqid()), 0, 6)),
|
||||
];
|
||||
|
||||
// Si hay elementos personalizados, agregarlos
|
||||
if (is_array($this->elements)) {
|
||||
foreach ($this->elements as $key => $value) {
|
||||
$replacements["[{$key}]"] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$code = str_replace(array_keys($replacements), array_values($replacements), $code);
|
||||
|
||||
// Incrementar secuencia
|
||||
if (strpos($this->format, '[SEQUENCE]') !== false && $this->auto_generate) {
|
||||
$this->increment('next_sequence');
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
// Método para obtener elementos disponibles
|
||||
public static function getAvailableElements()
|
||||
{
|
||||
return [
|
||||
'project' => [
|
||||
'label' => 'Código del Proyecto',
|
||||
'description' => 'Código único del proyecto',
|
||||
'variable' => '[PROJECT]',
|
||||
],
|
||||
'type' => [
|
||||
'label' => 'Tipo de Documento',
|
||||
'description' => 'Tipo de documento (DOC, IMG, PDF, etc.)',
|
||||
'variable' => '[TYPE]',
|
||||
],
|
||||
'year' => [
|
||||
'label' => 'Año',
|
||||
'description' => 'Año actual en formato configurable',
|
||||
'variable' => '[YEAR]',
|
||||
],
|
||||
'month' => [
|
||||
'label' => 'Mes',
|
||||
'description' => 'Mes actual (01-12)',
|
||||
'variable' => '[MONTH]',
|
||||
],
|
||||
'day' => [
|
||||
'label' => 'Día',
|
||||
'description' => 'Día actual (01-31)',
|
||||
'variable' => '[DAY]',
|
||||
],
|
||||
'sequence' => [
|
||||
'label' => 'Secuencia',
|
||||
'description' => 'Número secuencial autoincremental',
|
||||
'variable' => '[SEQUENCE]',
|
||||
],
|
||||
'random' => [
|
||||
'label' => 'Aleatorio',
|
||||
'description' => 'Cadena aleatoria de 6 caracteres',
|
||||
'variable' => '[RANDOM]',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@ class DashboardPolicy
|
||||
|
||||
public function view(User $user)
|
||||
{
|
||||
return $user->hasPermissionTo('view dashboard');
|
||||
return true; //$user->hasPermissionTo('view.dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
80
app/Services/ProjectCodeService.php
Normal file
80
app/Services/ProjectCodeService.php
Normal 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'),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
543
app/Services/ProjectCodeValidator.php
Normal file
543
app/Services/ProjectCodeValidator.php
Normal 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";
|
||||
}
|
||||
*/
|
||||
30
app/View/Components/Accordion.php
Normal file
30
app/View/Components/Accordion.php
Normal 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;
|
||||
}
|
||||
}
|
||||
26
app/View/Components/AccordionItem.php
Normal file
26
app/View/Components/AccordionItem.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
831
composer.lock
generated
File diff suppressed because it is too large
Load Diff
214
config/livewire-pdf.php
Normal file
214
config/livewire-pdf.php
Normal 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',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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}"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
93
docs/ISO19650_Naming_Convention.md
Normal file
93
docs/ISO19650_Naming_Convention.md
Normal 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.
|
||||
80
docs/ProjectNamingCodeGenerator.md
Normal file
80
docs/ProjectNamingCodeGenerator.md
Normal 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
1626
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
177
resources/js/pdfjs-5.2.133-dist/LICENSE
Normal file
177
resources/js/pdfjs-5.2.133-dist/LICENSE
Normal 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
|
||||
22614
resources/js/pdfjs-5.2.133-dist/build/pdf.mjs
Normal file
22614
resources/js/pdfjs-5.2.133-dist/build/pdf.mjs
Normal file
File diff suppressed because it is too large
Load Diff
1
resources/js/pdfjs-5.2.133-dist/build/pdf.mjs.map
Normal file
1
resources/js/pdfjs-5.2.133-dist/build/pdf.mjs.map
Normal file
File diff suppressed because one or more lines are too long
216
resources/js/pdfjs-5.2.133-dist/build/pdf.sandbox.mjs
Normal file
216
resources/js/pdfjs-5.2.133-dist/build/pdf.sandbox.mjs
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
57734
resources/js/pdfjs-5.2.133-dist/build/pdf.worker.mjs
Normal file
57734
resources/js/pdfjs-5.2.133-dist/build/pdf.worker.mjs
Normal file
File diff suppressed because one or more lines are too long
1
resources/js/pdfjs-5.2.133-dist/build/pdf.worker.mjs.map
Normal file
1
resources/js/pdfjs-5.2.133-dist/build/pdf.worker.mjs.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-EUC-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-EUC-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-EUC-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-EUC-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-RKSJ-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78ms-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78ms-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78ms-RKSJ-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/78ms-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/83pv-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/83pv-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90ms-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90ms-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90ms-RKSJ-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90ms-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90msp-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90msp-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90msp-RKSJ-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90msp-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90pv-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90pv-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90pv-RKSJ-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/90pv-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-RKSJ-H.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-RKSJ-H.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-RKSJ-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-RKSJ-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-V.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Add-V.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-0.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-0.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-1.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-1.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-2.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-2.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-3.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-3.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-4.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-4.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-5.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-5.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-6.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-6.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-UCS2.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-CNS1-UCS2.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-0.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-0.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-1.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-1.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-2.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-2.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-3.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-3.bcmap
Normal file
Binary file not shown.
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-4.bcmap
Normal file
BIN
resources/js/pdfjs-5.2.133-dist/web/cmaps/Adobe-GB1-4.bcmap
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user