Compare commits
13 Commits
d8ade3da75
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ff17f7a615 | |||
| dab7a867eb | |||
| 87458ccdad | |||
| a1ea8973a7 | |||
| b08d918f84 | |||
| f1591bb9b9 | |||
| 965d0198a0 | |||
| ba6f59c4c8 | |||
| 8546b99035 | |||
| 458c4b7973 | |||
| e3c1a0ce2b | |||
| 76170421aa | |||
| 2db3a0570a |
38
.gitea/workflows/deploy.yml
Normal file
38
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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 }}
|
||||||
|
VITE_BACK_ORIGIN: ${{ vars.VITE_BACK_ORIGIN }}
|
||||||
|
run: bun run 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"
|
||||||
11
.vercel/README.txt
Normal file
11
.vercel/README.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
> Why do I have a folder named ".vercel" in my project?
|
||||||
|
The ".vercel" folder is created when you link a directory to a Vercel project.
|
||||||
|
|
||||||
|
> What does the "project.json" file contain?
|
||||||
|
The "project.json" file contains:
|
||||||
|
- The ID of the Vercel project that you linked ("projectId")
|
||||||
|
- The ID of the user or team your Vercel project is owned by ("orgId")
|
||||||
|
|
||||||
|
> Should I commit the ".vercel" folder?
|
||||||
|
No, you should not share the ".vercel" folder with anyone.
|
||||||
|
Upon creation, it will be automatically added to your ".gitignore" file.
|
||||||
36
.vercel/output/builds.json
Normal file
36
.vercel/output/builds.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"//": "This file was generated by the `vercel build` command. It is not part of the Build Output API.",
|
||||||
|
"target": "preview",
|
||||||
|
"argv": [
|
||||||
|
"C:\\Program Files\\nodejs\\node.exe",
|
||||||
|
"C:\\Users\\alex\\.bun\\install\\global\\node_modules\\vercel\\dist\\vc.js",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"require": "@vercel/static-build",
|
||||||
|
"requirePath": "C:\\Users\\alex\\.bun\\install\\global\\node_modules\\@vercel\\static-build\\dist\\index",
|
||||||
|
"apiVersion": 2,
|
||||||
|
"src": "package.json",
|
||||||
|
"use": "@vercel/static-build",
|
||||||
|
"config": {
|
||||||
|
"zeroConfig": true,
|
||||||
|
"framework": "vite"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name": "Error",
|
||||||
|
"stack": "Error: Command \"npm run build\" exited with 2\n at ChildProcess.<anonymous> (C:\\Users\\alex\\.bun\\install\\global\\node_modules\\@vercel\\build-utils\\dist\\index.js:23221:9)\n at ChildProcess.emit (node:events:508:28)\n at ChildProcess.emit (node:domain:489:12)\n at cp.emit (C:\\Users\\alex\\.bun\\install\\global\\node_modules\\@vercel\\build-utils\\dist\\index.js:14249:29)\n at maybeClose (node:internal/child_process:1101:16)\n at ChildProcess._handle.onexit (node:internal/child_process:305:5)",
|
||||||
|
"message": "Command \"npm run build\" exited with 2",
|
||||||
|
"hideStackTrace": true,
|
||||||
|
"code": "BUILD_UTILS_SPAWN_2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": {
|
||||||
|
"name": "Error",
|
||||||
|
"stack": "Error: Command \"npm run build\" exited with 2\n at ChildProcess.<anonymous> (C:\\Users\\alex\\.bun\\install\\global\\node_modules\\@vercel\\build-utils\\dist\\index.js:23221:9)\n at ChildProcess.emit (node:events:508:28)\n at ChildProcess.emit (node:domain:489:12)\n at cp.emit (C:\\Users\\alex\\.bun\\install\\global\\node_modules\\@vercel\\build-utils\\dist\\index.js:14249:29)\n at maybeClose (node:internal/child_process:1101:16)\n at ChildProcess._handle.onexit (node:internal/child_process:305:5)",
|
||||||
|
"message": "Command \"npm run build\" exited with 2",
|
||||||
|
"hideStackTrace": true,
|
||||||
|
"code": "BUILD_UTILS_SPAWN_2"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.vercel/output/config.json
Normal file
3
.vercel/output/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": 3
|
||||||
|
}
|
||||||
1
.vercel/output/diagnostics/cli_traces.json
Normal file
1
.vercel/output/diagnostics/cli_traces.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"name":"vc.builder","duration":11410816,"timestamp":1109155148251,"id":"883875fc-d8b5-40c7-8fdd-f96c27a63d1b","parentId":"3583d148-7b32-444b-813e-a786bebb1401","tags":{"name":"@vercel/static-build"},"startTime":1764002346775},{"name":"vc.builder.diagnostics","duration":17,"timestamp":1109166559117,"id":"c6ad9c3e-80a6-4b2c-9d0b-9e6999814dda","parentId":"883875fc-d8b5-40c7-8fdd-f96c27a63d1b","tags":{},"startTime":1764002358186},{"name":"vc.doBuild","duration":11600581,"timestamp":1109154959803,"id":"3583d148-7b32-444b-813e-a786bebb1401","parentId":"dad52c92-34d7-49a1-a8c7-c26d68888fb8","tags":{},"startTime":1764002346587},{"name":"vc","duration":11643513,"timestamp":1109154916896,"id":"dad52c92-34d7-49a1-a8c7-c26d68888fb8","tags":{},"startTime":1764002346544}]
|
||||||
16
.vercel/project.json
Normal file
16
.vercel/project.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"projectId": "prj_TQf0vM7v0Pz1NyDhm3Ab0Jp4zB2E",
|
||||||
|
"orgId": "team_dURDB79ODIkvcyPxn5ZVT7xr",
|
||||||
|
"projectName": "acad-ia",
|
||||||
|
"settings": {
|
||||||
|
"createdAt": 1764000675314,
|
||||||
|
"framework": "vite",
|
||||||
|
"devCommand": null,
|
||||||
|
"installCommand": null,
|
||||||
|
"buildCommand": null,
|
||||||
|
"outputDirectory": null,
|
||||||
|
"rootDirectory": null,
|
||||||
|
"directoryListing": false,
|
||||||
|
"nodeVersion": "22.x"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
bun.lock
6
bun.lock
@@ -32,7 +32,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.4",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"lucide-react": "^0.540.0",
|
"lucide-react": "^0.540.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -752,7 +752,7 @@
|
|||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"jspdf": ["jspdf@3.0.3", "", { "dependencies": { "@babel/runtime": "^7.26.9", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ=="],
|
"jspdf": ["jspdf@3.0.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ=="],
|
||||||
|
|
||||||
"jspdf-autotable": ["jspdf-autotable@5.0.2", "", { "peerDependencies": { "jspdf": "^2 || ^3" } }, "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ=="],
|
"jspdf-autotable": ["jspdf-autotable@5.0.2", "", { "peerDependencies": { "jspdf": "^2 || ^3" } }, "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ=="],
|
||||||
|
|
||||||
@@ -1122,6 +1122,8 @@
|
|||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"jspdf/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
|
|||||||
5
nixpacks.toml
Normal file
5
nixpacks.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[phases.install]
|
||||||
|
cmds = ["bun install --frozen-lockfile"]
|
||||||
|
|
||||||
|
[phases.build]
|
||||||
|
cmds = ["bun run build"]
|
||||||
5810
package-lock.json
generated
5810
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.4",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
"lucide-react": "^0.540.0",
|
"lucide-react": "^0.540.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query"
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { supabase } from "@/auth/supabase"
|
import { supabase } from "@/auth/supabase"
|
||||||
import ReactMarkdown from "react-markdown"
|
|
||||||
import { useSupabaseAuth } from "@/auth/supabase"
|
import { useSupabaseAuth } from "@/auth/supabase"
|
||||||
|
|
||||||
export function HistorialCambiosModal({
|
export function HistorialCambiosModal({
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { AuroraButton } from "@/components/effect/aurora-button"
|
import { AuroraButton } from "@/components/effect/aurora-button"
|
||||||
import confetti from "canvas-confetti"
|
import confetti from "canvas-confetti"
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
|
||||||
import { supabase, useSupabaseAuth } from "@/auth/supabase"
|
import { supabase, useSupabaseAuth } from "@/auth/supabase"
|
||||||
import { Field } from "./Field"
|
import { Field } from "./Field"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
|
||||||
import { asignaturaKeys } from "./planQueries"
|
|
||||||
import { useRouter } from "@tanstack/react-router"
|
import { useRouter } from "@tanstack/react-router"
|
||||||
|
|
||||||
export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: () => void }) {
|
export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: () => void }) {
|
||||||
const qc = useQueryClient()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const supabaseAuth = useSupabaseAuth()
|
const supabaseAuth = useSupabaseAuth()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export function DownloadPlanPDF({ plan }: { plan: PlanLike }) {
|
|||||||
const sectionGap = 10 // Espacio entre recuadros de sección
|
const sectionGap = 10 // Espacio entre recuadros de sección
|
||||||
const bodyFontSize = 10.5
|
const bodyFontSize = 10.5
|
||||||
const headingFontSize = 12
|
const headingFontSize = 12
|
||||||
const subHeadingFontSize = 10
|
|
||||||
const bulletGlifo = "\u21D2" // Flecha doble (⇒) para el glifo
|
const bulletGlifo = "\u21D2" // Flecha doble (⇒) para el glifo
|
||||||
const bulletIndent = 6 // Sangría para el texto de la lista
|
const bulletIndent = 6 // Sangría para el texto de la lista
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const rgba = (rgb: [number, number, number], a: number) => `rgba(${rgb[0]},${rgb
|
|||||||
/* =====================================================
|
/* =====================================================
|
||||||
Expandable text
|
Expandable text
|
||||||
===================================================== */
|
===================================================== */
|
||||||
function ExpandableText({ text, mono = false }: { text?: string | string[] | null; mono?: boolean }) {
|
function ExpandableText({ text }: { text?: string | string[] | null; mono?: boolean }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
if (!text || (Array.isArray(text) && text.length === 0)) {
|
if (!text || (Array.isArray(text) && text.length === 0)) {
|
||||||
return <span className="text-neutral-400">—</span>
|
return <span className="text-neutral-400">—</span>
|
||||||
@@ -127,16 +127,6 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
|||||||
const [editing, setEditing] = useState<null | { key: keyof PlanTextFields; title: string }>(null)
|
const [editing, setEditing] = useState<null | { key: keyof PlanTextFields; title: string }>(null)
|
||||||
const [draft, setDraft] = useState("")
|
const [draft, setDraft] = useState("")
|
||||||
|
|
||||||
const plan_format={
|
|
||||||
"objetivo_general": "...",
|
|
||||||
"sistema_evaluacion": "...",
|
|
||||||
"perfil_ingreso": "...",
|
|
||||||
"perfil_egreso": "...",
|
|
||||||
"competencias_genericas": "...",
|
|
||||||
"competencias_especificas": "...",
|
|
||||||
"indicadores_desempeno": "...",
|
|
||||||
"pertinencia": "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- mutation con actualización optimista ---
|
// --- mutation con actualización optimista ---
|
||||||
const updateField = useMutation({
|
const updateField = useMutation({
|
||||||
@@ -313,12 +303,12 @@ export function AcademicSections({ planId, color }: { planId: string; color?: st
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<HistorialCambiosModal
|
<HistorialCambiosModal
|
||||||
open={openHistorial}
|
open={openHistorial}
|
||||||
onClose={() => setOpenHistorial(false)}
|
onClose={() => setOpenHistorial(false)}
|
||||||
planId={planId}
|
planId={planId}
|
||||||
onRestore={async (key, value) => {
|
onRestore={async (key, value) => {
|
||||||
updateField.mutate({ key, value })
|
updateField.mutate({ key: key as keyof PlanTextFields, value })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { supabase } from "@/auth/supabase";
|
import { supabase } from "@/auth/supabase";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,8 +11,6 @@ export function useSupabaseUpdateWithHistory<T extends Record<string, any>>(
|
|||||||
tableName: string,
|
tableName: string,
|
||||||
idKey: keyof T = "id" as keyof T
|
idKey: keyof T = "id" as keyof T
|
||||||
) {
|
) {
|
||||||
const qc = useQueryClient();
|
|
||||||
|
|
||||||
// Generar diferencias tipo JSON Patch
|
// Generar diferencias tipo JSON Patch
|
||||||
function generateDiff(oldData: T, newData: Partial<T>) {
|
function generateDiff(oldData: T, newData: Partial<T>) {
|
||||||
const changes: any[] = [];
|
const changes: any[] = [];
|
||||||
|
|||||||
@@ -149,8 +149,6 @@ function Layout() {
|
|||||||
|
|
||||||
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||||
const { claims } = useSupabaseAuth()
|
const { claims } = useSupabaseAuth()
|
||||||
const isAdmin = claims?.role === 'lci' || claims?.role === 'vicerrectoria'
|
|
||||||
|
|
||||||
const canSeeCarreras = ["jefe_carrera", 'vicerrectoria', 'secretario_academico', 'lci'].includes(claims?.role ?? '')
|
const canSeeCarreras = ["jefe_carrera", 'vicerrectoria', 'secretario_academico', 'lci'].includes(claims?.role ?? '')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ export const Route = createFileRoute("/_authenticated/archivos")({
|
|||||||
const stores = await callFilesAndVectorStoresApi<VectorStore[]>({
|
const stores = await callFilesAndVectorStoresApi<VectorStore[]>({
|
||||||
module: "vectorStores",
|
module: "vectorStores",
|
||||||
action: "list",
|
action: "list",
|
||||||
params: {},
|
params: {
|
||||||
|
limit: 10,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return stores ?? []
|
return stores ?? []
|
||||||
},
|
},
|
||||||
@@ -119,8 +121,8 @@ function StatusBadge({ status }: { status: string }) {
|
|||||||
status === "completed"
|
status === "completed"
|
||||||
? "Completado"
|
? "Completado"
|
||||||
: status === "in_progress"
|
: status === "in_progress"
|
||||||
? "Procesando"
|
? "Procesando"
|
||||||
: status
|
: status
|
||||||
const base = "text-[10px] px-2 py-0.5 rounded-full border"
|
const base = "text-[10px] px-2 py-0.5 rounded-full border"
|
||||||
if (status === "completed") {
|
if (status === "completed") {
|
||||||
return (
|
return (
|
||||||
@@ -184,7 +186,7 @@ function RouteComponent() {
|
|||||||
await callFilesAndVectorStoresApi({
|
await callFilesAndVectorStoresApi({
|
||||||
module: "vectorStores",
|
module: "vectorStores",
|
||||||
action: "delete",
|
action: "delete",
|
||||||
params: { id },
|
params: { vector_store_id: id },
|
||||||
})
|
})
|
||||||
|
|
||||||
await supabase
|
await supabase
|
||||||
@@ -472,50 +474,54 @@ function VectorStoreDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toBase64(f: File): Promise<string> {
|
|
||||||
const buf = await f.arrayBuffer()
|
|
||||||
const bytes = new Uint8Array(buf)
|
|
||||||
let binary = ""
|
|
||||||
for (let i = 0; i < bytes.byteLength; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i])
|
|
||||||
}
|
|
||||||
return btoa(binary)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUpload() {
|
async function handleUpload() {
|
||||||
if (!store || !file) {
|
if (!store || !file) {
|
||||||
alert("Selecciona un archivo")
|
alert("Selecciona un archivo")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
try {
|
try {
|
||||||
const fileBase64 = await toBase64(file)
|
// 1) Subir archivo a OpenAI vía Edge con FormData (igual que en tu script)
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("module", "files")
|
||||||
|
formData.append("action", "upload")
|
||||||
|
formData.append("file", file)
|
||||||
|
formData.append("purpose", "assistants") // o lo que uses en tu flujo
|
||||||
|
|
||||||
// 1) Subir archivo a OpenAI vía Edge (módulo files)
|
const { data, error } = await supabase.functions.invoke<any>(
|
||||||
const uploaded: any = await callFilesAndVectorStoresApi<any>({
|
"files-and-vector-stores-api",
|
||||||
module: "files",
|
{
|
||||||
action: "upload",
|
body: formData,
|
||||||
params: {
|
|
||||||
fileBase64,
|
|
||||||
filename: file.name,
|
|
||||||
mimeType: file.type,
|
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
|
|
||||||
const openaiFileId: string =
|
if (error) {
|
||||||
uploaded?.id ?? uploaded?.file?.id ?? uploaded?.data?.id
|
console.error(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploaded = data
|
||||||
|
// La respuesta es el objeto "file" de OpenAI:
|
||||||
|
// { object: "file", id: "file-xxx", ... }
|
||||||
|
const openaiFileId: string | undefined = uploaded?.id
|
||||||
|
|
||||||
if (!openaiFileId) {
|
if (!openaiFileId) {
|
||||||
|
console.error("Respuesta Edge inesperada:", uploaded)
|
||||||
throw new Error("La Edge Function no devolvió el id del archivo")
|
throw new Error("La Edge Function no devolvió el id del archivo")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Mapear archivo al Vector Store
|
// 2) Mapear archivo al Vector Store (JSON normal)
|
||||||
await callFilesAndVectorStoresApi<any>({
|
await callFilesAndVectorStoresApi<any>({
|
||||||
module: "vectorStoreFiles",
|
module: "vectorStoreFiles",
|
||||||
action: "create",
|
action: "create",
|
||||||
params: {
|
params: {
|
||||||
vector_store_id: store.id,
|
vector_store_id: store.id,
|
||||||
file_id: openaiFileId,
|
body: {
|
||||||
|
file_id: openaiFileId,
|
||||||
|
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -542,7 +548,6 @@ function VectorStoreDialog({
|
|||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteFile(fileId: string) {
|
async function handleDeleteFile(fileId: string) {
|
||||||
if (!store) return
|
if (!store) return
|
||||||
if (!confirm("¿Eliminar este archivo del repositorio y de OpenAI?")) return
|
if (!confirm("¿Eliminar este archivo del repositorio y de OpenAI?")) return
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFileRoute, Link, useRouter } from '@tanstack/react-router'
|
|||||||
import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query'
|
import { useSuspenseQuery, queryOptions, useQueryClient } from '@tanstack/react-query'
|
||||||
import { supabase } from '@/auth/supabase'
|
import { supabase } from '@/auth/supabase'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'
|
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from '@/components/ui/select'
|
||||||
@@ -168,25 +168,25 @@ function RouteComponent() {
|
|||||||
const [q, setQ] = useState(search.q ?? '')
|
const [q, setQ] = useState(search.q ?? '')
|
||||||
const [sem, setSem] = useState<string>('todos')
|
const [sem, setSem] = useState<string>('todos')
|
||||||
const [tipo, setTipo] = useState<string>('todos')
|
const [tipo, setTipo] = useState<string>('todos')
|
||||||
const [groupBy, setGroupBy] = useState<'semestre' | 'ninguno'>('semestre')
|
const [groupBy] = useState<'semestre' | 'ninguno'>('semestre')
|
||||||
const [flag, setFlag] = useState<'' | 'sinBibliografia' | 'sinCriterios' | 'sinContenidos'>(search.f ?? '')
|
const [flag, setFlag] = useState<'' | 'sinBibliografia' | 'sinCriterios' | 'sinContenidos'>(search.f ?? '')
|
||||||
|
|
||||||
const [facultad, setFacultad] = useState("todas")
|
const [facultad, setFacultad] = useState("todas")
|
||||||
const [carrera, setCarrera] = useState("todas")
|
const [carrera, setCarrera] = useState("todas")
|
||||||
|
|
||||||
/* useEffect(() => {
|
/* useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
router.navigate({
|
router.navigate({
|
||||||
to: '/asignaturas',
|
to: '/asignaturas',
|
||||||
search: { ...search, q },
|
search: { ...search, q },
|
||||||
replace: true,
|
replace: true,
|
||||||
})
|
})
|
||||||
}, 400)
|
}, 400)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [q]) */
|
}, [q]) */
|
||||||
|
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
setQ(value)
|
setQ(value)
|
||||||
router.navigate({
|
router.navigate({
|
||||||
@@ -199,30 +199,30 @@ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🟣 Lista única de facultades
|
// 🟣 Lista única de facultades
|
||||||
const facultadesList = useMemo(() => {
|
const facultadesList = useMemo(() => {
|
||||||
const unique = new Map<string, string>()
|
const unique = new Map<string, string>()
|
||||||
planes?.forEach((p) => {
|
planes?.forEach((p) => {
|
||||||
const fac = p.carrera?.facultad
|
const fac = p.carrera?.facultad
|
||||||
if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre)
|
if (fac?.id && fac?.nombre) unique.set(fac.id, fac.nombre)
|
||||||
})
|
})
|
||||||
return Array.from(unique.entries())
|
return Array.from(unique.entries())
|
||||||
}, [planes])
|
}, [planes])
|
||||||
|
|
||||||
// 🎓 Lista de carreras según la facultad seleccionada
|
// 🎓 Lista de carreras según la facultad seleccionada
|
||||||
const carrerasList = useMemo(() => {
|
const carrerasList = useMemo(() => {
|
||||||
const unique = new Map<string, string>()
|
const unique = new Map<string, string>()
|
||||||
planes?.forEach((p) => {
|
planes?.forEach((p) => {
|
||||||
if (
|
if (
|
||||||
p.carrera?.id &&
|
p.carrera?.id &&
|
||||||
p.carrera?.nombre &&
|
p.carrera?.nombre &&
|
||||||
(!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad)
|
(!facultad || facultad === "todas" || p.carrera?.facultad?.id === facultad)
|
||||||
) {
|
) {
|
||||||
unique.set(p.carrera.id, p.carrera.nombre)
|
unique.set(p.carrera.id, p.carrera.nombre)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return Array.from(unique.entries())
|
return Array.from(unique.entries())
|
||||||
}, [planes, facultad])
|
}, [planes, facultad])
|
||||||
|
|
||||||
|
|
||||||
// NEW: Clonado individual
|
// NEW: Clonado individual
|
||||||
@@ -256,12 +256,6 @@ const carrerasList = useMemo(() => {
|
|||||||
return Array.from(s).sort((a, b) => (a === '—' ? 1 : 0) - (b === '—' ? 1 : 0) || Number(a) - Number(b))
|
return Array.from(s).sort((a, b) => (a === '—' ? 1 : 0) - (b === '—' ? 1 : 0) || Number(a) - Number(b))
|
||||||
}, [asignaturas])
|
}, [asignaturas])
|
||||||
|
|
||||||
const tipos = useMemo(() => {
|
|
||||||
const s = new Set<string>()
|
|
||||||
asignaturas.forEach(a => s.add(a.tipo ?? '—'))
|
|
||||||
return Array.from(s).sort()
|
|
||||||
}, [asignaturas])
|
|
||||||
|
|
||||||
// Salud
|
// Salud
|
||||||
const salud = useMemo(() => {
|
const salud = useMemo(() => {
|
||||||
let sinBibliografia = 0, sinCriterios = 0, sinContenidos = 0
|
let sinBibliografia = 0, sinCriterios = 0, sinContenidos = 0
|
||||||
@@ -274,29 +268,29 @@ const carrerasList = useMemo(() => {
|
|||||||
}, [asignaturas])
|
}, [asignaturas])
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const t = q.trim().toLowerCase()
|
const t = q.trim().toLowerCase()
|
||||||
return asignaturas.filter(a => {
|
return asignaturas.filter(a => {
|
||||||
const matchesQ =
|
const matchesQ =
|
||||||
!t ||
|
!t ||
|
||||||
[a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre]
|
[a.nombre, a.clave, a.tipo, a.objetivos, a.plan?.nombre, a.plan?.carrera?.nombre, a.plan?.carrera?.facultad?.nombre]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.some(v => String(v).toLowerCase().includes(t))
|
.some(v => String(v).toLowerCase().includes(t))
|
||||||
|
|
||||||
const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem
|
const semOK = sem === 'todos' || String(a.semestre ?? '—') === sem
|
||||||
const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo
|
const tipoOK = tipo === 'todos' || (a.tipo ?? '—') === tipo
|
||||||
const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera
|
const carreraOK = carrera === 'todas' || a.plan?.carrera?.id === carrera
|
||||||
const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad
|
const facultadOK = facultad === 'todas' || a.plan?.carrera?.facultad?.id === facultad
|
||||||
const planOK = !search.planId || a.plan?.id === search.planId
|
const planOK = !search.planId || a.plan?.id === search.planId
|
||||||
|
|
||||||
const flagOK =
|
const flagOK =
|
||||||
!flag ||
|
!flag ||
|
||||||
(flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) ||
|
(flag === 'sinBibliografia' && (!a.bibliografia || a.bibliografia.length === 0)) ||
|
||||||
(flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) ||
|
(flag === 'sinCriterios' && (!a.criterios_evaluacion || !a.criterios_evaluacion.trim())) ||
|
||||||
(flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0))
|
(flag === 'sinContenidos' && (!a.contenidos || Object.keys(a.contenidos ?? {}).length === 0))
|
||||||
|
|
||||||
return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK && planOK
|
return matchesQ && semOK && tipoOK && flagOK && carreraOK && facultadOK && planOK
|
||||||
})
|
})
|
||||||
}, [q, sem, tipo, flag, carrera, facultad, asignaturas])
|
}, [q, sem, tipo, flag, carrera, facultad, asignaturas])
|
||||||
|
|
||||||
|
|
||||||
// Agrupación
|
// Agrupación
|
||||||
@@ -316,18 +310,19 @@ const carrerasList = useMemo(() => {
|
|||||||
}, [filtered, groupBy])
|
}, [filtered, groupBy])
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
const clearFilters = () => { setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag('') ; setFacultad('todas')
|
const clearFilters = () => {
|
||||||
|
setQ(''); setSem('todos'); setTipo('todos'); setCarrera('todas'); setFlag(''); setFacultad('todas')
|
||||||
// Actualiza la URL limpiando todos los query params
|
// Actualiza la URL limpiando todos los query params
|
||||||
router.navigate({
|
router.navigate({
|
||||||
to: '/asignaturas',
|
to: '/asignaturas',
|
||||||
search: {
|
search: {
|
||||||
q: '',
|
q: '',
|
||||||
planId: '',
|
planId: '',
|
||||||
carreraId: '',
|
carreraId: '',
|
||||||
facultadId: '',
|
facultadId: '',
|
||||||
f: ''
|
f: ''
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: util para clonar 1 asignatura
|
// NEW: util para clonar 1 asignatura
|
||||||
@@ -550,7 +545,12 @@ const carrerasList = useMemo(() => {
|
|||||||
value={search.planId ?? "todos"}
|
value={search.planId ?? "todos"}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
router.navigate({
|
router.navigate({
|
||||||
search: { ...search, planId: val === "todos" ? "" : val },
|
to: '/asignaturas',
|
||||||
|
search: {
|
||||||
|
...search,
|
||||||
|
planId: val === 'todos' ? '' : val,
|
||||||
|
},
|
||||||
|
replace: true,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -828,15 +828,14 @@ function AsignaturaCard({ a, onClone, onAddToCart }: { a: Asignatura; onClone: (
|
|||||||
const horasT = a.horas_teoricas ?? 0
|
const horasT = a.horas_teoricas ?? 0
|
||||||
const horasP = a.horas_practicas ?? 0
|
const horasP = a.horas_practicas ?? 0
|
||||||
const meta = tipoMeta(a.tipo)
|
const meta = tipoMeta(a.tipo)
|
||||||
const FacIcon = (Icons as any)[a.plan?.carrera?.facultad?.icon ?? 'Building2'] || Icons.Building2
|
|
||||||
console.log(a);
|
console.log(a);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="group relative overflow-hidden rounded-2xl border bg-white/75 dark:bg-neutral-900/60 shadow-sm hover:shadow-md transition-all"
|
<li className="group relative overflow-hidden rounded-2xl border bg-white/75 dark:bg-neutral-900/60 shadow-sm hover:shadow-md transition-all"
|
||||||
style={{
|
style={{
|
||||||
borderColor: a.plan?.carrera?.facultad?.color ?? '#ccc',
|
borderColor: a.plan?.carrera?.facultad?.color ?? '#ccc',
|
||||||
backgroundColor: `${a.plan?.carrera?.facultad?.color}15`, // 15 = transparencia HEX
|
backgroundColor: `${a.plan?.carrera?.facultad?.color}15`, // 15 = transparencia HEX
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { AcademicSections, planKeys } from "@/components/planes/academic-sections"
|
import { AcademicSections, planKeys } from "@/components/planes/academic-sections"
|
||||||
import { GradientMesh } from "../../../components/planes/GradientMesh"
|
import { GradientMesh } from "../../../components/planes/GradientMesh"
|
||||||
import { asignaturaExtraOptions, asignaturaKeys, asignaturasCountOptions, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries"
|
import { asignaturaExtraOptions, asignaturaKeys, asignaturasPreviewOptions, planByIdOptions, type AsignaturaLite, type PlanFull } from "@/components/planes/planQueries"
|
||||||
import { softAccentStyle } from "@/components/planes/planHelpers"
|
import { softAccentStyle } from "@/components/planes/planHelpers"
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { DialogFooter, DialogHeader } from "@/components/ui/dialog"
|
import { DialogFooter, DialogHeader } from "@/components/ui/dialog"
|
||||||
|
|||||||
@@ -17,13 +17,6 @@ import { toast } from "sonner"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------- Tipos -------------------- */
|
|
||||||
|
|
||||||
|
|
||||||
const SCOPED_ROLES = ["director_facultad", "secretario_academico", "jefe_carrera"] as const
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------- Query Keys & Fetcher -------------------- */
|
/* -------------------- Query Keys & Fetcher -------------------- */
|
||||||
const usersKeys = {
|
const usersKeys = {
|
||||||
@@ -149,35 +142,6 @@ function RouteComponent() {
|
|||||||
carrera_id?: string | null
|
carrera_id?: string | null
|
||||||
}>({ email: "", password: "" })
|
}>({ email: "", password: "" })
|
||||||
|
|
||||||
function genPassword() {
|
|
||||||
/*
|
|
||||||
Supabase requiere que las contraseñas tengan las siguientes características:
|
|
||||||
- Mínimo de 6 caracteres
|
|
||||||
- Debe contener al menos una letra minúscula
|
|
||||||
- Debe contener al menos una letra mayúscula
|
|
||||||
- Debe contener al menos un número
|
|
||||||
- Debe contener al menos un carácter especial
|
|
||||||
Para garantizar la seguridad, generaremos contraseñas de 12 caracteres en vez del mínimo de 6
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 1. Generar una permutación de los números de 1 al 12 con el método Fisher-Yates
|
|
||||||
|
|
||||||
const positions = Array.from({ length: 12 }, (_, i) => i);
|
|
||||||
for (let i = positions.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[positions[i], positions[j]] = [positions[j], positions[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Las correspondencias son las siguientes:
|
|
||||||
// - El primer número indica la posición de la letra minúscula
|
|
||||||
// - El segundo número indica la posición de la letra mayúscula
|
|
||||||
// - El tercer número indica la posición del número
|
|
||||||
// - El cuarto número indica la posición del carácter especial
|
|
||||||
// - En las demás posiciones puede haber cualquier caracter alfanumérico
|
|
||||||
|
|
||||||
const s = Array.from(crypto.getRandomValues(new Uint32Array(4))).map((n) => n.toString(36)).join("")
|
|
||||||
return s.slice(0, 14)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RolePill({ role }: { role: Role }) {
|
function RolePill({ role }: { role: Role }) {
|
||||||
const meta = ROLE_META[role]
|
const meta = ROLE_META[role]
|
||||||
@@ -197,61 +161,6 @@ function RouteComponent() {
|
|||||||
router.invalidate()
|
router.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
const upsertNombramiento = useMutation({
|
|
||||||
mutationFn: async (opts: {
|
|
||||||
user_id: string
|
|
||||||
puesto: "director_facultad" | "secretario_academico" | "jefe_carrera"
|
|
||||||
facultad_id?: string | null
|
|
||||||
carrera_id?: string | null
|
|
||||||
}) => {
|
|
||||||
// cierra vigentes
|
|
||||||
if (opts.puesto === "jefe_carrera") {
|
|
||||||
if (!opts.carrera_id) throw new Error("Selecciona carrera")
|
|
||||||
await supabase
|
|
||||||
.from("nombramientos")
|
|
||||||
.update({ hasta: new Date().toISOString().slice(0, 10) })
|
|
||||||
.eq("puesto", "jefe_carrera")
|
|
||||||
.eq("carrera_id", opts.carrera_id)
|
|
||||||
.is("hasta", null)
|
|
||||||
} else {
|
|
||||||
if (!opts.facultad_id) throw new Error("Selecciona facultad")
|
|
||||||
await supabase
|
|
||||||
.from("nombramientos")
|
|
||||||
.update({ hasta: new Date().toISOString().slice(0, 10) })
|
|
||||||
.eq("puesto", opts.puesto)
|
|
||||||
.eq("facultad_id", opts.facultad_id)
|
|
||||||
.is("hasta", null)
|
|
||||||
}
|
|
||||||
const { error } = await supabase.from("nombramientos").insert({
|
|
||||||
user_id: opts.user_id,
|
|
||||||
puesto: opts.puesto,
|
|
||||||
facultad_id: opts.facultad_id ?? null,
|
|
||||||
carrera_id: opts.carrera_id ?? null,
|
|
||||||
desde: new Date().toISOString().slice(0, 10),
|
|
||||||
hasta: null,
|
|
||||||
})
|
|
||||||
if (error) throw error
|
|
||||||
},
|
|
||||||
onError: (e: any) => toast.error(e?.message || "Error al registrar nombramiento"),
|
|
||||||
})
|
|
||||||
|
|
||||||
const toggleBan = useMutation({
|
|
||||||
mutationFn: async (u: UserClaims) => {
|
|
||||||
throw new Error("Funcionalidad de baneo no implementada aún.")
|
|
||||||
const banned = false // cuando se tenga acceso a ese campo
|
|
||||||
// const banned = !!u.banned_until && new Date(u.banned_until) > new Date()
|
|
||||||
const payload = banned ? { banned_until: null } : { banned_until: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000).toISOString() }
|
|
||||||
// const { error } = await supabase.auth.admin.updateUserById(u.id, payload as any)
|
|
||||||
// if (error) throw new Error(error.message)
|
|
||||||
return !banned
|
|
||||||
},
|
|
||||||
onSuccess: async (isBanned) => {
|
|
||||||
toast.success(isBanned ? "Usuario baneado" : "Usuario desbaneado")
|
|
||||||
await invalidateAll()
|
|
||||||
},
|
|
||||||
onError: (e: any) => toast.error(e?.message || "Error al cambiar estado de baneo"),
|
|
||||||
})
|
|
||||||
|
|
||||||
const createUser = useMutation({
|
const createUser = useMutation({
|
||||||
mutationFn: async (payload: typeof createForm) => {
|
mutationFn: async (payload: typeof createForm) => {
|
||||||
// Validaciones previas
|
// Validaciones previas
|
||||||
@@ -409,7 +318,7 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="outline" size="sm" onClick={() => toggleBan.mutate(u)} title={banned ? "Restaurar acceso" : "Inhabilitar la cuenta"} className="hidden sm:inline-flex">
|
<Button variant="outline" size="sm" onClick={() => {}} title={banned ? "Restaurar acceso" : "Inhabilitar la cuenta"} className="hidden sm:inline-flex">
|
||||||
<Icons.BanIcon className="w-4 h-4 mr-1" /> {banned ? "Restaurar acceso" : "Inhabilitar la cuenta"}
|
<Icons.BanIcon className="w-4 h-4 mr-1" /> {banned ? "Restaurar acceso" : "Inhabilitar la cuenta"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" className="hidden sm:inline-flex shrink-0" onClick={() => openEdit(u)}>
|
<Button variant="ghost" size="sm" className="hidden sm:inline-flex shrink-0" onClick={() => openEdit(u)}>
|
||||||
@@ -425,7 +334,7 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:hidden self-start shrink-0 flex gap-1">
|
<div className="sm:hidden self-start shrink-0 flex gap-1">
|
||||||
<Button variant="outline" size="icon" onClick={() => toggleBan.mutate(u)} aria-label="Ban/Unban"><Icons.BanIcon className="w-4 h-4" /></Button>
|
<Button variant="outline" size="icon" onClick={() => {}} aria-label="Ban/Unban"><Icons.BanIcon className="w-4 h-4" /></Button>
|
||||||
<Button variant="ghost" size="icon" onClick={() => openEdit(u)} aria-label="Editar"><Icons.Pencil className="w-4 h-4" /></Button>
|
<Button variant="ghost" size="icon" onClick={() => openEdit(u)} aria-label="Editar"><Icons.Pencil className="w-4 h-4" /></Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
14
staticwebapp.config.json
Normal file
14
staticwebapp.config.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"navigationFallback": {
|
||||||
|
"rewrite": "/index.html",
|
||||||
|
"exclude": [
|
||||||
|
"/assets/*",
|
||||||
|
"/*.css",
|
||||||
|
"/*.js",
|
||||||
|
"/*.ico",
|
||||||
|
"/*.png",
|
||||||
|
"/*.jpg",
|
||||||
|
"/*.svg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
12
swa-cli.config.json
Normal file
12
swa-cli.config.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
|
||||||
|
"configurations": {
|
||||||
|
"acad-ia": {
|
||||||
|
"appLocation": ".",
|
||||||
|
"outputLocation": "dist",
|
||||||
|
"appBuildCommand": "npm run build",
|
||||||
|
"run": "npm run dev",
|
||||||
|
"appDevserverUrl": "http://localhost:5173"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user