💄 added better looking ui
This commit is contained in:
parent
938d121d04
commit
3a9b3a8e67
27 changed files with 1415 additions and 467 deletions
6
env.d.ts
vendored
6
env.d.ts
vendored
|
|
@ -1 +1,7 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
5
public/assets/external.json
Normal file
5
public/assets/external.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"ext-git": "https://github.com",
|
||||
"ext-business-travel": "",
|
||||
"ext-ipt":""
|
||||
}
|
||||
94
src/App.vue
94
src/App.vue
|
|
@ -1,85 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
<router-link to="/">
|
||||
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQXnJ_NPFV-xorPwKgbahOQJhf1fxxy8UelEUl5X9ZaPTmQIEyi6IAEdZB9&s=10">
|
||||
</router-link>
|
||||
<h1 class="text-2xl font-bold">Micro Solution Platform</h1>
|
||||
</header>
|
||||
|
||||
<RouterView />
|
||||
<div id="MSP" class="main-container">
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a @click="openApp('about')">Über</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import { useAppLauncher } from '@/central_functions/openApp';
|
||||
const { openApp } = useAppLauncher();
|
||||
</script>
|
||||
21
src/assets/about.css
Normal file
21
src/assets/about.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
.about-page {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-page .navigation-back {
|
||||
margin-top: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.about-page a {
|
||||
color: #0056b3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.about-page a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
35
src/assets/appCards.css
Normal file
35
src/assets/appCards.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.app-card {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: var(--white);
|
||||
border: 3px solid #1f2937;
|
||||
border-radius: 10px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translate(-2px, -2px);
|
||||
box-shadow: 7px 7px 0px rgba(0,0,0,0.15);
|
||||
background-color: var(--secondary-color);
|
||||
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
pointer-events: none; /* Verhindert Probleme bei schnellen Klicks */
|
||||
}
|
||||
|
||||
.app-card:hover .app-title{
|
||||
color: var(--white);
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
25
src/assets/dashboard.css
Normal file
25
src/assets/dashboard.css
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
.app-grid {
|
||||
display: flex; /* Aktiviert das Flexbox-Layout */
|
||||
flex-direction: row; /* Sorgt für die Anordnung nebeneinander */
|
||||
gap: 20px; /* Abstand zwischen den Karten */
|
||||
align-items: center; /* Zentriert die Karten vertikal, falls sie unterschiedlich groß sind */
|
||||
padding: 10px;
|
||||
overflow-x: auto; /* Erlaubt horizontales Scrollen, falls es zu viele Karten werden */
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
height: 100%;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid #1f2937;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.collapsible-header:hover {
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
|
||||
.collapsible-header::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
230
src/assets/gantt.css
Normal file
230
src/assets/gantt.css
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
.app-wrapper {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background-color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.main-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
}
|
||||
.logo-area h1 {
|
||||
font-size: 1.8rem;
|
||||
color: #0f2d59;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
.range-picker {
|
||||
background: white;
|
||||
padding: 6px 18px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
font-weight: 600;
|
||||
}
|
||||
.range-picker select {
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
margin-left: 12px;
|
||||
border: 2px solid #222;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #0f2d59;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
.btn-io {
|
||||
background: #e3f2fd;
|
||||
color: #0f2d59;
|
||||
}
|
||||
.divider-space {
|
||||
margin: 0 15px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
.timeline-card {
|
||||
background: #ffffff;
|
||||
border: 2px solid #333;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeline-header-row,
|
||||
.project-row {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
}
|
||||
.sidebar-label,
|
||||
.project-info {
|
||||
padding: 15px 20px;
|
||||
border-right: 2px solid #333;
|
||||
background: #f9f9f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.sidebar-label.headline {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
background: #f3f3f3;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
.p-name {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: #000;
|
||||
}
|
||||
.topic-header {
|
||||
background: #ffffff;
|
||||
padding: 10px 20px;
|
||||
font-weight: 800;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #333;
|
||||
border-top: 2px solid #333;
|
||||
}
|
||||
.time-scale {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
.years-grid {
|
||||
display: grid;
|
||||
border-bottom: 1px solid #333;
|
||||
text-align: center;
|
||||
}
|
||||
.year-block {
|
||||
font-weight: 800;
|
||||
padding: 6px 0;
|
||||
font-size: 0.95rem;
|
||||
color: #0f2d59;
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
.year-block:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.months-grid {
|
||||
display: grid;
|
||||
text-align: center;
|
||||
}
|
||||
.month-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 8px 0;
|
||||
color: #555;
|
||||
border-right: 1px dashed #be9ba2;
|
||||
}
|
||||
.month-label.year-end {
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
.project-timeline-track {
|
||||
position: relative;
|
||||
height: 75px;
|
||||
}
|
||||
.grid-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
}
|
||||
.grid-line {
|
||||
border-right: 1px dashed rgba(51, 51, 51, 0.15);
|
||||
height: 100%;
|
||||
}
|
||||
.grid-line.year-end {
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
.phase-bar {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
height: 32px;
|
||||
border: 2px solid #222;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
z-index: 2;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.milestone {
|
||||
position: absolute;
|
||||
color: #ffd700;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.milestone.start {
|
||||
left: -10px;
|
||||
}
|
||||
.milestone.end {
|
||||
right: -10px;
|
||||
}
|
||||
.repeat-marker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #e91e63;
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
z-index: 3;
|
||||
}
|
||||
.badge {
|
||||
font-size: 0.65rem;
|
||||
background: #ffe8cc;
|
||||
color: #d97706;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
margin-left: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.no-data-notice {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #0f2d59;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.timeline-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
border: 2px solid #1f2937;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
Before Width: | Height: | Size: 276 B |
|
|
@ -1,35 +0,0 @@
|
|||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
83
src/assets/template.css
Normal file
83
src/assets/template.css
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
:root {
|
||||
--primary-color: #143665;
|
||||
--secondary-color: #143665;
|
||||
|
||||
--white: #fff;
|
||||
--transparency: 0.4;
|
||||
--border-size: 4px;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: var(--primary-color);
|
||||
height: 10vh;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
|
||||
header a {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header a img {
|
||||
height: 85%;
|
||||
width: auto;
|
||||
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
border: 2px solid #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
header h1{
|
||||
padding-left: 1%;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
padding: 1rem;
|
||||
background: #f3f4f6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer a {
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
31
src/central_functions/openApp.ts
Normal file
31
src/central_functions/openApp.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const urlMap = ref<Record<string, string>>({});
|
||||
let isLoaded = false;
|
||||
|
||||
export function useAppLauncher() {
|
||||
const router = useRouter();
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (isLoaded) return;
|
||||
try {
|
||||
const response = await fetch('/assets/external.json');
|
||||
urlMap.value = await response.json();
|
||||
isLoaded = true;
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der URLs:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const openApp = (appId: string) => {
|
||||
if (appId.includes('ext')) {
|
||||
const url = urlMap.value[appId] || 'https://www.google.com';
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
router.push(`/${appId}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { openApp, loadConfig };
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
|
||||
>Vue - Official</a
|
||||
>. If you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
12
src/components/appCard.vue
Normal file
12
src/components/appCard.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="app-card" @click="$emit('click')">
|
||||
<span class="app-title">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true }
|
||||
});
|
||||
defineEmits(['click']);
|
||||
</script>
|
||||
156
src/components/gantt/manageConfig.vue
Normal file
156
src/components/gantt/manageConfig.vue
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
// Lokale Sandbox-Kopie
|
||||
const localData = ref(null);
|
||||
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (newVal) {
|
||||
// VORHER: localData.value = structuredClone(props.data);
|
||||
localData.value = JSON.parse(JSON.stringify(props.data));
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const addStatus = () => {
|
||||
if (!localData.value.status) localData.value.status = [];
|
||||
localData.value.status.push({
|
||||
id: `status-${Date.now()}`,
|
||||
name: 'Neuer Status'
|
||||
});
|
||||
};
|
||||
|
||||
const removeStatus = (index) => {
|
||||
localData.value.status.splice(index, 1);
|
||||
};
|
||||
|
||||
const addPhaseType = () => {
|
||||
if (!localData.value.phase) localData.value.phase = [];
|
||||
localData.value.phase.push({
|
||||
id: `phase-${Date.now()}`,
|
||||
name: 'Neue Phase',
|
||||
color: '#3b82f6'
|
||||
});
|
||||
};
|
||||
|
||||
const removePhaseType = (index) => {
|
||||
localData.value.phase.splice(index, 1);
|
||||
};
|
||||
|
||||
const addGenericConfig = () => {
|
||||
const feedback = prompt("Welches Konfigurationselement oder Feature wünschst du dir? (z.B. 'Teams', 'Ressourcen-Planung')");
|
||||
if (!feedback || !feedback.trim()) return;
|
||||
|
||||
if (!localData.value.developerNotes) localData.value.developerNotes = [];
|
||||
|
||||
localData.value.developerNotes.push({
|
||||
id: `note-${Date.now()}`,
|
||||
timestamp: new Date().toLocaleString('de-DE'),
|
||||
requestedFeature: feedback.trim(),
|
||||
status: 'Backlog / Idee'
|
||||
});
|
||||
};
|
||||
|
||||
const triggerSave = () => {
|
||||
emit('save', localData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-backdrop" @click="emit('close')">
|
||||
<div class="modal-container" @click.stop v-if="localData">
|
||||
|
||||
<div class="modal-scroller">
|
||||
<button class="close-top-btn" @click="emit('close')">✕</button>
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="config-section">
|
||||
<div class="section-badge">status</div>
|
||||
<div class="config-items-list">
|
||||
<div v-for="(statusItem, index) in localData.status" :key="statusItem.id" class="config-item-row">
|
||||
<input type="text" v-model="statusItem.name" class="input-style" placeholder="Status Name" />
|
||||
<button class="delete-item-btn" @click="removeStatus(index)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="add-btn sub-add" @click="addStatus">+ add status</button>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider" />
|
||||
|
||||
<div class="config-section">
|
||||
<div class="section-badge">phase</div>
|
||||
<div class="config-items-list">
|
||||
<div v-for="(phaseItem, index) in localData.phase" :key="phaseItem.id" class="config-item-row">
|
||||
<input type="text" v-model="phaseItem.name" class="input-style flex-grow" placeholder="Phasen Name" />
|
||||
<input type="color" v-model="phaseItem.color" class="color-picker-input" />
|
||||
<button class="delete-item-btn" @click="removePhaseType(index)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="add-btn sub-add" @click="addPhaseType">+ add phase</button>
|
||||
</div>
|
||||
|
||||
<div class="global-action-area">
|
||||
<button class="add-btn global-add" @click="addGenericConfig">+ Konfigurationselement</button>
|
||||
<div v-if="localData.developerNotes && localData.developerNotes.length > 0" class="dev-log-container">
|
||||
<div class="dev-log-title">📋 Notizen an die Entwicklung:</div>
|
||||
<div v-for="note in localData.developerNotes" :key="note.id" class="dev-log-entry">
|
||||
<span class="dev-time">[{{ note.timestamp }}]</span>
|
||||
<span class="dev-text">{{ note.requestedFeature }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer-sticky">
|
||||
<button class="footer-btn cancel-btn" @click="emit('close')">Abbrechen</button>
|
||||
<button class="footer-btn save-btn" @click="triggerSave">Speichern</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-backdrop { position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.4); display: flex; justify-content: center; align-items: center; z-index: 1100; }
|
||||
.modal-container { background: white; border: 2px solid #222; border-radius: 24px; width: 90%; max-width: 550px; max-height: 80vh; display: flex; flex-direction: column; position: relative; box-shadow: 0 12px 40px rgba(0,0,0,0.15); overflow: hidden; }
|
||||
.modal-scroller { padding: 30px 30px 15px 30px; overflow-y: auto; flex: 1; }
|
||||
.close-top-btn { position: absolute; top: 15px; right: 20px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #666; }
|
||||
.modal-content { display: flex; flex-direction: column; gap: 25px; }
|
||||
.config-section { display: flex; flex-direction: column; gap: 12px; }
|
||||
.section-badge { align-self: flex-start; border: 2px solid #222; border-radius: 12px; padding: 4px 25px; font-weight: 700; background: #fff; text-transform: lowercase; }
|
||||
.config-items-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.config-item-row { display: flex; align-items: center; gap: 10px; }
|
||||
.input-style { border: 2px solid #222; border-radius: 12px; padding: 8px 15px; font-weight: 600; font-size: 1rem; width: 220px; }
|
||||
.flex-grow { flex: 1; }
|
||||
.color-picker-input { -webkit-appearance: none; appearance: none; width: 40px; height: 38px; background: transparent; border: 2px solid #222; border-radius: 10px; cursor: pointer; padding: 0; }
|
||||
.color-picker-input::-webkit-color-swatch { border: none; border-radius: 8px; }
|
||||
.delete-item-btn { background: none; border: none; color: #888; font-weight: bold; cursor: pointer; padding: 5px 10px; font-size: 1rem; }
|
||||
.delete-item-btn:hover { color: #cc0000; }
|
||||
.section-divider { border: none; border-top: 2px dashed #eee; }
|
||||
.add-btn { border: 2px solid #222; background: white; font-weight: 700; cursor: pointer; transition: background-color 0.15s; border-radius: 12px; padding: 6px 20px; }
|
||||
.add-btn:hover { background-color: #f5f5f5; }
|
||||
.global-action-area { border-top: 2px solid #222; padding-top: 20px; }
|
||||
|
||||
.modal-footer-sticky { border-top: 2px solid #222; background: #fdfdfd; padding: 15px 30px; display: flex; justify-content: flex-end; gap: 12px; }
|
||||
.footer-btn { border: 2px solid #222; border-radius: 12px; padding: 10px 25px; font-weight: 700; font-size: 1rem; cursor: pointer; }
|
||||
.cancel-btn { background: white; color: #333; }
|
||||
.cancel-btn:hover { background: #eee; }
|
||||
.save-btn { background: #222; color: white; }
|
||||
.save-btn:hover { background: #444; }
|
||||
|
||||
.dev-log-container { background: #f8fafc; border: 2px dashed #64748b; border-radius: 12px; padding: 12px; margin-top: 15px; }
|
||||
.dev-log-title { font-weight: 700; color: #475569; font-size: 0.85rem; margin-bottom: 5px; }
|
||||
.dev-log-entry { display: flex; gap: 6px; font-family: monospace; font-size: 0.8rem; }
|
||||
.dev-time { color: #94a3b8; }
|
||||
.dev-text { color: #334155; font-weight: 600; }
|
||||
</style>
|
||||
222
src/components/gantt/manageProject.vue
Normal file
222
src/components/gantt/manageProject.vue
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
highlightProjectId: [String, Number]
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
const localData = ref(null);
|
||||
const monthLabels = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
|
||||
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (newVal) {
|
||||
localData.value = JSON.parse(JSON.stringify(props.data));
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Hilfsfunktion: Holt die globale Farbe der ausgewählten Phase für die Live-Vorschau
|
||||
const getPhaseColor = (phaseId) => {
|
||||
const found = localData.value?.phase?.find(p => p.id === phaseId);
|
||||
return found ? found.color : '#ccc';
|
||||
};
|
||||
|
||||
const toggleMonth = (project, monthIdx) => {
|
||||
if (!project.repeat) project.repeat = { times: [] };
|
||||
const index = project.repeat.times.indexOf(monthIdx);
|
||||
if (index > -1) {
|
||||
project.repeat.times.splice(index, 1);
|
||||
} else {
|
||||
project.repeat.times.push(monthIdx);
|
||||
project.repeat.times.sort((a, b) => a - b);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRepeatableToggle = (project) => {
|
||||
if (project.isRepeatable) {
|
||||
project.phase = [];
|
||||
} else {
|
||||
if (project.repeat) project.repeat.times = [];
|
||||
}
|
||||
};
|
||||
|
||||
const addPhase = (project) => {
|
||||
if (!project.phase) project.phase = [];
|
||||
project.phase.push({
|
||||
id: `phase-segment-${Date.now()}`,
|
||||
phase: localData.value.phase[0]?.id || '',
|
||||
status: localData.value.status[0]?.id || '',
|
||||
start: '',
|
||||
end: '',
|
||||
milestoneAtStart: false,
|
||||
milestoneAtEnd: false
|
||||
});
|
||||
};
|
||||
|
||||
const addProjectToTopic = (topic) => {
|
||||
topic.project.push({
|
||||
id: `proj-${Date.now()}`,
|
||||
name: 'Neues Projekt',
|
||||
isRepeatable: false,
|
||||
phase: []
|
||||
});
|
||||
};
|
||||
|
||||
const triggerSave = () => {
|
||||
emit('save', localData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-backdrop" @click="emit('close')">
|
||||
<div class="modal-container" @click.stop v-if="localData">
|
||||
|
||||
<!-- SCROLLBARER INHALT -->
|
||||
<div class="modal-scroller">
|
||||
<button class="close-top-btn" @click="emit('close')">✕</button>
|
||||
|
||||
<div class="modal-content">
|
||||
<div v-for="topic in localData.topics" :key="topic.id" class="topic-group-wrapper">
|
||||
|
||||
<div class="topic-label">{{ topic.name }}</div>
|
||||
|
||||
<div class="projects-in-topic">
|
||||
<div
|
||||
v-for="project in topic.project"
|
||||
:key="project.id"
|
||||
class="project-config-card"
|
||||
:class="{ 'focused-highlight': project.id === highlightProjectId }"
|
||||
>
|
||||
<div class="config-row main-info">
|
||||
<input type="text" v-model="project.name" class="input-project-name" />
|
||||
|
||||
<label class="repeatable-badge" :class="{ active: project.isRepeatable }">
|
||||
<input type="checkbox" v-model="project.isRepeatable" @change="handleRepeatableToggle(project)" />
|
||||
Repeatable
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Repeatable Ansicht -->
|
||||
<div v-if="project.isRepeatable" class="repeat-section">
|
||||
<div class="months-circle-container">
|
||||
<button
|
||||
v-for="(month, idx) in monthLabels" :key="idx"
|
||||
class="month-circle"
|
||||
:class="{ selected: project.repeat?.times?.includes(idx + 1) }"
|
||||
@click="toggleMonth(project, idx + 1)"
|
||||
>{{ month }}</button>
|
||||
</div>
|
||||
<p class="info-text">Wiederkehrendes Projekt (Phasen deaktiviert)</p>
|
||||
</div>
|
||||
|
||||
<!-- Phasen Ansicht -->
|
||||
<div v-else class="phases-management-area">
|
||||
<div v-for="(p, pIdx) in project.phase" :key="p.id || pIdx" class="phase-config-card-inner">
|
||||
|
||||
<div class="phase-row-top">
|
||||
<!-- Status Dropdown -->
|
||||
<select v-model="p.status" class="select-dropdown">
|
||||
<option v-for="s in localData.status" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||
</select>
|
||||
|
||||
<!-- Phasen Typ Dropdown -->
|
||||
<select v-model="p.phase" class="select-dropdown flex-grow">
|
||||
<option v-for="ph in localData.phase" :key="ph.id" :value="ph.id">{{ ph.name || ph.id }}</option>
|
||||
</select>
|
||||
|
||||
<!-- Dynamischer Farbpunkt passend zur ausgewählten Phase -->
|
||||
<span class="phase-color-preview-dot" :style="{ backgroundColor: getPhaseColor(p.phase) }"></span>
|
||||
|
||||
<!-- Zeitspanne -->
|
||||
<div class="date-group">
|
||||
<input type="date" v-model="p.start" class="date-input" />
|
||||
<span>bis</span>
|
||||
<input type="date" v-model="p.end" class="date-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phase-row-bottom">
|
||||
<label class="checkbox-pill" :class="{ checked: p.milestoneAtStart }">
|
||||
<input type="checkbox" v-model="p.milestoneAtStart" /> milestone start
|
||||
</label>
|
||||
<label class="checkbox-pill" :class="{ checked: p.milestoneAtEnd }">
|
||||
<input type="checkbox" v-model="p.milestoneAtEnd" /> milestone end
|
||||
</label>
|
||||
<button class="remove-phase-btn" @click="project.phase.splice(pIdx, 1)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="add-btn phase-add" @click="addPhase(project)">+ add phase</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="add-btn project-add-inline" @click="addProjectToTopic(topic)">
|
||||
+ add Project to {{ topic.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FIXIERTER FOOTER -->
|
||||
<div class="modal-footer-sticky">
|
||||
<button class="footer-btn cancel-btn" @click="emit('close')">Abbrechen</button>
|
||||
<button class="footer-btn save-btn" @click="triggerSave">Speichern</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; justify-content: center; align-items: center; z-index: 1080; }
|
||||
.modal-container { background: white; border: 2px solid #222; border-radius: 24px; width: 95%; max-width: 850px; max-height: 80vh; display: flex; flex-direction: column; position: relative; box-shadow: 0 12px 40px rgba(0,0,0,0.15); overflow: hidden; }
|
||||
.modal-scroller { padding: 30px 30px 15px 30px; overflow-y: auto; flex: 1; }
|
||||
.close-top-btn { position: absolute; top: 15px; right: 20px; border: none; background: none; cursor: pointer; font-size: 1.2rem; }
|
||||
.modal-content { display: flex; flex-direction: column; gap: 40px; }
|
||||
.topic-group-wrapper { display: flex; flex-direction: column; gap: 15px; border-left: 4px solid #ffffff; padding-left: 20px; }
|
||||
.topic-label { align-self: flex-start; background: #f3f3f3; border: 2px solid #222; border-radius: 12px; padding: 5px 20px; font-weight: 800; font-size: 0.85rem; }
|
||||
.projects-in-topic { display: flex; flex-direction: column; gap: 20px; }
|
||||
.project-config-card { border: 2px solid #222; border-radius: 16px; padding: 20px; background: #fff; }
|
||||
.project-config-card.focused-highlight { border-color: #e91e63; background-color: #fff5f7; }
|
||||
.config-row { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; }
|
||||
.input-project-name { flex: 1; border: 2px solid #222; border-radius: 10px; padding: 8px 12px; font-weight: 700; font-size: 1.1rem; }
|
||||
.repeat-section { background: #f9f9f9; padding: 15px; border-radius: 12px; border: 1px dashed #ccc; }
|
||||
.months-circle-container { display: flex; gap: 6px; margin-bottom: 10px; }
|
||||
.month-circle { width: 34px; height: 34px; border: 2px solid #222; border-radius: 50%; background: white; font-weight: 800; cursor: pointer; }
|
||||
.month-circle.selected { background: #e91e63; color: white; border-color: #e91e63; }
|
||||
.info-text { font-size: 0.75rem; color: #666; font-style: italic; margin: 0; }
|
||||
.repeatable-badge { border: 2px solid #222; border-radius: 10px; padding: 8px 15px; font-weight: 700; cursor: pointer; display: flex; align-items: center; gap: 8px; }
|
||||
.repeatable-badge.active { background: #e3f2fd; }
|
||||
.phases-management-area { display: flex; flex-direction: column; gap: 12px; }
|
||||
.phase-config-card-inner { background: #fff; border: 1px solid #ddd; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.phase-row-top { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.flex-grow { flex: 1; min-width: 150px; }
|
||||
|
||||
/* Neuer runder Indikator für die Phasenfarbe im Editor */
|
||||
.phase-color-preview-dot { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #222; flex-shrink: 0; display: inline-block; }
|
||||
|
||||
.date-group { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 0.85rem; }
|
||||
.date-input { border: 2px solid #222; border-radius: 8px; padding: 4px 8px; font-family: inherit; }
|
||||
.phase-row-bottom { display: flex; gap: 10px; align-items: center; }
|
||||
.remove-phase-btn { margin-left: auto; background: none; border: none; color: #cc0000; cursor: pointer; font-weight: bold; }
|
||||
.select-dropdown { border: 2px solid #222; border-radius: 8px; padding: 6px; font-weight: 600; background: white; }
|
||||
.checkbox-pill { border: 2px solid #222; border-radius: 10px; padding: 5px 12px; font-size: 0.8rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px; }
|
||||
.checkbox-pill.checked { background: #fff9c4; }
|
||||
.add-btn { border: 2px solid #222; border-radius: 10px; background: white; font-weight: 700; cursor: pointer; padding: 8px 15px; }
|
||||
.phase-add { align-self: flex-start; margin-top: 5px; }
|
||||
.project-add-inline { align-self: flex-start; background: #fafafa; }
|
||||
|
||||
.modal-footer-sticky { border-top: 2px solid #222; background: #fdfdfd; padding: 15px 30px; display: flex; justify-content: flex-end; gap: 12px; }
|
||||
.footer-btn { border: 2px solid #222; border-radius: 12px; padding: 10px 25px; font-weight: 700; font-size: 1rem; cursor: pointer; }
|
||||
.cancel-btn { background: white; color: #333; }
|
||||
.cancel-btn:hover { background: #eee; }
|
||||
.save-btn { background: #222; color: white; }
|
||||
.save-btn:hover { background: #444; }
|
||||
</style>
|
||||
154
src/components/gantt/manageTopic.vue
Normal file
154
src/components/gantt/manageTopic.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: Boolean,
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save', 'open-project-settings']);
|
||||
|
||||
const localData = ref(null);
|
||||
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (newVal) {
|
||||
// VORHER: localData.value = structuredClone(props.data);
|
||||
localData.value = JSON.parse(JSON.stringify(props.data));
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const handleAddTopic = () => {
|
||||
if (!localData.value.topics) localData.value.topics = [];
|
||||
localData.value.topics.push({
|
||||
id: `topic-${Date.now()}`,
|
||||
name: 'Neues Thema',
|
||||
project: []
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveTopic = (index) => {
|
||||
if (confirm("Möchtest du dieses Thema samt all seiner Projekte wirklich löschen?")) {
|
||||
localData.value.topics.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProject = (topicId) => {
|
||||
const topic = localData.value.topics.find(t => t.id === topicId);
|
||||
if (topic) {
|
||||
if (!topic.project) topic.project = [];
|
||||
topic.project.push({
|
||||
id: `proj-${Date.now()}`,
|
||||
name: 'Neues Projekt',
|
||||
isRepeatable: false,
|
||||
phase: []
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveProject = (topic, projIndex) => {
|
||||
topic.project.splice(projIndex, 1);
|
||||
};
|
||||
|
||||
const handleOpenSettings = (project) => {
|
||||
emit('open-project-settings', project);
|
||||
};
|
||||
|
||||
const triggerSave = () => {
|
||||
emit('save', localData.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="modal-backdrop" @click="emit('close')">
|
||||
<div class="modal-container" @click.stop v-if="localData">
|
||||
|
||||
<div class="modal-scroller">
|
||||
<button class="close-top-btn" @click="emit('close')">✕</button>
|
||||
|
||||
<div class="modal-content">
|
||||
<div v-for="(topic, tIdx) in localData.topics" :key="topic.id" class="topic-group">
|
||||
|
||||
<div class="topic-header-row">
|
||||
<div class="topic-pill-header">
|
||||
<input type="text" v-model="topic.name" class="inline-input bold-text" placeholder="Thema Name" />
|
||||
</div>
|
||||
<button class="delete-icon-btn" @click="handleRemoveTopic(tIdx)" title="Thema löschen">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="project-list">
|
||||
<div v-for="(project, pIdx) in topic.project" :key="project.id" class="project-pill-row">
|
||||
<input type="text" v-model="project.name" class="inline-input" placeholder="Projekt Name" />
|
||||
|
||||
<div class="project-actions">
|
||||
<button class="delete-icon-btn small-x" @click="handleRemoveProject(topic, pIdx)" title="Projekt löschen">✕</button>
|
||||
<button class="settings-circle-btn" @click="handleOpenSettings(project)" title="Projekteinstellungen öffnen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="gear-icon">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="add-btn project-add" @click="handleAddProject(topic.id)">
|
||||
+ add Project
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button class="add-btn topic-add" @click="handleAddTopic">
|
||||
+ add Topic
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer-sticky">
|
||||
<button class="footer-btn cancel-btn" @click="emit('close')">Abbrechen</button>
|
||||
<button class="footer-btn save-btn" @click="triggerSave">Speichern</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-backdrop { position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.4); display: flex; justify-content: center; align-items: center; z-index: 1090; }
|
||||
.modal-container { background: white; border: 2px solid #222; border-radius: 24px; width: 90%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; position: relative; box-shadow: 0 12px 35px rgba(0,0,0,0.15); overflow: hidden; }
|
||||
.modal-scroller { padding: 30px 30px 15px 30px; overflow-y: auto; flex: 1; }
|
||||
.close-top-btn { position: absolute; top: 15px; right: 20px; background: none; border: none; font-size: 1.2rem; cursor: pointer; color: #666; }
|
||||
.modal-content { display: flex; flex-direction: column; gap: 25px; }
|
||||
.topic-group { display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px; }
|
||||
.topic-header-row { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
.topic-pill-header { border: 2px solid #222; border-radius: 12px; padding: 4px 15px; font-weight: 700; font-size: 1.1rem; background: #fff; flex-grow: 0; display: flex; align-items: center; }
|
||||
.inline-input { border: none; background: transparent; font-family: inherit; font-size: 1rem; width: 100%; outline: none; font-weight: 600; }
|
||||
.bold-text { font-weight: 700; font-size: 1.05rem; }
|
||||
|
||||
.project-list { display: flex; flex-direction: column; gap: 8px; padding-left: 15px; }
|
||||
.project-pill-row { display: flex; justify-content: space-between; align-items: center; border: 2px solid #222; border-radius: 12px; padding: 6px 12px 6px 20px; background: #fff; }
|
||||
.project-actions { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
.delete-icon-btn { background: none; border: none; color: #888; font-size: 1.1rem; cursor: pointer; font-weight: bold; }
|
||||
.delete-icon-btn:hover { color: #cc0000; }
|
||||
.small-x { font-size: 0.95rem; }
|
||||
|
||||
.settings-circle-btn { width: 32px; height: 32px; border: 2px solid #222; border-radius: 50%; background: white; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
|
||||
.settings-circle-btn:hover { background-color: #f0f0f0; }
|
||||
.gear-icon { width: 18px; height: 18px; color: #333; }
|
||||
|
||||
.add-btn { border: 2px solid #222; background: white; font-weight: 700; font-size: 1rem; cursor: pointer; text-align: center; }
|
||||
.add-btn:hover { background-color: #f5f5f5; }
|
||||
.project-add { border-radius: 12px; padding: 6px; width: 100%; font-size: 0.9rem; }
|
||||
.topic-add { border-radius: 12px; padding: 8px 25px; align-self: flex-start; margin-top: 5px; }
|
||||
|
||||
.modal-footer-sticky { border-top: 2px solid #222; background: #fdfdfd; padding: 15px 30px; display: flex; justify-content: flex-end; gap: 12px; }
|
||||
.footer-btn { border: 2px solid #222; border-radius: 12px; padding: 10px 25px; font-weight: 700; font-size: 1rem; cursor: pointer; }
|
||||
.cancel-btn { background: white; color: #333; }
|
||||
.cancel-btn:hover { background: #eee; }
|
||||
.save-btn { background: #222; color: white; }
|
||||
.save-btn:hover { background: #444; }
|
||||
</style>
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
import '@/assets/appCards.css'
|
||||
import '@/assets/dashboard.css'
|
||||
import '@/assets/gantt.css'
|
||||
import '@/assets/template.css'
|
||||
import '@/assets/about.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
// Korrigierter Router-Code
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
},
|
||||
{
|
||||
path: '/gantt',
|
||||
name: 'gantt',
|
||||
component: () => import('@/views/Gantt.vue'),
|
||||
},
|
||||
{
|
||||
path: '/md-viewer',
|
||||
name: 'md-viewer',
|
||||
component: () => import('@/views/Markdown.vue'),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
component: () => import('@/views/AboutView.vue'),
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
<div class="navigation-back">
|
||||
<router-link to="/">← Zurück zum Dashboard</router-link>
|
||||
</div>
|
||||
<div class="about-page">
|
||||
<h1>Über die Micro Solution Platform</h1>
|
||||
|
||||
<section>
|
||||
<h2>Zweck der Plattform</h2>
|
||||
<p>Die MSP dient zur Sammlung von kleinstapplikationen für das Team des IT-BC Produktionssteuerung.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Support & Kontakt</h2>
|
||||
<p>Bei Fehlern, Verbesserungsvorschlägen oder Fragen wende dich bitte an mich.</p>
|
||||
<p> Diese Applikation wird nicht aktiv erweitert.</p>
|
||||
|
||||
<strong>Applikations-Owner:</strong> Markus Benjamin Tabler <br>
|
||||
<a href="mailto:markus_benjamin.tabler@schwarz-produktion.com?subject=Anfrage%20zur%20MSP">
|
||||
E-Mail an Markus schreiben
|
||||
</a>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="meta-info">
|
||||
<p><small>© 2026 IT-BC Produktionssteuerung</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
51
src/views/Dashboard.vue
Normal file
51
src/views/Dashboard.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<div class="p-8">
|
||||
|
||||
<section class="mb-8">
|
||||
<details open>
|
||||
<summary class="collapsible-header">
|
||||
<h2 class="text-xl">Organisatorisches</h2>
|
||||
</summary>
|
||||
|
||||
<div class="app-grid pt-4">
|
||||
<AppCard title="Gantt" @click="openApp('gantt')" />
|
||||
<AppCard title="Markdown viewer" @click="openApp('markdown')" />
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<details open>
|
||||
<summary class="collapsible-header">
|
||||
<h2 class="text-xl">Notebook LM</h2>
|
||||
</summary>
|
||||
<div class="app-grid">
|
||||
<AppCard title="Dienstreise" @click="openApp('ext-business-travel')" />
|
||||
<AppCard title="Git" @click="openApp('ext-git')" />
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<details open>
|
||||
<summary class="collapsible-header">
|
||||
<h2 class="text-xl">Google Looker Studio</h2>
|
||||
</summary>
|
||||
<div class="app-grid">
|
||||
<AppCard title="IPT-Bericht" @click="openApp('ext-ipt')" />
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import AppCard from '@/components/appCard.vue'
|
||||
import { onMounted } from 'vue';
|
||||
import { useAppLauncher } from '@/central_functions/openApp.ts';
|
||||
|
||||
const { openApp, loadConfig } = useAppLauncher();
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
});
|
||||
</script>
|
||||
313
src/views/Gantt.vue
Normal file
313
src/views/Gantt.vue
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import ManageTopicsModal from '@/components/gantt/manageTopic.vue';
|
||||
import ManageProjectsModal from '@/components/gantt/manageProject.vue';
|
||||
import ManageConfigModal from '@/components/gantt/manageConfig.vue';
|
||||
|
||||
const isTopicsModalOpen = ref(false);
|
||||
const isProjectsModalOpen = ref(false);
|
||||
const isConfigModalOpen = ref(false);
|
||||
const activeProjectForEdit = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({ status: [], phase: [], topics: [] })
|
||||
}
|
||||
});
|
||||
|
||||
const roadmapData = ref({ status: [], phase: [], topics: [] });
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem('mvp_roadmap_data');
|
||||
if (saved) {
|
||||
try {
|
||||
roadmapData.value = JSON.parse(saved);
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Parsen der gespeicherten Daten", e);
|
||||
roadmapData.value = JSON.parse(JSON.stringify(props.data));
|
||||
}
|
||||
} else {
|
||||
roadmapData.value = JSON.parse(JSON.stringify(props.data));
|
||||
}
|
||||
});
|
||||
|
||||
watch(roadmapData, (newVal) => {
|
||||
localStorage.setItem('mvp_roadmap_data', JSON.stringify(newVal));
|
||||
}, { deep: true });
|
||||
|
||||
const fileInputRef = ref(null);
|
||||
|
||||
const triggerImportClick = () => {
|
||||
fileInputRef.value?.click();
|
||||
};
|
||||
|
||||
const handleImport = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(e.target.result);
|
||||
if (parsedData.status && parsedData.phase && parsedData.topics) {
|
||||
roadmapData.value = parsedData;
|
||||
alert("Daten erfolgreich importiert!");
|
||||
} else {
|
||||
alert("Ungültiges Format. Das JSON muss 'status', 'phase' and 'topics' enthalten.");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Fehler beim Lesen der Datei. Ist es ein gültiges JSON?");
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(roadmapData.value, null, 2));
|
||||
const downloadAnchor = document.createElement('a');
|
||||
downloadAnchor.setAttribute("href", dataStr);
|
||||
downloadAnchor.setAttribute("download", "roadmap_export.json");
|
||||
document.body.appendChild(downloadAnchor);
|
||||
downloadAnchor.click();
|
||||
downloadAnchor.remove();
|
||||
};
|
||||
|
||||
const addNewTopic = () => {
|
||||
roadmapData.value.topics.push({ id: Date.now(), name: 'Neues Thema', project: [] });
|
||||
};
|
||||
|
||||
const addNewProject = (topicId) => {
|
||||
const topic = roadmapData.value.topics.find(t => t.id === topicId);
|
||||
if (topic) {
|
||||
topic.project.push({ id: Date.now(), name: 'Neues Projekt', isRepeatable: false, phase: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSettingsFromTopics = (project) => {
|
||||
isTopicsModalOpen.value = false;
|
||||
activeProjectForEdit.value = project;
|
||||
isProjectsModalOpen.value = true;
|
||||
};
|
||||
|
||||
const handleSaveFromModal = (updatedData) => {
|
||||
roadmapData.value = updatedData;
|
||||
isTopicsModalOpen.value = false;
|
||||
isProjectsModalOpen.value = false;
|
||||
isConfigModalOpen.value = false;
|
||||
};
|
||||
|
||||
const allDates = computed(() => {
|
||||
const dates = [];
|
||||
roadmapData.value.topics.forEach(topic => {
|
||||
topic.project.forEach(proj => {
|
||||
proj.phase?.forEach(p => {
|
||||
if (p.start) dates.push(new Date(p.start));
|
||||
if (p.end) dates.push(new Date(p.end));
|
||||
});
|
||||
});
|
||||
});
|
||||
return dates;
|
||||
});
|
||||
|
||||
const minYearFound = computed(() => allDates.value.length ? Math.min(...allDates.value.map(d => d.getFullYear())) : 2026);
|
||||
const maxYearFound = computed(() => allDates.value.length ? Math.max(...allDates.value.map(d => d.getFullYear())) : 2027);
|
||||
|
||||
const selectedStartYear = ref(2026);
|
||||
const selectedEndYear = ref(2027);
|
||||
|
||||
watch([minYearFound, maxYearFound], ([minY, maxY]) => {
|
||||
if (allDates.value.length) {
|
||||
selectedStartYear.value = minY;
|
||||
selectedEndYear.value = maxY;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const viewStart = computed(() => new Date(`${selectedStartYear.value}-01-01`));
|
||||
const viewEnd = computed(() => new Date(`${selectedEndYear.value}-12-31`));
|
||||
|
||||
const gridColumns = computed(() => {
|
||||
const cols = [];
|
||||
let current = new Date(viewStart.value);
|
||||
while (current <= viewEnd.value) {
|
||||
cols.push(new Date(current));
|
||||
current.setMonth(current.getMonth() + 1);
|
||||
}
|
||||
return cols;
|
||||
});
|
||||
|
||||
const yearSpans = computed(() => {
|
||||
const spans = {};
|
||||
gridColumns.value.forEach(date => {
|
||||
const y = date.getFullYear();
|
||||
spans[y] = (spans[y] || 0) + 1;
|
||||
});
|
||||
return Object.entries(spans).map(([year, count]) => ({ year, count }));
|
||||
});
|
||||
|
||||
const availableYears = computed(() => {
|
||||
const years = [];
|
||||
const start = Math.min(selectedStartYear.value, minYearFound.value);
|
||||
const end = Math.max(selectedEndYear.value, maxYearFound.value);
|
||||
for (let y = start - 1; y <= end + 2; y++) {
|
||||
years.push(y);
|
||||
}
|
||||
return years;
|
||||
});
|
||||
|
||||
const getPosition = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const totalDuration = viewEnd.value - viewStart.value;
|
||||
const elapsed = date - viewStart.value;
|
||||
return (elapsed / totalDuration) * 100;
|
||||
};
|
||||
|
||||
const getWidth = (startStr, endStr) => {
|
||||
const start = new Date(startStr);
|
||||
const end = new Date(endStr);
|
||||
const totalDuration = viewEnd.value - viewStart.value;
|
||||
const phaseDuration = end - start;
|
||||
return (phaseDuration / totalDuration) * 100;
|
||||
};
|
||||
|
||||
const getPhaseColor = (phaseId) => {
|
||||
const phase = roadmapData.value.phase.find(p => p.id === phaseId);
|
||||
return phase ? phase.color : '#3b82f6';
|
||||
};
|
||||
|
||||
const getRepeatMarkers = (project) => {
|
||||
if (!project.isRepeatable || !project.repeat?.times) return [];
|
||||
const markers = [];
|
||||
for (let y = selectedStartYear.value; y <= selectedEndYear.value; y++) {
|
||||
project.repeat.times.forEach(monthNum => {
|
||||
const markerDate = new Date(`${y}-${String(monthNum).padStart(2, '0')}-15`);
|
||||
if (markerDate >= viewStart.value && markerDate <= viewEnd.value) {
|
||||
markers.push({ id: `${y}-${monthNum}`, position: getPosition(markerDate) });
|
||||
}
|
||||
});
|
||||
}
|
||||
return markers;
|
||||
};
|
||||
|
||||
const isDecember = (date) => date.getMonth() === 11;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ManageTopicsModal
|
||||
:isOpen="isTopicsModalOpen"
|
||||
:data="roadmapData"
|
||||
@close="isTopicsModalOpen = false"
|
||||
@open-project-settings="handleOpenSettingsFromTopics"
|
||||
@add-topic="addNewTopic"
|
||||
@add-project="addNewProject"
|
||||
@save="handleSaveFromModal"
|
||||
/>
|
||||
|
||||
<ManageProjectsModal
|
||||
:isOpen="isProjectsModalOpen"
|
||||
:data="roadmapData"
|
||||
:highlightProjectId="activeProjectForEdit?.id"
|
||||
@close="isProjectsModalOpen = false"
|
||||
@save="handleSaveFromModal"
|
||||
/>
|
||||
|
||||
<ManageConfigModal
|
||||
:isOpen="isConfigModalOpen"
|
||||
:data="roadmapData"
|
||||
@close="isConfigModalOpen = false"
|
||||
@save="handleSaveFromModal"
|
||||
/>
|
||||
|
||||
<div class="app-wrapper">
|
||||
<div>
|
||||
<div class="logo-area">
|
||||
<h1></h1>
|
||||
<div class="range-picker">
|
||||
<select v-model="selectedStartYear">
|
||||
<option v-for="y in availableYears" :key="y" :value="y">{{ y }}</option>
|
||||
</select>
|
||||
<span> bis </span>
|
||||
<select v-model="selectedEndYear">
|
||||
<option v-for="y in availableYears" :key="y" :value="y">{{ y }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<input type="file" ref="fileInputRef" style="display: none" accept=".json" @change="handleImport" />
|
||||
|
||||
<button class="btn btn-io" @click="triggerImportClick">📥 Import JSON</button>
|
||||
<button class="btn btn-io" @click="handleExport">📤 Export JSON</button>
|
||||
<span class="divider-space">|</span>
|
||||
<button class="btn" @click="isTopicsModalOpen = true">Manage Topics</button>
|
||||
<button class="btn" @click="isProjectsModalOpen = true">Manage Projects</button>
|
||||
<button class="btn btn-primary" @click="isConfigModalOpen = true">Manage Config</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-card">
|
||||
<div class="timeline-header-row">
|
||||
<div class="sidebar-label headline">Projekte / Themen</div>
|
||||
<div class="time-scale">
|
||||
<div class="years-grid" :style="{ gridTemplateColumns: `repeat(${gridColumns.length}, 1fr)` }">
|
||||
<div v-for="y in yearSpans" :key="y.year" :style="{ gridColumn: `span ${y.count}` }" class="year-block">
|
||||
{{ y.year }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="months-grid" :style="{ gridTemplateColumns: `repeat(${gridColumns.length}, 1fr)` }">
|
||||
<div v-for="date in gridColumns" :key="date" class="month-label" :class="{ 'year-end': isDecember(date) }">
|
||||
{{ date.toLocaleString('de-DE', { month: 'short' }).toUpperCase() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-content">
|
||||
<div v-if="roadmapData.topics.length === 0" class="no-data-notice">
|
||||
Keine Daten vorhanden. Klicke auf "Manage Topics" oder nutze "Import JSON" um zu starten!
|
||||
</div>
|
||||
|
||||
<div v-for="topic in roadmapData.topics" :key="topic.id" class="topic-section">
|
||||
<div class="topic-header">{{ topic.name }}</div>
|
||||
|
||||
<div v-for="project in topic.project" :key="project.id" class="project-row">
|
||||
<div class="project-info">
|
||||
<span class="p-name">{{ project.name }}</span>
|
||||
<span v-if="project.isRepeatable" class="badge">Repeatable</span>
|
||||
</div>
|
||||
|
||||
<div class="project-timeline-track">
|
||||
<div class="grid-overlay" :style="{ gridTemplateColumns: `repeat(${gridColumns.length}, 1fr)` }">
|
||||
<div v-for="date in gridColumns" :key="date" class="grid-line" :class="{ 'year-end': isDecember(date) }"></div>
|
||||
</div>
|
||||
|
||||
<template v-if="!project.isRepeatable">
|
||||
<div
|
||||
v-for="p in project.phase"
|
||||
:key="p.id"
|
||||
class="phase-bar"
|
||||
:style="{
|
||||
left: getPosition(p.start) + '%',
|
||||
width: getWidth(p.start, p.end) + '%',
|
||||
backgroundColor: getPhaseColor(p.phase)
|
||||
}"
|
||||
>
|
||||
<div v-if="p.milestoneAtStart" class="milestone start">◆</div>
|
||||
<span class="phase-text">{{ roadmapData.phase.find(ph => ph.id === p.phase)?.name || p.phase }}</span>
|
||||
<div v-if="p.milestoneAtEnd" class="milestone end">◆</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-for="marker in getRepeatMarkers(project)" :key="marker.id" class="repeat-marker" :style="{ left: marker.position + '%' }">
|
||||
<i>R</i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
0
src/views/Markdown.vue
Normal file
0
src/views/Markdown.vue
Normal file
|
|
@ -1,14 +1,11 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue