Merge remote-tracking branch 'origin/feat/wizard-plan-vista' into feature/IntegrarDetallePlan
This commit is contained in:
@@ -67,7 +67,7 @@ export function PasoConfiguracionPanel({
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="min-h-[100px]"
|
||||
className="min-h-25"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
@@ -213,7 +213,7 @@ export function PasoConfiguracionPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid max-h-[300px] gap-2 overflow-y-auto">
|
||||
<div className="grid max-h-75 gap-2 overflow-y-auto">
|
||||
{MATERIAS_MOCK.map((m) => (
|
||||
<div
|
||||
key={m.id}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function StepWithTooltip({
|
||||
{title}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[200px] text-xs">
|
||||
<TooltipContent className="max-w-50 text-xs">
|
||||
<p>{desc}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { TemplateSelectorCard } from './TemplateSelectorCard'
|
||||
|
||||
import type { CARRERAS } from '@/features/planes/nuevo/catalogs'
|
||||
import type {
|
||||
EstructuraPlanRow,
|
||||
FacultadRow,
|
||||
NivelPlanEstudio,
|
||||
TipoCiclo,
|
||||
} from '@/data/types/domain'
|
||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -12,25 +15,30 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
FACULTADES,
|
||||
NIVELES,
|
||||
TIPOS_CICLO,
|
||||
PLANTILLAS_ANEXO_1,
|
||||
PLANTILLAS_ANEXO_2,
|
||||
} from '@/features/planes/nuevo/catalogs'
|
||||
import { useCatalogosPlanes } from '@/data/hooks/usePlans'
|
||||
import { NIVELES, TIPOS_CICLO } from '@/features/planes/nuevo/catalogs'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function PasoBasicosForm({
|
||||
wizard,
|
||||
onChange,
|
||||
carrerasFiltradas,
|
||||
}: {
|
||||
wizard: NewPlanWizardState
|
||||
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||
carrerasFiltradas: typeof CARRERAS
|
||||
}) {
|
||||
const { data: catalogos } = useCatalogosPlanes()
|
||||
|
||||
// Preferir los catálogos remotos si están disponibles; si no, usar los locales
|
||||
const facultadesList = catalogos?.facultades ?? []
|
||||
const rawCarreras = catalogos?.carreras ?? []
|
||||
const estructurasPlanList = catalogos?.estructurasPlan ?? []
|
||||
|
||||
const filteredCarreras = rawCarreras.filter((c: any) => {
|
||||
const facId = wizard.datosBasicos.facultadId
|
||||
if (!facId) return true
|
||||
// soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local)
|
||||
return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId
|
||||
})
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
@@ -40,13 +48,18 @@ export function PasoBasicosForm({
|
||||
</Label>
|
||||
<Input
|
||||
id="nombrePlan"
|
||||
placeholder="Ej. Ingeniería en Sistemas 2026"
|
||||
placeholder="Ej. Ingeniería en Sistemas (2026)"
|
||||
value={wizard.datosBasicos.nombrePlan}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
||||
}))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
nombrePlan: e.target.value,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
/>
|
||||
@@ -57,14 +70,16 @@ export function PasoBasicosForm({
|
||||
<Select
|
||||
value={wizard.datosBasicos.facultadId}
|
||||
onValueChange={(value) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
facultadId: value,
|
||||
carreraId: '',
|
||||
},
|
||||
}))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
facultadId: value,
|
||||
carreraId: '',
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
@@ -79,7 +94,7 @@ export function PasoBasicosForm({
|
||||
<SelectValue placeholder="Ej. Facultad de Ingeniería" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FACULTADES.map((f) => (
|
||||
{facultadesList.map((f: FacultadRow) => (
|
||||
<SelectItem key={f.id} value={f.id}>
|
||||
{f.nombre}
|
||||
</SelectItem>
|
||||
@@ -93,10 +108,12 @@ export function PasoBasicosForm({
|
||||
<Select
|
||||
value={wizard.datosBasicos.carreraId}
|
||||
onValueChange={(value) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
||||
}))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
||||
}),
|
||||
)
|
||||
}
|
||||
disabled={!wizard.datosBasicos.facultadId}
|
||||
>
|
||||
@@ -112,7 +129,7 @@ export function PasoBasicosForm({
|
||||
<SelectValue placeholder="Ej. Ingeniería en Cibernética y Sistemas Computacionales" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{carrerasFiltradas.map((c) => (
|
||||
{filteredCarreras.map((c: any) => (
|
||||
<SelectItem key={c.id} value={c.id}>
|
||||
{c.nombre}
|
||||
</SelectItem>
|
||||
@@ -125,11 +142,13 @@ export function PasoBasicosForm({
|
||||
<Label htmlFor="nivel">Nivel</Label>
|
||||
<Select
|
||||
value={wizard.datosBasicos.nivel}
|
||||
onValueChange={(value) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, nivel: value },
|
||||
}))
|
||||
onValueChange={(value: NivelPlanEstudio) =>
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: { ...w.datosBasicos, nivel: value },
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
@@ -157,14 +176,16 @@ export function PasoBasicosForm({
|
||||
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
|
||||
<Select
|
||||
value={wizard.datosBasicos.tipoCiclo}
|
||||
onValueChange={(value) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
tipoCiclo: value as any,
|
||||
},
|
||||
}))
|
||||
onValueChange={(value: TipoCiclo) =>
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
tipoCiclo: value as any,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
@@ -180,8 +201,8 @@ export function PasoBasicosForm({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIPOS_CICLO.map((t) => (
|
||||
<SelectItem key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
<SelectItem key={t} value={t}>
|
||||
{t}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -196,22 +217,63 @@ export function PasoBasicosForm({
|
||||
min={1}
|
||||
value={wizard.datosBasicos.numCiclos ?? ''}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
// Keep undefined when the input is empty so the field stays optional
|
||||
numCiclos:
|
||||
e.target.value === '' ? undefined : Number(e.target.value),
|
||||
},
|
||||
}))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
// Keep undefined when the input is empty so the field stays optional
|
||||
numCiclos:
|
||||
e.target.value === ''
|
||||
? undefined
|
||||
: Number(e.target.value),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||
placeholder="Ej. 8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="estructuraPlan">Estructura de plan de estudios</Label>
|
||||
<Select
|
||||
value={wizard.datosBasicos.estructuraPlanId ?? ''}
|
||||
onValueChange={(value: string) =>
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
datosBasicos: {
|
||||
...w.datosBasicos,
|
||||
estructuraPlanId: value,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="tipoCiclo"
|
||||
className={cn(
|
||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
||||
!wizard.datosBasicos.estructuraPlanId
|
||||
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
||||
: 'font-medium not-italic', // Tiene Valor (Medium)
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="Ej. Plan base SEP/ULSA (2026)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{estructurasPlanList.map((t: EstructuraPlanRow) => (
|
||||
<SelectItem key={t.id} value={t.id}>
|
||||
{t.nombre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="my-3" />
|
||||
{/* <Separator className="my-3" />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<TemplateSelectorCard
|
||||
cardTitle="Plantilla de plan de estudios"
|
||||
@@ -247,7 +309,7 @@ export function PasoBasicosForm({
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Upload, File, X, FileText } from 'lucide-react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface UploadedFile {
|
||||
id: string
|
||||
name: string
|
||||
size: string
|
||||
type: string
|
||||
export interface UploadedFile {
|
||||
id: string // Necesario para React (key)
|
||||
file: File // La fuente de verdad (contiene name, size, type)
|
||||
preview?: string // Opcional: si fueran imágenes
|
||||
}
|
||||
|
||||
interface FileDropzoneProps {
|
||||
@@ -37,9 +37,7 @@ export function FileDropzone({
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? (crypto as any).randomUUID()
|
||||
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: file.name,
|
||||
size: formatFileSize(file.size),
|
||||
type: file.name.split('.').pop() || 'file',
|
||||
file,
|
||||
}))
|
||||
setFiles((prev) => {
|
||||
const room = Math.max(0, maxFiles - prev.length)
|
||||
@@ -97,12 +95,6 @@ export function FileDropzone({
|
||||
if (onFilesChangeRef.current) onFilesChangeRef.current(files)
|
||||
}, [files])
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'pdf':
|
||||
@@ -170,23 +162,25 @@ export function FileDropzone({
|
||||
{/* Uploaded files list */}
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{files.map((file) => (
|
||||
{files.map((item) => (
|
||||
<div
|
||||
key={file.id}
|
||||
key={item.id}
|
||||
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
|
||||
>
|
||||
{getFileIcon(file.type)}
|
||||
{getFileIcon(item.file.type)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-foreground truncate text-sm font-medium">
|
||||
{file.name}
|
||||
{item.file.name}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{formatFileSize(item.file.size)}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">{file.size}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-destructive h-8 w-8"
|
||||
onClick={() => removeFile(file.id)}
|
||||
onClick={() => removeFile(item.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FileDropzone } from './FileDropZone'
|
||||
import ReferenciasParaIA from './ReferenciasParaIA'
|
||||
|
||||
import type { UploadedFile } from './FileDropZone'
|
||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -30,7 +31,7 @@ export function PasoDetallesPanel({
|
||||
onGenerarIA: () => void
|
||||
isLoading: boolean
|
||||
}) {
|
||||
if (wizard.modoCreacion === 'MANUAL') {
|
||||
if (wizard.tipoOrigen === 'MANUAL') {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -43,7 +44,7 @@ export function PasoDetallesPanel({
|
||||
)
|
||||
}
|
||||
|
||||
if (wizard.modoCreacion === 'IA') {
|
||||
if (wizard.tipoOrigen === 'IA') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -116,14 +117,16 @@ export function PasoDetallesPanel({
|
||||
}
|
||||
})
|
||||
}
|
||||
onFilesChange={(files) =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
iaConfig: {
|
||||
...(w.iaConfig || ({} as any)),
|
||||
archivosAdjuntos: files,
|
||||
},
|
||||
}))
|
||||
onFilesChange={(files: Array<UploadedFile>) =>
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
iaConfig: {
|
||||
...(w.iaConfig || ({} as any)),
|
||||
archivosAdjuntos: files,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -162,10 +165,7 @@ export function PasoDetallesPanel({
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
wizard.modoCreacion === 'CLONADO' &&
|
||||
wizard.subModoClonado === 'INTERNO'
|
||||
) {
|
||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
@@ -269,10 +269,7 @@ export function PasoDetallesPanel({
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
wizard.modoCreacion === 'CLONADO' &&
|
||||
wizard.subModoClonado === 'TRADICIONAL'
|
||||
) {
|
||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
@@ -5,6 +5,8 @@ import BarraBusqueda from '../../BarraBusqueda'
|
||||
|
||||
import { FileDropzone } from './FileDropZone'
|
||||
|
||||
import type { UploadedFile } from './FileDropZone'
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
@@ -27,9 +29,7 @@ const ReferenciasParaIA = ({
|
||||
selectedRepositorioIds?: Array<string>
|
||||
onToggleArchivo?: (id: string, checked: boolean) => void
|
||||
onToggleRepositorio?: (id: string, checked: boolean) => void
|
||||
onFilesChange?: (
|
||||
files: Array<{ id: string; name: string; size: string; type: string }>,
|
||||
) => void
|
||||
onFilesChange?: (files: Array<UploadedFile>) => void
|
||||
}) => {
|
||||
const [busquedaArchivos, setBusquedaArchivos] = useState('')
|
||||
const [busquedaRepositorios, setBusquedaRepositorios] = useState('')
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as Icons from 'lucide-react'
|
||||
|
||||
import type {
|
||||
NewPlanWizardState,
|
||||
ModoCreacion,
|
||||
SubModoClonado,
|
||||
} from '@/features/planes/nuevo/types'
|
||||
import type { TipoOrigen } from '@/data/types/domain'
|
||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -21,8 +18,7 @@ export function PasoModoCardGroup({
|
||||
wizard: NewPlanWizardState
|
||||
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||
}) {
|
||||
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||
const isSelected = (m: TipoOrigen) => wizard.tipoOrigen === m
|
||||
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
|
||||
const key = e.key
|
||||
if (
|
||||
@@ -41,19 +37,21 @@ export function PasoModoCardGroup({
|
||||
<Card
|
||||
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
modoCreacion: 'MANUAL',
|
||||
subModoClonado: undefined,
|
||||
}))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'MANUAL',
|
||||
}),
|
||||
)
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
modoCreacion: 'MANUAL',
|
||||
subModoClonado: undefined,
|
||||
})),
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'MANUAL',
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
role="button"
|
||||
@@ -70,19 +68,21 @@ export function PasoModoCardGroup({
|
||||
<Card
|
||||
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
modoCreacion: 'IA',
|
||||
subModoClonado: undefined,
|
||||
}))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'IA',
|
||||
}),
|
||||
)
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange((w) => ({
|
||||
...w,
|
||||
modoCreacion: 'IA',
|
||||
subModoClonado: undefined,
|
||||
})),
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'IA',
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
role="button"
|
||||
@@ -99,11 +99,13 @@ export function PasoModoCardGroup({
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
||||
}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange((w) => ({ ...w, modoCreacion: 'CLONADO' })),
|
||||
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' })),
|
||||
)
|
||||
}
|
||||
role="button"
|
||||
@@ -115,22 +117,34 @@ export function PasoModoCardGroup({
|
||||
</CardTitle>
|
||||
<CardDescription>Desde un plan existente o archivos.</CardDescription>
|
||||
</CardHeader>
|
||||
{wizard.modoCreacion === 'CLONADO' && (
|
||||
{(wizard.tipoOrigen === 'OTRO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_INTERNO',
|
||||
}),
|
||||
)
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' })),
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_INTERNO',
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||
isSubSelected('INTERNO')
|
||||
isSelected('CLONADO_INTERNO')
|
||||
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||
: 'border-border text-muted-foreground'
|
||||
} `}
|
||||
@@ -144,15 +158,25 @@ export function PasoModoCardGroup({
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
||||
}),
|
||||
)
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent) =>
|
||||
handleKeyActivate(e, () =>
|
||||
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' })),
|
||||
onChange(
|
||||
(w): NewPlanWizardState => ({
|
||||
...w,
|
||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||
isSubSelected('TRADICIONAL')
|
||||
isSelected('CLONADO_TRADICIONAL')
|
||||
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||
: 'border-border text-muted-foreground'
|
||||
} `}
|
||||
|
||||
@@ -8,12 +8,11 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
PLANTILLAS_ANEXO_1,
|
||||
PLANTILLAS_ANEXO_2,
|
||||
PLANES_EXISTENTES,
|
||||
ARCHIVOS,
|
||||
REPOSITORIOS,
|
||||
} from '@/features/planes/nuevo/catalogs'
|
||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||
|
||||
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||
return (
|
||||
@@ -32,12 +31,6 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||
const repositoriosRef =
|
||||
wizard.iaConfig?.repositoriosReferencia ?? []
|
||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
||||
const plantillaPlan = PLANTILLAS_ANEXO_1.find(
|
||||
(x) => x.id === wizard.datosBasicos.plantillaPlanId,
|
||||
)
|
||||
const plantillaMapa = PLANTILLAS_ANEXO_2.find(
|
||||
(x) => x.id === wizard.datosBasicos.plantillaMapaId,
|
||||
)
|
||||
const contenido = (
|
||||
<>
|
||||
<div>
|
||||
@@ -68,89 +61,56 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||
{wizard.datosBasicos.tipoCiclo})
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="text-muted-foreground">
|
||||
Plantilla plan:{' '}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{(plantillaPlan?.name ||
|
||||
wizard.datosBasicos.plantillaPlanId ||
|
||||
'—') +
|
||||
' · ' +
|
||||
(wizard.datosBasicos.plantillaPlanVersion || '—')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">
|
||||
Mapa curricular:{' '}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{(plantillaMapa?.name ||
|
||||
wizard.datosBasicos.plantillaMapaId ||
|
||||
'—') +
|
||||
' · ' +
|
||||
(wizard.datosBasicos.plantillaMapaVersion || '—')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="text-muted-foreground">Modo: </span>
|
||||
<span className="font-medium">
|
||||
{wizard.modoCreacion === 'MANUAL' && 'Manual'}
|
||||
{wizard.modoCreacion === 'IA' && 'Generado con IA'}
|
||||
{wizard.modoCreacion === 'CLONADO' &&
|
||||
wizard.subModoClonado === 'INTERNO' &&
|
||||
{wizard.tipoOrigen === 'MANUAL' && 'Manual'}
|
||||
{wizard.tipoOrigen === 'IA' && 'Generado con IA'}
|
||||
{wizard.tipoOrigen === 'CLONADO_INTERNO' &&
|
||||
'Clonado desde plan del sistema'}
|
||||
{wizard.modoCreacion === 'CLONADO' &&
|
||||
wizard.subModoClonado === 'TRADICIONAL' &&
|
||||
{wizard.tipoOrigen === 'CLONADO_TRADICIONAL' &&
|
||||
'Importado desde documentos tradicionales'}
|
||||
</span>
|
||||
</div>
|
||||
{wizard.modoCreacion === 'CLONADO' &&
|
||||
wizard.subModoClonado === 'INTERNO' && (
|
||||
<div className="mt-2">
|
||||
<span className="text-muted-foreground">
|
||||
Plan origen:{' '}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{(() => {
|
||||
const p = PLANES_EXISTENTES.find(
|
||||
(x) => x.id === wizard.clonInterno?.planOrigenId,
|
||||
)
|
||||
return (
|
||||
p?.nombre || wizard.clonInterno?.planOrigenId || '—'
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{wizard.modoCreacion === 'CLONADO' &&
|
||||
wizard.subModoClonado === 'TRADICIONAL' && (
|
||||
<div className="mt-2">
|
||||
<div className="font-medium">Documentos adjuntos</div>
|
||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||
<li>
|
||||
<span className="text-foreground">
|
||||
Word del plan:
|
||||
</span>{' '}
|
||||
{wizard.clonTradicional?.archivoWordPlanId?.name ||
|
||||
'—'}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">
|
||||
Mapa curricular:
|
||||
</span>{' '}
|
||||
{wizard.clonTradicional?.archivoMapaExcelId?.name ||
|
||||
'—'}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">Asignaturas:</span>{' '}
|
||||
{wizard.clonTradicional?.archivoAsignaturasExcelId
|
||||
?.name || '—'}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{wizard.modoCreacion === 'IA' && (
|
||||
{wizard.tipoOrigen === 'CLONADO_INTERNO' && (
|
||||
<div className="mt-2">
|
||||
<span className="text-muted-foreground">Plan origen: </span>
|
||||
<span className="font-medium">
|
||||
{(() => {
|
||||
const p = PLANES_EXISTENTES.find(
|
||||
(x) => x.id === wizard.clonInterno?.planOrigenId,
|
||||
)
|
||||
return (
|
||||
p?.nombre || wizard.clonInterno?.planOrigenId || '—'
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{wizard.tipoOrigen === 'CLONADO_TRADICIONAL' && (
|
||||
<div className="mt-2">
|
||||
<div className="font-medium">Documentos adjuntos</div>
|
||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||
<li>
|
||||
<span className="text-foreground">Word del plan:</span>{' '}
|
||||
{wizard.clonTradicional?.archivoWordPlanId?.name || '—'}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">
|
||||
Mapa curricular:
|
||||
</span>{' '}
|
||||
{wizard.clonTradicional?.archivoMapaExcelId?.name ||
|
||||
'—'}
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground">Asignaturas:</span>{' '}
|
||||
{wizard.clonTradicional?.archivoAsignaturasExcelId
|
||||
?.name || '—'}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{wizard.tipoOrigen === 'IA' && (
|
||||
<div className="bg-muted/50 mt-2 rounded-md p-3">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Enfoque: </span>
|
||||
@@ -208,8 +168,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||
{adjuntos.map((f) => (
|
||||
<li key={f.id}>
|
||||
<span className="text-foreground">{f.name}</span>{' '}
|
||||
<span>· {f.size}</span>
|
||||
<span className="text-foreground">
|
||||
{f.file.name}
|
||||
</span>{' '}
|
||||
<span>· {formatFileSize(f.file.size)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function StepWithTooltip({
|
||||
{title}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[200px] text-xs">
|
||||
<TooltipContent className="max-w-50 text-xs">
|
||||
<p>{desc}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
||||
Reference in New Issue
Block a user