shapshot
This commit is contained in:
32
bun.lock
32
bun.lock
@@ -1,22 +1,24 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "redmine-api-administracion",
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"marked": "^16.4.1",
|
||||
"marked": "^16.4.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tsconfig/svelte": "^5.0.5",
|
||||
"@types/node": "^24.6.0",
|
||||
"svelte": "^5.39.6",
|
||||
"svelte-check": "^4.3.2",
|
||||
"@tsconfig/svelte": "^5.0.6",
|
||||
"@types/node": "^24.10.3",
|
||||
"svelte": "^5.46.0",
|
||||
"svelte-check": "^4.3.4",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.2.7",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -137,11 +139,11 @@
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
|
||||
|
||||
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.5", "", {}, "sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ=="],
|
||||
"@tsconfig/svelte": ["@tsconfig/svelte@5.0.6", "", {}, "sha512-yGxYL0I9eETH1/DR9qVJey4DAsCdeau4a9wYPKuXfEhm8lFO8wg+LLYJjIpAm6Fw7HSlhepPhYPDop75485yWQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
|
||||
"@types/node": ["@types/node@24.10.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
@@ -161,15 +163,19 @@
|
||||
|
||||
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
|
||||
|
||||
"csv-parse": ["csv-parse@6.1.0", "", {}, "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esrap": ["esrap@2.1.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg=="],
|
||||
"esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
@@ -183,7 +189,7 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"marked": ["marked@16.4.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg=="],
|
||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
@@ -205,9 +211,9 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"svelte": ["svelte@5.43.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA=="],
|
||||
"svelte": ["svelte@5.46.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="],
|
||||
"svelte-check": ["svelte-check@4.3.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw=="],
|
||||
|
||||
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
|
||||
|
||||
@@ -219,7 +225,7 @@
|
||||
|
||||
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
|
||||
|
||||
"vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="],
|
||||
"vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>redmine-api-administracion</title>
|
||||
</head>
|
||||
<body data-bs-theme="light">
|
||||
<body data-bs-theme="dark">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
13
package.json
13
package.json
@@ -11,17 +11,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tsconfig/svelte": "^5.0.5",
|
||||
"@types/node": "^24.6.0",
|
||||
"svelte": "^5.39.6",
|
||||
"svelte-check": "^4.3.2",
|
||||
"@tsconfig/svelte": "^5.0.6",
|
||||
"@types/node": "^24.10.3",
|
||||
"svelte": "^5.46.0",
|
||||
"svelte-check": "^4.3.4",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7"
|
||||
"vite": "^7.2.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"csv-parse": "^6.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"marked": "^16.4.1"
|
||||
"marked": "^16.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
52
public/nodos.csv
Normal file
52
public/nodos.csv
Normal file
@@ -0,0 +1,52 @@
|
||||
# Descripcion Dias Dependencias
|
||||
0 inicio 0 _
|
||||
78 Modelo de Datos y Estructura del Sistema 73 0
|
||||
79 Organización de entidades 73 78
|
||||
80 Definición de tipos de información 12 78
|
||||
81 Manejo de formatos de exportación 12 78
|
||||
82 Identificación y trazabilidad interna 17 78
|
||||
83 Reglas estructurales del sistema 12 78
|
||||
84 Gestión de estados 10 78
|
||||
85 Propiedades 57 78
|
||||
86 Administración del ciclo de vida de una propiedad 17 78,79,83
|
||||
88 Visualización de información de cada propiedad 18 85,84
|
||||
89 Organización y listado de propiedades 23 85
|
||||
90 Duración y condiciones 10 85
|
||||
91 Contratos 54 85
|
||||
92 Parámetros del contrato 14 85,84
|
||||
93 Actualización y ajustes contractuales 9 91
|
||||
94 Categorización y búsqueda 10 91,92
|
||||
96 Notificaciones relacionadas 10 85
|
||||
97 Registro y seguimiento de pagos 19 91
|
||||
98 Herramientas específicas por tipo de usuario 30 99
|
||||
99 Usuarios y Seguridad 84 78
|
||||
100 Manejo de perfiles 15 99
|
||||
101 Control de permisos y grupos 11 99
|
||||
102 Autenticación y gestión de credenciales 9 99
|
||||
103 Administración interna de usuarios 10 99
|
||||
104 Control de sesiones 20 102
|
||||
105 Herramientas de búsqueda rápida 11 94
|
||||
106 Relación con Clientes 77 99
|
||||
108 Gestion de clientes 23 106
|
||||
109 Gestión de garantes 12 106
|
||||
110 Funciones propias del propietario e inquilino 10 106
|
||||
111 Navegación 24 106,99
|
||||
112 Búsquedas y Organización del Contenido 81 94,111
|
||||
113 Filtros 14 94
|
||||
114 Paginación 13 94
|
||||
115 Clasificación de propiedades y contratos 10 85,91
|
||||
117 Integraciones internas del sistema 11 78
|
||||
118 Estadísticas y Reportes 65 82,97,106
|
||||
119 Métricas del sistema 19 118
|
||||
120 Informes financieros y contractuales 7 118
|
||||
121 Seguimiento histórico 12 118
|
||||
122 Indicadores de ventas y alquileres 31 118
|
||||
123 Interfaz y Experiencia de Usuario 96 99,85
|
||||
124 Diseño de páginas específicas por rol 13 123,100
|
||||
126 Pantallas de administración 16 123,103
|
||||
127 Presentación visual del sistema 36 123
|
||||
128 Elementos Generales y Complementarios 69 123
|
||||
129 Manejo de divisas 19 97
|
||||
130 Auditoría 21 99,121
|
||||
131 Registros de actividad 5 130
|
||||
999 Fin 0 80,81,86,88,89,90,93,96,98,101,104,105,108,109,110,112,113,114,115,117,119,120,122,124,126,127,128,129,131
|
||||
|
@@ -1,7 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Header from "./componentes/header.svelte";
|
||||
import CalcularDias from "./componentes/paginas/CalcularDias.svelte";
|
||||
import CaminoCritico from "./componentes/paginas/CaminoCritico.svelte";
|
||||
import IndiceDeControl from "./componentes/paginas/IndiceDeControl.svelte";
|
||||
import RelacionesNodos from "./componentes/paginas/RelacionesNodos.svelte";
|
||||
import { TablaNodos } from "./componentes/paginas/TablaNodos.svelte";
|
||||
import TablaProyect from "./componentes/paginas/TablaProyect.svelte";
|
||||
import Tarjeta from "./componentes/tarjeta.svelte";
|
||||
import html2canvas from "html2canvas";
|
||||
|
||||
@@ -78,8 +82,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if pagina === 1}
|
||||
<CalcularDias {issues} />
|
||||
<CalcularDias issuesp={issues} />
|
||||
{:else if pagina === 2}
|
||||
<IndiceDeControl {issues} />
|
||||
{:else if pagina === 3}
|
||||
<TablaProyect {issues} />
|
||||
{:else if pagina === 4}
|
||||
<RelacionesNodos />
|
||||
{:else if pagina === 5}
|
||||
<CaminoCritico />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# === CONFIGURACIÓN ===
|
||||
archivo_issues = "issues.json" # tu archivo exportado
|
||||
presupuesto_total = 41980.0 # presupuesto del proyecto
|
||||
|
||||
# === FUNCIONES AUXILIARES ===
|
||||
def parse_date(valor):
|
||||
if not valor:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(valor.replace("Z", "+00:00")).date()
|
||||
except:
|
||||
try:
|
||||
return datetime.strptime(valor.split("T")[0], "%Y-%m-%d").date()
|
||||
except:
|
||||
return None
|
||||
|
||||
# === CARGA DE DATOS ===
|
||||
with open(archivo_issues, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
issues = data.get("issues", [])
|
||||
for it in issues:
|
||||
it["_start"] = parse_date(it.get("start_date"))
|
||||
it["_due"] = parse_date(it.get("due_date"))
|
||||
it["_closed"] = parse_date(it.get("closed_on"))
|
||||
it["_done"] = float(it.get("done_ratio", 0)) / 100.0
|
||||
|
||||
# === GENERAR TIMELINE ===
|
||||
fechas = []
|
||||
for it in issues:
|
||||
for f in [it["_start"], it["_due"], it["_closed"]]:
|
||||
if f:
|
||||
fechas.append(f)
|
||||
|
||||
min_fecha, max_fecha = min(fechas), max(fechas)
|
||||
timeline = pd.date_range(start=min_fecha, end=max_fecha, freq="D")
|
||||
|
||||
# === CALCULAR PV y EV ===
|
||||
total = len(issues)
|
||||
pv_vals, ev_vals, fechas_plot = [], [], []
|
||||
|
||||
for d in timeline:
|
||||
fecha = d.date()
|
||||
pv = sum(1 for it in issues if it["_due"] and it["_due"] <= fecha) / total
|
||||
ev = sum(it["_done"] for it in issues if it["_closed"] and it["_closed"] <= fecha)
|
||||
ev += sum(it["_done"] for it in issues if (not it["_closed"]) and it["_due"] and it["_due"] <= fecha)
|
||||
ev = min(ev, total)
|
||||
pv_vals.append(pv * presupuesto_total)
|
||||
ev_vals.append(ev / total * presupuesto_total)
|
||||
fechas_plot.append(fecha)
|
||||
|
||||
# === GRÁFICO CURVA S ===
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.plot(fechas_plot, pv_vals, label="PV (Valor planificado)", color="blue")
|
||||
plt.plot(fechas_plot, ev_vals, label="EV (Valor ganado)", color="green")
|
||||
plt.title("Curva S — Proyecto AlquilaFacil")
|
||||
plt.xlabel("Fecha")
|
||||
plt.ylabel("Valor acumulado (USD)")
|
||||
plt.legend()
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# === RESUMEN FINAL ===
|
||||
print("----- RESUMEN CURVA S -----")
|
||||
print(f"Fecha inicio: {min_fecha}")
|
||||
print(f"Fecha fin: {max_fecha}")
|
||||
print(f"Presupuesto total: {presupuesto_total}")
|
||||
print(f"PV final: {pv_vals[-1]:.2f}")
|
||||
print(f"EV final: {ev_vals[-1]:.2f}")
|
||||
print(f"Issues totales: {total}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,15 @@
|
||||
<button class="btn btn-primary" onclick={() => (pagina = 2)}
|
||||
>Indice de Control</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => (pagina = 3)}
|
||||
>Tabla</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => (pagina = 4)}
|
||||
>Nodos</button
|
||||
>
|
||||
<button class="btn btn-primary" onclick={() => (pagina = 5)}
|
||||
>Camino Critico</button
|
||||
>
|
||||
<button
|
||||
class="btn"
|
||||
aria-label="Cambiar tema"
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { Issue } from "../../types";
|
||||
|
||||
let { issues }: { issues: Issue[] } = $props();
|
||||
let { issuesp }: { issuesp: Issue[] } = $props();
|
||||
|
||||
let issues = $state(issuesp);
|
||||
|
||||
onMount(() => {
|
||||
if (!orden) {
|
||||
issues = issues.sort((a, b) => a.id - b.id);
|
||||
} else {
|
||||
issues = issues.sort((a, b) => b.id - a.id);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
calcularDiasPorIssue();
|
||||
@@ -9,6 +20,16 @@
|
||||
});
|
||||
|
||||
let diasCalculados = $state(0);
|
||||
let offsetDias = $state(0);
|
||||
let orden = $state(false);
|
||||
|
||||
function sortIssues() {
|
||||
if (!orden) {
|
||||
issues = issues.sort((a, b) => a.id - b.id);
|
||||
} else {
|
||||
issues = issues.sort((a, b) => b.id - a.id);
|
||||
}
|
||||
}
|
||||
|
||||
function calcularDiasTotales() {
|
||||
if (!issues || issues.length === 0) {
|
||||
@@ -80,6 +101,21 @@
|
||||
<h5 class="mb-0">Calculadora de Días</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-1 flex gap-2">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick={() => {
|
||||
const textToCopy = issues
|
||||
.map(
|
||||
(issue) =>
|
||||
`${issue.subject}\t${issue.cantDias || 0}`,
|
||||
)
|
||||
.join("\n");
|
||||
navigator.clipboard.writeText(textToCopy);
|
||||
}}>Copiar datos</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if diasCalculados > 0}
|
||||
<div class="alert alert-info">
|
||||
<strong>Resultado:</strong>
|
||||
@@ -88,16 +124,55 @@
|
||||
{/if}
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>Issues:</h6>
|
||||
<h6>
|
||||
Issues:
|
||||
<input type="number" bind:value={offsetDias} /> Offset
|
||||
<input
|
||||
type="checkbox"
|
||||
class="ms-2"
|
||||
bind:checked={orden}
|
||||
onchange={sortIssues}
|
||||
/>
|
||||
{#if orden}
|
||||
9..1
|
||||
{:else}
|
||||
1..9
|
||||
{/if}
|
||||
</h6>
|
||||
<ul class="list-group">
|
||||
{#each issues as issue}
|
||||
<li
|
||||
class="list-group-item d-flex justify-content-between align-items-center"
|
||||
>
|
||||
{`${issue.id} | ${issue.subject}`}
|
||||
<span class="badge bg-secondary"
|
||||
>{issue.cantDias || 0} días</span
|
||||
<button
|
||||
class="btn btn-secondary p-0.5"
|
||||
onclick={() => {
|
||||
const textToCopy = `${issue.subject}\t${issue.cantDias || 0}`;
|
||||
navigator.clipboard.writeText(textToCopy);
|
||||
}}>copy</button
|
||||
>
|
||||
<div>
|
||||
<span class="badge bg-secondary"
|
||||
>Inicio {new Date(
|
||||
new Date(issue.start_date).getTime() +
|
||||
offsetDias * 24 * 60 * 60 * 1000,
|
||||
).toLocaleDateString()}</span
|
||||
>
|
||||
<span class="badge bg-secondary"
|
||||
>{issue.cantDias || 0} días
|
||||
</span>
|
||||
<span class="badge bg-secondary"
|
||||
>Fin {new Date(
|
||||
new Date(issue.start_date).getTime() +
|
||||
(issue.cantDias + offsetDias) *
|
||||
24 *
|
||||
60 *
|
||||
60 *
|
||||
1000,
|
||||
).toLocaleDateString()}</span
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
118
src/componentes/paginas/CaminoCritico.svelte
Normal file
118
src/componentes/paginas/CaminoCritico.svelte
Normal file
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import type { Nodo, Tabla } from "../../types";
|
||||
import { TablaNodos } from "./TablaNodos.svelte";
|
||||
|
||||
let tabla = new TablaNodos();
|
||||
$effect(async () => {
|
||||
if (tabla.issues.length !== 0) {
|
||||
console.log(
|
||||
(await tabla.nodosNoReferenciados()).map((x) => x.id).join(","),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let mostrarfiltro = $state(false);
|
||||
let filtro: Promise<Nodo[]> = $state(Promise.resolve([]));
|
||||
$effect(() => {
|
||||
if (mostrarfiltro) {
|
||||
filtro = Promise.resolve(tabla.filtroNodo());
|
||||
} else {
|
||||
filtro = Promise.resolve([]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#await tabla.calcularCaminoCritico()}
|
||||
<div>carga</div>
|
||||
{:then nodos}
|
||||
<div class="container">
|
||||
<div class="text-center mb-2">
|
||||
El Camino Critico es:
|
||||
{#each nodos.filter((x) => x.esCritico) as noduvis}
|
||||
<kbd class="me-1">
|
||||
{noduvis.id}
|
||||
</kbd>
|
||||
{" "}
|
||||
{/each}
|
||||
<input type="checkbox" bind:checked={mostrarfiltro} />
|
||||
</div>
|
||||
<table class="table table-striped border shadow">
|
||||
<thead>
|
||||
<tr class="sticky-top">
|
||||
<th>ID</th>
|
||||
<th>dias</th>
|
||||
<th>deps</th>
|
||||
<th>ES</th>
|
||||
<th>EF</th>
|
||||
<th>LS</th>
|
||||
<th>LF</th>
|
||||
<th>Crit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#await filtro then filteredIssues}
|
||||
{#if filteredIssues.length > 0}
|
||||
{#each filteredIssues as nodo}
|
||||
<tr>
|
||||
{@render Td(nodo.id)}
|
||||
{@render Td(nodo.duracion)}
|
||||
{@render Td(
|
||||
nodo.dependencias.length <= 3
|
||||
? nodo.dependencias
|
||||
: nodo.dependencias
|
||||
.slice(0, 3)
|
||||
.join(", ") + "...",
|
||||
)}
|
||||
{@render Td(nodo.ES)}
|
||||
{@render Td(nodo.EF)}
|
||||
{@render Td(nodo.LS)}
|
||||
{@render Td(nodo.LF)}
|
||||
{@render Td(nodo.esCritico)}
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each nodos as nodo}
|
||||
<tr>
|
||||
<td>{nodo.id}</td>
|
||||
<td>{nodo.duracion}</td>
|
||||
<td>
|
||||
{#if nodo.dependencias.length <= 3}
|
||||
{nodo.dependencias}
|
||||
{:else}
|
||||
{nodo.dependencias
|
||||
.slice(0, 3)
|
||||
.join(", ")}...
|
||||
{/if}
|
||||
</td>
|
||||
<td>{nodo.ES}</td>
|
||||
<td>{nodo.EF}</td>
|
||||
<td>{nodo.LS}</td>
|
||||
<td>{nodo.LF}</td>
|
||||
<td>{nodo.esCritico}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:catch}
|
||||
<p>error</p>
|
||||
{/await}
|
||||
{#snippet Td(a: any)}
|
||||
<td
|
||||
class="table-active-hover"
|
||||
onclick={() => navigator.clipboard.writeText(a.toString())}>{a}</td
|
||||
>
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
.table-active-hover {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
src/componentes/paginas/RelacionesNodos.svelte
Normal file
90
src/componentes/paginas/RelacionesNodos.svelte
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import type { Issue, Tabla } from "../../types";
|
||||
import { TablaNodos } from "./TablaNodos.svelte";
|
||||
|
||||
let tabla: TablaNodos = new TablaNodos();
|
||||
|
||||
let txtfiltro = $state("");
|
||||
let filtro: Promise<Tabla[]> = $state(Promise.resolve([]));
|
||||
</script>
|
||||
|
||||
<div class="container" style="width: 50%;">
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={txtfiltro}
|
||||
oninput={() => {
|
||||
filtro = Promise.resolve(
|
||||
tabla.filtroIssue(txtfiltro.split(" ")),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<table class="table table-striped border shadow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Dias</th>
|
||||
<th>Dep</th>
|
||||
<th>Desc</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#await filtro then filteredIssues}
|
||||
{#if filteredIssues.length > 0}
|
||||
{#each filteredIssues as issue}
|
||||
<tr>
|
||||
{@render Td(issue["#"])}
|
||||
{@render Td(issue.Dias)}
|
||||
{#if issue["#"] == 999}
|
||||
<td>...</td>
|
||||
{:else}
|
||||
{@render Td(issue.Dependencias)}
|
||||
{/if}
|
||||
{@render Td(issue.Descripcion)}
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
{#await tabla.issues}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<p>Loading...</p>
|
||||
</td>
|
||||
</tr>
|
||||
{:then issues}
|
||||
{#each issues as issue}
|
||||
<tr>
|
||||
{@render Td(issue["#"])}
|
||||
{@render Td(issue.Dias)}
|
||||
{#if issue["#"] == 999}
|
||||
<td>...</td>
|
||||
{:else}
|
||||
{@render Td(issue.Dependencias)}
|
||||
{/if}
|
||||
{@render Td(issue.Descripcion)}
|
||||
</tr>
|
||||
{/each}
|
||||
{/await}
|
||||
{/if}
|
||||
{/await}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{#snippet Td(a: any)}
|
||||
<td
|
||||
class="table-active-hover"
|
||||
onclick={() => navigator.clipboard.writeText(a.toString())}>{a}</td
|
||||
>
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
td {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
src/componentes/paginas/TablaNodos.svelte.ts
Normal file
108
src/componentes/paginas/TablaNodos.svelte.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { parse } from "csv-parse/browser/esm/sync";
|
||||
import type { Nodo, Tabla } from "../../types";
|
||||
|
||||
export class TablaNodos {
|
||||
issues: Promise<Tabla[]> = $state(this.setIssues());
|
||||
|
||||
async setIssues(): Promise<Tabla[]> {
|
||||
const response = await fetch("/nodos.csv");
|
||||
let contenido = await response.text();
|
||||
return parse(contenido, {
|
||||
columns: true,
|
||||
delimiter: "\t",
|
||||
trim: true,
|
||||
relax_column_count: true,
|
||||
});
|
||||
}
|
||||
|
||||
async calcularCaminoCritico() {
|
||||
// @ts-ignore
|
||||
let nodos: Nodo[] = (await this.issues).map((x) => ({
|
||||
id: x["#"],
|
||||
duracion: x.Dias,
|
||||
dependencias: x.Dependencias?.toString().split(",") || [],
|
||||
}));
|
||||
|
||||
nodos.sort((a, b) => {
|
||||
if (a.dependencias.includes(b.id)) {
|
||||
return 1;
|
||||
}
|
||||
if (b.dependencias.includes(a.id)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Calcular Early Start y Early Finish
|
||||
nodos.forEach((nodo) => {
|
||||
if (nodo.dependencias.length === 0) {
|
||||
nodo.ES = 0;
|
||||
} else {
|
||||
nodo.ES = Math.max(
|
||||
...nodo.dependencias.map((dep) => {
|
||||
const nodoDependencia = nodos.find((n) => n.id === dep.trim());
|
||||
return nodoDependencia?.EF || 0;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
nodo.EF = nodo.ES + (parseInt(nodo.duracion) || 0);
|
||||
});
|
||||
|
||||
nodos.reverse().forEach((nodo) => {
|
||||
const esUltimo = !nodos.some((n) => n.dependencias.includes(nodo.id));
|
||||
if (esUltimo) {
|
||||
nodo.LF = nodo.EF;
|
||||
} else {
|
||||
const nodosSucesores = nodos.filter((n) =>
|
||||
n.dependencias.includes(nodo.id),
|
||||
);
|
||||
nodo.LF = Math.min(...nodosSucesores.map((n) => n.LS));
|
||||
}
|
||||
|
||||
nodo.LS = nodo.LF - (parseInt(nodo.duracion) || 0);
|
||||
});
|
||||
nodos.reverse();
|
||||
|
||||
nodos.forEach((nodo) => {
|
||||
const slack = nodo.LS - nodo.ES;
|
||||
nodo.esCritico = slack === 0;
|
||||
});
|
||||
|
||||
return Promise.resolve(nodos);
|
||||
}
|
||||
|
||||
async nodosNoReferenciados() {
|
||||
//@ts-ignore
|
||||
const nodos: Nodo[] = (await this.issues).map((x) => ({
|
||||
id: x["#"],
|
||||
duracion: x.Dias,
|
||||
dependencias: x.Dependencias?.toString().split(",") || [],
|
||||
}));
|
||||
|
||||
const todosLosIds = new Set(nodos.map((nodo) => nodo.id));
|
||||
const idsReferenciados = new Set();
|
||||
|
||||
nodos.forEach((nodo) => {
|
||||
nodo.dependencias.forEach((dep) => {
|
||||
idsReferenciados.add(dep.trim());
|
||||
});
|
||||
});
|
||||
|
||||
const noReferenciados = nodos.filter(
|
||||
(nodo) => !idsReferenciados.has(nodo.id),
|
||||
);
|
||||
return noReferenciados;
|
||||
}
|
||||
|
||||
async filtroIssue(ids: string[]) {
|
||||
return Promise.resolve(
|
||||
(await this.issues).filter((issue) => ids.includes(issue["#"])),
|
||||
);
|
||||
}
|
||||
|
||||
async filtroNodo() {
|
||||
let nodos = await this.calcularCaminoCritico();
|
||||
return Promise.resolve(nodos.filter((x) => x.esCritico));
|
||||
}
|
||||
}
|
||||
88
src/componentes/paginas/TablaProyect.svelte
Normal file
88
src/componentes/paginas/TablaProyect.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { type Issue } from "../../types";
|
||||
|
||||
let { issues } = $props();
|
||||
let items = $state(
|
||||
issues.map((x: any) => {
|
||||
if (x.tracker.id == 1) {
|
||||
return x;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let orden = $state(false);
|
||||
function sortIssues() {
|
||||
if (!orden) {
|
||||
items = items.sort((a, b) => a.id - b.id);
|
||||
} else {
|
||||
items = items.sort((a, b) => b.id - a.id);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
sortIssues();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="ms-2"
|
||||
bind:checked={orden}
|
||||
onchange={sortIssues}
|
||||
/>
|
||||
{#if orden}
|
||||
9..1
|
||||
{:else}
|
||||
1..9
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
onclick={() => {
|
||||
const tableContent =
|
||||
'<table border="1"><tr><th>ID</th><th>Descripcion</th><th>Estado</th></tr>' +
|
||||
items
|
||||
.filter(Boolean)
|
||||
.map(
|
||||
(issue: Issue) =>
|
||||
`<tr><td>${issue.id}</td><td>${issue.subject}</td><td>${issue.status?.name || ""}</td></tr>`,
|
||||
)
|
||||
.join("") +
|
||||
"</table>";
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(tableContent)
|
||||
.then(() => {
|
||||
console.log("Contenido copiado a la papelera");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Fallo: ", err);
|
||||
});
|
||||
}}>a</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto" style="width: 45%;">
|
||||
<table class="table table-striped table-responsive table-bordered table-sm">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Descripcion</th>
|
||||
<th>Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each items.filter(Boolean) as issue}
|
||||
<tr>
|
||||
<td>{issue.id}</td>
|
||||
<td>{issue.subject}</td>
|
||||
<td>{issue.status?.name}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
18
src/types.d.ts
vendored
18
src/types.d.ts
vendored
@@ -18,3 +18,21 @@ export interface Issue {
|
||||
updated_on: string;
|
||||
closed_on: string;
|
||||
}
|
||||
|
||||
export interface Tabla {
|
||||
"#": number;
|
||||
Descripcion: string;
|
||||
Dias: number;
|
||||
Dependencias: number[];
|
||||
}
|
||||
interface Nodo {
|
||||
id: number;
|
||||
duracion: number;
|
||||
ES: number; // Early Start
|
||||
EF: number; // Early Finish
|
||||
LS: number; // Late Start
|
||||
LF: number; // Late Finish
|
||||
holgura: number;
|
||||
esCritico: boolean;
|
||||
dependencias: number[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user