Que el contenido temático se muestre en el historial , Actualizar esta sección de seriación fix#197 fix #195 #218
@@ -229,12 +229,38 @@ export function IAAsignaturaTab() {
|
|||||||
}
|
}
|
||||||
}, [activeChats, loadingConv])
|
}, [activeChats, loadingConv])
|
||||||
|
|
||||||
const filteredFields = useMemo(() => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
if (!showSuggestions) return availableFields
|
const value = e.target.value
|
||||||
|
const selectionStart = e.target.selectionStart // Posición del cursor
|
||||||
|
setInput(value)
|
||||||
|
|
||||||
|
// Buscamos si el carácter anterior al cursor es ':'
|
||||||
|
const lastChar = value.slice(selectionStart - 1, selectionStart)
|
||||||
|
|
||||||
|
if (lastChar === ':') {
|
||||||
|
setShowSuggestions(true)
|
||||||
|
} else if (!value.includes(':')) {
|
||||||
|
// Si borran todos los dos puntos, cerramos
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredFields = useMemo(() => {
|
||||||
|
if (!showSuggestions || !input) return availableFields
|
||||||
|
|
||||||
|
// 1. Encontrar el ":" más cercano a la IZQUIERDA del cursor
|
||||||
|
// Usamos una posición de referencia (si no tienes ref, usaremos el final del string,
|
||||||
|
// pero para mayor precisión lo ideal es usar e.target.selectionStart en el onChange)
|
||||||
|
|
||||||
// Extraemos lo que hay después del último ':' para filtrar
|
|
||||||
const lastColonIndex = input.lastIndexOf(':')
|
const lastColonIndex = input.lastIndexOf(':')
|
||||||
const query = input.slice(lastColonIndex + 1).toLowerCase()
|
if (lastColonIndex === -1) return availableFields
|
||||||
|
|
||||||
|
// 2. Extraer solo el fragmento de "búsqueda"
|
||||||
|
// Cortamos desde el ":" hasta el final, y luego tomamos solo la primera palabra
|
||||||
|
const textAfterColon = input.slice(lastColonIndex + 1)
|
||||||
|
const query = textAfterColon.split(/\s/)[0].toLowerCase() // Se detiene al encontrar un espacio
|
||||||
|
|
||||||
|
if (!query) return availableFields
|
||||||
|
|
||||||
return availableFields.filter(
|
return availableFields.filter(
|
||||||
(f) =>
|
(f) =>
|
||||||
@@ -254,24 +280,28 @@ export function IAAsignaturaTab() {
|
|||||||
|
|
||||||
// 3. Función para insertar el campo y limpiar el prompt
|
// 3. Función para insertar el campo y limpiar el prompt
|
||||||
const handleSelectField = (field: SelectedField) => {
|
const handleSelectField = (field: SelectedField) => {
|
||||||
// 1. Agregamos al array de objetos (para tu lógica de API)
|
|
||||||
if (!selectedFields.find((f) => f.key === field.key)) {
|
if (!selectedFields.find((f) => f.key === field.key)) {
|
||||||
setSelectedFields((prev) => [...prev, field])
|
setSelectedFields((prev) => [...prev, field])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Lógica de autocompletado en el texto
|
|
||||||
const lastColonIndex = input.lastIndexOf(':')
|
const lastColonIndex = input.lastIndexOf(':')
|
||||||
if (lastColonIndex !== -1) {
|
if (lastColonIndex !== -1) {
|
||||||
// Tomamos lo que había antes del ":" + el Nombre del Campo + un espacio
|
const parteAntesDelColon = input.slice(0, lastColonIndex)
|
||||||
const nuevoTexto = input.slice(0, lastColonIndex) + `${field.label} `
|
|
||||||
|
// Buscamos si hay texto después de la palabra que estamos escribiendo
|
||||||
|
const textoDespuesDelColon = input.slice(lastColonIndex + 1)
|
||||||
|
const espacioIndex = textoDespuesDelColon.indexOf(' ')
|
||||||
|
|
||||||
|
// Si hay un espacio, guardamos lo que sigue. Si no, es el final del texto.
|
||||||
|
const parteRestante =
|
||||||
|
espacioIndex !== -1 ? textoDespuesDelColon.slice(espacioIndex) : ''
|
||||||
|
|
||||||
|
// Reconstruimos: [Antes] + [Label] + [Lo que ya estaba después]
|
||||||
|
const nuevoTexto = `${parteAntesDelColon}${field.label}${parteRestante}`
|
||||||
setInput(nuevoTexto)
|
setInput(nuevoTexto)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Cerramos el buscador y devolvemos el foco al textarea
|
|
||||||
setShowSuggestions(false)
|
setShowSuggestions(false)
|
||||||
|
|
||||||
// Opcional: Si tienes una ref del textarea, puedes hacer:
|
|
||||||
// textareaRef.current?.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveName = (id: string) => {
|
const handleSaveName = (id: string) => {
|
||||||
@@ -740,10 +770,28 @@ export function IAAsignaturaTab() {
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setInput(e.target.value)
|
const val = e.target.value
|
||||||
if (e.target.value.endsWith(':')) setShowSuggestions(true)
|
const cursor = e.target.selectionStart
|
||||||
else if (showSuggestions && !e.target.value.includes(':'))
|
setInput(val)
|
||||||
|
|
||||||
|
const textBeforeCursor = val.slice(0, cursor)
|
||||||
|
const lastColonIndex = textBeforeCursor.lastIndexOf(':')
|
||||||
|
|
||||||
|
if (lastColonIndex !== -1) {
|
||||||
|
const textSinceColon = textBeforeCursor.slice(
|
||||||
|
lastColonIndex + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Si hay un espacio después del ":", cerramos sugerencias (ya no es un comando)
|
||||||
|
// Si no hay espacio, activamos
|
||||||
|
if (!textSinceColon.includes(' ')) {
|
||||||
|
setShowSuggestions(true)
|
||||||
|
} else {
|
||||||
setShowSuggestions(false)
|
setShowSuggestions(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setShowSuggestions(false)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
|||||||
@@ -218,27 +218,31 @@ function AsignaturasPage() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-slate-50/50">
|
<TableRow className="bg-slate-50/50">
|
||||||
<TableHead className="w-30">Clave</TableHead>
|
<TableHead className="w-30 px-6 py-4">Clave</TableHead>
|
||||||
<TableHead>Nombre</TableHead>
|
<TableHead className="px-6 py-4">Nombre</TableHead>
|
||||||
<TableHead className="text-center">Créditos</TableHead>
|
<TableHead className="px-6 py-4 text-center">Créditos</TableHead>
|
||||||
<TableHead className="text-center">Ciclo</TableHead>
|
<TableHead className="px-6 py-4 text-center">Ciclo</TableHead>
|
||||||
<TableHead>Línea Curricular</TableHead>
|
<TableHead className="px-6 py-4">Línea Curricular</TableHead>
|
||||||
<TableHead>Tipo</TableHead>
|
<TableHead className="px-6 py-4">Tipo</TableHead>
|
||||||
<TableHead>Estado</TableHead>
|
<TableHead className="px-6 py-4">Estado</TableHead>
|
||||||
<TableHead className="w-12.5"></TableHead>
|
<TableHead className="w-12.5 px-6 py-4"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{filteredAsignaturas.length === 0 ? (
|
{filteredAsignaturas.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-40 text-center">
|
<TableCell colSpan={8} className="h-40 px-6 py-8 text-center">
|
||||||
<div className="text-muted-foreground flex flex-col items-center justify-center">
|
<div className="text-muted-foreground flex flex-col items-center justify-center gap-3">
|
||||||
<BookOpen className="mb-2 h-10 w-10 opacity-20" />
|
<BookOpen className="h-10 w-10 opacity-20" />
|
||||||
<p className="font-medium">No se encontraron asignaturas</p>
|
<div>
|
||||||
<p className="text-xs">
|
<p className="font-medium">
|
||||||
|
No se encontraron asignaturas
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
Intenta cambiar los filtros de búsqueda
|
Intenta cambiar los filtros de búsqueda
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
@@ -251,25 +255,25 @@ function AsignaturasPage() {
|
|||||||
to: '/planes/$planId/asignaturas/$asignaturaId',
|
to: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
params: {
|
params: {
|
||||||
planId,
|
planId,
|
||||||
asignaturaId: asignatura.id, // 👈 puede ser índice, consecutivo o slug
|
asignaturaId: asignatura.id,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
realId: asignatura.id, // 👈 ID largo oculto
|
realId: asignatura.id,
|
||||||
asignaturaId: asignatura.id,
|
asignaturaId: asignatura.id,
|
||||||
} as any,
|
} as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableCell className="font-mono text-xs font-bold text-slate-400">
|
<TableCell className="px-6 py-4 font-mono text-xs font-bold text-slate-400">
|
||||||
{asignatura.clave}
|
{asignatura.clave}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-semibold text-slate-700">
|
<TableCell className="px-6 py-4 font-semibold text-slate-700">
|
||||||
{asignatura.nombre}
|
{asignatura.nombre}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center font-medium">
|
<TableCell className="px-6 py-4 text-center font-medium">
|
||||||
{asignatura.creditos}
|
{asignatura.creditos}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="px-6 py-4 text-center">
|
||||||
{asignatura.ciclo ? (
|
{asignatura.ciclo ? (
|
||||||
<Badge variant="outline" className="font-normal">
|
<Badge variant="outline" className="font-normal">
|
||||||
Ciclo {asignatura.ciclo}
|
Ciclo {asignatura.ciclo}
|
||||||
@@ -278,10 +282,10 @@ function AsignaturasPage() {
|
|||||||
<span className="text-slate-300">—</span>
|
<span className="text-slate-300">—</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-slate-600">
|
<TableCell className="px-6 py-4 text-sm text-slate-600">
|
||||||
{getLineaNombre(asignatura.lineaCurricularId)}
|
{getLineaNombre(asignatura.lineaCurricularId)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="px-6 py-4">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`}
|
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`}
|
||||||
@@ -289,7 +293,7 @@ function AsignaturasPage() {
|
|||||||
{tipoConfig[asignatura.tipo].label}
|
{tipoConfig[asignatura.tipo].label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="px-6 py-4">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`}
|
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`}
|
||||||
@@ -297,7 +301,7 @@ function AsignaturasPage() {
|
|||||||
{statusConfig[asignatura.estado].label}
|
{statusConfig[asignatura.estado].label}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="px-6 py-4">
|
||||||
<div className="opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<ChevronRight className="h-5 w-5 text-slate-400" />
|
<ChevronRight className="h-5 w-5 text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import {
|
import { Plus, ChevronDown, AlertTriangle, Trash2, Pencil } from 'lucide-react'
|
||||||
Plus,
|
import * as Icons from 'lucide-react'
|
||||||
ChevronDown,
|
|
||||||
AlertTriangle,
|
|
||||||
Trash2,
|
|
||||||
Pencil,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useMemo, useState, useEffect, Fragment } from 'react'
|
import { useMemo, useState, useEffect, Fragment } from 'react'
|
||||||
|
|
||||||
import type { TipoAsignatura } from '@/data'
|
import type { TipoAsignatura } from '@/data'
|
||||||
@@ -36,6 +31,12 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
import {
|
import {
|
||||||
useCreateLinea,
|
useCreateLinea,
|
||||||
useDeleteLinea,
|
useDeleteLinea,
|
||||||
@@ -45,12 +46,6 @@ import {
|
|||||||
useUpdateAsignatura,
|
useUpdateAsignatura,
|
||||||
useUpdateLinea,
|
useUpdateLinea,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
||||||
const palette = [
|
const palette = [
|
||||||
@@ -137,8 +132,6 @@ function StatItem({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
import * as Icons from 'lucide-react'
|
|
||||||
|
|
||||||
const estadoConfig: Record<
|
const estadoConfig: Record<
|
||||||
Asignatura['estado'],
|
Asignatura['estado'],
|
||||||
{
|
{
|
||||||
@@ -198,7 +191,7 @@ function AsignaturaCardItem({
|
|||||||
isDragging: boolean
|
isDragging: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}) {
|
}) {
|
||||||
const estado = estadoConfig[asignatura.estado] ?? estadoConfig.borrador
|
const estado = estadoConfig[asignatura.estado]
|
||||||
const EstadoIcon = estado.icon
|
const EstadoIcon = estado.icon
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -210,10 +203,10 @@ function AsignaturaCardItem({
|
|||||||
onDragStart={(e) => onDragStart(e, asignatura.id)}
|
onDragStart={(e) => onDragStart(e, asignatura.id)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={[
|
className={[
|
||||||
'group relative h-[200px] w-[272px] shrink-0 overflow-hidden rounded-[22px] border text-left',
|
'group relative h-50 w-40 shrink-0 overflow-hidden rounded-[22px] border text-left',
|
||||||
'transition-all duration-300 ease-out',
|
'transition-all duration-300 ease-out',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30',
|
'focus-visible:ring-ring/30 focus-visible:ring-2 focus-visible:outline-none',
|
||||||
'active:cursor-grabbing cursor-grab',
|
'cursor-grab active:cursor-grabbing',
|
||||||
isDragging
|
isDragging
|
||||||
? 'scale-[0.985] opacity-45 shadow-none'
|
? 'scale-[0.985] opacity-45 shadow-none'
|
||||||
: 'hover:-translate-y-1 hover:shadow-lg',
|
: 'hover:-translate-y-1 hover:shadow-lg',
|
||||||
@@ -235,7 +228,7 @@ function AsignaturaCardItem({
|
|||||||
|
|
||||||
{/* glow decorativo */}
|
{/* glow decorativo */}
|
||||||
<div
|
<div
|
||||||
className="absolute -top-10 -right-10 h-28 w-28 rounded-full blur-2xl transition-transform duration-500 group-hover:scale-110"
|
className="absolute -top-10 -right-10 h-28 w-28 rounded-full blur-2xl"
|
||||||
style={{ backgroundColor: hexToRgba(lineaColor, 0.22) }}
|
style={{ backgroundColor: hexToRgba(lineaColor, 0.22) }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -243,7 +236,7 @@ function AsignaturaCardItem({
|
|||||||
{/* top */}
|
{/* top */}
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div
|
<div
|
||||||
className="inline-flex h-8 max-w-[200px] items-center gap-1.5 rounded-full border px-2.5 text-[11px] font-semibold"
|
className="inline-flex h-8 max-w-32 items-center gap-1.5 rounded-full border px-2.5 text-[11px] font-semibold"
|
||||||
style={{
|
style={{
|
||||||
borderColor: hexToRgba(lineaColor, 0.2),
|
borderColor: hexToRgba(lineaColor, 0.2),
|
||||||
backgroundColor: hexToRgba(lineaColor, 0.1),
|
backgroundColor: hexToRgba(lineaColor, 0.1),
|
||||||
@@ -251,37 +244,29 @@ function AsignaturaCardItem({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icons.KeyRound className="h-3.5 w-3.5 shrink-0" />
|
<Icons.KeyRound className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="truncate">{asignatura.clave || 'Sin clave'}</span>
|
<span className="truncate">
|
||||||
</div>
|
{asignatura.clave || 'Sin clave'}
|
||||||
|
|
||||||
<div className="relative flex h-8 items-center overflow-hidden rounded-full bg-background/70 px-2 backdrop-blur-sm">
|
|
||||||
<div className="flex gap-4 items-center gap-1.5 transition-transform duration-300 group-hover:-translate-x-[72px]">
|
|
||||||
<span className={`h-2.5 w-2.5 rounded-full ${estado.dot}`} />
|
|
||||||
<EstadoIcon
|
|
||||||
className={[
|
|
||||||
'h-3.5 w-3.5 text-foreground/65',
|
|
||||||
asignatura.estado === 'generando' ? 'animate-spin' : '',
|
|
||||||
].join(' ')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
'absolute right-2 flex translate-x-6 items-center gap-1.5 opacity-0 transition-all duration-300',
|
|
||||||
'group-hover:translate-x-0 group-hover:opacity-100'
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
<span className="text-[11px] font-semibold whitespace-nowrap">
|
|
||||||
{estado.label}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="bg-background/70 flex h-8 items-center rounded-full px-2 backdrop-blur-sm">
|
||||||
|
<EstadoIcon className="text-foreground/65 h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left">
|
||||||
|
<span className="text-xs font-semibold">
|
||||||
|
{estado.label}
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* titulo */}
|
{/* titulo */}
|
||||||
<div className="mt-4 min-h-[72px]">
|
<div className="mt-4 min-h-18">
|
||||||
<h3
|
<h3
|
||||||
className="overflow-hidden text-[18px] leading-[1.08] font-bold text-foreground"
|
className="text-foreground text-md overflow-hidden leading-[1.08] font-bold"
|
||||||
style={{
|
style={{
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 3,
|
WebkitLineClamp: 3,
|
||||||
@@ -295,53 +280,59 @@ function AsignaturaCardItem({
|
|||||||
{/* bottom */}
|
{/* bottom */}
|
||||||
<div className="mt-auto grid grid-cols-3 gap-2">
|
<div className="mt-auto grid grid-cols-3 gap-2">
|
||||||
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
<div className="text-muted-foreground mb-1 flex items-center gap-1.5">
|
||||||
<Icons.Award className="h-3.5 w-3.5" />
|
<Icons.Award className="h-3.5 w-3.5" />
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
<span className="text-[10px] font-medium tracking-wide uppercase">
|
||||||
CR
|
CR
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-foreground">
|
<div className="text-foreground text-sm font-bold">
|
||||||
{asignatura.creditos}
|
{asignatura.creditos}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
<div className="text-muted-foreground mb-1 flex items-center gap-1.5">
|
||||||
<Icons.Clock3 className="h-3.5 w-3.5" />
|
<Icons.Clock3 className="h-3.5 w-3.5" />
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
<span className="text-[10px] font-medium tracking-wide uppercase">
|
||||||
HD
|
HD
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-foreground">
|
<div className="text-foreground text-sm font-bold">
|
||||||
{asignatura.hd}
|
{asignatura.hd}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
<div className="rounded-2xl border border-white/40 bg-white/55 px-2.5 py-2 backdrop-blur-sm dark:border-white/10 dark:bg-white/5">
|
||||||
<div className="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
<div className="text-muted-foreground mb-1 flex items-center gap-1.5">
|
||||||
<Icons.BookOpenText className="h-3.5 w-3.5" />
|
<Icons.BookOpenText className="h-3.5 w-3.5" />
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
<span className="text-[10px] font-medium tracking-wide uppercase">
|
||||||
HI
|
HI
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-bold text-foreground">
|
<div className="text-foreground text-sm font-bold">
|
||||||
{asignatura.hi}
|
{asignatura.hi}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* drag affordance */}
|
{/* drag affordance */}
|
||||||
<div className="pointer-events-none absolute right-3 bottom-3 rounded-full bg-background/70 p-1.5 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:opacity-100">
|
<div className="bg-background/70 pointer-events-none absolute right-3 bottom-3 rounded-full p-1.5 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:opacity-100">
|
||||||
<Icons.GripVertical className="h-4 w-4 text-muted-foreground/55" />
|
<Icons.GripVertical className="text-muted-foreground/55 h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
<div className="text-xs">
|
<div className="text-lg">
|
||||||
{lineaNombre ? `${lineaNombre} · ` : ''}
|
{/* ciclo */}
|
||||||
|
{asignatura.ciclo ? (
|
||||||
|
<span className="font-bold">C{asignatura.ciclo} · </span>
|
||||||
|
) : null}
|
||||||
|
{lineaNombre ? (
|
||||||
|
<span className="font-medium">{lineaNombre} · </span>
|
||||||
|
) : null}
|
||||||
{asignatura.nombre}
|
{asignatura.nombre}
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -689,7 +680,7 @@ function MapaCurricularPage() {
|
|||||||
return <div className="p-10 text-center">Cargando mapa curricular...</div>
|
return <div className="p-10 text-center">Cargando mapa curricular...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-2 py-6">
|
<div className="container">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -768,11 +759,10 @@ function MapaCurricularPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto pb-6">
|
<div className="overflow-x-auto pb-6">
|
||||||
<div className="min-w-[1500px]">
|
|
||||||
<div
|
<div
|
||||||
className="grid gap-3"
|
className="grid gap-2"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `220px repeat(${ciclosTotales}, minmax(auto, 1fr)) 120px`,
|
gridTemplateColumns: `140px repeat(${ciclosTotales}, minmax(auto, 1fr)) 120px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="self-end px-2 text-xs font-bold text-slate-400">
|
<div className="self-end px-2 text-xs font-bold text-slate-400">
|
||||||
@@ -798,10 +788,11 @@ function MapaCurricularPage() {
|
|||||||
return (
|
return (
|
||||||
<Fragment key={linea.id}>
|
<Fragment key={linea.id}>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-4 transition-all ${lineColors[idx % lineColors.length]
|
className={`group relative flex items-center justify-between rounded-xl border-l-4 p-3 transition-all ${
|
||||||
|
lineColors[idx % lineColors.length]
|
||||||
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="min-w-0 flex-1 overflow-hidden">
|
||||||
<span
|
<span
|
||||||
contentEditable={editingLineaId === linea.id}
|
contentEditable={editingLineaId === linea.id}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
@@ -814,7 +805,8 @@ function MapaCurricularPage() {
|
|||||||
setTempNombreLinea(linea.nombre)
|
setTempNombreLinea(linea.nombre)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`block w-full text-xs font-bold break-words outline-none ${editingLineaId === linea.id
|
className={`block w-full truncate text-xs font-bold break-words outline-none ${
|
||||||
|
editingLineaId === linea.id
|
||||||
? 'cursor-text border-b border-teal-500/50 pb-1'
|
? 'cursor-text border-b border-teal-500/50 pb-1'
|
||||||
: 'cursor-pointer'
|
: 'cursor-pointer'
|
||||||
}`}
|
}`}
|
||||||
@@ -822,7 +814,7 @@ function MapaCurricularPage() {
|
|||||||
{linea.nombre}
|
{linea.nombre}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="ml-2 flex flex-shrink-0 items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingLineaId(linea.id)}
|
onClick={() => setEditingLineaId(linea.id)}
|
||||||
className="..."
|
className="..."
|
||||||
@@ -843,13 +835,12 @@ function MapaCurricularPage() {
|
|||||||
key={`${linea.id}-${ciclo}`}
|
key={`${linea.id}-${ciclo}`}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleDrop(e, ciclo, linea.id)}
|
onDrop={(e) => handleDrop(e, ciclo, linea.id)}
|
||||||
className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
|
className="min-h-35 space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20"
|
||||||
>
|
>
|
||||||
{asignaturas
|
{asignaturas
|
||||||
.filter(
|
.filter(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.ciclo === ciclo &&
|
m.ciclo === ciclo && m.lineaCurricularId === linea.id,
|
||||||
m.lineaCurricularId === linea.id,
|
|
||||||
)
|
)
|
||||||
.map((m) => (
|
.map((m) => (
|
||||||
<AsignaturaCardItem
|
<AsignaturaCardItem
|
||||||
@@ -868,10 +859,27 @@ function MapaCurricularPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="flex flex-col justify-center rounded-xl border border-slate-100 bg-slate-50 p-4 text-[10px] font-medium text-slate-500">
|
<div
|
||||||
<div>Cr: {sub.cr}</div>
|
className={`flex flex-col justify-center rounded-xl border p-4 text-[10px] font-medium ${
|
||||||
<div>HD: {sub.hd}</div>
|
sub.cr === 0 && sub.hd === 0 && sub.hi === 0
|
||||||
<div>HI: {sub.hi}</div>
|
? 'border-slate-100 bg-slate-50/50 text-slate-300'
|
||||||
|
: 'border-slate-200 bg-slate-50 text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sub.cr === 0 && sub.hd === 0 && sub.hi === 0 ? (
|
||||||
|
<div className="text-slate-400">—</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="font-bold text-slate-700">
|
||||||
|
Cr: {sub.cr}
|
||||||
|
</div>
|
||||||
|
<div className="text-slate-600">
|
||||||
|
HD: {sub.hd} • HI: {sub.hi}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
@@ -885,15 +893,25 @@ function MapaCurricularPage() {
|
|||||||
|
|
||||||
{ciclosArray.map((ciclo) => {
|
{ciclosArray.map((ciclo) => {
|
||||||
const t = getTotalesCiclo(ciclo)
|
const t = getTotalesCiclo(ciclo)
|
||||||
|
const isEmpty = t.cr === 0 && t.hd === 0 && t.hi === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`footer-${ciclo}`}
|
key={`footer-${ciclo}`}
|
||||||
className="rounded-lg bg-slate-50 p-2 text-center text-[10px]"
|
className={`rounded-lg p-2 text-center text-[10px] ${
|
||||||
|
isEmpty ? 'bg-slate-100/50 text-slate-400' : 'bg-slate-50'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
{isEmpty ? (
|
||||||
|
<div className="py-1 text-xs text-slate-400">—</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="font-bold text-slate-700">Cr: {t.cr}</div>
|
<div className="font-bold text-slate-700">Cr: {t.cr}</div>
|
||||||
<div>
|
<div>
|
||||||
HD: {t.hd} • HI: {t.hi}
|
HD: {t.hd} • HI: {t.hi}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -904,36 +922,35 @@ function MapaCurricularPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Asignaturas Sin Asignar */}
|
{/* Asignaturas Sin Asignar */}
|
||||||
<div className="mt-12 rounded-[28px] border border-border bg-card/80 p-5 shadow-sm backdrop-blur-sm">
|
<div className="border-border bg-card/80 mt-12 rounded-[28px] border p-5 shadow-sm backdrop-blur-sm">
|
||||||
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
<div className="bg-muted text-muted-foreground flex h-9 w-9 items-center justify-center rounded-2xl">
|
||||||
<Icons.Inbox className="h-4.5 w-4.5" />
|
<Icons.Inbox className="h-4.5 w-4.5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-bold tracking-wide text-foreground uppercase">
|
<h3 className="text-foreground text-sm font-bold tracking-wide uppercase">
|
||||||
Bandeja de entrada
|
Bandeja de entrada
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-muted px-2 text-[11px] font-semibold text-muted-foreground">
|
<div className="bg-muted text-muted-foreground inline-flex h-6 min-w-6 items-center justify-center rounded-full px-2 text-[11px] font-semibold">
|
||||||
{unassignedAsignaturas.length}
|
{unassignedAsignaturas.length}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
<p className="text-muted-foreground mt-0.5 text-sm">
|
||||||
Asignaturas sin ciclo o línea curricular
|
Asignaturas sin ciclo o línea curricular
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 rounded-full border border-dashed border-border bg-background/80 px-3 py-1.5 text-xs text-muted-foreground">
|
<div className="border-border bg-background/80 text-muted-foreground flex items-center gap-2 rounded-full border border-dashed px-3 py-1.5 text-xs">
|
||||||
<Icons.MoveDown className="h-3.5 w-3.5" />
|
<Icons.MoveDown className="h-3.5 w-3.5" />
|
||||||
<span>Arrastra aquí para desasignar</span>
|
<span>Arrastra aquí para desasignar</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -969,18 +986,18 @@ function MapaCurricularPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-[188px] flex-col items-center justify-center rounded-[20px] border border-border/70 bg-background/70 px-6 text-center">
|
<div className="border-border/70 bg-background/70 flex min-h-[188px] flex-col items-center justify-center rounded-[20px] border px-6 text-center">
|
||||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
<div className="bg-muted text-muted-foreground mb-3 flex h-12 w-12 items-center justify-center rounded-2xl">
|
||||||
<Icons.CheckCheck className="h-5 w-5" />
|
<Icons.CheckCheck className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm font-semibold text-foreground">
|
<p className="text-foreground text-sm font-semibold">
|
||||||
No hay asignaturas pendientes
|
No hay asignaturas pendientes
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="mt-1 max-w-md text-sm text-muted-foreground">
|
<p className="text-muted-foreground mt-1 max-w-md text-sm">
|
||||||
Todo está colocado en el mapa. Arrastra una asignatura aquí para quitarle
|
Todo está colocado en el mapa. Arrastra una asignatura aquí para
|
||||||
ciclo y línea curricular.
|
quitarle ciclo y línea curricular.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user