Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 379e2d3826 | |||
| cb5422f57c | |||
| 67724181fd | |||
| cbaf96c6b5 | |||
| 0fb831fb58 | |||
| 0d1aa61022 | |||
| 84281a88f2 | |||
| d91018c612 | |||
| 658b2e245c | |||
| 30562fead0 | |||
| 2b91004129 | |||
| 96a045dc67 | |||
| a8229f12d5 | |||
| dd4ac5374a | |||
| 670e0b1d14 | |||
| 93fe247a19 | |||
| 32ebfde9ed | |||
| 32f0c4c4d4 | |||
| 6a520ef6b1 |
@@ -0,0 +1,37 @@
|
||||
name: Deploy to Azure Static Web Apps
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
|
||||
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
|
||||
run: bunx --bun vite build
|
||||
|
||||
# No hace falta instalar el CLI globalmente, usamos bunx
|
||||
- name: Deploy to Azure Static Web Apps
|
||||
env:
|
||||
AZURE_SWA_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOYMENT_TOKEN }}
|
||||
run: |
|
||||
bunx @azure/static-web-apps-cli deploy ./dist \
|
||||
--env production \
|
||||
--deployment-token "$AZURE_SWA_DEPLOYMENT_TOKEN"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -688,6 +688,12 @@ export function ContenidoTematico() {
|
||||
>
|
||||
{({ handleRef }) => (
|
||||
<>
|
||||
{index === 0 && (
|
||||
<InsertUnidadOverlay
|
||||
position="top"
|
||||
onInsert={() => insertUnidadAt(index)}
|
||||
/>
|
||||
)}
|
||||
<InsertUnidadOverlay
|
||||
position="bottom"
|
||||
onInsert={() => insertUnidadAt(index + 1)}
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function plan_asignaturas_list(
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.select(
|
||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,prerrequisito_asignatura_id',
|
||||
)
|
||||
.eq('plan_estudio_id', planId)
|
||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||
|
||||
@@ -2036,6 +2036,12 @@ function DatosBasicosManualStep({
|
||||
publisher: e.target.value.slice(0, 300),
|
||||
})
|
||||
}
|
||||
onBlur={() => {
|
||||
const trimmed = draft.publisher.trim()
|
||||
if (trimmed !== draft.publisher) {
|
||||
onChangeDraft({ ...draft, publisher: trimmed })
|
||||
}
|
||||
}}
|
||||
maxLength={300}
|
||||
/>
|
||||
</div>
|
||||
@@ -2434,9 +2440,17 @@ const FormatoYCitasStep = forwardRef<
|
||||
onChange={(e) => {
|
||||
const raw = e.currentTarget.value.slice(0, 300)
|
||||
onChangeRef(r.id, {
|
||||
publisher: raw.trim() || undefined,
|
||||
publisher: raw.length > 0 ? raw : undefined,
|
||||
})
|
||||
}}
|
||||
onBlur={() => {
|
||||
const trimmed = publisherText.trim()
|
||||
if (trimmed !== publisherText) {
|
||||
onChangeRef(r.id, {
|
||||
publisher: trimmed || undefined,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Archive,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
RotateCcw,
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||
|
||||
@@ -128,6 +129,7 @@ function RouteComponent() {
|
||||
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
||||
const queryClient = useQueryClient()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const isInitialLoad = useRef(true)
|
||||
const [showArchived, setShowArchived] = useState(false)
|
||||
const [editingChatId, setEditingChatId] = useState<string | null>(null)
|
||||
const editableRef = useRef<HTMLSpanElement>(null)
|
||||
@@ -204,20 +206,20 @@ function RouteComponent() {
|
||||
return messages
|
||||
})
|
||||
}, [mensajesDelChat, activeChatId, availableFields])
|
||||
const scrollToBottom = () => {
|
||||
const scrollToBottom = (behavior = 'smooth') => {
|
||||
if (scrollRef.current) {
|
||||
// Buscamos el viewport interno del ScrollArea de Radix
|
||||
const scrollContainer = scrollRef.current.querySelector(
|
||||
'[data-radix-scroll-area-viewport]',
|
||||
)
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: scrollContainer.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
behavior: behavior, // 'instant' para carga inicial, 'smooth' para mensajes nuevos
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { activeChats, archivedChats } = useMemo(() => {
|
||||
const allChats = lastConversation || []
|
||||
return {
|
||||
@@ -229,22 +231,22 @@ function RouteComponent() {
|
||||
}, [lastConversation])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(mensajesDelChat)
|
||||
|
||||
scrollToBottom()
|
||||
}, [chatMessages, isLoading])
|
||||
|
||||
/* useEffect(() => {
|
||||
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input
|
||||
const camposActualizados = selectedFields.filter((field) =>
|
||||
input.includes(field.label),
|
||||
)
|
||||
|
||||
// Solo actualizamos el estado si hubo un cambio real (para evitar bucles infinitos)
|
||||
if (camposActualizados.length !== selectedFields.length) {
|
||||
setSelectedFields(camposActualizados)
|
||||
if (chatMessages.length > 0) {
|
||||
if (isInitialLoad.current) {
|
||||
// Si es el primer render con mensajes, vamos al final al instante
|
||||
scrollToBottom('instant')
|
||||
isInitialLoad.current = false
|
||||
} else {
|
||||
// Si ya estaba cargado y llegan nuevos, hacemos el smooth
|
||||
scrollToBottom('smooth')
|
||||
}
|
||||
}, [input, selectedFields]) */
|
||||
}
|
||||
}, [chatMessages])
|
||||
|
||||
// 2. Resetear el flag cuando cambies de chat activo
|
||||
useEffect(() => {
|
||||
isInitialLoad.current = true
|
||||
}, [activeChatId])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingConv || isSending) return
|
||||
@@ -508,27 +510,38 @@ function RouteComponent() {
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 pr-2">
|
||||
{' '}
|
||||
{/* Agregamos un pr-2 para que el scrollbar no tape botones */}
|
||||
{!showArchived ? (
|
||||
activeChats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
onClick={() => setActiveChatId(chat.id)}
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||
className={`group relative flex w-full items-center overflow-hidden rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||
activeChatId === chat.id
|
||||
? 'bg-slate-100 font-medium text-slate-900'
|
||||
: 'text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{/* LADO IZQUIERDO: Icono + Texto con Tooltip */}
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
{/* LADO IZQUIERDO: Icono + Texto */}
|
||||
<div
|
||||
className="flex min-w-0 flex-1 items-center gap-3 transition-all duration-200"
|
||||
style={{
|
||||
// Aplicamos la máscara solo cuando el mouse está encima para que se note el desvanecimiento
|
||||
// donde aparecen los botones
|
||||
maskImage:
|
||||
'linear-gradient(to right, black 70%, transparent 95%)',
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(to right, black 70%, transparent 95%)',
|
||||
}}
|
||||
>
|
||||
{/* pr-12 reserva espacio para los botones absolutos */}
|
||||
<FileText size={16} className="shrink-0 opacity-40" />
|
||||
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{/* Este contenedor es el que obliga al span a truncarse */}
|
||||
<div className="max-w-[calc(100%-48px)] min-w-0 flex-1">
|
||||
<TooltipTrigger asChild className="min-w-0 flex-1">
|
||||
<div className="min-w-0 flex-1">
|
||||
<span
|
||||
ref={
|
||||
editingChatId === chat.id ? editableRef : null
|
||||
@@ -574,8 +587,6 @@ function RouteComponent() {
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{/* Tooltip: Solo aparece si no estás editando y el texto es largo */}
|
||||
{editingChatId !== chat.id && (
|
||||
<TooltipContent
|
||||
side="right"
|
||||
@@ -588,9 +599,9 @@ function RouteComponent() {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
{/* LADO DERECHO: Acciones con shrink-0 para que no se muevan */}
|
||||
{/* LADO DERECHO: Acciones ABSOLUTAS */}
|
||||
<div
|
||||
className={`flex shrink-0 items-center gap-1 pl-2 opacity-0 transition-opacity group-hover:opacity-100 ${
|
||||
className={`absolute top-1/2 right-2 z-20 flex -translate-y-1/2 items-center gap-1 rounded-md px-1 opacity-0 transition-opacity group-hover:opacity-100 ${
|
||||
activeChatId === chat.id ? 'bg-slate-100' : 'bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
@@ -614,7 +625,7 @@ function RouteComponent() {
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
/* Sección de archivados */
|
||||
/* Sección de archivados (Simplificada para mantener consistencia) */
|
||||
<div className="animate-in fade-in slide-in-from-left-2 px-1">
|
||||
<p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
|
||||
Archivados
|
||||
@@ -622,18 +633,18 @@ function RouteComponent() {
|
||||
{archivedChats.map((chat) => (
|
||||
<div
|
||||
key={chat.id}
|
||||
className="group relative mb-1 flex w-full items-center justify-between overflow-hidden rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
|
||||
className="group relative mb-1 flex w-full items-center overflow-hidden rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3 pr-10">
|
||||
<Archive size={14} className="shrink-0 opacity-30" />
|
||||
<span className="block min-w-0 flex-1 truncate">
|
||||
<span className="block truncate">
|
||||
{chat.nombre ||
|
||||
`Archivado ${chat.creado_en.split('T')[0]}`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => unarchiveChat(e, chat.id)}
|
||||
className="ml-2 shrink-0 rounded bg-slate-50/80 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-teal-600"
|
||||
className="absolute top-1/2 right-2 shrink-0 -translate-y-1/2 rounded bg-slate-100 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-teal-600"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Plus,
|
||||
ChevronDown,
|
||||
AlertTriangle,
|
||||
GripVertical,
|
||||
Trash2,
|
||||
Pencil,
|
||||
} from 'lucide-react'
|
||||
@@ -46,16 +45,33 @@ import {
|
||||
useUpdateAsignatura,
|
||||
useUpdateLinea,
|
||||
} from '@/data'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
|
||||
// --- Mapeadores (Fuera del componente para mayor limpieza) ---
|
||||
const palette = [
|
||||
'#4F46E5', // índigo
|
||||
'#7C3AED', // violeta
|
||||
'#EA580C', // naranja
|
||||
'#059669', // esmeralda
|
||||
'#DC2626', // rojo
|
||||
'#0891B2', // cyan
|
||||
'#CA8A04', // ámbar
|
||||
'#C026D3', // fucsia
|
||||
]
|
||||
|
||||
const mapLineasToLineaCurricular = (
|
||||
lineasApi: Array<any> = [],
|
||||
): Array<LineaCurricular> => {
|
||||
return lineasApi.map((linea) => ({
|
||||
return lineasApi.map((linea, index) => ({
|
||||
id: linea.id,
|
||||
nombre: linea.nombre,
|
||||
orden: linea.orden ?? 0,
|
||||
color: '#1976d2',
|
||||
color: palette[index % palette.length],
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -76,7 +92,7 @@ const mapAsignaturasToAsignaturas = (
|
||||
// Mapeo directo de los nuevos campos de la API
|
||||
hd: asig.horas_academicas ?? 0,
|
||||
hi: asig.horas_independientes ?? 0,
|
||||
prerrequisitos: [],
|
||||
prerrequisito_asignatura_id: asig.prerrequisito_asignatura_id ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -121,52 +137,216 @@ function StatItem({
|
||||
)
|
||||
}
|
||||
|
||||
import * as Icons from 'lucide-react'
|
||||
|
||||
const estadoConfig: Record<
|
||||
Asignatura['estado'],
|
||||
{
|
||||
label: string
|
||||
dot: string
|
||||
soft: string
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
}
|
||||
> = {
|
||||
borrador: {
|
||||
label: 'Borrador',
|
||||
dot: 'bg-slate-500',
|
||||
soft: 'bg-slate-100 text-slate-700',
|
||||
icon: Icons.FileText,
|
||||
},
|
||||
revisada: {
|
||||
label: 'Revisada',
|
||||
dot: 'bg-amber-500',
|
||||
soft: 'bg-amber-100 text-amber-700',
|
||||
icon: Icons.ScanSearch,
|
||||
},
|
||||
aprobada: {
|
||||
label: 'Aprobada',
|
||||
dot: 'bg-emerald-500',
|
||||
soft: 'bg-emerald-100 text-emerald-700',
|
||||
icon: Icons.BadgeCheck,
|
||||
},
|
||||
generando: {
|
||||
label: 'Generando',
|
||||
dot: 'bg-sky-500',
|
||||
soft: 'bg-sky-100 text-sky-700',
|
||||
icon: Icons.LoaderCircle,
|
||||
},
|
||||
}
|
||||
|
||||
function hexToRgba(hex: string, alpha: number) {
|
||||
const clean = hex.replace('#', '')
|
||||
const bigint = parseInt(clean, 16)
|
||||
const r = (bigint >> 16) & 255
|
||||
const g = (bigint >> 8) & 255
|
||||
const b = bigint & 255
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
||||
}
|
||||
|
||||
function AsignaturaCardItem({
|
||||
asignatura,
|
||||
lineaColor,
|
||||
lineaNombre,
|
||||
onDragStart,
|
||||
isDragging,
|
||||
onClick,
|
||||
}: {
|
||||
asignatura: Asignatura
|
||||
lineaColor: string
|
||||
lineaNombre?: string
|
||||
onDragStart: (e: React.DragEvent, id: string) => void
|
||||
isDragging: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
const estado = estadoConfig[asignatura.estado] ?? estadoConfig.borrador
|
||||
const EstadoIcon = estado.icon
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, asignatura.id)}
|
||||
onClick={onClick}
|
||||
className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${
|
||||
className={[
|
||||
'group relative h-[200px] w-[272px] shrink-0 overflow-hidden rounded-[22px] border text-left',
|
||||
'transition-all duration-300 ease-out',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30',
|
||||
'active:cursor-grabbing cursor-grab',
|
||||
isDragging
|
||||
? 'scale-95 opacity-40'
|
||||
: 'hover:border-teal-400 hover:shadow-md'
|
||||
}`}
|
||||
? 'scale-[0.985] opacity-45 shadow-none'
|
||||
: 'hover:-translate-y-1 hover:shadow-lg',
|
||||
].join(' ')}
|
||||
style={{
|
||||
borderColor: hexToRgba(lineaColor, 0.18),
|
||||
background: `
|
||||
radial-gradient(circle at top right, ${hexToRgba(lineaColor, 0.22)} 0%, transparent 34%),
|
||||
linear-gradient(180deg, ${hexToRgba(lineaColor, 0.12)} 0%, ${hexToRgba(lineaColor, 0.04)} 42%, var(--card) 100%)
|
||||
`,
|
||||
}}
|
||||
title={asignatura.nombre}
|
||||
>
|
||||
<div className="mb-1 flex items-start justify-between">
|
||||
<span className="font-mono text-[10px] font-bold text-slate-400">
|
||||
{asignatura.clave}
|
||||
</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`px-1 py-0 text-[9px] uppercase ${statusBadge[asignatura.estado] || ''}`}
|
||||
{/* franja */}
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 h-2"
|
||||
style={{ backgroundColor: lineaColor }}
|
||||
/>
|
||||
|
||||
{/* glow decorativo */}
|
||||
<div
|
||||
className="absolute -top-10 -right-10 h-28 w-28 rounded-full blur-2xl transition-transform duration-500 group-hover:scale-110"
|
||||
style={{ backgroundColor: hexToRgba(lineaColor, 0.22) }}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-full flex-col p-4">
|
||||
{/* top */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div
|
||||
className="inline-flex h-8 max-w-[200px] items-center gap-1.5 rounded-full border px-2.5 text-[11px] font-semibold"
|
||||
style={{
|
||||
borderColor: hexToRgba(lineaColor, 0.2),
|
||||
backgroundColor: hexToRgba(lineaColor, 0.1),
|
||||
color: lineaColor,
|
||||
}}
|
||||
>
|
||||
{asignatura.estado}
|
||||
</Badge>
|
||||
<Icons.KeyRound className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{asignatura.clave || 'Sin clave'}</span>
|
||||
</div>
|
||||
<p className="mb-1 text-xs leading-tight font-bold text-slate-700">
|
||||
{asignatura.nombre}
|
||||
</p>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<span className="text-[10px] text-slate-500">
|
||||
{asignatura.creditos} CR • HD:{asignatura.hd} • HI:{asignatura.hi}
|
||||
</span>
|
||||
<GripVertical
|
||||
size={12}
|
||||
className="text-slate-300 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* titulo */}
|
||||
<div className="mt-4 min-h-[72px]">
|
||||
<h3
|
||||
className="overflow-hidden text-[18px] leading-[1.08] font-bold text-foreground"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{asignatura.nombre}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* bottom */}
|
||||
<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="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
||||
<Icons.Award className="h-3.5 w-3.5" />
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
||||
CR
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm font-bold text-foreground">
|
||||
{asignatura.creditos}
|
||||
</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="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
||||
<Icons.Clock3 className="h-3.5 w-3.5" />
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
||||
HD
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm font-bold text-foreground">
|
||||
{asignatura.hd}
|
||||
</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="mb-1 flex items-center gap-1.5 text-muted-foreground">
|
||||
<Icons.BookOpenText className="h-3.5 w-3.5" />
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide">
|
||||
HI
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm font-bold text-foreground">
|
||||
{asignatura.hi}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Icons.GripVertical className="h-4 w-4 text-muted-foreground/55" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent side="bottom">
|
||||
<div className="text-xs">
|
||||
{lineaNombre ? `${lineaNombre} · ` : ''}
|
||||
{asignatura.nombre}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -336,6 +516,7 @@ function MapaCurricularPage() {
|
||||
horas_independientes?: TablesUpdate<'asignaturas'>['horas_independientes']
|
||||
numero_ciclo?: TablesUpdate<'asignaturas'>['numero_ciclo']
|
||||
linea_plan_id?: TablesUpdate<'asignaturas'>['linea_plan_id']
|
||||
prerrequisito_asignatura_id?: string | null
|
||||
}
|
||||
const patch: Partial<AsignaturaPatch> = {
|
||||
nombre: editingData.nombre,
|
||||
@@ -345,6 +526,7 @@ function MapaCurricularPage() {
|
||||
horas_independientes: editingData.hi,
|
||||
numero_ciclo: editingData.ciclo,
|
||||
linea_plan_id: editingData.lineaCurricularId,
|
||||
prerrequisito_asignatura_id: editingData.prerrequisito_asignatura_id,
|
||||
tipo: editingData.tipo.toUpperCase() as TipoAsignatura, // Asegurar que coincida con el ENUM (OBLIGATORIA/OPTATIVA)
|
||||
}
|
||||
|
||||
@@ -490,7 +672,7 @@ function MapaCurricularPage() {
|
||||
e: React.FocusEvent<HTMLSpanElement>,
|
||||
id: string,
|
||||
) => {
|
||||
const nuevoNombre = e.currentTarget.textContent?.trim() || ''
|
||||
const nuevoNombre = e.currentTarget.textContent.trim() || ''
|
||||
|
||||
// Buscamos la línea original para comparar
|
||||
const lineaOriginal = lineas.find((l) => l.id === id)
|
||||
@@ -616,8 +798,7 @@ function MapaCurricularPage() {
|
||||
return (
|
||||
<Fragment key={linea.id}>
|
||||
<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-4 transition-all ${lineColors[idx % lineColors.length]
|
||||
} ${editingLineaId === linea.id ? 'bg-white ring-2 ring-teal-500/20' : ''}`}
|
||||
>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
@@ -633,8 +814,7 @@ function MapaCurricularPage() {
|
||||
setTempNombreLinea(linea.nombre)
|
||||
}
|
||||
}}
|
||||
className={`block w-full text-xs font-bold break-words outline-none ${
|
||||
editingLineaId === linea.id
|
||||
className={`block w-full text-xs font-bold break-words outline-none ${editingLineaId === linea.id
|
||||
? 'cursor-text border-b border-teal-500/50 pb-1'
|
||||
: 'cursor-pointer'
|
||||
}`}
|
||||
@@ -675,6 +855,8 @@ function MapaCurricularPage() {
|
||||
<AsignaturaCardItem
|
||||
key={m.id}
|
||||
asignatura={m}
|
||||
lineaColor={linea.color || '#1976d2'}
|
||||
lineaNombre={linea.nombre}
|
||||
isDragging={draggedAsignatura === m.id}
|
||||
onDragStart={handleDragStart}
|
||||
onClick={() => {
|
||||
@@ -725,45 +907,81 @@ function MapaCurricularPage() {
|
||||
</div>
|
||||
|
||||
{/* Asignaturas Sin Asignar */}
|
||||
<div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<h3 className="text-sm font-bold tracking-wider uppercase">
|
||||
Bandeja de Entrada / Asignaturas sin asignar
|
||||
</h3>
|
||||
<Badge variant="secondary">{unassignedAsignaturas.length}</Badge>
|
||||
<div className="mt-12 rounded-[28px] border border-border bg-card/80 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="min-w-0">
|
||||
<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">
|
||||
<Icons.Inbox className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">
|
||||
Arrastra una asignatura aquí para quitarla del mapa
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-bold tracking-wide text-foreground uppercase">
|
||||
Bandeja de entrada
|
||||
</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">
|
||||
{unassignedAsignaturas.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
Asignaturas sin ciclo o línea curricular
|
||||
</p>
|
||||
</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">
|
||||
<Icons.MoveDown className="h-3.5 w-3.5" />
|
||||
<span>Arrastra aquí para desasignar</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex min-h-[120px] flex-wrap gap-4 rounded-xl border-2 border-dashed p-4 transition-colors ${
|
||||
draggedAsignatura
|
||||
? 'border-teal-300 bg-teal-50/50'
|
||||
: 'border-slate-200 bg-white/50'
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={(e) => handleDrop(e, null, null)} // Limpia ciclo y línea
|
||||
onDrop={(e) => handleDrop(e, null, null)}
|
||||
className={[
|
||||
'rounded-[24px] border-2 border-dashed p-4 transition-all duration-300',
|
||||
'min-h-[220px]',
|
||||
draggedAsignatura
|
||||
? 'border-primary/35 bg-primary/6 shadow-inner'
|
||||
: 'border-border bg-muted/20',
|
||||
].join(' ')}
|
||||
>
|
||||
{unassignedAsignaturas.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{unassignedAsignaturas.map((m) => (
|
||||
<div key={m.id} className="w-[200px]">
|
||||
<div key={m.id} className="w-[272px] shrink-0">
|
||||
<AsignaturaCardItem
|
||||
asignatura={m}
|
||||
lineaColor="#94A3B8"
|
||||
lineaNombre="Sin asignar"
|
||||
isDragging={draggedAsignatura === m.id}
|
||||
onDragStart={handleDragStart}
|
||||
onClick={() => {
|
||||
setEditingData(m) // Cargamos los datos en el estado de edición
|
||||
setEditingData(m)
|
||||
setIsEditModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{unassignedAsignaturas.length === 0 && (
|
||||
<div className="flex w-full items-center justify-center text-sm text-slate-400">
|
||||
No hay asignaturas pendientes. Arrastra una asignatura aquí para
|
||||
desasignarla.
|
||||
</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="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
|
||||
<Icons.CheckCheck className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
No hay asignaturas pendientes
|
||||
</p>
|
||||
|
||||
<p className="mt-1 max-w-md text-sm text-muted-foreground">
|
||||
Todo está colocado en el mapa. Arrastra una asignatura aquí para quitarle
|
||||
ciclo y línea curricular.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -935,65 +1153,55 @@ function MapaCurricularPage() {
|
||||
{/* Fila 4: Seriación (Prerrequisitos) */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-500 uppercase">
|
||||
Seriación (Prerrequisitos)
|
||||
Seriación (Prerrequisito)
|
||||
</label>
|
||||
<Select
|
||||
value={seriacionValue}
|
||||
// Cambiamos a manejo de valor único basado en el ID de la columna
|
||||
value={editingData.prerrequisito_asignatura_id || undefined}
|
||||
onValueChange={(val) => {
|
||||
if (val === 'none') {
|
||||
setSeriacionValue('')
|
||||
return
|
||||
}
|
||||
if (!editingData.prerrequisitos.includes(val)) {
|
||||
console.log(editingData)
|
||||
|
||||
setEditingData({
|
||||
...editingData,
|
||||
prerrequisitos: [...editingData.prerrequisitos, val],
|
||||
prerrequisito_asignatura_id: val === 'none' ? null : val,
|
||||
})
|
||||
}
|
||||
setSeriacionValue('')
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full bg-white">
|
||||
<SelectValue placeholder="Seleccionar asignatura..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">-- Sin Seriación --</SelectItem>
|
||||
|
||||
{asignaturas
|
||||
.filter((m) => m.id !== editingData.id)
|
||||
.map((m) => (
|
||||
<SelectItem key={m.id} value={m.id}>
|
||||
{m.nombre} ({m.clave})
|
||||
.filter((asig) => {
|
||||
// 1. No es la misma materia
|
||||
const noEsMisma = asig.id !== editingData.id
|
||||
// 2. El ciclo debe ser estrictamente MENOR
|
||||
const esCicloMenor =
|
||||
asig.ciclo !== null &&
|
||||
editingData.ciclo !== null &&
|
||||
asig.ciclo < editingData.ciclo
|
||||
|
||||
return noEsMisma && esCicloMenor
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.ciclo || 0) - (b.ciclo || 0) ||
|
||||
a.nombre.localeCompare(b.nombre),
|
||||
)
|
||||
.map((asig) => (
|
||||
<SelectItem key={asig.id} value={asig.id}>
|
||||
<span className="font-bold text-teal-600">
|
||||
[C{asig.ciclo}]
|
||||
</span>{' '}
|
||||
{asig.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Visualización de los prerrequisitos seleccionados */}
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{editingData.prerrequisitos.map((pre) => (
|
||||
<Badge
|
||||
key={pre}
|
||||
variant="secondary"
|
||||
className="bg-slate-100 text-slate-600"
|
||||
>
|
||||
{pre}
|
||||
<button
|
||||
className="ml-1 hover:text-red-500"
|
||||
onClick={() => {
|
||||
setEditingData({
|
||||
...editingData,
|
||||
prerrequisitos: editingData.prerrequisitos.filter(
|
||||
(p) => p !== pre,
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{/* Visualización del Prerrequisito con el Nombre */}
|
||||
</div>
|
||||
|
||||
{/* Fila 5: Tipo */}
|
||||
|
||||
@@ -166,30 +166,20 @@ function AsignaturaLayout() {
|
||||
onSave={(val) => handleUpdateHeader('nombre', val)}
|
||||
/>
|
||||
</h1>
|
||||
{
|
||||
// console.log(headerData),
|
||||
|
||||
console.log(asignaturaApi.planes_estudio?.nombre)
|
||||
}
|
||||
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
||||
<span className="flex items-center gap-1">
|
||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||
Pertenece al plan:{' '}
|
||||
<span className="text-blue-100">
|
||||
{(asignaturaApi.planes_estudio?.datos as DatosPlan)
|
||||
.nombre || ''}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="text-blue-100">
|
||||
{(asignaturaApi.planes_estudio?.datos as DatosPlan)
|
||||
.nombre ?? ''}
|
||||
{(asignaturaApi.planes_estudio as DatosPlan).nombre || ''}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-blue-300">
|
||||
Pertenece al plan:{' '}
|
||||
<span className="cursor-pointer underline">
|
||||
{asignaturaApi.planes_estudio?.nombre}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-2 text-right">
|
||||
|
||||
+139
-12
@@ -4,18 +4,145 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-Light.otf') format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-LightItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-Regular.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-RegularItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-Bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-BoldItalic.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-Black.otf') format('opentype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Sans';
|
||||
src: url('/fonts/indivisa/IndivisaTextSans-BlackItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Serif */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-Light.otf') format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-LightItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-Regular.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-RegularItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-Bold.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-BoldItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-Black.otf') format('opentype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Indivisa Serif';
|
||||
src: url('/fonts/indivisa/IndivisaTextSerif-BlackItalic.otf')
|
||||
format('opentype');
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply m-0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family:
|
||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
strong,
|
||||
b,
|
||||
.font-bold {
|
||||
font-family: 'Indivisa Sans', serif;
|
||||
font-weight: 900;
|
||||
/* Inter letter space */
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -51,9 +178,9 @@ code {
|
||||
--sidebar-accent-foreground: oklch(0.6304 0.2472 28.2698);
|
||||
--sidebar-border: oklch(0.9401 0 0);
|
||||
--sidebar-ring: oklch(0 0 0);
|
||||
--font-sans: Plus Jakarta Sans, sans-serif;
|
||||
--font-serif: Lora, serif;
|
||||
--font-mono: IBM Plex Mono, monospace;
|
||||
--font-sans: 'Indivisa Sans', sans-serif;
|
||||
--font-serif: 'Indivisa Serif', serif;
|
||||
--font-mono: 'Indivisa Sans', monospace;
|
||||
--radius: 1.4rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 2px;
|
||||
@@ -101,7 +228,7 @@ code {
|
||||
--chart-1: oklch(0.6686 0.1794 251.7436);
|
||||
--chart-2: oklch(0.6342 0.2516 22.4415);
|
||||
--chart-3: oklch(0.8718 0.1716 90.9505);
|
||||
--chart-4: oklch(0.4503 0.229 263.0881);
|
||||
--chart-4: oklch(11.492% 0.00001 271.152);
|
||||
--chart-5: oklch(0.8322 0.146 185.9404);
|
||||
--sidebar: oklch(0.1564 0.0688 261.2771);
|
||||
--sidebar-foreground: oklch(0.9551 0 0);
|
||||
@@ -111,9 +238,9 @@ code {
|
||||
--sidebar-accent-foreground: oklch(0.6786 0.2095 24.6583);
|
||||
--sidebar-border: oklch(0.3289 0.0092 268.3843);
|
||||
--sidebar-ring: oklch(0.6048 0.2166 257.2136);
|
||||
--font-sans: Plus Jakarta Sans, sans-serif;
|
||||
--font-serif: Lora, serif;
|
||||
--font-mono: IBM Plex Mono, monospace;
|
||||
--font-sans: 'Indivisa Sans', sans-serif;
|
||||
--font-serif: 'Indivisa Serif', serif;
|
||||
--font-mono: 'Indivisa Sans', monospace;
|
||||
--radius: 1.4rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 2px;
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export interface Asignatura {
|
||||
orden?: number
|
||||
hd: number // <--- Añadir
|
||||
hi: number // <--- Añadir
|
||||
prerrequisitos: Array<string>
|
||||
prerrequisito_asignatura_id: string | null
|
||||
}
|
||||
|
||||
export interface Plan {
|
||||
|
||||
@@ -156,6 +156,7 @@ export type Database = {
|
||||
plan_estudio_id: string
|
||||
tipo: Database['public']['Enums']['tipo_asignatura']
|
||||
tipo_origen: Database['public']['Enums']['tipo_origen'] | null
|
||||
prerrequisito_asignatura_id?: string
|
||||
}
|
||||
Insert: {
|
||||
actualizado_en?: string
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"navigationFallback": {
|
||||
"rewrite": "/index.html",
|
||||
"exclude": [
|
||||
"/assets/*",
|
||||
"/*.css",
|
||||
"/*.js",
|
||||
"/*.ico",
|
||||
"/*.png",
|
||||
"/*.jpg",
|
||||
"/*.svg"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user