archivo de configuracion de nginx agregado para el puerto 9000
This commit is contained in:
@@ -10,7 +10,15 @@
|
|||||||
"Bash(ls resources/views/)",
|
"Bash(ls resources/views/)",
|
||||||
"Bash(npm run *)",
|
"Bash(npm run *)",
|
||||||
"Bash(mv database/migrations/2026_04_28_170416_create_post_likes_table.php database/migrations/2026_04_28_170418_create_post_likes_table.php)",
|
"Bash(mv database/migrations/2026_04_28_170416_create_post_likes_table.php database/migrations/2026_04_28_170418_create_post_likes_table.php)",
|
||||||
"Bash(mv database/migrations/2026_04_28_170417_create_post_comments_table.php database/migrations/2026_04_28_170419_create_post_comments_table.php)"
|
"Bash(mv database/migrations/2026_04_28_170417_create_post_comments_table.php database/migrations/2026_04_28_170419_create_post_comments_table.php)",
|
||||||
|
"Bash(xargs ls *)",
|
||||||
|
"Bash(php -l routes/web.php)",
|
||||||
|
"Bash(php -l app/Http/Controllers/TareaController.php)",
|
||||||
|
"Bash(php -l app/Http/Controllers/EntregaController.php)",
|
||||||
|
"Bash(php -l app/Http/Controllers/GrupoController.php)",
|
||||||
|
"Bash(php -l app/Models/Tarea.php)",
|
||||||
|
"Bash(php -l app/Models/Entrega.php)",
|
||||||
|
"Bash(php -l app/Livewire/GrupoDocente.php)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Anuncio;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class AnuncioController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$anuncios = Anuncio::with('author')->latest()->paginate(15);
|
||||||
|
return view('anuncio.index', compact('anuncios'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('anuncio.create', [
|
||||||
|
'types' => Anuncio::TYPES,
|
||||||
|
'roles' => Anuncio::ROLES,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'title' => 'required|string|max:200',
|
||||||
|
'body' => 'nullable|string|max:3000',
|
||||||
|
'image' => 'nullable|image|max:5120',
|
||||||
|
'type' => 'required|in:info,promocion,mantenimiento,aviso',
|
||||||
|
'audience' => 'required|array|min:1',
|
||||||
|
'audience.*' => 'string',
|
||||||
|
'expires_at' => 'nullable|date|after:now',
|
||||||
|
'active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data['user_id'] = auth()->id();
|
||||||
|
$data['active'] = $request->boolean('active', true);
|
||||||
|
$data['expires_at'] = $request->expires_at ?: null;
|
||||||
|
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
$data['image_path'] = $request->file('image')->store('anuncios', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($data['image']);
|
||||||
|
Anuncio::create($data);
|
||||||
|
|
||||||
|
return redirect()->route('anuncio.index')->with('success', 'Anuncio creado correctamente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Anuncio $anuncio)
|
||||||
|
{
|
||||||
|
return view('anuncio.edit', [
|
||||||
|
'anuncio' => $anuncio,
|
||||||
|
'types' => Anuncio::TYPES,
|
||||||
|
'roles' => Anuncio::ROLES,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Anuncio $anuncio)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'title' => 'required|string|max:200',
|
||||||
|
'body' => 'nullable|string|max:3000',
|
||||||
|
'image' => 'nullable|image|max:5120',
|
||||||
|
'type' => 'required|in:info,promocion,mantenimiento,aviso',
|
||||||
|
'audience' => 'required|array|min:1',
|
||||||
|
'audience.*' => 'string',
|
||||||
|
'expires_at' => 'nullable|date',
|
||||||
|
'active' => 'boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data['active'] = $request->boolean('active', true);
|
||||||
|
$data['expires_at'] = $request->expires_at ?: null;
|
||||||
|
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
if ($anuncio->image_path) {
|
||||||
|
Storage::disk('public')->delete($anuncio->image_path);
|
||||||
|
}
|
||||||
|
$data['image_path'] = $request->file('image')->store('anuncios', 'public');
|
||||||
|
} elseif ($request->boolean('remove_image') && $anuncio->image_path) {
|
||||||
|
Storage::disk('public')->delete($anuncio->image_path);
|
||||||
|
$data['image_path'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($data['image']);
|
||||||
|
$anuncio->update($data);
|
||||||
|
|
||||||
|
return redirect()->route('anuncio.index')->with('success', 'Anuncio actualizado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Anuncio $anuncio)
|
||||||
|
{
|
||||||
|
if ($anuncio->image_path) {
|
||||||
|
Storage::disk('public')->delete($anuncio->image_path);
|
||||||
|
}
|
||||||
|
$anuncio->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Anuncio eliminado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleActive(Anuncio $anuncio)
|
||||||
|
{
|
||||||
|
$anuncio->update(['active' => !$anuncio->active]);
|
||||||
|
return back()->with('success', $anuncio->active ? 'Anuncio activado.' : 'Anuncio desactivado.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,27 @@ use Illuminate\Support\Facades\Storage;
|
|||||||
|
|
||||||
class ChatController extends Controller
|
class ChatController extends Controller
|
||||||
{
|
{
|
||||||
// Lista de amigos con quienes puedo chatear
|
// Lista de conversaciones (amigos + historial con no-amigos)
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$me = auth()->user();
|
$me = auth()->user();
|
||||||
$friends = User::whereIn('id', $me->friendIds())->orderBy('name')->get();
|
|
||||||
return view('chat.index', compact('friends'));
|
// IDs de usuarios con quienes hay mensajes intercambiados
|
||||||
|
$sentTo = Message::where('user_id', $me->id)->pluck('receiver_id');
|
||||||
|
$receivedFrom = Message::where('receiver_id', $me->id)->pluck('user_id');
|
||||||
|
$conversationIds = $sentTo->merge($receivedFrom)->unique()->values();
|
||||||
|
|
||||||
|
// Amigos (con o sin historial de mensajes)
|
||||||
|
$friendIds = collect($me->friendIds());
|
||||||
|
$allIds = $conversationIds->merge($friendIds)->unique()->values();
|
||||||
|
|
||||||
|
$users = User::whereIn('id', $allIds)->orderBy('name')->get()
|
||||||
|
->map(function (User $user) use ($me) {
|
||||||
|
$user->is_friend = $me->isFriendWith($user);
|
||||||
|
return $user;
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('chat.index', compact('users'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mensajes entre yo y otro usuario
|
// Mensajes entre yo y otro usuario
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Alumno;
|
||||||
|
use App\Models\Entrega;
|
||||||
|
use App\Models\Tarea;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controller;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class EntregaController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$alumno = Alumno::whereHas('alumnos', fn($q) => $q->where('id', Auth::id()))->first();
|
||||||
|
|
||||||
|
if (!$alumno) {
|
||||||
|
return view('tarea.mis_tareas', ['tareas' => collect(), 'entregasMap' => collect()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$gruposIds = $alumno->grupos()->pluck('grupos.id');
|
||||||
|
|
||||||
|
$tareas = Tarea::whereIn('grupo_id', $gruposIds)
|
||||||
|
->with(['material', 'grupo'])
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$entregasMap = Entrega::where('alumno_id', $alumno->id)
|
||||||
|
->whereIn('tarea_id', $tareas->pluck('id'))
|
||||||
|
->get()
|
||||||
|
->keyBy('tarea_id');
|
||||||
|
|
||||||
|
return view('tarea.mis_tareas', compact('tareas', 'entregasMap', 'alumno'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request, Tarea $tarea)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'archivo' => 'nullable|file|max:20480',
|
||||||
|
'texto' => 'nullable|string|max:5000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$request->hasFile('archivo') && !filled($request->texto)) {
|
||||||
|
return back()->withErrors(['entrega' => 'Debes escribir un texto o adjuntar un archivo.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$alumno = Alumno::whereHas('alumnos', fn($q) => $q->where('id', Auth::id()))->firstOrFail();
|
||||||
|
|
||||||
|
$entrega = Entrega::where('tarea_id', $tarea->id)
|
||||||
|
->where('alumno_id', $alumno->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$entrega) {
|
||||||
|
$entrega = Entrega::create([
|
||||||
|
'tarea_id' => $tarea->id,
|
||||||
|
'alumno_id' => $alumno->id,
|
||||||
|
'status' => 'sin_subir',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entrega->status !== 'sin_subir') {
|
||||||
|
return back()->with('error', 'Ya entregaste esta tarea y no puedes volver a subirla.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = null;
|
||||||
|
if ($request->hasFile('archivo')) {
|
||||||
|
// Almacena en disco local (privado) igual que el FileController espera
|
||||||
|
$path = $request->file('archivo')->store("entregas/{$tarea->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$entrega->update([
|
||||||
|
'archivo' => $path,
|
||||||
|
'texto' => $request->texto,
|
||||||
|
'status' => 'pendiente',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('info', 'Tarea enviada correctamente. Queda pendiente de calificación.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Alumno;
|
use App\Models\Alumno;
|
||||||
|
use App\Models\Docente;
|
||||||
use App\Models\Grupo;
|
use App\Models\Grupo;
|
||||||
use App\Models\Plan;
|
use App\Models\Plan;
|
||||||
use App\Models\Plane;
|
use App\Models\Plane;
|
||||||
@@ -11,6 +12,7 @@ use App\Models\Turno;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Controller;
|
use Illuminate\Routing\Controller;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class GrupoController extends Controller
|
class GrupoController extends Controller
|
||||||
{
|
{
|
||||||
@@ -38,12 +40,55 @@ class GrupoController extends Controller
|
|||||||
$query->whereIn('plantel_id', $plantelesIds);
|
$query->whereIn('plantel_id', $plantelesIds);
|
||||||
})
|
})
|
||||||
->get();
|
->get();
|
||||||
|
} elseif ($user->hasRole('Docente')) {
|
||||||
|
|
||||||
|
$grupos = Grupo::with('plan')
|
||||||
|
->whereHas('docentes', function ($query) use ($user) {
|
||||||
|
$query->whereHas('docentes', function ($q) use ($user) {
|
||||||
|
$q->where('users.id', $user->id);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('grupo.index', compact('grupos'));
|
return view('grupo.index', compact('grupos'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function verMaterias(Grupo $grupo)
|
||||||
|
{
|
||||||
|
$asignaciones = DB::table('docente_grupo')
|
||||||
|
->where('docente_grupo.grupo_id', $grupo->id)
|
||||||
|
->join('materials', 'docente_grupo.materia_id', '=', 'materials.id')
|
||||||
|
->join('docentes', 'docente_grupo.docente_id', '=', 'docentes.id')
|
||||||
|
->leftJoin('docente_user', 'docentes.id', '=', 'docente_user.docente_id')
|
||||||
|
->leftJoin('users', 'docente_user.user_id', '=', 'users.id')
|
||||||
|
->select(
|
||||||
|
'materials.id as material_id',
|
||||||
|
'materials.name as material_name',
|
||||||
|
'materials.clave as material_clave',
|
||||||
|
'materials.ciclo',
|
||||||
|
'materials.horas',
|
||||||
|
'docentes.id as docente_id',
|
||||||
|
'users.name as docente_nombre',
|
||||||
|
'users.apellidoPaterno',
|
||||||
|
'users.apellidoMaterno'
|
||||||
|
)
|
||||||
|
->get()
|
||||||
|
->unique('material_id');
|
||||||
|
|
||||||
|
return view('grupo.ver_materias', compact('grupo', 'asignaciones'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function administrar(Grupo $grupo)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
$docente = Docente::whereHas('docentes', fn($q) => $q->where('id', $user->id))->firstOrFail();
|
||||||
|
$materiales = $docente->materias()->wherePivot('grupo_id', $grupo->id)->get();
|
||||||
|
|
||||||
|
return view('grupo.administrar', compact('grupo', 'materiales'));
|
||||||
|
}
|
||||||
|
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
$turnos= Turno::all();
|
$turnos= Turno::all();
|
||||||
|
|||||||
@@ -2,63 +2,148 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Alumno;
|
||||||
|
use App\Models\Docente;
|
||||||
|
use App\Models\Entrega;
|
||||||
|
use App\Models\Grupo;
|
||||||
|
use App\Models\Material;
|
||||||
|
use App\Models\Tarea;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Controller;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
class TareaController extends Controller
|
class TareaController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
public function index(Grupo $grupo, Material $material)
|
||||||
* Display a listing of the resource.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
{
|
||||||
//
|
$tareas = Tarea::where('grupo_id', $grupo->id)
|
||||||
|
->where('material_id', $material->id)
|
||||||
|
->withCount([
|
||||||
|
'entregas',
|
||||||
|
'entregas as pendientes_count' => fn($q) => $q->where('status', 'pendiente'),
|
||||||
|
'entregas as calificadas_count' => fn($q) => $q->where('status', 'calificada'),
|
||||||
|
])
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('tarea.index', compact('grupo', 'material', 'tareas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function create(Grupo $grupo, Material $material)
|
||||||
* Show the form for creating a new resource.
|
|
||||||
*/
|
|
||||||
public function create()
|
|
||||||
{
|
{
|
||||||
//
|
return view('tarea.create', compact('grupo', 'material'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function store(Request $request, Grupo $grupo, Material $material)
|
||||||
* Store a newly created resource in storage.
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
{
|
||||||
//
|
$request->validate([
|
||||||
|
'titulo' => 'required|string|max:255',
|
||||||
|
'descripcion' => 'nullable|string',
|
||||||
|
'fecha_entrega' => 'nullable|date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$docente = Docente::whereHas('docentes', fn($q) => $q->where('id', Auth::id()))->firstOrFail();
|
||||||
|
|
||||||
|
$tarea = Tarea::create([
|
||||||
|
'titulo' => $request->titulo,
|
||||||
|
'descripcion' => $request->descripcion,
|
||||||
|
'fecha_entrega' => $request->fecha_entrega,
|
||||||
|
'material_id' => $material->id,
|
||||||
|
'grupo_id' => $grupo->id,
|
||||||
|
'docente_id' => $docente->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($grupo->alumnos as $alumno) {
|
||||||
|
Entrega::create([
|
||||||
|
'tarea_id' => $tarea->id,
|
||||||
|
'alumno_id' => $alumno->id,
|
||||||
|
'status' => 'sin_subir',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('grupo.material.tareas', [$grupo, $material])
|
||||||
|
->with('info', 'Tarea creada correctamente.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function show(Grupo $grupo, Material $material, Tarea $tarea)
|
||||||
* Display the specified resource.
|
|
||||||
*/
|
|
||||||
public function show(string $id)
|
|
||||||
{
|
{
|
||||||
//
|
$alumnos = $grupo->alumnos()->with('alumnos')->get();
|
||||||
|
|
||||||
|
$entregasExistentes = Entrega::where('tarea_id', $tarea->id)->pluck('alumno_id')->toArray();
|
||||||
|
|
||||||
|
foreach ($alumnos as $alumno) {
|
||||||
|
if (!in_array($alumno->id, $entregasExistentes)) {
|
||||||
|
Entrega::create([
|
||||||
|
'tarea_id' => $tarea->id,
|
||||||
|
'alumno_id' => $alumno->id,
|
||||||
|
'status' => 'sin_subir',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entregas = Entrega::where('tarea_id', $tarea->id)
|
||||||
|
->with('alumno.alumnos')
|
||||||
|
->get()
|
||||||
|
->keyBy('alumno_id');
|
||||||
|
|
||||||
|
return view('tarea.show', compact('grupo', 'material', 'tarea', 'alumnos', 'entregas'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function edit(Grupo $grupo, Material $material, Tarea $tarea)
|
||||||
* Show the form for editing the specified resource.
|
|
||||||
*/
|
|
||||||
public function edit(string $id)
|
|
||||||
{
|
{
|
||||||
//
|
return view('tarea.create', compact('grupo', 'material', 'tarea'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function update(Request $request, Grupo $grupo, Material $material, Tarea $tarea)
|
||||||
* Update the specified resource in storage.
|
|
||||||
*/
|
|
||||||
public function update(Request $request, string $id)
|
|
||||||
{
|
{
|
||||||
//
|
$request->validate([
|
||||||
|
'titulo' => 'required|string|max:255',
|
||||||
|
'descripcion' => 'nullable|string',
|
||||||
|
'fecha_entrega' => 'nullable|date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tarea->update([
|
||||||
|
'titulo' => $request->titulo,
|
||||||
|
'descripcion' => $request->descripcion,
|
||||||
|
'fecha_entrega' => $request->fecha_entrega,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('grupo.material.tareas', [$grupo, $material])
|
||||||
|
->with('info', 'Tarea actualizada correctamente.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function destroy(Grupo $grupo, Material $material, Tarea $tarea)
|
||||||
* Remove the specified resource from storage.
|
|
||||||
*/
|
|
||||||
public function destroy(string $id)
|
|
||||||
{
|
{
|
||||||
//
|
// Borrar archivos físicos de cada entrega antes de eliminar los registros
|
||||||
|
$tarea->entregas()
|
||||||
|
->whereNotNull('archivo')
|
||||||
|
->pluck('archivo')
|
||||||
|
->each(fn($path) => Storage::delete($path));
|
||||||
|
|
||||||
|
$tarea->delete(); // las entregas se eliminan en cascada por FK
|
||||||
|
|
||||||
|
return redirect()->route('grupo.material.tareas', [$grupo, $material])
|
||||||
|
->with('info', 'Tarea eliminada.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calificar(Request $request, Tarea $tarea, Alumno $alumno)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'calificacion' => 'required|numeric|min:0|max:10',
|
||||||
|
'comentario' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$entrega = Entrega::where('tarea_id', $tarea->id)
|
||||||
|
->where('alumno_id', $alumno->id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$entrega->update([
|
||||||
|
'calificacion' => $request->calificacion,
|
||||||
|
'comentario' => $request->comentario,
|
||||||
|
'status' => 'calificada',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('info', 'Calificación guardada.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Docente;
|
use App\Models\Docente;
|
||||||
|
use App\Models\Entrega;
|
||||||
use App\Models\Material;
|
use App\Models\Material;
|
||||||
|
use App\Models\Tarea;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class GrupoDocente extends Component
|
class GrupoDocente extends Component
|
||||||
@@ -16,42 +19,45 @@ class GrupoDocente extends Component
|
|||||||
public $materia_id;
|
public $materia_id;
|
||||||
public $materiasSelect;
|
public $materiasSelect;
|
||||||
|
|
||||||
public function mount($grupo)
|
// Para reemplazo de docente
|
||||||
{
|
public $reemplazoDocenteActualId;
|
||||||
$this->grupo = $grupo; // <- primero asignar el grupo
|
public $reemplazoMateriaId;
|
||||||
|
public $reemplazoDocenteId = '';
|
||||||
|
public $reemplazoMateriaNombre = '';
|
||||||
|
|
||||||
$user = Auth::user();
|
public function mount($grupo)
|
||||||
$plantelesIds = $user->plantelUsuarios->pluck('id');
|
{
|
||||||
|
$this->grupo = $grupo;
|
||||||
|
|
||||||
$this->docentes = Docente::with(['docentes.plantelUsuarios'])
|
$user = Auth::user();
|
||||||
->whereHas('docentes', function($query) use ($plantelesIds) {
|
$plantelesIds = $user->plantelUsuarios->pluck('id');
|
||||||
$query->whereHas('plantelUsuarios', function($q) use ($plantelesIds) {
|
|
||||||
$q->whereIn('plantels.id', $plantelesIds);
|
$this->docentes = Docente::with(['docentes.plantelUsuarios'])
|
||||||
});
|
->whereHas('docentes', function ($query) use ($plantelesIds) {
|
||||||
|
$query->whereHas('plantelUsuarios', function ($q) use ($plantelesIds) {
|
||||||
|
$q->whereIn('plantels.id', $plantelesIds);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$this->materias = Material::whereHas('carreraMaterial', function ($query) use ($grupo) {
|
||||||
|
$query->where('carreras.id', $grupo->plan->carrera);
|
||||||
})
|
})
|
||||||
|
->orderBy('ciclo')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
// Todas las materias de la carrera sin filtro de ciclo
|
$materiasAsignadas = DB::table('docente_grupo')
|
||||||
$this->materias = Material::whereHas('carreraMaterial', function($query) use ($grupo) {
|
->where('grupo_id', $this->grupo->id)
|
||||||
$query->where('carreras.id', $grupo->plan->carrera);
|
->pluck('materia_id')
|
||||||
})
|
->toArray();
|
||||||
->orderBy('ciclo')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$materiasAsignadas = DB::table('docente_grupo')
|
$this->materiasSelect = Material::whereHas('carreraMaterial', function ($query) use ($grupo) {
|
||||||
->where('grupo_id', $this->grupo->id)
|
$query->where('carreras.id', $grupo->plan->carrera);
|
||||||
->pluck('materia_id')
|
})
|
||||||
->toArray();
|
->where('ciclo', $this->grupo->ciclo)
|
||||||
|
->when(!empty($materiasAsignadas), fn($q) => $q->whereNotIn('id', $materiasAsignadas))
|
||||||
$this->materiasSelect = Material::whereHas('carreraMaterial', function($query) use ($grupo) {
|
->get();
|
||||||
$query->where('carreras.id', $grupo->plan->carrera);
|
}
|
||||||
})
|
|
||||||
->where('ciclo', $this->grupo->ciclo) // solo ciclo actual
|
|
||||||
->when(!empty($materiasAsignadas), function($query) use ($materiasAsignadas) {
|
|
||||||
$query->whereNotIn('id', $materiasAsignadas); // excluir ya asignadas
|
|
||||||
})
|
|
||||||
->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function guardar()
|
public function guardar()
|
||||||
{
|
{
|
||||||
@@ -61,33 +67,79 @@ $this->materiasSelect = Material::whereHas('carreraMaterial', function($query) u
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$this->grupo->docentes()->attach($this->docente_id, [
|
$this->grupo->docentes()->attach($this->docente_id, [
|
||||||
'materia_id' => $this->materia_id
|
'materia_id' => $this->materia_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->reset(['docente_id', 'materia_id']);
|
$this->reset(['docente_id', 'materia_id']);
|
||||||
$this->dispatch('close-modal-docente');
|
$this->dispatch('close-modal-docente');
|
||||||
|
|
||||||
$this->dispatch('swal',
|
$this->dispatch('swal',
|
||||||
icon: 'success',
|
icon: 'success', title: '¡Listo!',
|
||||||
title: '¡Listo!',
|
|
||||||
text: 'Docente asignado correctamente',
|
text: 'Docente asignado correctamente',
|
||||||
timer: 3000,
|
timer: 3000, confirm: false, closeModal: true,
|
||||||
confirm: false,
|
|
||||||
closeModal: true,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function eliminar($docenteId, $materiaId)
|
public function eliminar($docenteId, $materiaId)
|
||||||
{
|
{
|
||||||
|
// Borrar archivos físicos de las entregas antes de eliminar en cascada
|
||||||
|
$tareaIds = Tarea::where('grupo_id', $this->grupo->id)
|
||||||
|
->where('material_id', $materiaId)
|
||||||
|
->where('docente_id', $docenteId)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
Entrega::whereIn('tarea_id', $tareaIds)
|
||||||
|
->whereNotNull('archivo')
|
||||||
|
->pluck('archivo')
|
||||||
|
->each(fn($path) => Storage::delete($path));
|
||||||
|
|
||||||
|
Tarea::whereIn('id', $tareaIds)->delete();
|
||||||
|
|
||||||
$this->grupo->docentes()->wherePivot('materia_id', $materiaId)->detach($docenteId);
|
$this->grupo->docentes()->wherePivot('materia_id', $materiaId)->detach($docenteId);
|
||||||
|
|
||||||
$this->dispatch('swal',
|
$this->dispatch('swal',
|
||||||
icon: 'success',
|
icon: 'success', title: 'Eliminado',
|
||||||
title: 'Eliminado',
|
text: 'Docente y sus tareas removidos correctamente',
|
||||||
text: 'Docente removido de la materia correctamente',
|
timer: 2000, confirm: false, closeModal: true,
|
||||||
timer: 2000,
|
);
|
||||||
confirm: false,
|
}
|
||||||
closeModal: true,
|
|
||||||
|
public function abrirReemplazo($docenteId, $materiaId, $materiaNombre)
|
||||||
|
{
|
||||||
|
$this->reemplazoDocenteActualId = $docenteId;
|
||||||
|
$this->reemplazoMateriaId = $materiaId;
|
||||||
|
$this->reemplazoMateriaNombre = $materiaNombre;
|
||||||
|
$this->reemplazoDocenteId = '';
|
||||||
|
$this->dispatch('open-modal-reemplazo');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reemplazar()
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'reemplazoDocenteId' => 'required|exists:docentes,id|different:reemplazoDocenteActualId',
|
||||||
|
], [
|
||||||
|
'reemplazoDocenteId.required' => 'Selecciona un docente.',
|
||||||
|
'reemplazoDocenteId.different' => 'Debes elegir un docente diferente al actual.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Actualizar el registro en docente_grupo
|
||||||
|
DB::table('docente_grupo')
|
||||||
|
->where('grupo_id', $this->grupo->id)
|
||||||
|
->where('materia_id', $this->reemplazoMateriaId)
|
||||||
|
->where('docente_id', $this->reemplazoDocenteActualId)
|
||||||
|
->update(['docente_id' => $this->reemplazoDocenteId]);
|
||||||
|
|
||||||
|
// Reasignar las tareas al nuevo docente (conserva entregas intactas)
|
||||||
|
Tarea::where('grupo_id', $this->grupo->id)
|
||||||
|
->where('material_id', $this->reemplazoMateriaId)
|
||||||
|
->where('docente_id', $this->reemplazoDocenteActualId)
|
||||||
|
->update(['docente_id' => $this->reemplazoDocenteId]);
|
||||||
|
|
||||||
|
$this->reset(['reemplazoDocenteId', 'reemplazoMateriaId', 'reemplazoDocenteActualId', 'reemplazoMateriaNombre']);
|
||||||
|
$this->dispatch('close-modal-reemplazo');
|
||||||
|
$this->dispatch('swal',
|
||||||
|
icon: 'success', title: '¡Docente reemplazado!',
|
||||||
|
text: 'El nuevo docente fue asignado y las tareas previas se conservaron.',
|
||||||
|
timer: 3500, confirm: false, closeModal: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Anuncio extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id', 'title', 'body', 'image_path',
|
||||||
|
'type', 'audience', 'expires_at', 'active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'audience' => 'array',
|
||||||
|
'expires_at' => 'datetime',
|
||||||
|
'active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Configuración visual por tipo
|
||||||
|
public const TYPES = [
|
||||||
|
'info' => ['label' => 'Información', 'color' => 'primary', 'icon' => 'fa-info-circle'],
|
||||||
|
'promocion' => ['label' => 'Promoción', 'color' => 'success', 'icon' => 'fa-tag'],
|
||||||
|
'mantenimiento' => ['label' => 'Mantenimiento', 'color' => 'warning', 'icon' => 'fa-tools'],
|
||||||
|
'aviso' => ['label' => 'Aviso', 'color' => 'danger', 'icon' => 'fa-exclamation-triangle'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public const ROLES = [
|
||||||
|
'all' => 'Todos',
|
||||||
|
'Alumno' => 'Alumnos',
|
||||||
|
'Docente' => 'Docentes',
|
||||||
|
'Admin' => 'Administradores',
|
||||||
|
'Control escolar' => 'Control escolar',
|
||||||
|
'Coordinación académica' => 'Coordinación académica',
|
||||||
|
'Programador' => 'Programadores',
|
||||||
|
'Promotor' => 'Promotores',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function author(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo activos y no vencidos
|
||||||
|
public function scopeVigentes(Builder $q): void
|
||||||
|
{
|
||||||
|
$q->where('active', true)
|
||||||
|
->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtra por roles del usuario
|
||||||
|
public function scopeParaRoles(Builder $q, array $roles): void
|
||||||
|
{
|
||||||
|
$q->where(function ($q) use ($roles) {
|
||||||
|
$q->whereJsonContains('audience', 'all');
|
||||||
|
foreach ($roles as $role) {
|
||||||
|
$q->orWhereJsonContains('audience', $role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function typeConfig(): array
|
||||||
|
{
|
||||||
|
return self::TYPES[$this->type] ?? self::TYPES['info'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(): bool
|
||||||
|
{
|
||||||
|
return $this->expires_at && $this->expires_at->isPast();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ class Docente extends Model
|
|||||||
// Relación con Material
|
// Relación con Material
|
||||||
public function materias(): BelongsToMany
|
public function materias(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Material::class, 'docente_grupo')
|
return $this->belongsToMany(Material::class, 'docente_grupo', 'docente_id', 'materia_id')
|
||||||
->withPivot('grupo_id');
|
->withPivot('grupo_id');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Entrega extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public function tarea() { return $this->belongsTo(Tarea::class); }
|
||||||
|
public function alumno() { return $this->belongsTo(Alumno::class); }
|
||||||
|
}
|
||||||
@@ -3,17 +3,13 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
|
|
||||||
class Tarea extends Model
|
class Tarea extends Model
|
||||||
{
|
{
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
public $timestamps = false;
|
public function material() { return $this->belongsTo(Material::class); }
|
||||||
|
public function grupo() { return $this->belongsTo(Grupo::class); }
|
||||||
// Relación con Carrera
|
public function docente() { return $this->belongsTo(Docente::class); }
|
||||||
public function carreraMaterial(): BelongsToMany
|
public function entregas() { return $this->hasMany(Entrega::class); }
|
||||||
{
|
|
||||||
return $this->belongsToMany(Carrera::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?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('anuncios', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('body')->nullable();
|
||||||
|
$table->string('image_path')->nullable();
|
||||||
|
$table->enum('type', ['info', 'promocion', 'mantenimiento', 'aviso'])
|
||||||
|
->default('info');
|
||||||
|
$table->json('audience'); // ["all"] | ["Alumno","Docente"] | etc.
|
||||||
|
$table->timestamp('expires_at')->nullable();
|
||||||
|
$table->boolean('active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('anuncios');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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('tareas', function (Blueprint $table) {
|
||||||
|
$table->string('titulo');
|
||||||
|
$table->text('descripcion')->nullable();
|
||||||
|
$table->date('fecha_entrega')->nullable();
|
||||||
|
$table->unsignedBigInteger('material_id');
|
||||||
|
$table->unsignedBigInteger('grupo_id');
|
||||||
|
$table->unsignedBigInteger('docente_id');
|
||||||
|
|
||||||
|
$table->foreign('material_id')->references('id')->on('materials')->onDelete('cascade');
|
||||||
|
$table->foreign('grupo_id')->references('id')->on('grupos')->onDelete('cascade');
|
||||||
|
$table->foreign('docente_id')->references('id')->on('docentes')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tareas', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['material_id']);
|
||||||
|
$table->dropForeign(['grupo_id']);
|
||||||
|
$table->dropForeign(['docente_id']);
|
||||||
|
$table->dropColumn(['titulo', 'descripcion', 'fecha_entrega', 'material_id', 'grupo_id', 'docente_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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('entregas', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('tarea_id');
|
||||||
|
$table->unsignedBigInteger('alumno_id');
|
||||||
|
$table->string('archivo')->nullable();
|
||||||
|
$table->enum('status', ['sin_subir', 'pendiente', 'calificada'])->default('sin_subir');
|
||||||
|
$table->decimal('calificacion', 5, 2)->nullable();
|
||||||
|
$table->text('comentario')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['tarea_id', 'alumno_id']);
|
||||||
|
$table->foreign('tarea_id')->references('id')->on('tareas')->onDelete('cascade');
|
||||||
|
$table->foreign('alumno_id')->references('id')->on('alumnos')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('entregas');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?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('entregas', function (Blueprint $table) {
|
||||||
|
$table->text('texto')->nullable()->after('archivo');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('entregas', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('texto');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -140,5 +140,9 @@ class RoleSeeder extends Seeder
|
|||||||
Permission::create(['name'=>'documentoTipo.create','description'=>'Crear documentación'])->syncRoles([$role1]);
|
Permission::create(['name'=>'documentoTipo.create','description'=>'Crear documentación'])->syncRoles([$role1]);
|
||||||
Permission::create(['name'=>'documentoTipo.edit','description'=>'Editar documentación'])->syncRoles([$role1]);
|
Permission::create(['name'=>'documentoTipo.edit','description'=>'Editar documentación'])->syncRoles([$role1]);
|
||||||
Permission::create(['name'=>'documentoTipo.destroy','description'=>'Eliminar documentación'])->syncRoles([$role1]);
|
Permission::create(['name'=>'documentoTipo.destroy','description'=>'Eliminar documentación'])->syncRoles([$role1]);
|
||||||
|
|
||||||
|
Permission::create(['name'=>'comunidad','description'=>'Comunidad'])->syncRoles([$role1]);
|
||||||
|
Permission::create(['name'=>'amigos','description'=>'Amistades'])->syncRoles([$role1]);
|
||||||
|
Permission::create(['name'=>'mensajes','description'=>'Mensajería'])->syncRoles([$role1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /app/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('title', 'Nuevo Anuncio')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4" style="max-width:760px; margin:auto">
|
||||||
|
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-plus-circle mr-1"></i> Nuevo Anuncio
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('anuncio.index') }}" class="btn btn-sm btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left mr-1"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach($errors->all() as $e)
|
||||||
|
<li>{{ $e }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('anuncio.store') }}" method="POST" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
{{-- Título --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Título <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
|
||||||
|
value="{{ old('title') }}" maxlength="200" placeholder="Título del anuncio" required>
|
||||||
|
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Cuerpo --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Descripción</label>
|
||||||
|
<textarea name="body" class="form-control @error('body') is-invalid @enderror"
|
||||||
|
rows="4" maxlength="3000" placeholder="Texto del anuncio (opcional)">{{ old('body') }}</textarea>
|
||||||
|
@error('body')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Tipo --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Tipo <span class="text-danger">*</span></label>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@foreach($types as $key => $cfg)
|
||||||
|
<div class="form-check form-check-inline mr-3">
|
||||||
|
<input class="form-check-input" type="radio" name="type" id="type_{{ $key }}"
|
||||||
|
value="{{ $key }}" {{ old('type', 'info') === $key ? 'checked' : '' }} required>
|
||||||
|
<label class="form-check-label" for="type_{{ $key }}">
|
||||||
|
<span class="badge badge-{{ $cfg['color'] }}">
|
||||||
|
<i class="fas {{ $cfg['icon'] }} mr-1"></i>{{ $cfg['label'] }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@error('type')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Audiencia --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Audiencia <span class="text-danger">*</span></label>
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="row">
|
||||||
|
@foreach($roles as $key => $label)
|
||||||
|
<div class="col-6 col-md-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input audience-check" type="checkbox"
|
||||||
|
name="audience[]" id="aud_{{ $key }}" value="{{ $key }}"
|
||||||
|
{{ in_array($key, old('audience', [])) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="aud_{{ $key }}">{{ $label }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@error('audience')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Imagen --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Imagen</label>
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input @error('image') is-invalid @enderror"
|
||||||
|
id="imageInput" name="image" accept="image/*">
|
||||||
|
<label class="custom-file-label" for="imageInput">Seleccionar imagen (máx. 5 MB)</label>
|
||||||
|
</div>
|
||||||
|
@error('image')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
|
||||||
|
<div id="imagePreview" class="mt-2 d-none">
|
||||||
|
<img id="previewImg" src="" alt="Vista previa" class="img-fluid rounded" style="max-height:200px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Vencimiento --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Fecha de vencimiento</label>
|
||||||
|
<input type="datetime-local" name="expires_at"
|
||||||
|
class="form-control @error('expires_at') is-invalid @enderror"
|
||||||
|
value="{{ old('expires_at') }}">
|
||||||
|
<small class="text-muted">Dejar vacío para que no tenga vencimiento.</small>
|
||||||
|
@error('expires_at')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Activo --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="activeSwitch"
|
||||||
|
name="active" value="1" {{ old('active', '1') ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label font-weight-bold" for="activeSwitch">Publicar inmediatamente</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<a href="{{ route('anuncio.index') }}" class="btn btn-secondary mr-2">Cancelar</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save mr-1"></i> Guardar anuncio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
// Custom file label
|
||||||
|
document.getElementById('imageInput').addEventListener('change', function () {
|
||||||
|
const file = this.files[0];
|
||||||
|
this.nextElementSibling.textContent = file ? file.name : 'Seleccionar imagen (máx. 5 MB)';
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('previewImg').src = e.target.result;
|
||||||
|
document.getElementById('imagePreview').classList.remove('d-none');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
document.getElementById('imagePreview').classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Todos" exclusivo con los demás
|
||||||
|
document.querySelectorAll('.audience-check').forEach(function (cb) {
|
||||||
|
cb.addEventListener('change', function () {
|
||||||
|
if (this.value === 'all' && this.checked) {
|
||||||
|
document.querySelectorAll('.audience-check').forEach(c => { if (c !== this) c.checked = false; });
|
||||||
|
} else if (this.value !== 'all' && this.checked) {
|
||||||
|
const allCb = document.querySelector('.audience-check[value="all"]');
|
||||||
|
if (allCb) allCb.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('title', 'Editar Anuncio')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4" style="max-width:760px; margin:auto">
|
||||||
|
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-edit mr-1"></i> Editar Anuncio
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('anuncio.index') }}" class="btn btn-sm btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left mr-1"></i> Volver
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach($errors->all() as $e)
|
||||||
|
<li>{{ $e }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('anuncio.update', $anuncio) }}" method="POST" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
{{-- Título --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Título <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
|
||||||
|
value="{{ old('title', $anuncio->title) }}" maxlength="200" required>
|
||||||
|
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Cuerpo --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Descripción</label>
|
||||||
|
<textarea name="body" class="form-control @error('body') is-invalid @enderror"
|
||||||
|
rows="4" maxlength="3000">{{ old('body', $anuncio->body) }}</textarea>
|
||||||
|
@error('body')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Tipo --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Tipo <span class="text-danger">*</span></label>
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
@foreach($types as $key => $cfg)
|
||||||
|
<div class="form-check form-check-inline mr-3">
|
||||||
|
<input class="form-check-input" type="radio" name="type" id="type_{{ $key }}"
|
||||||
|
value="{{ $key }}" {{ old('type', $anuncio->type) === $key ? 'checked' : '' }} required>
|
||||||
|
<label class="form-check-label" for="type_{{ $key }}">
|
||||||
|
<span class="badge badge-{{ $cfg['color'] }}">
|
||||||
|
<i class="fas {{ $cfg['icon'] }} mr-1"></i>{{ $cfg['label'] }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@error('type')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Audiencia --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Audiencia <span class="text-danger">*</span></label>
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="row">
|
||||||
|
@foreach($roles as $key => $label)
|
||||||
|
<div class="col-6 col-md-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input audience-check" type="checkbox"
|
||||||
|
name="audience[]" id="aud_{{ $key }}" value="{{ $key }}"
|
||||||
|
{{ in_array($key, old('audience', $anuncio->audience ?? [])) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="aud_{{ $key }}">{{ $label }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@error('audience')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Imagen actual + nueva --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Imagen</label>
|
||||||
|
@if($anuncio->image_path)
|
||||||
|
<div class="mb-2">
|
||||||
|
<img src="{{ Storage::url($anuncio->image_path) }}" alt="Imagen actual"
|
||||||
|
class="img-fluid rounded" style="max-height:150px">
|
||||||
|
<div class="form-check mt-1">
|
||||||
|
<input class="form-check-input" type="checkbox" name="remove_image" id="removeImage" value="1">
|
||||||
|
<label class="form-check-label text-danger" for="removeImage">Eliminar imagen actual</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input @error('image') is-invalid @enderror"
|
||||||
|
id="imageInput" name="image" accept="image/*">
|
||||||
|
<label class="custom-file-label" for="imageInput">
|
||||||
|
{{ $anuncio->image_path ? 'Reemplazar imagen (máx. 5 MB)' : 'Seleccionar imagen (máx. 5 MB)' }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@error('image')<div class="text-danger small mt-1">{{ $message }}</div>@enderror
|
||||||
|
<div id="imagePreview" class="mt-2 d-none">
|
||||||
|
<img id="previewImg" src="" alt="Vista previa" class="img-fluid rounded" style="max-height:200px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Vencimiento --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="font-weight-bold">Fecha de vencimiento</label>
|
||||||
|
<input type="datetime-local" name="expires_at"
|
||||||
|
class="form-control @error('expires_at') is-invalid @enderror"
|
||||||
|
value="{{ old('expires_at', $anuncio->expires_at ? $anuncio->expires_at->format('Y-m-d\TH:i') : '') }}">
|
||||||
|
<small class="text-muted">Dejar vacío para que no tenga vencimiento.</small>
|
||||||
|
@error('expires_at')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Activo --}}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="activeSwitch"
|
||||||
|
name="active" value="1" {{ old('active', $anuncio->active) ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label font-weight-bold" for="activeSwitch">Anuncio activo</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<a href="{{ route('anuncio.index') }}" class="btn btn-secondary mr-2">Cancelar</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save mr-1"></i> Actualizar anuncio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
document.getElementById('imageInput').addEventListener('change', function () {
|
||||||
|
const file = this.files[0];
|
||||||
|
this.nextElementSibling.textContent = file ? file.name : 'Seleccionar imagen (máx. 5 MB)';
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('previewImg').src = e.target.result;
|
||||||
|
document.getElementById('imagePreview').classList.remove('d-none');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} else {
|
||||||
|
document.getElementById('imagePreview').classList.add('d-none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.audience-check').forEach(function (cb) {
|
||||||
|
cb.addEventListener('change', function () {
|
||||||
|
if (this.value === 'all' && this.checked) {
|
||||||
|
document.querySelectorAll('.audience-check').forEach(c => { if (c !== this) c.checked = false; });
|
||||||
|
} else if (this.value !== 'all' && this.checked) {
|
||||||
|
const allCb = document.querySelector('.audience-check[value="all"]');
|
||||||
|
if (allCb) allCb.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('title', 'Anuncios')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3 d-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
<i class="fas fa-bullhorn mr-1"></i> Anuncios
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('anuncio.create') }}" class="btn btn-sm btn-primary shadow-sm">
|
||||||
|
<i class="fas fa-plus fa-sm text-white-50 mr-1"></i> Nuevo anuncio
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ session('success') }}
|
||||||
|
<button type="button" class="close" data-dismiss="alert"><span>×</span></button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-hover" style="width:100%">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:40px">#</th>
|
||||||
|
<th>Título</th>
|
||||||
|
<th style="width:130px">Tipo</th>
|
||||||
|
<th style="width:160px">Audiencia</th>
|
||||||
|
<th style="width:130px">Vence</th>
|
||||||
|
<th style="width:80px" class="text-center">Activo</th>
|
||||||
|
<th style="width:130px" class="text-center">Opciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($anuncios as $anuncio)
|
||||||
|
@php $cfg = $anuncio->typeConfig(); @endphp
|
||||||
|
<tr class="{{ $anuncio->isExpired() ? 'table-secondary text-muted' : '' }}">
|
||||||
|
<td>{{ $anuncio->id }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-{{ $cfg['color'] }} mr-1">
|
||||||
|
<i class="fas {{ $cfg['icon'] }}"></i>
|
||||||
|
</span>
|
||||||
|
{{ $anuncio->title }}
|
||||||
|
@if($anuncio->image_path)
|
||||||
|
<i class="fas fa-image text-secondary ml-1" title="Tiene imagen"></i>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-{{ $cfg['color'] }}">{{ $cfg['label'] }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@foreach($anuncio->audience as $aud)
|
||||||
|
<span class="badge badge-light border mr-1">{{ \App\Models\Anuncio::ROLES[$aud] ?? $aud }}</span>
|
||||||
|
@endforeach
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($anuncio->expires_at)
|
||||||
|
<span class="{{ $anuncio->isExpired() ? 'text-danger' : 'text-secondary' }}">
|
||||||
|
<i class="fas fa-clock mr-1"></i>
|
||||||
|
{{ $anuncio->expires_at->format('d/m/Y H:i') }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Sin vencimiento</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<form action="{{ route('anuncio.toggleActive', $anuncio) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
@method('PATCH')
|
||||||
|
<button type="submit" class="btn btn-sm {{ $anuncio->active ? 'btn-success' : 'btn-secondary' }}"
|
||||||
|
title="{{ $anuncio->active ? 'Desactivar' : 'Activar' }}">
|
||||||
|
<i class="fas {{ $anuncio->active ? 'fa-toggle-on' : 'fa-toggle-off' }}"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{{ route('anuncio.edit', $anuncio) }}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form action="{{ route('anuncio.destroy', $anuncio) }}" method="POST" class="d-inline delete-form">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center text-muted py-4">
|
||||||
|
<i class="fas fa-bullhorn fa-2x mb-2 d-block"></i>
|
||||||
|
No hay anuncios creados.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-center mt-3">
|
||||||
|
{{ $anuncios->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
@include('layouts._partials.delete')
|
||||||
|
@endsection
|
||||||
@@ -9,23 +9,36 @@
|
|||||||
<h5 class="mb-0"><i class="fas fa-comments"></i> Chats</h5>
|
<h5 class="mb-0"><i class="fas fa-comments"></i> Chats</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0" style="overflow-y: auto;">
|
<div class="card-body p-0" style="overflow-y: auto;">
|
||||||
@forelse($friends as $friend)
|
@forelse($users as $user)
|
||||||
<a href="{{ route('chat.show', $friend) }}" class="text-decoration-none">
|
<a href="{{ route('chat.show', $user) }}" class="text-decoration-none">
|
||||||
<div class="d-flex align-items-center p-3 border-bottom chat-user-item">
|
<div class="d-flex align-items-center p-3 border-bottom chat-user-item">
|
||||||
<img src="{{ $friend->profile_photo_url }}"
|
<div class="position-relative mr-3 flex-shrink-0">
|
||||||
class="rounded-circle mr-3"
|
<img src="{{ $user->profile_photo_url }}"
|
||||||
width="45" height="45"
|
class="rounded-circle"
|
||||||
style="object-fit:cover">
|
width="45" height="45"
|
||||||
<div>
|
style="object-fit:cover;
|
||||||
<div class="font-weight-bold text-dark">{{ $friend->name }}</div>
|
{{ $user->is_friend ? '' : 'filter:grayscale(60%); opacity:.8' }}">
|
||||||
<small class="text-muted">{{ $friend->getRoleNames()->first() }}</small>
|
@unless($user->is_friend)
|
||||||
|
<span class="position-absolute"
|
||||||
|
style="bottom:0;right:0;background:#dc3545;border-radius:50%;
|
||||||
|
width:16px;height:16px;display:flex;align-items:center;
|
||||||
|
justify-content:center;border:2px solid #fff">
|
||||||
|
<i class="fas fa-lock" style="font-size:.45rem;color:#fff"></i>
|
||||||
|
</span>
|
||||||
|
@endunless
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<div class="font-weight-bold text-dark text-truncate">{{ $user->name }}</div>
|
||||||
|
<small class="{{ $user->is_friend ? 'text-muted' : 'text-danger' }}">
|
||||||
|
{{ $user->is_friend ? $user->getRoleNames()->first() : 'Ya no son amigos' }}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@empty
|
@empty
|
||||||
<div class="text-center text-muted py-5">
|
<div class="text-center text-muted py-5">
|
||||||
<i class="fas fa-user-friends fa-3x mb-3 d-block"></i>
|
<i class="fas fa-user-friends fa-3x mb-3 d-block"></i>
|
||||||
<p class="mb-2">Aún no tienes amigos para chatear.</p>
|
<p class="mb-2">Aún no tienes conversaciones.</p>
|
||||||
<a href="{{ route('feed') }}" class="btn btn-primary btn-sm">
|
<a href="{{ route('feed') }}" class="btn btn-primary btn-sm">
|
||||||
<i class="fas fa-search mr-1"></i> Buscar personas
|
<i class="fas fa-search mr-1"></i> Buscar personas
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,15 +1,59 @@
|
|||||||
<x-app-layout>
|
@extends('layouts.landing')
|
||||||
<x-slot name="header">
|
|
||||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
||||||
{{ __('Dashboard') }}
|
|
||||||
</h2>
|
|
||||||
</x-slot>
|
|
||||||
|
|
||||||
<div class="py-12">
|
@section('title', 'Inicio')
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
||||||
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
|
@section('content')
|
||||||
<x-welcome />
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
{{-- Anuncios vigentes para este usuario --}}
|
||||||
|
@if(isset($anuncios) && $anuncios->count())
|
||||||
|
<div class="mb-4">
|
||||||
|
@foreach($anuncios as $anuncio)
|
||||||
|
@php $cfg = $anuncio->typeConfig(); @endphp
|
||||||
|
<div class="alert alert-{{ $cfg['color'] }} alert-dismissible fade show shadow-sm mb-3" role="alert">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="fas {{ $cfg['icon'] }} fa-lg mr-3 mt-1"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="font-weight-bold mb-1">{{ $anuncio->title }}</h6>
|
||||||
|
@if($anuncio->body)
|
||||||
|
<p class="mb-1" style="white-space:pre-line">{{ $anuncio->body }}</p>
|
||||||
|
@endif
|
||||||
|
@if($anuncio->image_path)
|
||||||
|
<img src="{{ Storage::url($anuncio->image_path) }}"
|
||||||
|
alt="{{ $anuncio->title }}"
|
||||||
|
class="img-fluid rounded mt-2"
|
||||||
|
style="max-height:300px; max-width:100%; object-fit:contain">
|
||||||
|
@endif
|
||||||
|
@if($anuncio->expires_at)
|
||||||
|
<div class="mt-1">
|
||||||
|
<small class="opacity-75">
|
||||||
|
<i class="fas fa-clock mr-1"></i>
|
||||||
|
Válido hasta {{ $anuncio->expires_at->format('d/m/Y H:i') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Cerrar">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Contenido de bienvenida --}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<i class="fas fa-graduation-cap fa-3x text-primary mb-3"></i>
|
||||||
|
<h4 class="font-weight-bold">Bienvenido al Sistema Educativo SECUIEP</h4>
|
||||||
|
<p class="text-muted">Utiliza el menú lateral para acceder a los módulos disponibles.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-app-layout>
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('head')
|
||||||
|
@include('layouts._partials.tablaStyle')
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('title', 'Administrar grupo')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
Materias asignadas — Grupo: {{ $grupo->clave }}
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('grupo.index') }}" class="btn btn-sm btn-secondary shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if (session('info'))
|
||||||
|
<div class="alert alert-success"><strong>{{ session('info') }}</strong></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($materiales->isEmpty())
|
||||||
|
<div class="alert alert-warning">No tienes materias asignadas en este grupo.</div>
|
||||||
|
@else
|
||||||
|
<div class="row">
|
||||||
|
@foreach ($materiales as $material)
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card border-left-primary shadow h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||||
|
{{ $material->clave }}
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-2 font-weight-bold text-gray-800">
|
||||||
|
{{ $material->name }}
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
Ciclo: {{ $material->ciclo }} · {{ $material->horas }} hrs
|
||||||
|
</p>
|
||||||
|
<a href="{{ route('grupo.material.tareas', [$grupo, $material]) }}"
|
||||||
|
class="btn btn-primary btn-sm btn-block">
|
||||||
|
<i class="fas fa-tasks fa-sm"></i> Gestionar Tareas
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -88,8 +88,12 @@
|
|||||||
<option value="0" {{ 0 == $grupo->status ? 'selected' : '' }}>Inactivo</option>
|
<option value="0" {{ 0 == $grupo->status ? 'selected' : '' }}>Inactivo</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@hasrole('Control escolar')
|
||||||
|
|
||||||
<input class="btn btn-primary" type="submit" value="Guardar">
|
<input class="btn btn-primary" type="submit" value="Guardar">
|
||||||
|
|
||||||
|
@endhasrole
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,6 +54,22 @@
|
|||||||
</div>
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
|
|
||||||
|
@hasrole('Docente')
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<a class="btn btn-primary" href="{{ route('grupo.administrar', $grupo->id) }}">
|
||||||
|
<i class="fas fa-tasks fa-sm"></i> Administrar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endhasrole
|
||||||
|
|
||||||
|
@hasrole('Coordinación académica')
|
||||||
|
<div class="col-sm-6 col-md-6">
|
||||||
|
<a class="btn btn-info" href="{{ route('grupo.ver.materias', $grupo->id) }}">
|
||||||
|
<i class="fas fa-eye fa-sm"></i> Ver Tareas
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endhasrole
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('title', 'Materias del grupo')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
Materias — Grupo: {{ $grupo->clave }}
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('grupo.index') }}" class="btn btn-sm btn-secondary shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if ($asignaciones->isEmpty())
|
||||||
|
<div class="alert alert-warning">Este grupo no tiene materias asignadas.</div>
|
||||||
|
@else
|
||||||
|
<div class="row">
|
||||||
|
@foreach ($asignaciones as $asig)
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card border-left-info shadow h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||||
|
{{ $asig->material_clave }}
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-1 font-weight-bold text-gray-800">
|
||||||
|
{{ $asig->material_name }}
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-1">
|
||||||
|
Ciclo: {{ $asig->ciclo }} · {{ $asig->horas }} hrs
|
||||||
|
</p>
|
||||||
|
<p class="small mb-3">
|
||||||
|
<i class="fas fa-chalkboard-teacher fa-sm text-gray-500"></i>
|
||||||
|
<span class="text-gray-700">
|
||||||
|
{{ $asig->apellidoPaterno }} {{ $asig->apellidoMaterno }} {{ $asig->docente_nombre }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<a href="{{ route('grupo.material.tareas', [$grupo->id, $asig->material_id]) }}"
|
||||||
|
class="btn btn-info btn-sm btn-block">
|
||||||
|
<i class="fas fa-tasks fa-sm"></i> Ver Tareas
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<footer class="sticky-footer bg-white">
|
<footer class="sticky-footer bg-white">
|
||||||
<div class="container my-auto">
|
<div class="container my-auto">
|
||||||
<div class="copyright text-center my-auto">
|
<div class="copyright text-center my-auto">
|
||||||
<span>Copyright © Your Website 2021</span>
|
<span>Copyright © Your Website {{date('Y')}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -9,4 +9,4 @@
|
|||||||
|
|
||||||
<a class="scroll-to-top rounded" href="#page-top">
|
<a class="scroll-to-top rounded" href="#page-top">
|
||||||
<i class="fas fa-angle-up"></i>
|
<i class="fas fa-angle-up"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<i class="fa fa-bars"></i>
|
<i class="fa fa-bars"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@can(['comunidad','amigos'])
|
||||||
<!-- Topbar Search -->
|
<!-- Topbar Search -->
|
||||||
<div class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search position-relative">
|
<div class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search position-relative">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -25,8 +26,10 @@
|
|||||||
min-width:300px"></div>
|
min-width:300px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@endcan
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
|
|
||||||
|
|
||||||
<!-- Nav Item - Search (XS) -->
|
<!-- Nav Item - Search (XS) -->
|
||||||
<li class="nav-item no-arrow d-sm-none position-relative">
|
<li class="nav-item no-arrow d-sm-none position-relative">
|
||||||
<a class="nav-link" href="#" id="mobile-search-toggle" role="button">
|
<a class="nav-link" href="#" id="mobile-search-toggle" role="button">
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@can(['comunidad','amigos'])
|
||||||
<!-- Nav Item - Notificaciones -->
|
<!-- Nav Item - Notificaciones -->
|
||||||
<li class="nav-item dropdown no-arrow mx-1">
|
<li class="nav-item dropdown no-arrow mx-1">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
|
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
|
||||||
@@ -98,6 +102,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
@can('mensajes')
|
||||||
<!-- Nav Item - Mensajes -->
|
<!-- Nav Item - Mensajes -->
|
||||||
<li class="nav-item dropdown no-arrow mx-1">
|
<li class="nav-item dropdown no-arrow mx-1">
|
||||||
<a class="nav-link dropdown-toggle" href="{{ route('chat.index') }}" id="messagesDropdown"
|
<a class="nav-link dropdown-toggle" href="{{ route('chat.index') }}" id="messagesDropdown"
|
||||||
@@ -143,6 +150,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@endcan
|
||||||
|
|
||||||
<div class="topbar-divider d-none d-sm-block"></div>
|
<div class="topbar-divider d-none d-sm-block"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
<div>
|
<div>
|
||||||
{{-- Modal --}}
|
{{-- Modal: Agregar docente --}}
|
||||||
<div wire:ignore.self class="modal fade" id="modalDocente" tabindex="-1" aria-hidden="true">
|
<div wire:ignore.self class="modal fade" id="modalDocente" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title">Agregar Docente al Grupo</h5>
|
<h5 class="modal-title">Asignar Docente al Grupo</h5>
|
||||||
<button type="button" class="close" data-dismiss="modal">
|
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||||
<span>×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{{-- Docente --}}
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label>Docente</label>
|
<label>Docente</label>
|
||||||
<select class="form-control" wire:model="docente_id">
|
<select class="form-control" wire:model="docente_id">
|
||||||
@@ -18,30 +15,23 @@
|
|||||||
@foreach ($docentes as $docente)
|
@foreach ($docentes as $docente)
|
||||||
@php $usr = $docente->docentes->first() @endphp
|
@php $usr = $docente->docentes->first() @endphp
|
||||||
<option value="{{ $docente->id }}">
|
<option value="{{ $docente->id }}">
|
||||||
{{ $usr?->name }}
|
{{ $usr?->apellidoPaterno }} {{ $usr?->apellidoMaterno }} {{ $usr?->name }}
|
||||||
{{ $usr?->apellidoPaterno }}
|
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('docente_id')
|
@error('docente_id')<span class="text-danger small">{{ $message }}</span>@enderror
|
||||||
<span class="text-danger small">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Materia --}}
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label>Materia</label>
|
<label>Materia</label>
|
||||||
<select class="form-control" wire:model="materia_id">
|
<select class="form-control" wire:model="materia_id">
|
||||||
<option value="">Seleccionar materia...</option>
|
<option value="">Seleccionar materia...</option>
|
||||||
@foreach ($materiasSelect as $materia)
|
@foreach ($materiasSelect as $materia)
|
||||||
<option value="{{ $materia->id }}">
|
<option value="{{ $materia->id }}">
|
||||||
ciclo {{ $materia->ciclo }} - {{ $materia->name }}
|
Ciclo {{ $materia->ciclo }} — {{ $materia->name }}
|
||||||
</option>
|
</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('materia_id')
|
@error('materia_id')<span class="text-danger small">{{ $message }}</span>@enderror
|
||||||
<span class="text-danger small">{{ $message }}</span>
|
|
||||||
@enderror
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -52,148 +42,176 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Tabla docentes asignados --}}
|
{{-- Modal: Reemplazar docente --}}
|
||||||
<table id="tcont2" class="table table-striped table-bordered table-hover" style="width:100%;">
|
<div wire:ignore.self class="modal fade" id="modalReemplazo" tabindex="-1" aria-hidden="true">
|
||||||
<thead>
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Reemplazar Docente</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal"><span>×</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info mb-3">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Materia: <strong>{{ $reemplazoMateriaNombre }}</strong><br>
|
||||||
|
<small>Las tareas ya asignadas por el docente actual se conservarán y quedarán bajo el nuevo docente.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Nuevo docente</label>
|
||||||
|
<select class="form-control" wire:model="reemplazoDocenteId">
|
||||||
|
<option value="">Seleccionar docente...</option>
|
||||||
|
@foreach ($docentes as $docente)
|
||||||
|
@php $usr = $docente->docentes->first() @endphp
|
||||||
|
@if ($docente->id != $reemplazoDocenteActualId)
|
||||||
|
<option value="{{ $docente->id }}">
|
||||||
|
{{ $usr?->apellidoPaterno }} {{ $usr?->apellidoMaterno }} {{ $usr?->name }}
|
||||||
|
</option>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('reemplazoDocenteId')<span class="text-danger small">{{ $message }}</span>@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
|
||||||
|
<button wire:click="reemplazar" class="btn btn-warning">
|
||||||
|
<i class="fas fa-exchange-alt fa-sm"></i> Confirmar reemplazo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Tabla materias / docentes --}}
|
||||||
|
<table id="tcont2" class="table table-striped table-bordered table-hover" style="width:100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Materia</th>
|
||||||
|
<th>Ciclo</th>
|
||||||
|
<th>Docente asignado</th>
|
||||||
|
@hasrole('Coordinación académica')
|
||||||
|
<th>Ver tareas</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
@endhasrole
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($materias as $materia)
|
||||||
|
@php
|
||||||
|
$docenteAsignado = $grupo->docentes->firstWhere('pivot.materia_id', $materia->id);
|
||||||
|
$usr = $docenteAsignado?->docentes->first();
|
||||||
|
@endphp
|
||||||
<tr>
|
<tr>
|
||||||
<th>Id</th>
|
<td>{{ $materia->name }}</td>
|
||||||
<th>Materia</th>
|
<td>{{ $materia->ciclo }}</td>
|
||||||
<th>Ciclo</th>
|
<td>
|
||||||
<th>Docente asignado</th>
|
@if ($usr)
|
||||||
|
{{ $usr->apellidoPaterno }} {{ $usr->apellidoMaterno }} {{ $usr->name }}
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">Sin docente</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
@hasrole('Coordinación académica')
|
@hasrole('Coordinación académica')
|
||||||
<th>Opciones</th>
|
{{-- Columna Ver tareas --}}
|
||||||
@endhasrole
|
<td>
|
||||||
|
@if ($docenteAsignado)
|
||||||
|
<a href="{{ route('grupo.material.tareas', [$grupo->id, $materia->id]) }}"
|
||||||
|
class="btn btn-sm btn-info">
|
||||||
|
<i class="fas fa-tasks fa-sm"></i> Ver tareas
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<span class="text-muted small">Sin asignar</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
{{-- Columna Acciones --}}
|
||||||
|
<td>
|
||||||
|
@if ($docenteAsignado)
|
||||||
|
<button
|
||||||
|
wire:click="abrirReemplazo({{ $docenteAsignado->id }}, {{ $materia->id }}, '{{ addslashes($materia->name) }}')"
|
||||||
|
class="btn btn-sm btn-warning" title="Reemplazar docente">
|
||||||
|
<i class="fas fa-exchange-alt fa-sm"></i> Reemplazar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="confirmarEliminar({{ $docenteAsignado->id }}, {{ $materia->id }}, '{{ $usr?->apellidoPaterno }} {{ $usr?->name }}', '{{ addslashes($materia->name) }}')"
|
||||||
|
class="btn btn-sm btn-danger" title="Quitar docente">
|
||||||
|
<i class="fas fa-user-minus fa-sm"></i>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
@endhasrole
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
@empty
|
||||||
<tbody>
|
<tr><td colspan="5">Sin materias en este plan.</td></tr>
|
||||||
@forelse ($materias as $materia)
|
@endforelse
|
||||||
@php
|
</tbody>
|
||||||
$docenteAsignado = $grupo->docentes->firstWhere('pivot.materia_id', $materia->id);
|
|
||||||
$usr = $docenteAsignado?->docentes->first();
|
|
||||||
@endphp
|
|
||||||
<tr>
|
|
||||||
<td>{{ $materia->id }}</td>
|
|
||||||
<td>{{ $materia->name }}</td>
|
|
||||||
<td>{{ $materia->ciclo }}</td>
|
|
||||||
<td>
|
|
||||||
@if($usr)
|
|
||||||
{{ $usr->name }} {{ $usr->apellidoPaterno }}
|
|
||||||
@else
|
|
||||||
<span class="badge badge-secondary">Sin docente</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
@hasrole('Coordinación académica')
|
|
||||||
<td>
|
|
||||||
@if($docenteAsignado)
|
|
||||||
<button
|
|
||||||
onclick="confirmarEliminar({{ $docenteAsignado->id }}, {{ $materia->id }}, '{{ $usr?->name }} {{ $usr?->apellidoPaterno }}', '{{ $materia->name }}')"
|
|
||||||
style="border:none; background:none;">
|
|
||||||
<i class="fa-regular fa-circle-user-circle-minus fa-2xl" style="color: rgb(255, 0, 0);"></i>
|
|
||||||
</button>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
|
|
||||||
@endhasrole
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr>Sin materias en este plan.</tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@push('scripts')
|
@push('scripts')
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initTable);
|
document.addEventListener('DOMContentLoaded', initTable);
|
||||||
|
|
||||||
document.addEventListener('livewire:init', () => {
|
document.addEventListener('livewire:init', () => {
|
||||||
// ✅ Este hook es el correcto para Livewire v3
|
|
||||||
Livewire.hook('commit', ({ component, succeed }) => {
|
Livewire.hook('commit', ({ component, succeed }) => {
|
||||||
succeed(() => {
|
succeed(() => setTimeout(() => initTable(), 50));
|
||||||
setTimeout(() => initTable(), 50);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Livewire.on('open-modal-docente', () => $('#modalDocente').modal('show'));
|
||||||
|
Livewire.on('close-modal-docente', () => $('#modalDocente').modal('hide'));
|
||||||
|
Livewire.on('open-modal-reemplazo', () => $('#modalReemplazo').modal('show'));
|
||||||
|
Livewire.on('close-modal-reemplazo',() => $('#modalReemplazo').modal('hide'));
|
||||||
});
|
});
|
||||||
|
|
||||||
function confirmarEliminar(docenteId, materiaId, nombreDocente, nombreMateria) {
|
function confirmarEliminar(docenteId, materiaId, nombreDocente, nombreMateria) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "¿Eliminar docente?",
|
title: "¿Quitar docente?",
|
||||||
html: `¿Deseas quitar a <b>${nombreDocente}</b> de <b>${nombreMateria}</b>?`,
|
html: `¿Deseas quitar a <b>${nombreDocente}</b> de <b>${nombreMateria}</b>?`,
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: "Sí, eliminar",
|
confirmButtonText: "Sí, quitar",
|
||||||
cancelButtonText: "Cancelar",
|
cancelButtonText: "Cancelar",
|
||||||
confirmButtonColor: '#e74a3b',
|
confirmButtonColor: '#e74a3b',
|
||||||
}).then((result) => {
|
}).then(result => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) @this.eliminar(docenteId, materiaId);
|
||||||
@this.eliminar(docenteId, materiaId);
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTable() {
|
function initTable() {
|
||||||
// ✅ Destroy correcto
|
|
||||||
if ($.fn.DataTable.isDataTable('#tcont2')) {
|
if ($.fn.DataTable.isDataTable('#tcont2')) {
|
||||||
$('#tcont2').DataTable().destroy();
|
$('#tcont2').DataTable().destroy();
|
||||||
$('#tcont2').empty(); // limpia el DOM completamente
|
$('#tcont2').empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
new DataTable('#tcont2', {
|
new DataTable('#tcont2', {
|
||||||
retrieve: true, // ✅ evita el error de reinicialización
|
retrieve: true,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
order: [],
|
order: [],
|
||||||
language: {
|
language: {
|
||||||
"decimal": "",
|
emptyTable: "No hay información",
|
||||||
"emptyTable": "No hay información",
|
info: "Mostrando _START_ a _END_ de _TOTAL_ entradas",
|
||||||
"info": "Mostrando _START_ a _END_ de _TOTAL_ Entradas",
|
infoEmpty: "Mostrando 0 de 0 entradas",
|
||||||
"infoEmpty": "Mostrando 0 to 0 of 0 Entradas",
|
infoFiltered: "(filtrado de _MAX_ entradas)",
|
||||||
"infoFiltered": "(Filtrado de _MAX_ total entradas)",
|
lengthMenu: "Mostrar _MENU_ entradas",
|
||||||
"infoPostFix": "",
|
loadingRecords: "Cargando...",
|
||||||
"thousands": ",",
|
processing: "Procesando...",
|
||||||
"lengthMenu": "Mostrar _MENU_ Entradas",
|
search: "Buscar:",
|
||||||
"loadingRecords": "Cargando...",
|
zeroRecords: "Sin resultados",
|
||||||
"processing": "Procesando...",
|
thousands: ",",
|
||||||
"search": "Buscar:",
|
|
||||||
"zeroRecords": "Sin resultados encontrados"
|
|
||||||
},
|
},
|
||||||
lengthMenu: [
|
lengthMenu: [[10, 25, 50, -1], ['10', '25', '50', 'Todos']],
|
||||||
[10, 25, 50, -1],
|
|
||||||
['10 filas', '25 filas', '50 filas', 'Todos']
|
|
||||||
],
|
|
||||||
layout: {
|
layout: {
|
||||||
topStart: {
|
topStart: { buttons: ['colvis', 'pageLength'] },
|
||||||
buttons: ['colvis', 'pageLength']
|
|
||||||
},
|
|
||||||
topEnd: ['search'],
|
topEnd: ['search'],
|
||||||
bottomStart: [{
|
bottomStart: [{ buttons: [
|
||||||
buttons: [
|
{ extend: 'excel', title: 'Excel' },
|
||||||
{ extend: 'excel', title: 'Excel' },
|
{ extend: 'pdfHtml5', title: 'PDF' },
|
||||||
{ extend: 'pdfHtml5', title: 'PDF' },
|
{ extend: 'print' },
|
||||||
{ extend: 'print' }
|
]}, 'info'],
|
||||||
]
|
},
|
||||||
}, 'info'],
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
$("#tcont2_wrapper").removeClass("form-inline").addClass("w-100");
|
||||||
$("#tcont2_wrapper").removeClass("form-inline");
|
|
||||||
$("#tcont2_wrapper").addClass("w-100");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('livewire:init', () => {
|
|
||||||
Livewire.on('open-modal-docente', () => {
|
|
||||||
$('#modalDocente').modal('show');
|
|
||||||
});
|
|
||||||
Livewire.on('close-modal-docente', () => {
|
|
||||||
$('#modalDocente').modal('hide');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('title', isset($tarea) ? 'Editar Tarea' : 'Nueva Tarea')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
{{ isset($tarea) ? 'Editar Tarea' : 'Nueva Tarea' }}
|
||||||
|
— {{ $material->name }} · Grupo: {{ $grupo->clave }}
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('grupo.material.tareas', [$grupo, $material]) }}"
|
||||||
|
class="btn btn-sm btn-secondary shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (isset($tarea))
|
||||||
|
<form action="{{ route('grupo.material.tareas.update', [$grupo, $material, $tarea]) }}" method="POST">
|
||||||
|
@method('PUT')
|
||||||
|
@else
|
||||||
|
<form action="{{ route('grupo.material.tareas.store', [$grupo, $material]) }}" method="POST">
|
||||||
|
@endif
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="titulo">Título <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" id="titulo" name="titulo" class="form-control"
|
||||||
|
value="{{ old('titulo', $tarea->titulo ?? '') }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="descripcion">Descripción / Instrucciones</label>
|
||||||
|
<textarea id="descripcion" name="descripcion" class="form-control" rows="5">{{ old('descripcion', $tarea->descripcion ?? '') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fecha_entrega">Fecha límite de entrega</label>
|
||||||
|
<input type="date" id="fecha_entrega" name="fecha_entrega" class="form-control"
|
||||||
|
value="{{ old('fecha_entrega', isset($tarea) && $tarea->fecha_entrega ? $tarea->fecha_entrega : '') }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save fa-sm"></i>
|
||||||
|
{{ isset($tarea) ? 'Guardar cambios' : 'Crear tarea' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('head')
|
||||||
|
@include('layouts._partials.tablaStyle')
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('title', 'Tareas')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
Tareas — {{ $material->name }} · Grupo: {{ $grupo->clave }}
|
||||||
|
</h6>
|
||||||
|
<div>
|
||||||
|
{{-- Botón regresar según rol --}}
|
||||||
|
@hasrole('Coordinación académica')
|
||||||
|
<a href="{{ route('grupo.ver.materias', $grupo) }}" class="btn btn-sm btn-secondary shadow-sm mr-2">
|
||||||
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('grupo.administrar', $grupo) }}" class="btn btn-sm btn-secondary shadow-sm mr-2">
|
||||||
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('grupo.material.tareas.create', [$grupo, $material]) }}"
|
||||||
|
class="btn btn-sm btn-primary shadow-sm">
|
||||||
|
<i class="fas fa-plus fa-sm text-white-50"></i> Nueva Tarea
|
||||||
|
</a>
|
||||||
|
@endhasrole
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if (session('info'))
|
||||||
|
<div class="alert alert-success"><strong>{{ session('info') }}</strong></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($tareas->isEmpty())
|
||||||
|
<div class="alert alert-info">Aún no hay tareas para esta materia.</div>
|
||||||
|
@else
|
||||||
|
<table id="tcont" class="table table-striped table-bordered table-hover" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Título</th>
|
||||||
|
<th>Fecha límite</th>
|
||||||
|
<th>Pendientes</th>
|
||||||
|
<th>Calificadas</th>
|
||||||
|
<th>Total alumnos</th>
|
||||||
|
<th>Opciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($tareas as $tarea)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $tarea->titulo }}</td>
|
||||||
|
<td>
|
||||||
|
@if ($tarea->fecha_entrega)
|
||||||
|
{{ \Carbon\Carbon::parse($tarea->fecha_entrega)->format('d/m/Y') }}
|
||||||
|
@if (\Carbon\Carbon::parse($tarea->fecha_entrega)->isPast())
|
||||||
|
<span class="badge badge-danger ml-1">Vencida</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-success ml-1">Activa</span>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Sin fecha</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td><span class="badge badge-warning">{{ $tarea->pendientes_count }}</span></td>
|
||||||
|
<td><span class="badge badge-success">{{ $tarea->calificadas_count }}</span></td>
|
||||||
|
<td>{{ $tarea->entregas_count }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('grupo.material.tareas.show', [$grupo, $material, $tarea]) }}"
|
||||||
|
class="btn btn-sm btn-info">
|
||||||
|
<i class="fas fa-eye"></i> Ver
|
||||||
|
</a>
|
||||||
|
@hasrole('Docente')
|
||||||
|
<a href="{{ route('grupo.material.tareas.edit', [$grupo, $material, $tarea]) }}"
|
||||||
|
class="btn btn-sm btn-warning">
|
||||||
|
<i class="fas fa-edit"></i> Editar
|
||||||
|
</a>
|
||||||
|
<form class="delete-form d-inline"
|
||||||
|
action="{{ route('grupo.material.tareas.destroy', [$grupo, $material, $tarea]) }}"
|
||||||
|
method="POST">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endhasrole
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
@include('layouts._partials.tablaScript')
|
||||||
|
@include('layouts._partials.delete')
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('head')
|
||||||
|
@include('layouts._partials.tablaStyle')
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('title', 'Mis Tareas')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Mis Tareas</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if (session('info'))
|
||||||
|
<div class="alert alert-success"><strong>{{ session('info') }}</strong></div>
|
||||||
|
@endif
|
||||||
|
@if (session('error'))
|
||||||
|
<div class="alert alert-danger"><strong>{{ session('error') }}</strong></div>
|
||||||
|
@endif
|
||||||
|
@if ($errors->has('entrega'))
|
||||||
|
<div class="alert alert-warning"><strong>{{ $errors->first('entrega') }}</strong></div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (!isset($tareas) || $tareas->isEmpty())
|
||||||
|
<div class="alert alert-info">No tienes tareas asignadas en tus grupos.</div>
|
||||||
|
@else
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="tcont" class="table table-striped table-bordered table-hover" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tarea</th>
|
||||||
|
<th>Materia</th>
|
||||||
|
<th>Grupo</th>
|
||||||
|
<th>Fecha límite</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Calificación</th>
|
||||||
|
<th>Tu entrega / Acción</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($tareas as $tarea)
|
||||||
|
@php $entrega = $entregasMap[$tarea->id] ?? null; $status = $entrega?->status ?? 'sin_subir'; @endphp
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ $tarea->titulo }}</strong>
|
||||||
|
@if ($tarea->descripcion)
|
||||||
|
<p class="text-muted small mb-0">{{ Str::limit($tarea->descripcion, 80) }}</p>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>{{ $tarea->material->name }}</td>
|
||||||
|
<td>{{ $tarea->grupo->clave }}</td>
|
||||||
|
<td>
|
||||||
|
@if ($tarea->fecha_entrega)
|
||||||
|
{{ \Carbon\Carbon::parse($tarea->fecha_entrega)->format('d/m/Y') }}
|
||||||
|
@if (\Carbon\Carbon::parse($tarea->fecha_entrega)->isPast())
|
||||||
|
<span class="badge badge-danger">Vencida</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-success">Activa</span>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Sin fecha</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if ($status === 'sin_subir')
|
||||||
|
<span class="badge badge-secondary">Sin subir</span>
|
||||||
|
@elseif ($status === 'pendiente')
|
||||||
|
<span class="badge badge-warning">Pendiente</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-success">Calificada</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if ($status === 'calificada' && $entrega)
|
||||||
|
<strong class="text-success">{{ number_format($entrega->calificacion, 1) }}</strong>
|
||||||
|
@if ($entrega->comentario)
|
||||||
|
<br><small class="text-muted">{{ $entrega->comentario }}</small>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if ($status === 'sin_subir')
|
||||||
|
{{-- Formulario de entrega: texto y/o archivo --}}
|
||||||
|
<form action="{{ route('tarea.entregar', $tarea) }}" method="POST"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<textarea name="texto" class="form-control form-control-sm"
|
||||||
|
rows="3" placeholder="Escribe tu respuesta aquí (opcional)..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-1">
|
||||||
|
<div class="custom-file">
|
||||||
|
<input type="file" class="custom-file-input" id="archivo_{{ $tarea->id }}" name="archivo">
|
||||||
|
<label class="custom-file-label small" for="archivo_{{ $tarea->id }}">
|
||||||
|
Adjuntar archivo (opcional)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm btn-block mt-1">
|
||||||
|
<i class="fas fa-upload fa-sm"></i> Entregar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@elseif ($status === 'pendiente' || $status === 'calificada')
|
||||||
|
{{-- Mostrar lo que entregó --}}
|
||||||
|
@if ($entrega?->texto)
|
||||||
|
<div class="border rounded p-2 bg-light mb-2" style="max-height:120px;overflow-y:auto;font-size:.85rem;">
|
||||||
|
{{ $entrega->texto }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if ($entrega?->archivo)
|
||||||
|
<a href="{{ route('files.serve', $entrega->archivo) }}"
|
||||||
|
target="_blank" class="btn btn-sm btn-outline-info">
|
||||||
|
<i class="fas fa-eye fa-sm"></i> Ver archivo entregado
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@if (!$entrega?->texto && !$entrega?->archivo)
|
||||||
|
<span class="text-muted small">Sin contenido registrado</span>
|
||||||
|
@endif
|
||||||
|
@if ($status === 'pendiente')
|
||||||
|
<br><small class="text-warning"><i class="fas fa-clock"></i> Esperando calificación</small>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
@include('layouts._partials.tablaScript')
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.custom-file-input').forEach(input => {
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
const fileName = this.files[0]?.name || 'Adjuntar archivo (opcional)';
|
||||||
|
this.nextElementSibling.textContent = fileName;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
@extends('layouts.landing')
|
||||||
|
|
||||||
|
@section('head')
|
||||||
|
@include('layouts._partials.tablaStyle')
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('title', 'Detalle de Tarea')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
{{-- Info de la tarea --}}
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<div class="d-sm-flex align-items-center justify-content-between">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">
|
||||||
|
{{ $tarea->titulo }}
|
||||||
|
— {{ $material->name }} · Grupo: {{ $grupo->clave }}
|
||||||
|
</h6>
|
||||||
|
<a href="{{ route('grupo.material.tareas', [$grupo, $material]) }}"
|
||||||
|
class="btn btn-sm btn-secondary shadow-sm">
|
||||||
|
<i class="fas fa-arrow-left fa-sm"></i> Regresar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@if (session('info'))
|
||||||
|
<div class="alert alert-success"><strong>{{ session('info') }}</strong></div>
|
||||||
|
@endif
|
||||||
|
@if ($tarea->descripcion)
|
||||||
|
<p class="mb-2"><strong>Instrucciones:</strong> {{ $tarea->descripcion }}</p>
|
||||||
|
@endif
|
||||||
|
@if ($tarea->fecha_entrega)
|
||||||
|
<p class="mb-0">
|
||||||
|
<strong>Fecha límite:</strong>
|
||||||
|
{{ \Carbon\Carbon::parse($tarea->fecha_entrega)->format('d/m/Y') }}
|
||||||
|
@if (\Carbon\Carbon::parse($tarea->fecha_entrega)->isPast())
|
||||||
|
<span class="badge badge-danger ml-1">Vencida</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-success ml-1">Activa</span>
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Entregas de alumnos --}}
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Entregas de alumnos</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body table-responsive">
|
||||||
|
<table id="tcont" class="table table-striped table-bordered table-hover" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Alumno</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Entrega</th>
|
||||||
|
@hasrole('Docente')
|
||||||
|
<th>Calificar / Calificación</th>
|
||||||
|
@endhasrole
|
||||||
|
@hasrole('Coordinación académica')
|
||||||
|
<th>Calificación</th>
|
||||||
|
@endhasrole
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($alumnos as $alumno)
|
||||||
|
@php $entrega = $entregas[$alumno->id] ?? null; @endphp
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle">
|
||||||
|
{{ $alumno->alumnos->first()?->apellidoPaterno }}
|
||||||
|
{{ $alumno->alumnos->first()?->apellidoMaterno }}
|
||||||
|
{{ $alumno->alumnos->first()?->name }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="align-middle">
|
||||||
|
@if (!$entrega || $entrega->status === 'sin_subir')
|
||||||
|
<span class="badge badge-secondary">Sin subir</span>
|
||||||
|
@elseif ($entrega->status === 'pendiente')
|
||||||
|
<span class="badge badge-warning">Pendiente</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-success">Calificada</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="align-middle" style="max-width:280px">
|
||||||
|
@if ($entrega && ($entrega->texto || $entrega->archivo))
|
||||||
|
@if ($entrega->texto)
|
||||||
|
<div class="border rounded p-2 bg-light mb-1"
|
||||||
|
style="max-height:100px;overflow-y:auto;font-size:.85rem;white-space:pre-wrap;">{{ $entrega->texto }}</div>
|
||||||
|
@endif
|
||||||
|
@if ($entrega->archivo)
|
||||||
|
<a href="{{ route('files.serve', $entrega->archivo) }}"
|
||||||
|
target="_blank" class="btn btn-sm btn-outline-info">
|
||||||
|
<i class="fas fa-file fa-sm"></i> Ver archivo
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="text-muted small">Sin entrega</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{{-- Docente: puede calificar --}}
|
||||||
|
@hasrole('Docente')
|
||||||
|
<td class="align-middle" style="min-width:300px">
|
||||||
|
@if ($entrega && in_array($entrega->status, ['pendiente', 'calificada']))
|
||||||
|
<form action="{{ route('tarea.calificar', [$tarea, $alumno]) }}" method="POST"
|
||||||
|
class="d-flex align-items-center flex-wrap" style="gap:4px">
|
||||||
|
@csrf
|
||||||
|
<input type="number" name="calificacion" min="0" max="10" step="0.1"
|
||||||
|
class="form-control form-control-sm" style="width:80px"
|
||||||
|
placeholder="0–10"
|
||||||
|
value="{{ $entrega->status === 'calificada' ? $entrega->calificacion : '' }}"
|
||||||
|
required>
|
||||||
|
<input type="text" name="comentario" class="form-control form-control-sm"
|
||||||
|
style="width:140px" placeholder="Comentario"
|
||||||
|
value="{{ $entrega->comentario ?? '' }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-sm {{ $entrega->status === 'calificada' ? 'btn-warning' : 'btn-success' }}">
|
||||||
|
<i class="fas fa-{{ $entrega->status === 'calificada' ? 'redo' : 'check' }} fa-sm"></i>
|
||||||
|
{{ $entrega->status === 'calificada' ? 'Recalificar' : 'Calificar' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<span class="text-muted small">Sin entrega aún</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
@endhasrole
|
||||||
|
|
||||||
|
{{-- Coordinación: solo lectura de calificación --}}
|
||||||
|
@hasrole('Coordinación académica')
|
||||||
|
<td class="align-middle">
|
||||||
|
@if ($entrega?->status === 'calificada')
|
||||||
|
<strong class="text-success">{{ number_format($entrega->calificacion, 1) }}</strong>
|
||||||
|
@if ($entrega->comentario)
|
||||||
|
<br><small class="text-muted">{{ $entrega->comentario }}</small>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
@endhasrole
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
@include('layouts._partials.tablaScript')
|
||||||
|
@endsection
|
||||||
Reference in New Issue
Block a user