This commit is contained in:
2025-12-15 20:05:40 -03:00
parent a685ac1683
commit 82ce7cbfaf
14 changed files with 1495 additions and 958 deletions

View File

@@ -1,22 +1,24 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "redmine-api-administracion", "name": "redmine-api-administracion",
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"csv-parse": "^6.1.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"marked": "^16.4.1", "marked": "^16.4.2",
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5", "@tsconfig/svelte": "^5.0.6",
"@types/node": "^24.6.0", "@types/node": "^24.10.3",
"svelte": "^5.39.6", "svelte": "^5.46.0",
"svelte-check": "^4.3.2", "svelte-check": "^4.3.4",
"typescript": "~5.9.3", "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=="], "@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/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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="],

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>redmine-api-administracion</title> <title>redmine-api-administracion</title>
</head> </head>
<body data-bs-theme="light"> <body data-bs-theme="dark">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

View File

@@ -11,17 +11,18 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5", "@tsconfig/svelte": "^5.0.6",
"@types/node": "^24.6.0", "@types/node": "^24.10.3",
"svelte": "^5.39.6", "svelte": "^5.46.0",
"svelte-check": "^4.3.2", "svelte-check": "^4.3.4",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.1.7" "vite": "^7.2.7"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"csv-parse": "^6.1.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"marked": "^16.4.1" "marked": "^16.4.2"
} }
} }

52
public/nodos.csv Normal file
View 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 # Descripcion Dias Dependencias
2 0 inicio 0 _
3 78 Modelo de Datos y Estructura del Sistema 73 0
4 79 Organización de entidades 73 78
5 80 Definición de tipos de información 12 78
6 81 Manejo de formatos de exportación 12 78
7 82 Identificación y trazabilidad interna 17 78
8 83 Reglas estructurales del sistema 12 78
9 84 Gestión de estados 10 78
10 85 Propiedades 57 78
11 86 Administración del ciclo de vida de una propiedad 17 78,79,83
12 88 Visualización de información de cada propiedad 18 85,84
13 89 Organización y listado de propiedades 23 85
14 90 Duración y condiciones 10 85
15 91 Contratos 54 85
16 92 Parámetros del contrato 14 85,84
17 93 Actualización y ajustes contractuales 9 91
18 94 Categorización y búsqueda 10 91,92
19 96 Notificaciones relacionadas 10 85
20 97 Registro y seguimiento de pagos 19 91
21 98 Herramientas específicas por tipo de usuario 30 99
22 99 Usuarios y Seguridad 84 78
23 100 Manejo de perfiles 15 99
24 101 Control de permisos y grupos 11 99
25 102 Autenticación y gestión de credenciales 9 99
26 103 Administración interna de usuarios 10 99
27 104 Control de sesiones 20 102
28 105 Herramientas de búsqueda rápida 11 94
29 106 Relación con Clientes 77 99
30 108 Gestion de clientes 23 106
31 109 Gestión de garantes 12 106
32 110 Funciones propias del propietario e inquilino 10 106
33 111 Navegación 24 106,99
34 112 Búsquedas y Organización del Contenido 81 94,111
35 113 Filtros 14 94
36 114 Paginación 13 94
37 115 Clasificación de propiedades y contratos 10 85,91
38 117 Integraciones internas del sistema 11 78
39 118 Estadísticas y Reportes 65 82,97,106
40 119 Métricas del sistema 19 118
41 120 Informes financieros y contractuales 7 118
42 121 Seguimiento histórico 12 118
43 122 Indicadores de ventas y alquileres 31 118
44 123 Interfaz y Experiencia de Usuario 96 99,85
45 124 Diseño de páginas específicas por rol 13 123,100
46 126 Pantallas de administración 16 123,103
47 127 Presentación visual del sistema 36 123
48 128 Elementos Generales y Complementarios 69 123
49 129 Manejo de divisas 19 97
50 130 Auditoría 21 99,121
51 131 Registros de actividad 5 130
52 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

View File

@@ -1,7 +1,11 @@
<script lang="ts"> <script lang="ts">
import Header from "./componentes/header.svelte"; import Header from "./componentes/header.svelte";
import CalcularDias from "./componentes/paginas/CalcularDias.svelte"; import CalcularDias from "./componentes/paginas/CalcularDias.svelte";
import CaminoCritico from "./componentes/paginas/CaminoCritico.svelte";
import IndiceDeControl from "./componentes/paginas/IndiceDeControl.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 Tarjeta from "./componentes/tarjeta.svelte";
import html2canvas from "html2canvas"; import html2canvas from "html2canvas";
@@ -78,8 +82,14 @@
</div> </div>
{/if} {/if}
{:else if pagina === 1} {:else if pagina === 1}
<CalcularDias {issues} /> <CalcularDias issuesp={issues} />
{:else if pagina === 2} {:else if pagina === 2}
<IndiceDeControl {issues} /> <IndiceDeControl {issues} />
{:else if pagina === 3}
<TablaProyect {issues} />
{:else if pagina === 4}
<RelacionesNodos />
{:else if pagina === 5}
<CaminoCritico />
{/if} {/if}
</main> </main>

View File

@@ -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

View File

@@ -17,6 +17,15 @@
<button class="btn btn-primary" onclick={() => (pagina = 2)} <button class="btn btn-primary" onclick={() => (pagina = 2)}
>Indice de Control</button >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 <button
class="btn" class="btn"
aria-label="Cambiar tema" aria-label="Cambiar tema"

View File

@@ -1,7 +1,18 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import type { Issue } from "../../types"; 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(() => { $effect(() => {
calcularDiasPorIssue(); calcularDiasPorIssue();
@@ -9,6 +20,16 @@
}); });
let diasCalculados = $state(0); 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() { function calcularDiasTotales() {
if (!issues || issues.length === 0) { if (!issues || issues.length === 0) {
@@ -80,6 +101,21 @@
<h5 class="mb-0">Calculadora de Días</h5> <h5 class="mb-0">Calculadora de Días</h5>
</div> </div>
<div class="card-body"> <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} {#if diasCalculados > 0}
<div class="alert alert-info"> <div class="alert alert-info">
<strong>Resultado:</strong> <strong>Resultado:</strong>
@@ -88,16 +124,55 @@
{/if} {/if}
<div class="mt-3"> <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"> <ul class="list-group">
{#each issues as issue} {#each issues as issue}
<li <li
class="list-group-item d-flex justify-content-between align-items-center" class="list-group-item d-flex justify-content-between align-items-center"
> >
{`${issue.id} | ${issue.subject}`} {`${issue.id} | ${issue.subject}`}
<span class="badge bg-secondary" <button
>{issue.cantDias || 0} días</span 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> </li>
{/each} {/each}
</ul> </ul>

View 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>

View 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>

View 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));
}
}

View 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
View File

@@ -18,3 +18,21 @@ export interface Issue {
updated_on: string; updated_on: string;
closed_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[];
}