diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c796fbf8..cfa11400 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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)" ] } } diff --git a/app/Http/Controllers/AnuncioController.php b/app/Http/Controllers/AnuncioController.php new file mode 100644 index 00000000..8d18d0f7 --- /dev/null +++ b/app/Http/Controllers/AnuncioController.php @@ -0,0 +1,108 @@ +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.'); + } +} diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 5b0cf315..c04ff7e4 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -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')); + $me = auth()->user(); + + // 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 diff --git a/app/Http/Controllers/EntregaController.php b/app/Http/Controllers/EntregaController.php new file mode 100644 index 00000000..1e591d4e --- /dev/null +++ b/app/Http/Controllers/EntregaController.php @@ -0,0 +1,80 @@ + $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.'); + } +} diff --git a/app/Http/Controllers/GrupoController.php b/app/Http/Controllers/GrupoController.php index 336c7025..861264e7 100644 --- a/app/Http/Controllers/GrupoController.php +++ b/app/Http/Controllers/GrupoController.php @@ -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(); diff --git a/app/Http/Controllers/TareaController.php b/app/Http/Controllers/TareaController.php index 4795554a..ba27c322 100644 --- a/app/Http/Controllers/TareaController.php +++ b/app/Http/Controllers/TareaController.php @@ -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', + ]); + } + + return redirect()->route('grupo.material.tareas', [$grupo, $material]) + ->with('info', 'Tarea creada correctamente.'); } - /** - * Display the specified resource. - */ - public function show(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', + ]); + } + } + + $entregas = Entrega::where('tarea_id', $tarea->id) + ->with('alumno.alumnos') + ->get() + ->keyBy('alumno_id'); + + return view('tarea.show', compact('grupo', 'material', 'tarea', 'alumnos', 'entregas')); } - /** - * Show the form for editing the specified resource. - */ - public function edit(string $id) + public function edit(Grupo $grupo, Material $material, Tarea $tarea) { - // + return view('tarea.create', compact('grupo', 'material', 'tarea')); } - /** - * Update the specified resource in storage. - */ - public function update(Request $request, string $id) + 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.'); } - /** - * Remove the specified resource from storage. - */ - public function destroy(string $id) + 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.'); } } diff --git a/app/Livewire/GrupoDocente.php b/app/Livewire/GrupoDocente.php index 45f754e2..d6538206 100644 --- a/app/Livewire/GrupoDocente.php +++ b/app/Livewire/GrupoDocente.php @@ -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,42 +19,45 @@ class GrupoDocente extends Component public $materia_id; public $materiasSelect; -public function mount($grupo) -{ - $this->grupo = $grupo; // <- primero asignar el grupo + // Para reemplazo de docente + public $reemplazoDocenteActualId; + public $reemplazoMateriaId; + public $reemplazoDocenteId = ''; + public $reemplazoMateriaNombre = ''; - $user = Auth::user(); - $plantelesIds = $user->plantelUsuarios->pluck('id'); + public function mount($grupo) + { + $this->grupo = $grupo; - $this->docentes = Docente::with(['docentes.plantelUsuarios']) - ->whereHas('docentes', function($query) use ($plantelesIds) { - $query->whereHas('plantelUsuarios', function($q) use ($plantelesIds) { - $q->whereIn('plantels.id', $plantelesIds); - }); + $user = Auth::user(); + $plantelesIds = $user->plantelUsuarios->pluck('id'); + + $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(); - // 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); - }) - ->orderBy('ciclo') - ->get(); + $materiasAsignadas = DB::table('docente_grupo') + ->where('grupo_id', $this->grupo->id) + ->pluck('materia_id') + ->toArray(); -$materiasAsignadas = DB::table('docente_grupo') - ->where('grupo_id', $this->grupo->id) - ->pluck('materia_id') - ->toArray(); - -$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 - }) - ->get(); -} + $this->materiasSelect = Material::whereHas('carreraMaterial', function ($query) use ($grupo) { + $query->where('carreras.id', $grupo->plan->carrera); + }) + ->where('ciclo', $this->grupo->ciclo) + ->when(!empty($materiasAsignadas), fn($q) => $q->whereNotIn('id', $materiasAsignadas)) + ->get(); + } public function guardar() { @@ -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, ); } diff --git a/app/Models/Anuncio.php b/app/Models/Anuncio.php new file mode 100644 index 00000000..c3fff5e0 --- /dev/null +++ b/app/Models/Anuncio.php @@ -0,0 +1,73 @@ + '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(); + } +} diff --git a/app/Models/Docente.php b/app/Models/Docente.php index ec9f6cf7..3af7b74a 100644 --- a/app/Models/Docente.php +++ b/app/Models/Docente.php @@ -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'); } } diff --git a/app/Models/Entrega.php b/app/Models/Entrega.php new file mode 100644 index 00000000..2531b33f --- /dev/null +++ b/app/Models/Entrega.php @@ -0,0 +1,13 @@ +belongsTo(Tarea::class); } + public function alumno() { return $this->belongsTo(Alumno::class); } +} diff --git a/app/Models/Tarea.php b/app/Models/Tarea.php index 937cbaf0..bff2a6bf 100644 --- a/app/Models/Tarea.php +++ b/app/Models/Tarea.php @@ -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); } } diff --git a/database/migrations/2026_04_28_174807_create_anuncios_table.php b/database/migrations/2026_04_28_174807_create_anuncios_table.php new file mode 100644 index 00000000..ee98976f --- /dev/null +++ b/database/migrations/2026_04_28_174807_create_anuncios_table.php @@ -0,0 +1,36 @@ +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'); + } +}; diff --git a/database/migrations/2026_04_30_113648_add_fields_to_tareas_table.php b/database/migrations/2026_04_30_113648_add_fields_to_tareas_table.php new file mode 100644 index 00000000..2ad6075c --- /dev/null +++ b/database/migrations/2026_04_30_113648_add_fields_to_tareas_table.php @@ -0,0 +1,37 @@ +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']); + }); + } +}; diff --git a/database/migrations/2026_04_30_113648_create_entregas_table.php b/database/migrations/2026_04_30_113648_create_entregas_table.php new file mode 100644 index 00000000..48d84323 --- /dev/null +++ b/database/migrations/2026_04_30_113648_create_entregas_table.php @@ -0,0 +1,37 @@ +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'); + } +}; diff --git a/database/migrations/2026_04_30_115155_add_texto_to_entregas_table.php b/database/migrations/2026_04_30_115155_add_texto_to_entregas_table.php new file mode 100644 index 00000000..7625acbd --- /dev/null +++ b/database/migrations/2026_04_30_115155_add_texto_to_entregas_table.php @@ -0,0 +1,25 @@ +text('texto')->nullable()->after('archivo'); + }); + } + + public function down(): void + { + Schema::table('entregas', function (Blueprint $table) { + $table->dropColumn('texto'); + }); + } +}; diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index acc71ac6..fb117268 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -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]); } } diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..18187906 --- /dev/null +++ b/nginx.conf @@ -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; + } +} diff --git a/resources/views/anuncio/create.blade.php b/resources/views/anuncio/create.blade.php new file mode 100644 index 00000000..a0c79fe8 --- /dev/null +++ b/resources/views/anuncio/create.blade.php @@ -0,0 +1,162 @@ +@extends('layouts.landing') + +@section('title', 'Nuevo Anuncio') + +@section('content') +
| # | +Título | +Tipo | +Audiencia | +Vence | +Activo | +Opciones | +
|---|---|---|---|---|---|---|
| {{ $anuncio->id }} | ++ + + + {{ $anuncio->title }} + @if($anuncio->image_path) + + @endif + | ++ {{ $cfg['label'] }} + | ++ @foreach($anuncio->audience as $aud) + {{ \App\Models\Anuncio::ROLES[$aud] ?? $aud }} + @endforeach + | ++ @if($anuncio->expires_at) + + + {{ $anuncio->expires_at->format('d/m/Y H:i') }} + + @else + Sin vencimiento + @endif + | ++ + | ++ + + + + | +
| + + No hay anuncios creados. + | +||||||
Aún no tienes amigos para chatear.
+Aún no tienes conversaciones.
Buscar personas diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 4db8ebf3..40908115 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,15 +1,59 @@ -{{ $anuncio->body }}
+ @endif + @if($anuncio->image_path) +Utiliza el menú lateral para acceder a los módulos disponibles.
++ Ciclo: {{ $material->ciclo }} · {{ $material->horas }} hrs +
+ + Gestionar Tareas + ++ Ciclo: {{ $asig->ciclo }} · {{ $asig->horas }} hrs +
++ + + {{ $asig->apellidoPaterno }} {{ $asig->apellidoMaterno }} {{ $asig->docente_nombre }} + +
+ + Ver Tareas + +