archivo de configuracion de nginx agregado para el puerto 9000

This commit is contained in:
2026-05-02 16:57:29 -06:00
parent 2af36ea272
commit 881e67fb12
33 changed files with 2035 additions and 245 deletions
+9 -1
View File
@@ -10,7 +10,15 @@
"Bash(ls resources/views/)",
"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_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)"
]
}
}
+108
View File
@@ -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.');
}
}
+18 -3
View File
@@ -10,12 +10,27 @@ use Illuminate\Support\Facades\Storage;
class ChatController extends Controller
{
// Lista de amigos con quienes puedo chatear
// Lista de conversaciones (amigos + historial con no-amigos)
public function index()
{
$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
@@ -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.');
}
}
+45
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Alumno;
use App\Models\Docente;
use App\Models\Grupo;
use App\Models\Plan;
use App\Models\Plane;
@@ -11,6 +12,7 @@ use App\Models\Turno;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class GrupoController extends Controller
{
@@ -38,12 +40,55 @@ class GrupoController extends Controller
$query->whereIn('plantel_id', $plantelesIds);
})
->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'));
}
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()
{
$turnos= Turno::all();
+122 -37
View File
@@ -2,63 +2,148 @@
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\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class TareaController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
public function index(Grupo $grupo, Material $material)
{
//
$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'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
public function create(Grupo $grupo, Material $material)
{
//
return view('tarea.create', compact('grupo', 'material'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
public function store(Request $request, Grupo $grupo, Material $material)
{
//
$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',
]);
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
return redirect()->route('grupo.material.tareas', [$grupo, $material])
->with('info', 'Tarea creada correctamente.');
}
/**
* Show the form for editing the specified resource.
*/
public function edit(string $id)
public function show(Grupo $grupo, Material $material, Tarea $tarea)
{
//
$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',
]);
}
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
$entregas = Entrega::where('tarea_id', $tarea->id)
->with('alumno.alumnos')
->get()
->keyBy('alumno_id');
return view('tarea.show', compact('grupo', 'material', 'tarea', 'alumnos', 'entregas'));
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
public function edit(Grupo $grupo, Material $material, Tarea $tarea)
{
//
return view('tarea.create', compact('grupo', 'material', 'tarea'));
}
public function update(Request $request, Grupo $grupo, Material $material, Tarea $tarea)
{
$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)
{
// 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.');
}
}
+71 -19
View File
@@ -2,9 +2,12 @@
namespace App\Livewire;
use App\Models\Docente;
use App\Models\Entrega;
use App\Models\Material;
use App\Models\Tarea;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Livewire\Component;
class GrupoDocente extends Component
@@ -16,9 +19,15 @@ class GrupoDocente extends Component
public $materia_id;
public $materiasSelect;
// Para reemplazo de docente
public $reemplazoDocenteActualId;
public $reemplazoMateriaId;
public $reemplazoDocenteId = '';
public $reemplazoMateriaNombre = '';
public function mount($grupo)
{
$this->grupo = $grupo; // <- primero asignar el grupo
$this->grupo = $grupo;
$user = Auth::user();
$plantelesIds = $user->plantelUsuarios->pluck('id');
@@ -31,7 +40,6 @@ public function mount($grupo)
})
->get();
// Todas las materias de la carrera sin filtro de ciclo
$this->materias = Material::whereHas('carreraMaterial', function ($query) use ($grupo) {
$query->where('carreras.id', $grupo->plan->carrera);
})
@@ -46,10 +54,8 @@ $materiasAsignadas = DB::table('docente_grupo')
$this->materiasSelect = Material::whereHas('carreraMaterial', function ($query) use ($grupo) {
$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
})
->where('ciclo', $this->grupo->ciclo)
->when(!empty($materiasAsignadas), fn($q) => $q->whereNotIn('id', $materiasAsignadas))
->get();
}
@@ -61,33 +67,79 @@ $this->materiasSelect = Material::whereHas('carreraMaterial', function($query) u
]);
$this->grupo->docentes()->attach($this->docente_id, [
'materia_id' => $this->materia_id
'materia_id' => $this->materia_id,
]);
$this->reset(['docente_id', 'materia_id']);
$this->dispatch('close-modal-docente');
$this->dispatch('swal',
icon: 'success',
title: '¡Listo!',
icon: 'success', title: '¡Listo!',
text: 'Docente asignado correctamente',
timer: 3000,
confirm: false,
closeModal: true,
timer: 3000, confirm: false, closeModal: true,
);
}
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->dispatch('swal',
icon: 'success',
title: 'Eliminado',
text: 'Docente removido de la materia correctamente',
timer: 2000,
confirm: false,
closeModal: true,
icon: 'success', title: 'Eliminado',
text: 'Docente y sus tareas removidos correctamente',
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,
);
}
+73
View File
@@ -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();
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ class Docente extends Model
// Relación con Material
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');
}
}
+13
View File
@@ -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); }
}
+4 -8
View File
@@ -3,17 +3,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Tarea extends Model
{
protected $guarded = [];
public $timestamps = false;
// Relación con Carrera
public function carreraMaterial(): BelongsToMany
{
return $this->belongsToMany(Carrera::class);
}
public function material() { return $this->belongsTo(Material::class); }
public function grupo() { return $this->belongsTo(Grupo::class); }
public function docente() { return $this->belongsTo(Docente::class); }
public function entregas() { return $this->hasMany(Entrega::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');
});
}
};
+4
View File
@@ -140,5 +140,9 @@ class RoleSeeder extends Seeder
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.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
View File
@@ -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;
}
}
+162
View File
@@ -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
+173
View File
@@ -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
+115
View File
@@ -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>&times;</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
+22 -9
View File
@@ -9,23 +9,36 @@
<h5 class="mb-0"><i class="fas fa-comments"></i> Chats</h5>
</div>
<div class="card-body p-0" style="overflow-y: auto;">
@forelse($friends as $friend)
<a href="{{ route('chat.show', $friend) }}" class="text-decoration-none">
@forelse($users as $user)
<a href="{{ route('chat.show', $user) }}" class="text-decoration-none">
<div class="d-flex align-items-center p-3 border-bottom chat-user-item">
<img src="{{ $friend->profile_photo_url }}"
class="rounded-circle mr-3"
<div class="position-relative mr-3 flex-shrink-0">
<img src="{{ $user->profile_photo_url }}"
class="rounded-circle"
width="45" height="45"
style="object-fit:cover">
<div>
<div class="font-weight-bold text-dark">{{ $friend->name }}</div>
<small class="text-muted">{{ $friend->getRoleNames()->first() }}</small>
style="object-fit:cover;
{{ $user->is_friend ? '' : 'filter:grayscale(60%); opacity:.8' }}">
@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>
</a>
@empty
<div class="text-center text-muted py-5">
<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">
<i class="fas fa-search mr-1"></i> Buscar personas
</a>
+55 -11
View File
@@ -1,15 +1,59 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
@extends('layouts.landing')
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<x-welcome />
@section('title', 'Inicio')
@section('content')
<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">&times;</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>
</x-app-layout>
</div>
</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 &mdash; 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 }} &middot; {{ $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
+4
View File
@@ -88,8 +88,12 @@
<option value="0" {{ 0 == $grupo->status ? 'selected' : '' }}>Inactivo</option>
</select>
</div>
@hasrole('Control escolar')
<input class="btn btn-primary" type="submit" value="Guardar">
@endhasrole
</form>
</div>
</div>
+16
View File
@@ -54,6 +54,22 @@
</div>
@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>
</tr>
@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 &mdash; 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 }} &middot; {{ $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">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>Copyright &copy; Your Website 2021</span>
<span>Copyright &copy; Your Website {{date('Y')}}</span>
</div>
</div>
</footer>
@@ -8,6 +8,7 @@
<i class="fa fa-bars"></i>
</button>
@can(['comunidad','amigos'])
<!-- 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="input-group">
@@ -25,8 +26,10 @@
min-width:300px"></div>
</div>
@endcan
<ul class="navbar-nav ml-auto">
<!-- Nav Item - Search (XS) -->
<li class="nav-item no-arrow d-sm-none position-relative">
<a class="nav-link" href="#" id="mobile-search-toggle" role="button">
@@ -50,6 +53,7 @@
</div>
</li>
@can(['comunidad','amigos'])
<!-- Nav Item - Notificaciones -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
@@ -98,6 +102,9 @@
</div>
</li>
@endcan
@can('mensajes')
<!-- Nav Item - Mensajes -->
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="{{ route('chat.index') }}" id="messagesDropdown"
@@ -143,6 +150,7 @@
</a>
</div>
</li>
@endcan
<div class="topbar-divider d-none d-sm-block"></div>
+102 -84
View File
@@ -1,16 +1,13 @@
<div>
{{-- Modal --}}
{{-- Modal: Agregar docente --}}
<div wire:ignore.self class="modal fade" id="modalDocente" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Agregar Docente al Grupo</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
<h5 class="modal-title">Asignar Docente al Grupo</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
{{-- Docente --}}
<div class="mb-3">
<label>Docente</label>
<select class="form-control" wire:model="docente_id">
@@ -18,30 +15,23 @@
@foreach ($docentes as $docente)
@php $usr = $docente->docentes->first() @endphp
<option value="{{ $docente->id }}">
{{ $usr?->name }}
{{ $usr?->apellidoPaterno }}
{{ $usr?->apellidoPaterno }} {{ $usr?->apellidoMaterno }} {{ $usr?->name }}
</option>
@endforeach
</select>
@error('docente_id')
<span class="text-danger small">{{ $message }}</span>
@enderror
@error('docente_id')<span class="text-danger small">{{ $message }}</span>@enderror
</div>
{{-- Materia --}}
<div class="mb-3">
<label>Materia</label>
<select class="form-control" wire:model="materia_id">
<option value="">Seleccionar materia...</option>
@foreach ($materiasSelect as $materia)
<option value="{{ $materia->id }}">
ciclo {{ $materia->ciclo }} - {{ $materia->name }}
Ciclo {{ $materia->ciclo }} {{ $materia->name }}
</option>
@endforeach
</select>
@error('materia_id')
<span class="text-danger small">{{ $message }}</span>
@enderror
@error('materia_id')<span class="text-danger small">{{ $message }}</span>@enderror
</div>
</div>
<div class="modal-footer">
@@ -52,18 +42,57 @@
</div>
</div>
{{-- Tabla docentes asignados --}}
{{-- Modal: Reemplazar docente --}}
<div wire:ignore.self class="modal fade" id="modalReemplazo" tabindex="-1" aria-hidden="true">
<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>&times;</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>Id</th>
<th>Materia</th>
<th>Ciclo</th>
<th>Docente asignado</th>
@hasrole('Coordinación académica')
<th>Opciones</th>
<th>Ver tareas</th>
<th>Acciones</th>
@endhasrole
</tr>
</thead>
<tbody>
@@ -73,127 +102,116 @@
$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 }}
{{ $usr->apellidoPaterno }} {{ $usr->apellidoMaterno }} {{ $usr->name }}
@else
<span class="badge badge-secondary">Sin docente</span>
@endif
</td>
@hasrole('Coordinación académica')
{{-- Columna Ver tareas --}}
<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
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>
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>
@empty
<tr>Sin materias en este plan.</tr>
<tr><td colspan="5">Sin materias en este plan.</td></tr>
@endforelse
</tbody>
</table>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', initTable);
document.addEventListener('livewire:init', () => {
// ✅ Este hook es el correcto para Livewire v3
Livewire.hook('commit', ({ component, succeed }) => {
succeed(() => {
setTimeout(() => initTable(), 50);
});
succeed(() => 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) {
Swal.fire({
title: "¿Eliminar docente?",
title: "¿Quitar docente?",
html: `¿Deseas quitar a <b>${nombreDocente}</b> de <b>${nombreMateria}</b>?`,
icon: "warning",
showCancelButton: true,
confirmButtonText: "Sí, eliminar",
confirmButtonText: "Sí, quitar",
cancelButtonText: "Cancelar",
confirmButtonColor: '#e74a3b',
}).then((result) => {
if (result.isConfirmed) {
@this.eliminar(docenteId, materiaId);
}
}).then(result => {
if (result.isConfirmed) @this.eliminar(docenteId, materiaId);
});
}
function initTable() {
// ✅ Destroy correcto
if ($.fn.DataTable.isDataTable('#tcont2')) {
$('#tcont2').DataTable().destroy();
$('#tcont2').empty(); // limpia el DOM completamente
$('#tcont2').empty();
}
new DataTable('#tcont2', {
retrieve: true, // ✅ evita el error de reinicialización
retrieve: true,
responsive: true,
order: [],
language: {
"decimal": "",
"emptyTable": "No hay información",
"info": "Mostrando _START_ a _END_ de _TOTAL_ Entradas",
"infoEmpty": "Mostrando 0 to 0 of 0 Entradas",
"infoFiltered": "(Filtrado de _MAX_ total entradas)",
"infoPostFix": "",
"thousands": ",",
"lengthMenu": "Mostrar _MENU_ Entradas",
"loadingRecords": "Cargando...",
"processing": "Procesando...",
"search": "Buscar:",
"zeroRecords": "Sin resultados encontrados"
emptyTable: "No hay información",
info: "Mostrando _START_ a _END_ de _TOTAL_ entradas",
infoEmpty: "Mostrando 0 de 0 entradas",
infoFiltered: "(filtrado de _MAX_ entradas)",
lengthMenu: "Mostrar _MENU_ entradas",
loadingRecords: "Cargando...",
processing: "Procesando...",
search: "Buscar:",
zeroRecords: "Sin resultados",
thousands: ",",
},
lengthMenu: [
[10, 25, 50, -1],
['10 filas', '25 filas', '50 filas', 'Todos']
],
lengthMenu: [[10, 25, 50, -1], ['10', '25', '50', 'Todos']],
layout: {
topStart: {
buttons: ['colvis', 'pageLength']
},
topStart: { buttons: ['colvis', 'pageLength'] },
topEnd: ['search'],
bottomStart: [{
buttons: [
bottomStart: [{ buttons: [
{ extend: 'excel', title: 'Excel' },
{ extend: 'pdfHtml5', title: 'PDF' },
{ extend: 'print' }
]
}, 'info'],
{ extend: 'print' },
]}, '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>
@endpush
+67
View File
@@ -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' }}
&mdash; {{ $material->name }} &middot; 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
+109
View File
@@ -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 &mdash; {{ $material->name }} &middot; 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
+153
View File
@@ -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
+156
View File
@@ -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 }}
&mdash; {{ $material->name }} &middot; 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="010"
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