Sistema de archivos multimedia: MediaManager, checkbox imágenes en mapa, modal visor, subida por feature/proyecto
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\Attributes\On;
|
||||
use App\Models\Media;
|
||||
use App\Models\Project;
|
||||
use App\Models\Phase;
|
||||
use App\Models\Layer;
|
||||
use App\Models\Feature;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MediaManager extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
// Polimórfico: a qué entidad pertenece
|
||||
public $mediableType;
|
||||
public $mediableId;
|
||||
|
||||
public $entity; // instancia cargada
|
||||
public $mediaItems = [];
|
||||
|
||||
// Subida
|
||||
public $uploadFiles = [];
|
||||
public $uploadDescription = '';
|
||||
public $uploadCategory = 'image';
|
||||
|
||||
// Modal visor
|
||||
public $showViewer = false;
|
||||
public $viewingMedia = null;
|
||||
|
||||
protected $rules = [
|
||||
'uploadFiles.*' => 'required|file|max:102400', // 100MB total
|
||||
'uploadDescription' => 'nullable|string|max:500',
|
||||
'uploadCategory' => 'required|in:image,document,other',
|
||||
];
|
||||
|
||||
protected $messages = [
|
||||
'uploadFiles.*.max' => 'Cada archivo debe pesar menos de 100MB.',
|
||||
];
|
||||
|
||||
public function mount($mediableType, $mediableId)
|
||||
{
|
||||
$this->mediableType = $mediableType;
|
||||
$this->mediableId = $mediableId;
|
||||
|
||||
$this->entity = $mediableType::findOrFail($mediableId);
|
||||
$this->loadMedia();
|
||||
}
|
||||
|
||||
public function loadMedia()
|
||||
{
|
||||
$this->mediaItems = Media::where('mediable_type', $this->mediableType)
|
||||
->where('mediable_id', $this->mediableId)
|
||||
->with('uploader')
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
|
||||
public function upload()
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (!$user->can('upload layers') && !$user->hasRole('Admin')) {
|
||||
session()->flash('error', 'Sin permisos.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
|
||||
if (empty($this->uploadFiles)) {
|
||||
session()->flash('error', 'Selecciona al menos un archivo.');
|
||||
return;
|
||||
}
|
||||
|
||||
$uploaded = 0;
|
||||
foreach ($this->uploadFiles as $file) {
|
||||
$mime = $file->getMimeType();
|
||||
$ext = $file->getClientOriginalExtension();
|
||||
$size = $file->getSize();
|
||||
$name = $file->getClientOriginalName();
|
||||
|
||||
// Determinar categoría automática
|
||||
$category = $this->uploadCategory;
|
||||
if (str_starts_with($mime, 'image/')) {
|
||||
$category = 'image';
|
||||
} elseif (in_array($mime, ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'])) {
|
||||
$category = 'document';
|
||||
}
|
||||
|
||||
// Guardar en disco
|
||||
$entityType = class_basename($this->entity);
|
||||
$dir = "uploads/{$entityType}s/{$this->mediableId}/media";
|
||||
$path = $file->store($dir, 'public');
|
||||
|
||||
Media::create([
|
||||
'mediable_type' => $this->mediableType,
|
||||
'mediable_id' => $this->mediableId,
|
||||
'name' => $name,
|
||||
'file_path' => $path,
|
||||
'file_type' => $mime,
|
||||
'file_extension' => $ext,
|
||||
'file_size' => $size,
|
||||
'category' => $category,
|
||||
'description' => $this->uploadDescription,
|
||||
'uploaded_by' => $user->id,
|
||||
]);
|
||||
|
||||
$uploaded++;
|
||||
}
|
||||
|
||||
$this->reset(['uploadFiles', 'uploadDescription']);
|
||||
$this->loadMedia();
|
||||
|
||||
// Notificar al mapa si corresponde
|
||||
$this->dispatch('mediaUploaded', [
|
||||
'mediableType' => $this->mediableType,
|
||||
'mediableId' => $this->mediableId,
|
||||
]);
|
||||
|
||||
session()->flash('message', "$uploaded archivo(s) subido(s) correctamente.");
|
||||
}
|
||||
|
||||
public function deleteMedia($mediaId)
|
||||
{
|
||||
$media = Media::findOrFail($mediaId);
|
||||
|
||||
$user = Auth::user();
|
||||
if (!$user->hasRole('Admin') && $media->uploaded_by !== $user->id) {
|
||||
session()->flash('error', 'No puedes borrar archivos de otro usuario.');
|
||||
return;
|
||||
}
|
||||
|
||||
$media->delete();
|
||||
$this->loadMedia();
|
||||
session()->flash('message', 'Archivo eliminado.');
|
||||
}
|
||||
|
||||
public function viewMedia($mediaId)
|
||||
{
|
||||
$media = Media::findOrFail($mediaId);
|
||||
if (!$media->is_image) {
|
||||
// Si no es imagen, abrir en nueva pestaña
|
||||
$this->dispatch('openUrl', $media->url);
|
||||
return;
|
||||
}
|
||||
$this->viewingMedia = $media;
|
||||
$this->showViewer = true;
|
||||
}
|
||||
|
||||
public function closeViewer()
|
||||
{
|
||||
$this->showViewer = false;
|
||||
$this->viewingMedia = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.media-manager', [
|
||||
'entityName' => class_basename($this->entity) . ': ' . ($this->entity->name ?? $this->entity->id),
|
||||
'images' => $this->mediaItems->filter(fn($m) => $m->is_image),
|
||||
'documents' => $this->mediaItems->filter(fn($m) => !$m->is_image),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ class ProjectMap extends Component
|
||||
public $inspectionFormData = [];
|
||||
public $inspectionHistory = [];
|
||||
|
||||
// Imágenes en mapa
|
||||
public $showFeatureImages = false;
|
||||
public $featureImageMarkers = [];
|
||||
|
||||
public function mount(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
@@ -246,6 +250,64 @@ class ProjectMap extends Component
|
||||
$this->resetInspectionForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle mostrar imágenes en el mapa.
|
||||
*/
|
||||
public function toggleFeatureImages()
|
||||
{
|
||||
$this->showFeatureImages = !$this->showFeatureImages;
|
||||
$this->loadFeatureImageMarkers();
|
||||
$this->dispatch('featureImagesToggled', $this->showFeatureImages, $this->featureImageMarkers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cargar marcadores de imágenes para el mapa.
|
||||
*/
|
||||
public function loadFeatureImageMarkers()
|
||||
{
|
||||
if (!$this->showFeatureImages) {
|
||||
$this->featureImageMarkers = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$markers = [];
|
||||
foreach ($this->phases as $phase) {
|
||||
foreach ($phase->layers as $layer) {
|
||||
foreach ($layer->features as $feature) {
|
||||
$image = $feature->images()->first();
|
||||
if ($image) {
|
||||
$geo = $feature->geometry;
|
||||
$coords = null;
|
||||
if ($geo && isset($geo['coordinates'])) {
|
||||
if ($geo['type'] === 'Point') {
|
||||
$coords = [
|
||||
'lat' => $geo['coordinates'][1],
|
||||
'lng' => $geo['coordinates'][0],
|
||||
];
|
||||
} elseif (in_array($geo['type'], ['Polygon', 'LineString'])) {
|
||||
$coords = [
|
||||
'lat' => $geo['coordinates'][0][1] ?? null,
|
||||
'lng' => $geo['coordinates'][0][0] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($coords && $coords['lat'] && $coords['lng']) {
|
||||
$markers[] = [
|
||||
'feature_id' => $feature->id,
|
||||
'name' => $feature->name,
|
||||
'lat' => $coords['lat'],
|
||||
'lng' => $coords['lng'],
|
||||
'image_url' => $image->url,
|
||||
'image_name' => $image->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->featureImageMarkers = $markers;
|
||||
}
|
||||
|
||||
public function toggleFullscreen()
|
||||
{
|
||||
$this->formFullscreen = !$this->formFullscreen;
|
||||
|
||||
@@ -29,4 +29,14 @@ class Feature extends Model
|
||||
{
|
||||
return $this->hasMany(Inspection::class, 'feature_id');
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable');
|
||||
}
|
||||
|
||||
public function images()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable')->where('category', 'image');
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,9 @@ class Layer extends Model
|
||||
{
|
||||
return $this->hasMany(Feature::class);
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Media extends Model
|
||||
{
|
||||
protected $table = 'media';
|
||||
|
||||
protected $fillable = [
|
||||
'mediable_type', 'mediable_id',
|
||||
'name', 'file_path', 'file_type', 'file_extension', 'file_size',
|
||||
'category', 'description', 'metadata', 'uploaded_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'file_size' => 'integer',
|
||||
];
|
||||
|
||||
// Relación polimórfica: pertenece a Project, Phase, Layer o Feature
|
||||
public function mediable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function uploader()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'uploaded_by');
|
||||
}
|
||||
|
||||
// Helper: URL pública del archivo
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return Storage::url($this->file_path);
|
||||
}
|
||||
|
||||
// Helper: si es imagen
|
||||
public function getIsImageAttribute()
|
||||
{
|
||||
return str_starts_with($this->file_type, 'image/');
|
||||
}
|
||||
|
||||
// Helper: tamaño formateado
|
||||
public function getFormattedSizeAttribute()
|
||||
{
|
||||
$bytes = $this->file_size;
|
||||
if ($bytes >= 1073741824) return round($bytes / 1073741824, 2) . ' GB';
|
||||
if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB';
|
||||
if ($bytes >= 1024) return round($bytes / 1024) . ' KB';
|
||||
return $bytes . ' B';
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeImages($query)
|
||||
{
|
||||
return $query->where('category', 'image');
|
||||
}
|
||||
|
||||
public function scopeDocuments($query)
|
||||
{
|
||||
return $query->where('category', 'document');
|
||||
}
|
||||
|
||||
// Boot: borrar archivo físico al eliminar registro
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($media) {
|
||||
Storage::delete($media->file_path);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,14 @@ class Phase extends Model
|
||||
{
|
||||
return $this->hasManyThrough(Feature::class, Layer::class);
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable');
|
||||
}
|
||||
|
||||
public function images()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable')->where('category', 'image');
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,16 @@ class Project extends Model
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable');
|
||||
}
|
||||
|
||||
public function images()
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable')->where('category', 'image');
|
||||
}
|
||||
|
||||
// Scope to filter accessible projects for non-admin users
|
||||
public function scopeAccessibleBy($query, User $user)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user