añadido el tema para el indice de control
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -5,6 +5,7 @@
|
|||||||
"name": "redmine-api-administracion",
|
"name": "redmine-api-administracion",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
},
|
},
|
||||||
@@ -82,6 +83,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||||
|
|
||||||
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="],
|
||||||
@@ -150,6 +153,8 @@
|
|||||||
|
|
||||||
"bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="],
|
"bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="],
|
||||||
|
|
||||||
|
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"marked": "^16.4.1"
|
"marked": "^16.4.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<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 IndiceDeControl from "./componentes/paginas/IndiceDeControl.svelte";
|
||||||
import Tarjeta from "./componentes/tarjeta.svelte";
|
import Tarjeta from "./componentes/tarjeta.svelte";
|
||||||
import html2canvas from "html2canvas";
|
import html2canvas from "html2canvas";
|
||||||
|
|
||||||
@@ -78,5 +79,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if pagina === 1}
|
{:else if pagina === 1}
|
||||||
<CalcularDias {issues} />
|
<CalcularDias {issues} />
|
||||||
|
{:else if pagina === 2}
|
||||||
|
<IndiceDeControl {issues} />
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
76
src/assets/fpinga.py
Normal file
76
src/assets/fpinga.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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}")
|
||||||
146
src/componentes/CurvaS.svelte
Normal file
146
src/componentes/CurvaS.svelte
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Chart, type ChartConfiguration } from "chart.js/auto";
|
||||||
|
import html2canvas from "html2canvas";
|
||||||
|
import type { Issue } from "../types";
|
||||||
|
|
||||||
|
let { issues, cpi = 0.96 }: { issues: Issue[]; cpi: number } = $props();
|
||||||
|
|
||||||
|
const presupuesto = 41980;
|
||||||
|
|
||||||
|
function parseDate(s: string | null | undefined): Date | null {
|
||||||
|
if (!s) return null;
|
||||||
|
return new Date(s.split("T")[0] + "T00:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates: string[] = [];
|
||||||
|
const pvValues: number[] = [];
|
||||||
|
const evValues: number[] = [];
|
||||||
|
|
||||||
|
if (issues && issues.length) {
|
||||||
|
// 1️⃣ Determinar rango de fechas (inicio y fin global)
|
||||||
|
let minDate: Date | null = null;
|
||||||
|
let maxDate: Date | null = null;
|
||||||
|
|
||||||
|
for (const i of issues) {
|
||||||
|
const s = parseDate(i.start_date);
|
||||||
|
const d = parseDate(i.due_date);
|
||||||
|
const c = parseDate(i.closed_on);
|
||||||
|
for (const dt of [s, d, c]) {
|
||||||
|
if (!dt) continue;
|
||||||
|
if (!minDate || dt < minDate) minDate = dt;
|
||||||
|
if (!maxDate || dt > maxDate) maxDate = dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback a hoy si faltan fechas
|
||||||
|
minDate ??= new Date();
|
||||||
|
maxDate ??= new Date();
|
||||||
|
|
||||||
|
// 2️⃣ Generar timeline diaria
|
||||||
|
for (
|
||||||
|
let d = new Date(minDate);
|
||||||
|
d <= maxDate;
|
||||||
|
d.setDate(d.getDate() + 1)
|
||||||
|
) {
|
||||||
|
dates.push(d.toISOString().slice(0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalIssues = issues.length;
|
||||||
|
|
||||||
|
// 3️⃣ Calcular PV como curva S teórica
|
||||||
|
for (const dateStr of dates) {
|
||||||
|
const current = new Date(dateStr);
|
||||||
|
const progress =
|
||||||
|
(current.getTime() - minDate.getTime()) /
|
||||||
|
(maxDate.getTime() - minDate.getTime());
|
||||||
|
|
||||||
|
// Curva sigmoide (forma de S)
|
||||||
|
const sCurve = 1 / (1 + Math.exp(-12 * (progress - 0.36)));
|
||||||
|
const pv = sCurve * presupuesto;
|
||||||
|
|
||||||
|
pvValues.push(pv);
|
||||||
|
}
|
||||||
|
for (const dateStr of dates) {
|
||||||
|
const current = new Date(dateStr);
|
||||||
|
const evCount = issues.filter((it) => {
|
||||||
|
const due = parseDate(it.due_date);
|
||||||
|
return due && due <= current;
|
||||||
|
}).length;
|
||||||
|
const pv = (evCount / totalIssues) * presupuesto;
|
||||||
|
evValues.push(pv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const cfg: ChartConfiguration = {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: dates,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "PV (Valor planificado)",
|
||||||
|
data: pvValues,
|
||||||
|
borderColor: "rgba(0, 123, 255, 1)",
|
||||||
|
backgroundColor: "rgba(0, 123, 255, 0.1)",
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "EV (Valor ganado)",
|
||||||
|
data: evValues,
|
||||||
|
borderColor: "rgba(40, 167, 69, 1)",
|
||||||
|
backgroundColor: "rgba(40, 167, 69, 0.1)",
|
||||||
|
fill: false,
|
||||||
|
tension: 0.3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Indice de control",
|
||||||
|
font: { size: 16 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
title: { display: true, text: "Fecha", color: "#ccc" },
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "Valor acumulado (USD)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new Chart(canvas, cfg);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function downloadChart() {
|
||||||
|
const el = canvas?.parentElement;
|
||||||
|
if (!el) return;
|
||||||
|
const img = await html2canvas(el);
|
||||||
|
const url = img.toDataURL("image/png");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "curva-s.png";
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" onclick={downloadChart}>Descargar PNG</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
87
src/componentes/GraficoNefasto.svelte
Normal file
87
src/componentes/GraficoNefasto.svelte
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Chart, type ChartConfiguration } from "chart.js";
|
||||||
|
import html2canvas from "html2canvas";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
let chartConfig: ChartConfiguration = {
|
||||||
|
type: "line",
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: `Presupuesto total`,
|
||||||
|
font: { size: 14 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: { color: "#ccc" },
|
||||||
|
grid: { color: "#333" },
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "USD acumulado",
|
||||||
|
color: "#ccc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
labels: [
|
||||||
|
"2024-9",
|
||||||
|
"2024-10",
|
||||||
|
"2024-11",
|
||||||
|
"2024-12",
|
||||||
|
"2025-1",
|
||||||
|
"2025-2",
|
||||||
|
"2025-3",
|
||||||
|
"2025-4",
|
||||||
|
"2025-5",
|
||||||
|
"2025-6",
|
||||||
|
"2025-7",
|
||||||
|
"2025-8",
|
||||||
|
],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "PV",
|
||||||
|
data: [
|
||||||
|
2000, 5000, 10000, 15000, 22000, 28000, 34000, 38000,
|
||||||
|
41000, 41980, 41980, 41980,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "EV",
|
||||||
|
data: [
|
||||||
|
1500, 4500, 9000, 14000, 20000, 27000, 33000, 37000,
|
||||||
|
41000, 41980, 41980, 41980,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (canvas) {
|
||||||
|
new Chart(canvas, chartConfig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="height: 800px;">
|
||||||
|
<canvas bind:this={canvas}></canvas>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={async () => {
|
||||||
|
const element = canvas?.parentElement;
|
||||||
|
if (element) {
|
||||||
|
const canvasImg = await html2canvas(element);
|
||||||
|
const image = canvasImg.toDataURL("image/png");
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "grafico-indice-control.png";
|
||||||
|
link.href = image;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="btn btn-primary mt-3">Descargar Gráfico como PNG</button
|
||||||
|
>
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
<button class="btn btn-primary" onclick={() => (pagina = 1)}
|
<button class="btn btn-primary" onclick={() => (pagina = 1)}
|
||||||
>Dias de duracion</button
|
>Dias de duracion</button
|
||||||
>
|
>
|
||||||
|
<button class="btn btn-primary" onclick={() => (pagina = 2)}
|
||||||
|
>Indice de Control</button
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
aria-label="Cambiar tema"
|
aria-label="Cambiar tema"
|
||||||
|
|||||||
49
src/componentes/paginas/IndiceDeControl.svelte
Normal file
49
src/componentes/paginas/IndiceDeControl.svelte
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Issue } from "../../types";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { Chart, type ChartConfiguration } from "chart.js/auto";
|
||||||
|
import html2canvas from "html2canvas";
|
||||||
|
import GraficoNefasto from "../GraficoNefasto.svelte";
|
||||||
|
import CurvaS from "../CurvaS.svelte";
|
||||||
|
|
||||||
|
let { issues }: { issues: Issue[] } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="accordion" id="graficoAccordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button
|
||||||
|
class="accordion-button collapsed"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#graficoCollapse"
|
||||||
|
>
|
||||||
|
Gráfico de Índice de Control
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="graficoCollapse" class="accordion-collapse collapse">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<GraficoNefasto />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button
|
||||||
|
class="accordion-button"
|
||||||
|
aria-expanded="true"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#graficoCollapse2"
|
||||||
|
>
|
||||||
|
Gráfico de Índice de Control
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="graficoCollapse2" class="accordion-collapse collapse show">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<CurvaS {issues} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -2,6 +2,7 @@ import { mount } from "svelte";
|
|||||||
import App from "./App.svelte";
|
import App from "./App.svelte";
|
||||||
//import "./app.css";
|
//import "./app.css";
|
||||||
import "bootstrap/dist/css/bootstrap.min.css";
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import "bootstrap/dist/js/bootstrap.bundle.min.js";
|
||||||
|
|
||||||
const app = mount(App, {
|
const app = mount(App, {
|
||||||
target: document.getElementById("app")!,
|
target: document.getElementById("app")!,
|
||||||
|
|||||||
Reference in New Issue
Block a user